feat: aggregate task progress updates
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
ToolCallRequest,
|
||||
} from "../protocol";
|
||||
import type { ToolCallRecord } from "./tool-call-state";
|
||||
import type { TaskRecord } from "./task-state";
|
||||
|
||||
export type ToolInteractionHandlers = {
|
||||
submit: (callID: string, submission: JsonObject) => boolean;
|
||||
@@ -21,6 +22,11 @@ export type ToolInteractionHandlers = {
|
||||
clearDraft: (callID: string) => void;
|
||||
};
|
||||
|
||||
export type TaskInteractionHandlers = {
|
||||
requestCancel: (operationID: string) => boolean;
|
||||
read: (operationID: string) => TaskRecord | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Trusted presentation context for the first-party chat shell. It owns only
|
||||
* DOM nodes and display-local state; it deliberately has no Store, Transport,
|
||||
@@ -29,17 +35,20 @@ export type ToolInteractionHandlers = {
|
||||
export class TrustedDOMRendererContext {
|
||||
private thinking: HTMLElement | undefined;
|
||||
private readonly userRows = new Map<string, HTMLElement>();
|
||||
private readonly taskCards = new Map<string, { card: HTMLElement; title: HTMLElement; progress: HTMLProgressElement; status: HTMLElement; cancel: HTMLButtonElement }>();
|
||||
private readonly expirationTimers = new Set<number>();
|
||||
|
||||
public constructor(
|
||||
private readonly messages: HTMLElement,
|
||||
private readonly presence: HTMLElement,
|
||||
private readonly toolInteractions?: ToolInteractionHandlers,
|
||||
private readonly taskInteractions?: TaskInteractionHandlers,
|
||||
) {}
|
||||
|
||||
public reset(): void {
|
||||
this.thinking = undefined;
|
||||
this.userRows.clear();
|
||||
this.taskCards.clear();
|
||||
for (const timer of this.expirationTimers) window.clearTimeout(timer);
|
||||
this.expirationTimers.clear();
|
||||
}
|
||||
@@ -63,6 +72,14 @@ export class TrustedDOMRendererContext {
|
||||
}
|
||||
|
||||
public renderProgress(item: Extract<ConversationItem, { kind: "progress" }>): void {
|
||||
if (item.task) {
|
||||
this.renderTaskProgress(this.taskInteractions?.read(item.task.operation_id) ?? {
|
||||
...item.task,
|
||||
conversation_id: item.envelope?.conversation_id ?? "",
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const percent = Math.max(0, Math.min(100, item.percent));
|
||||
this.renderSystemMessage(`${item.title || "Agent 任务"}:${percent}%${item.status ? ` · ${item.status}` : ""}`);
|
||||
}
|
||||
@@ -164,6 +181,47 @@ export class TrustedDOMRendererContext {
|
||||
return row;
|
||||
}
|
||||
|
||||
private renderTaskProgress(task: TaskRecord): void {
|
||||
const existing = this.taskCards.get(task.operation_id);
|
||||
if (existing) {
|
||||
this.updateTaskCard(existing, task);
|
||||
return;
|
||||
}
|
||||
const card = document.createElement("article");
|
||||
card.className = "task-card";
|
||||
card.dataset.operationId = task.operation_id;
|
||||
const title = document.createElement("strong");
|
||||
const progress = document.createElement("progress");
|
||||
progress.max = 100;
|
||||
const status = document.createElement("small");
|
||||
status.className = "task-card-status";
|
||||
const cancel = document.createElement("button");
|
||||
cancel.type = "button";
|
||||
cancel.className = "secondary-action";
|
||||
cancel.textContent = "请求取消";
|
||||
cancel.addEventListener("click", () => {
|
||||
if (!this.taskInteractions?.requestCancel(task.operation_id)) return;
|
||||
const current = this.taskInteractions.read(task.operation_id);
|
||||
if (current) this.updateTaskCard(this.taskCards.get(task.operation_id)!, current);
|
||||
});
|
||||
const entry = { card, title, progress, status, cancel };
|
||||
card.append(title, progress, status, cancel);
|
||||
this.taskCards.set(task.operation_id, entry);
|
||||
this.updateTaskCard(entry, task);
|
||||
this.messages.append(card);
|
||||
this.scrollLatest();
|
||||
}
|
||||
|
||||
private updateTaskCard(entry: { card: HTMLElement; title: HTMLElement; progress: HTMLProgressElement; status: HTMLElement; cancel: HTMLButtonElement }, task: TaskRecord): void {
|
||||
entry.card.dataset.status = task.status;
|
||||
entry.title.textContent = task.title;
|
||||
entry.progress.value = Math.max(0, Math.min(100, task.percent));
|
||||
const requested = task.cancel_requested_at !== undefined && task.status !== "cancelled";
|
||||
entry.status.textContent = requested ? `取消请求已记录 · ${task.percent}%` : `${task.status} · ${task.percent}%`;
|
||||
entry.cancel.hidden = !task.cancellable || task.status === "completed" || task.status === "failed" || task.status === "cancelled";
|
||||
entry.cancel.disabled = requested;
|
||||
}
|
||||
|
||||
private renderActionGroup(call: ToolCallRequest, group: ActionGroup, record?: ToolCallRecord): void {
|
||||
const card = document.createElement("article");
|
||||
card.className = "action-group-card";
|
||||
|
||||
Reference in New Issue
Block a user