Show live task progress while agents work (#3227)

* feat(agents): surface live task progress

Keep current task state above the composer and turn task snapshots into semantic timeline activity. Normalize provider task protocols while preserving genuine plan flows.

* fix(agents): admit tasks through stream sequencing

* test(agents): expect normalized task status
This commit is contained in:
Mohamed Boudra
2026-08-11 18:23:30 +02:00
committed by GitHub
parent b2ff359b60
commit db6520bf36
37 changed files with 1342 additions and 60 deletions
+37 -1
View File
@@ -9,6 +9,7 @@ import {
handoffCreatedAgentUserMessageToStream,
removeSubmittedUserMessage,
type StreamItem,
type TodoEntry,
type UserMessageItem,
} from "@/types/stream";
import {
@@ -394,6 +395,26 @@ export function selectAgentTurnPresentation(
);
}
function latestTasksFromStream(items: readonly StreamItem[]): TodoEntry[] {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (item?.kind === "todo_list") return item.items;
}
return [];
}
function updateAgentTasks(
current: Map<string, TodoEntry[]>,
agentId: string,
taskSnapshot: TodoEntry[] | undefined,
): Map<string, TodoEntry[]> {
if (taskSnapshot === undefined || equal(current.get(agentId) ?? [], taskSnapshot)) return current;
const next = new Map(current);
if (taskSnapshot.length > 0) next.set(agentId, taskSnapshot);
else next.delete(agentId);
return next;
}
export type WorkspaceRestoreStatus = "restoring" | "failed" | "needs-host-upgrade";
// Per-session state
@@ -426,6 +447,7 @@ export interface SessionState {
// Stream state (head/tail model)
agentStreamTail: Map<string, StreamItem[]>;
agentStreamHead: Map<string, StreamItem[]>;
agentTasks: Map<string, TodoEntry[]>;
agentTurnLiveness: Map<string, TurnLiveness>;
messageSubmissions: Map<string, MessageSubmissionRecord[]>;
agentTimelineCursor: Map<string, AgentTimelineCursorState>;
@@ -523,6 +545,7 @@ interface SessionStoreActions {
tail?: StreamItem[];
head?: StreamItem[];
acknowledgedClientMessageIds?: readonly string[];
taskSnapshot?: TodoEntry[];
},
) => void;
applyAgentTurnLiveness: (
@@ -692,6 +715,7 @@ function createInitialSessionState(
currentAssistantMessage: "",
agentStreamTail: new Map(),
agentStreamHead: new Map(),
agentTasks: new Map(),
agentTurnLiveness: new Map(),
messageSubmissions: new Map(),
agentTimelineCursor: new Map(),
@@ -812,8 +836,11 @@ export const useSessionStore = create<SessionStore>()(
const session = createInitialSessionState(serverId, null);
const timeline = replica.timeline;
const agentStreamTail = new Map<string, StreamItem[]>();
const agentTasks = new Map<string, TodoEntry[]>();
if (timeline) {
agentStreamTail.set(timeline.agentId, timeline.items);
const tasks = latestTasksFromStream(timeline.items);
if (tasks.length > 0) agentTasks.set(timeline.agentId, tasks);
}
const agentLastActivity = new Map(prev.agentLastActivity);
for (const agent of replica.agents.values()) {
@@ -830,6 +857,7 @@ export const useSessionStore = create<SessionStore>()(
workspaces: replica.workspaces,
projects: replica.projects,
agentStreamTail,
agentTasks,
},
},
agentLastActivity,
@@ -1149,8 +1177,10 @@ export const useSessionStore = create<SessionStore>()(
state.acknowledgedClientMessageIds ?? [],
);
const changedSubmissions = observedSubmissions !== currentSubmissions;
const agentTasks = updateAgentTasks(session.agentTasks, agentId, state.taskSnapshot);
const changedTasks = agentTasks !== session.agentTasks;
if (!changedTail && !changedHead && !changedSubmissions) {
if (!changedTail && !changedHead && !changedSubmissions && !changedTasks) {
return prev;
}
@@ -1172,6 +1202,7 @@ export const useSessionStore = create<SessionStore>()(
...session,
agentStreamTail: nextTail,
agentStreamHead: nextHead,
agentTasks,
messageSubmissions,
},
},
@@ -1601,6 +1632,10 @@ export const useSessionStore = create<SessionStore>()(
nextAuthoritative.set(agentId, true);
nextSyncGeneration.set(agentId, session.historySyncGeneration);
}
const tasks = latestTasksFromStream([...state.items, ...state.head]);
const agentTasks = new Map(session.agentTasks);
if (tasks.length > 0) agentTasks.set(agentId, tasks);
else agentTasks.delete(agentId);
return {
...prev,
@@ -1610,6 +1645,7 @@ export const useSessionStore = create<SessionStore>()(
...session,
agentStreamTail: nextTail,
agentStreamHead: nextHead,
agentTasks,
agentTimelineCursor: nextCursor,
agentTimelineHasOlder: nextHasOlder,
agentTimelineHasNewer: nextHasNewer,