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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user