From b3c33f69e7b969b76d6618b0b122e8ad0e5bccdf Mon Sep 17 00:00:00 2001 From: kyugao Date: Mon, 3 Aug 2026 16:19:18 +0800 Subject: [PATCH] feat: reconcile tool result echoes --- tauri/README.md | 6 ++++++ tauri/src/runtime/trusted-dom-renderers.ts | 25 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/tauri/README.md b/tauri/README.md index de90c14..7c6e1f7 100644 --- a/tauri/README.md +++ b/tauri/README.md @@ -74,6 +74,12 @@ M0-01~M0-08 已完成。Mac 经 Linux Tailscale Web Host `http://100.121.118.1 Conversation Store v5 持久化任务快照,刷新或重新登录后仍只显示任务最新状态。尚未创建取消 outbox、尚未发送任何网络事件、尚未实现 result echo 或 Hermes Adapter 契约。`npm test` 当前为 4 个文件 / 22 个测试通过,`npm run build` 通过。 +### M1-05 `tool.result` 回声(已完成) + +每张可信 Tool Call 卡片均以 `call_id` 建立展示索引。同步到达的合法 `tool.result` 或 `tool.cancel` 只会更新同一张 choice / confirm / input 卡片的最终状态,并禁用控件;它们不会再被渲染为独立系统消息或“不支持的 Agent 内容”。未关联到本地卡片的 result/cancel 保持无展示副作用。 + +本项没有接入 Adapter 的实际发送、Agent 侧幂等消费或审计;这些是 M1-06 的唯一范围。`npm test` 当前为 4 个文件 / 22 个测试通过,`npm run build` 通过。 + ### 当前结构债务(M0 的改造对象) `src/main.ts` 目前包含 App Shell、页面 DOM、会话 cursor、同步循环与 Runtime 编排;M0-05 已将登录、发送与同步 HTTP `fetch` 迁入 `src/runtime/transport-adapter.ts`,M0-06 已将所有可信 DOM renderer 迁入 `src/runtime/trusted-dom-renderers.ts`。在 M0 准入前不新增 Surface 或 Capability 功能。 diff --git a/tauri/src/runtime/trusted-dom-renderers.ts b/tauri/src/runtime/trusted-dom-renderers.ts index 9490382..2a869e4 100644 --- a/tauri/src/runtime/trusted-dom-renderers.ts +++ b/tauri/src/runtime/trusted-dom-renderers.ts @@ -35,6 +35,7 @@ export type TaskInteractionHandlers = { export class TrustedDOMRendererContext { private thinking: HTMLElement | undefined; private readonly userRows = new Map(); + private readonly toolCards = new Map(); private readonly taskCards = new Map(); private readonly expirationTimers = new Set(); @@ -48,6 +49,7 @@ export class TrustedDOMRendererContext { public reset(): void { this.thinking = undefined; this.userRows.clear(); + this.toolCards.clear(); this.taskCards.clear(); for (const timer of this.expirationTimers) window.clearTimeout(timer); this.expirationTimers.clear(); @@ -107,11 +109,15 @@ export class TrustedDOMRendererContext { } public renderToolResult(item: Extract): void { - this.renderSystemMessage(`收到工具调用结果(${item.result.call_id} · ${item.result.status})。`); + const entry = this.toolCards.get(item.result.call_id); + if (!entry) return; + this.updateToolCardFinal(entry, item.result.status, item.result.error); } public renderToolCancel(item: Extract): void { - this.renderSystemMessage(`工具调用已取消(${item.cancel.call_id})。`); + const entry = this.toolCards.get(item.cancel.call_id); + if (!entry) return; + this.updateToolCardFinal(entry, "cancelled", item.cancel.reason); } public renderSurface(item: Extract): void { @@ -286,6 +292,7 @@ export class TrustedDOMRendererContext { card.append(heading, detail, actions); if (group.mode === "multi-choice") card.append(submitButton); card.append(status); + this.toolCards.set(call.call_id, { card, status, controls: buttons }); this.messages.append(card); this.scheduleExpiry(card, status, call, buttons); this.scrollLatest(); @@ -323,6 +330,7 @@ export class TrustedDOMRendererContext { }); actions.append(approve, cancel); card.append(heading, detail, actions, status); + this.toolCards.set(call.call_id, { card, status, controls: buttons }); this.messages.append(card); this.scheduleExpiry(card, status, call, buttons); this.scrollLatest(); @@ -387,6 +395,7 @@ export class TrustedDOMRendererContext { actions.append(submit, cancel); nativeForm.append(errors, actions); card.append(heading, detail, nativeForm, status); + this.toolCards.set(call.call_id, { card, status, controls }); this.messages.append(card); this.scheduleExpiry(card, status, call, controls); this.scrollLatest(); @@ -432,6 +441,18 @@ export class TrustedDOMRendererContext { status.textContent = "该交互已结束或已提交"; } + private updateToolCardFinal(entry: { card: HTMLElement; status: HTMLElement; controls: readonly (HTMLButtonElement | HTMLInputElement | HTMLTextAreaElement)[] }, finalStatus: "completed" | "failed" | "cancelled" | "expired", error?: string): void { + entry.card.dataset.status = finalStatus; + entry.status.textContent = finalStatus === "completed" + ? "已完成" + : finalStatus === "cancelled" + ? "已取消" + : finalStatus === "expired" + ? "该交互已过期" + : `处理失败${error ? `:${error}` : ""}`; + for (const control of entry.controls) control.disabled = true; + } + private showThinking(detail: string): void { this.thinking?.remove(); this.thinking = this.appendBubble("agent", detail);