From a212652dc0c1374ae062308b4a84ddc875c784da Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:08:08 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20agent=20Interact=20tab=20UX=20=E2=80=94?= =?UTF-8?q?=20immediate=20messages,=20ordering,=20scroll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Immediate-mode messages now trigger a background tick so the agent actually processes and responds (previously they were just stored) Frontend (Interact tab): - Reverse message order so newest appear at bottom near the input box - Filter out agent responses with empty content (blank bubbles) - Add "Agent is thinking..." indicator with pulsing dot while processing - Show timestamps instead of raw mode/status labels - Poll for new messages every 3s so responses appear automatically - Only auto-scroll to bottom on initial tab load, not on every poll update (prevents hijacking the user's scroll position) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/AgentsPage.tsx | 52 +++++++++++++++---- src/openjarvis/server/agent_manager_routes.py | 30 ++++++++++- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/AgentsPage.tsx b/frontend/src/pages/AgentsPage.tsx index 702dee16..babe1cca 100644 --- a/frontend/src/pages/AgentsPage.tsx +++ b/frontend/src/pages/AgentsPage.tsx @@ -1166,7 +1166,7 @@ function AgentCard({ // Detail view — Interact tab // --------------------------------------------------------------------------- -function InteractTab({ agentId }: { agentId: string }) { +function InteractTab({ agentId, agentStatus }: { agentId: string; agentStatus: string }) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [sending, setSending] = useState(false); @@ -1183,10 +1183,19 @@ function InteractTab({ agentId }: { agentId: string }) { useEffect(() => { loadMessages(); + // Poll for new messages while the tab is visible (agent responses + // arrive asynchronously after a background tick completes). + const interval = setInterval(loadMessages, 3000); + return () => clearInterval(interval); }, [loadMessages]); + // Scroll to bottom only on initial load, not on every poll update. + const hasScrolled = useRef(false); useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + if (!hasScrolled.current && messages.length > 0) { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + hasScrolled.current = true; + } }, [messages]); async function handleSend(mode: 'immediate' | 'queued') { @@ -1203,15 +1212,24 @@ function InteractTab({ agentId }: { agentId: string }) { } } + // Reverse so newest messages appear at the bottom (closest to input). + // Filter out agent responses with empty content. + const displayMessages = [...messages] + .filter((m) => m.direction === 'user_to_agent' || m.content.trim()) + .reverse(); + + const isAgentWorking = agentStatus === 'running'; + const hasPending = messages.some((m) => m.status === 'pending'); + return (
- {messages.length === 0 && ( + {displayMessages.length === 0 && !isAgentWorking && (
No messages yet. Send a message to interact with this agent.
)} - {messages.map((msg) => ( + {displayMessages.map((msg) => (

{msg.content}

-

- {msg.mode} · {msg.status} +

+ {msg.status === 'pending' ? 'sending...' : new Date(msg.created_at * 1000).toLocaleTimeString()}

))} + {/* Progress indicator while agent is working */} + {(isAgentWorking || hasPending) && ( +
+
+
+ + {sending ? 'Sending message...' : 'Agent is thinking...'} +
+
+
+ )}
{/* Input area */} @@ -1792,7 +1826,7 @@ export function AgentsPage() { )} {/* Tab: Interact */} - {detailTab === 'interact' && } + {detailTab === 'interact' && } {/* Tab: Tasks */} {detailTab === 'tasks' && ( diff --git a/src/openjarvis/server/agent_manager_routes.py b/src/openjarvis/server/agent_manager_routes.py index 58dcfe84..d92d51ab 100644 --- a/src/openjarvis/server/agent_manager_routes.py +++ b/src/openjarvis/server/agent_manager_routes.py @@ -597,7 +597,35 @@ def create_agent_manager_router( # Store user message in DB (always, regardless of stream mode) msg = manager.send_message(agent_id, req.content, mode=req.mode) - if not req.stream: + if not req.stream and req.mode != "immediate": + return msg + + if not req.stream and req.mode == "immediate": + # Non-streaming immediate: trigger a background tick so the + # agent processes the message, then return the stored msg. + import threading + + from openjarvis.agents.executor import AgentExecutor + from openjarvis.core.events import get_event_bus + from openjarvis.system import SystemBuilder + + def _immediate_tick(): + try: + executor = AgentExecutor( + manager=manager, event_bus=get_event_bus(), + ) + try: + system = SystemBuilder().build() + executor.set_system(system) + except Exception: + return + executor.execute_tick(agent_id) + except Exception: + pass + + threading.Thread( + target=_immediate_tick, daemon=True, + ).start() return msg # --- Streaming mode: run agent and return SSE response ---