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
@@ -80,6 +80,62 @@ function permutations<T>(values: readonly T[]): T[][] {
);
}
function taskTexts(tasks: ReadonlyArray<{ text: string }>): string[] {
return tasks.map((task) => task.text);
}
describe("agent task state", () => {
it("notifies the task subscriber only when its task snapshot changes", () => {
initializeTestSession();
const snapshots: string[][] = [];
const unsubscribe = useSessionStore.subscribe(
(state) => state.sessions["test-server"]?.agentTasks.get("agent-1") ?? [],
(tasks) => snapshots.push(taskTexts(tasks)),
);
const tasks = [{ id: "1", text: "Inspect", status: "pending" as const, completed: false }];
useSessionStore
.getState()
.setAgentStreamState("test-server", "agent-1", { taskSnapshot: tasks });
useSessionStore.getState().setMessages("test-server", []);
useSessionStore
.getState()
.setAgentStreamState("test-server", "agent-1", { taskSnapshot: [...tasks] });
useSessionStore.getState().setAgentStreamState("test-server", "agent-1", {
taskSnapshot: [{ ...tasks[0], status: "completed", completed: true }],
});
unsubscribe();
expect(snapshots).toEqual([["Inspect"], ["Inspect"]]);
});
it("restores the latest task snapshot from an authoritative timeline", () => {
initializeTestSession();
const todo: StreamItem = {
kind: "todo_list",
id: "todo-1",
provider: "codex",
timestamp: new Date("2026-08-11T10:00:00.000Z"),
activity: { type: "started", task: "Verify" },
items: [{ text: "Verify", status: "in_progress", completed: false }],
};
useSessionStore.getState().applyAgentTimelineResponseState("test-server", "agent-1", {
items: [todo],
head: [],
range: null,
older: "none",
newer: false,
synchronized: true,
acknowledgedClientMessageIds: [],
});
expect(useSessionStore.getState().sessions["test-server"]?.agentTasks.get("agent-1")).toEqual(
todo.items,
);
});
});
describe("agent timeline state", () => {
it("commits canonical items, range, and older availability as one synced state", () => {
initializeTestSession();