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>
This commit is contained in:
David Kaya
2026-04-07 10:03:06 +02:00
co-authored by Copilot
parent 3a71baf104
commit ecba1be92a
2 changed files with 20 additions and 6 deletions
+15
View File
@@ -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<string>();
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]);
@@ -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;
}