From ecba1be92a166308152b0eb121041ea96225d4bb Mon Sep 17 00:00:00 2001 From: David Kaya Date: Tue, 7 Apr 2026 10:03:06 +0200 Subject: [PATCH] fix: show turn activity panel immediately when run starts The panel was only emitted as a DisplayItem when classified thinking messages existed. During most of the turn, thinking messages haven't been reclassified yet, so the panel appeared late with all activities already populated. Two fixes: - After the message loop, inject a turn-activity item for any active run that wasn't already attached to a thinking group. This makes the panel appear as soon as the run record exists. - Remove the guard that required run events to exist before rendering. The panel now shows in its 'Working' state immediately when the run starts, and populates progressively as events arrive. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/renderer/components/ChatPane.tsx | 15 +++++++++++++++ .../components/chat/TurnActivityPanel.tsx | 11 +++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/renderer/components/ChatPane.tsx b/src/renderer/components/ChatPane.tsx index 39f144e..1a5f477 100644 --- a/src/renderer/components/ChatPane.tsx +++ b/src/renderer/components/ChatPane.tsx @@ -143,6 +143,9 @@ export function ChatPane({ let lastUserMessageId: string | undefined; const messages = session.messages; const busy = session.status === 'running'; + // Track which runs have been attached to a turn-activity item so we + // can detect orphaned runs that need their own panel. + const consumedRunIds = new Set(); for (let i = 0; i < messages.length; i++) { const message = messages[i]; @@ -173,6 +176,7 @@ export function ChatPane({ if (pendingThinking.length > 0) { const run = lastUserMessageId ? runsByTrigger.get(lastUserMessageId) : undefined; + if (run) consumedRunIds.add(run.id); items.push({ type: 'turn-activity', thinkingMessages: pendingThinking, run, turnStartedAt: run?.startedAt }); pendingThinking = []; } @@ -184,9 +188,20 @@ export function ChatPane({ if (pendingThinking.length > 0) { const run = lastUserMessageId ? runsByTrigger.get(lastUserMessageId) : undefined; + if (run) consumedRunIds.add(run.id); items.push({ type: 'turn-activity', thinkingMessages: pendingThinking, run, turnStartedAt: run?.startedAt }); } + // If the session is busy but no turn-activity was emitted for the + // active run (thinking messages haven't been reclassified yet), inject + // one so the panel appears as soon as the run starts producing events. + if (lastUserMessageId) { + const activeRun = runsByTrigger.get(lastUserMessageId); + if (activeRun && !consumedRunIds.has(activeRun.id)) { + items.push({ type: 'turn-activity', thinkingMessages: [], run: activeRun, turnStartedAt: activeRun.startedAt }); + } + } + return items; }, [session.messages, session.runs, session.status]); diff --git a/src/renderer/components/chat/TurnActivityPanel.tsx b/src/renderer/components/chat/TurnActivityPanel.tsx index f26f3a4..4aaa5e6 100644 --- a/src/renderer/components/chat/TurnActivityPanel.tsx +++ b/src/renderer/components/chat/TurnActivityPanel.tsx @@ -273,17 +273,16 @@ export function TurnActivityPanel({ const [expanded, setExpanded] = useState(false); const wasActiveRef = useRef(isActive); - // Auto-expand when the turn is active and activity appears. + // Auto-expand when the turn is active (run exists or thinking arrives). // Auto-collapse once the turn finishes. useEffect(() => { - const hasActivity = thinkingMessages.length > 0 || (run?.events.length ?? 0) > 0; - if (isActive && hasActivity) { + if (isActive && (thinkingMessages.length > 0 || run)) { setExpanded(true); } else if (wasActiveRef.current && !isActive) { setExpanded(false); } wasActiveRef.current = isActive; - }, [isActive, thinkingMessages.length, run?.events.length]); + }, [isActive, thinkingMessages.length, run]); const toggle = useCallback(() => setExpanded((prev) => !prev), []); @@ -302,8 +301,8 @@ export function TurnActivityPanel({ [thinkingMessages, run?.events], ); - // Nothing to show yet - if (thinkingMessages.length === 0 && (!run || run.events.length === 0)) { + // Nothing to show — no thinking messages, no run, and not active + if (thinkingMessages.length === 0 && !run) { return null; }