Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions web/src/stores/handlers/sessionHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { WSMessage } from '../../api/websocket';
import type { PanelTab } from '../../types/chat';
import { applyStreamEvent, rebuildPanelTabsFromBuffer, deriveStatus } from '../helpers/bufferReplay';
import { applyStreamEvent, rebuildPanelTabsFromBuffer, deriveStatus, extractTodosFromBuffer } from '../helpers/bufferReplay';
import type { Get, Set } from './types';

// ------------------------------------------------------------------ //
Expand Down Expand Up @@ -89,15 +89,25 @@ export function handleSessionStatus(
}
}

set({
// Restore todos panel from the freshest TodoWrite in the buffer. Without
// this, a client that reconnects mid-turn (page refresh, WS drop, tab
// backgrounded) sees a stale snapshot from persisted history because the
// buffered TodoWrite tool_use events fed only streamingBlocks.
const restoredTodos = extractTodosFromBuffer(bufferedEvents);

const update: Record<string, unknown> = {
isStreaming: true,
streamingBlocks: blocks,
agentStatus: deriveStatus(blocks),
panels: restored.panels,
activePanelId: restored.activePanelId,
panelVisible: restored.panels.length > 0,
pendingInteraction: restoredInteraction,
});
};
if (restoredTodos !== null) {
update.currentTodos = restoredTodos;
}
set(update);
} else {
set({ isStreaming: true, streamingBlocks: blocks, agentStatus: deriveStatus(blocks) });
}
Expand Down
24 changes: 24 additions & 0 deletions web/src/stores/helpers/bufferReplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,27 @@ export function extractTodosFromMessages(messages: ChatMessage[]): TodoItem[] {
}
return [];
}

/**
* Extract the latest TodoWrite todos from a list of buffered WS events.
* Used during live reconnect (session_status with buffered_events): the
* persisted message history may not yet include the in-flight turn, so the
* todos panel needs to read the freshest state from the buffer.
*
* Returns null when the buffer contains no top-level TodoWrite, so the caller
* can preserve whatever currentTodos was already set from persisted history.
* Unlike extractTodosFromMessages, this does NOT skip the all-completed case;
* the panel's own auto-hide handles that animation once the user sees it.
*/
export function extractTodosFromBuffer(events: WSMessage[]): TodoItem[] | null {
for (let i = events.length - 1; i >= 0; i--) {
const event = events[i];
if (event.type !== 'tool_use') continue;
if (event.tool !== 'TodoWrite') continue;
// Sub-agent (Task) child TodoWrite calls belong to the panel, not the main todos.
if ('parent_tool_use_id' in event && event.parent_tool_use_id) continue;
const todos = (event.input as { todos?: TodoItem[] } | undefined)?.todos;
if (Array.isArray(todos)) return todos as TodoItem[];
}
return null;
}