fix(chat): close all active execution summaries on turn end

This commit is contained in:
2026-08-06 10:48:53 +08:00
parent 9f5ea6eb4a
commit 0601c2ced1
3 changed files with 36 additions and 0 deletions
+7
View File
@@ -480,6 +480,13 @@ function projectExecutionProgress(item: ConversationItem, now: string): void {
const terminal = item.kind === "error" ? "failed" : (item.kind === "agent-status" && item.status === "idle" ? "completed" : undefined); const terminal = item.kind === "error" ? "failed" : (item.kind === "agent-status" && item.status === "idle" ? "completed" : undefined);
if (!terminal) return; if (!terminal) return;
const executionID = item.kind === "agent-status" ? item.execution_id : undefined; const executionID = item.kind === "agent-status" ? item.execution_id : undefined;
if (!executionID) {
const summaries = executionProgress.finishAllActive(terminal, now);
if (summaries.length === 0) return;
runtime.replaceExecutionSummaries(executionProgress.snapshot());
for (const summary of summaries) rendererContext.renderExecutionSummary(summary);
return;
}
const summary = executionProgress.finish(executionID, terminal, now); const summary = executionProgress.finish(executionID, terminal, now);
if (!summary) return; if (!summary) return;
runtime.replaceExecutionSummaries(executionProgress.snapshot()); runtime.replaceExecutionSummaries(executionProgress.snapshot());
@@ -43,4 +43,15 @@ describe("ExecutionProgressState", () => {
expect.objectContaining({ id: "exec_002", status: "completed" }), expect.objectContaining({ id: "exec_002", status: "completed" }),
]); ]);
}); });
it("finishes every live summary when the turn terminal has no execution id", () => {
const state = new ExecutionProgressState();
state.begin("exec_001", T0);
state.begin("exec_002", T1);
expect(state.finishAllActive("completed", T1)).toEqual([
expect.objectContaining({ id: "exec_001", status: "completed", finished_at: T1 }),
expect.objectContaining({ id: "exec_002", status: "completed", finished_at: T1 }),
]);
expect(state.snapshot().every(summary => summary.status === "completed")).toBe(true);
});
}); });
@@ -72,6 +72,24 @@ export class ExecutionProgressState {
return copy(active); return copy(active);
} }
/**
* A status terminal without an execution_id closes the current Agent turn.
* If several execution summaries were opened during that turn, all of them
* must leave the live state together; closing only the first leaves a card
* ticking forever in the renderer.
*/
public finishAllActive(status: "completed" | "failed", now: string): readonly ExecutionProgressSummary[] {
const finished: ExecutionProgressSummary[] = [];
for (const summary of this.summaries) {
if (summary.status !== "running") continue;
summary.status = status;
summary.updated_at = now;
summary.finished_at = now;
finished.push(copy(summary));
}
return finished;
}
/** Ends only the matching running execution; never steals another turn's card. */ /** Ends only the matching running execution; never steals another turn's card. */
public finish(id: string | undefined, status: "completed" | "failed", now: string): ExecutionProgressSummary | undefined { public finish(id: string | undefined, status: "completed" | "failed", now: string): ExecutionProgressSummary | undefined {
if (!id) return this.finishActive(status, now); if (!id) return this.finishActive(status, now);