news 2026/9/28 2:35:55

ClawX ACP 会话计划指示器:基于 update_plan 工具调用的纯渲染层实现与数据校验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ClawX ACP 会话计划指示器:基于 update_plan 工具调用的纯渲染层实现与数据校验
  • 人工智能
  • AI 应用
  • 桌面应用
  • 交互助手

【免费下载链接】ClawX

ClawX is a desktop app that provides a graphical interface for OpenClaw AI agents. It turns CLI-based AI orchestration into a desktop experience without using the terminal. China website is https://clawx.com.cn.

项目地址:https://gitcode.com/gh_mirrors/cl/ClawX
点击查看免费下载

导读

ClawX 桌面端为 OpenClaw ACP(Agent Client Protocol)会话提供了一个特殊的 Composer 计划指示器:当 Agent 通过结构化update_plan工具调用更新当前任务计划时,渲染器会从活动 ACP 时间线中提取最新有效的计划数据,以只读、默认折叠的进度胶囊形式展示在聊天输入区上方,用户无需进入终端即可实时掌握 Agent 正在执行的任务清单。本文围绕harness/specs/tasks/acp-session-plan-indicator.md这一 Harness 任务规格展开,完整讲解该特性的数据来源、结构校验规则、状态选择算法、UI 交互与无障碍细节,并结合仓库源码(current-plan.ts、AcpSessionPlan.tsx、ChatInput.tsx)与测试用例深入剖析其实现原理与边界行为。

功能概述与设计边界

核心目标

该任务的目标非常明确:把当前 ACP 会话中最新一次可回放的update_plan投射到活动聊天输入区,且不引入任何额外持久化。它属于acp-chat-experience场景下的 UI 特性(taskType:ui-feature),从任务规格中的intent可以看出,这是一种"纯投射(projection)"设计:

  • 数据源是 ACP 会话自身的回放时间线(timeline),而不是单独存储的计划文件;
  • 指示器只做展示,不做任何增删改查;
  • 会话切换、页面重载、应用重启后,只有当 ACP 回放再次提供该会话的结构化update_plan输入时,计划才会被恢复。

Scope:纯渲染层实现

任务规格在Scope一节明确划定了边界:

This task defines a Renderer-only ACP timeline projection. It does not change Main-owned ACP transport, history replay, or session routing.

也就是说,该特性不触碰 Electron 主进程(Main)拥有的 ACP 传输、历史回放与会话路由。持久的 ACP 回放与权威边界在 harness/reference/acp-chat.md 中单独阐述,指示器只是渲染器侧对已有时间线数据的一次只读解读。

Out of Scope:什么不该做

规格明确列出了三个"绝不越界"的行为,实现时严禁触碰:

  1. 禁止从工具标题、工具输出或助手文本中推断计划步骤—— 计划内容只能来自经过校验的结构化ToolCallItem.input;
  2. 禁止在 ACP 回放不再提供有效结构化输入时保留计划—— 计划指示器必须隐藏而非"回忆";
  3. 禁止编辑、完成、删除或以任何方式变更 OpenClaw 的计划步骤—— 指示器是纯只读的。

这三点约束直接体现在 acp-chat-state-and-history.md 规则的第 35 段,作为全局权威规则之一被requiredRules引用。

数据来源:ACP 时间线与 update_plan 工具调用

时间线快照(TimelineSnapshot)

计划投影的输入是AcpTimelineSnapshot,它来自渲染器对 ACP 事件的语义归约(semantic reduction)。从源码可以确认,该快照至少包含两个关键字段:

  • itemOrder:按时间顺序排列的项目 ID 列表;
  • itemsById:以项目 ID 为键的对象映射。

getCurrentAcpPlan正是依靠这两个字段,从时间线尾部向前遍历工具调用项目来寻找最新的有效计划。

识别 update_plan 调用

在 current-plan.ts 中,识别逻辑通过工具标题完成:

function isUpdatePlanTitle(title: string): boolean { const separatorIndex = title.indexOf(':'); return separatorIndex > 0 && title.slice(0, separatorIndex) === 'update_plan'; }

即:工具标题形如update_plan: ...(冒号前部分严格等于update_plan)即视为计划更新调用。但请注意,标题只用于"认出"这是一个计划调用,绝不用于提取计划内容——计划条目只允许来自校验过的结构化input数据。这一点在测试中也有体现:planCall('other-tool', initialPlan, 'completed', 'read_file: package.json')不会被识别为计划调用,返回null。

计划数据的结构校验(Validation)

输入结构要求

projectPlan函数(current-plan.ts)是整条链路的核心校验器。它要求input满足以下结构:

{ "plan": [ { "step": "Inspect the session", "status": "completed" }, { "step": "Project the current plan", "status": "in_progress" }, { "step": "Render the indicator", "status": "pending" } ] }

具体校验规则如下:

校验项规则违反时的处理
input顶层类型必须是对象且非数组返回null
plan字段必须是数组且非空返回null
每个条目必须是对象且非数组返回null
step字段必须是非空字符串(trim后长度 > 0)返回null
status字段必须属于pending/in_progress/completed之一返回null
in_progress条目数至多 1 个返回null

isPlanStatus类型守卫(current-plan.ts)精确限制了三种合法状态:

function isPlanStatus(value: unknown): value is AcpCurrentPlanStatus { return value === 'pending' || value === 'in_progress' || value === 'completed'; }

校验通过后,投影结果AcpCurrentPlan的结构为:

type AcpCurrentPlan = { steps: AcpCurrentPlanStep[]; // 有序步骤,含 step 文本与 status completedCount: number; // 已完成数 totalCount: number; // 总步骤数 };

测试用例对校验的完整覆盖

acp-current-plan.test.ts 用it.each覆盖了全部无效输入分支:

  • missing input(缺 input)
  • an empty plan(空数组)
  • a non-object entry(条目非对象)
  • a blank step(空白步骤文本)
  • an unknown status(未知状态,如blocked)
  • multiple in-progress steps(多个进行中步骤)

所有这些情况下,较新的候选计划会被跳过,回退到更早的有效计划。

最新有效计划的选取与失败回退算法

getCurrentAcpPlan(current-plan.ts)实现了规格中要求的"最新优先 + 失败回退"策略:

export function getCurrentAcpPlan(snapshot: AcpTimelineSnapshot): AcpCurrentPlan | null { for (let index = snapshot.itemOrder.length - 1; index >= 0; index -= 1) { const item = snapshot.itemsById[snapshot.itemOrder[index]]; if (!item || item.kind !== 'tool-call' || item.status === 'failed' || !isUpdatePlanTitle(item.title)) { continue; } const plan = projectPlan(item.input); if (plan) return plan; } return null; }

算法行为

  1. 从尾部向前遍历:最新的工具调用优先被检查;
  2. 三重过滤:跳过非tool-call项目、failed状态的项目、非update_plan标题的项目;
  3. 即时生效:状态为running/in_progress的运行中计划调用,只要结构化输入有效,会立即显示(测试shows a valid running plan update immediately验证了这一点);
  4. 失败回退:当更新的update_plan调用状态为failed或输入校验失败时,继续向前寻找上一个有效计划(测试falls back to the prior valid plan after a newer update fails与skips a newer candidate with %s验证了这一点);
  5. 无计划返回 null:整个时间线中不存在任何有效计划时返回null,此时输入区不显示指示器。

为什么是"回退"而不是"报错"

从产品语义看,这一设计保证了用户体验的连续性:Agent 某次计划更新因失败或格式异常未能落地时,用户仍能看到它上一次明确承诺的计划,而不是看到空白。同时规格限定回退"只回退到更早的有效结构化输入",绝不从文本、输出或全局缓存重建数据。

会话隔离:计划恢复严格绑定活动会话

渲染器侧的组合逻辑

计划投影在 src/pages/Chat/index.tsx 中完成,使用useMemo保证只读派生:

const currentPlan = useMemo( () => visibleAcpTimeline.sessionId === currentSessionKey ? getCurrentAcpPlan(visibleAcpTimeline) : null, [currentSessionKey, visibleAcpTimeline], );

关键点在于:只有当可见时间线的sessionId与当前选中会话键一致时才会计算计划。这保证了:

  • 切换会话时,计划指示器立即与新的活动会话绑定;
  • 时间线变化(如新的回放数据到达)时,计划随之重新计算;
  • 不同会话的计划互不串扰。

E2E 对会话隔离的验证

tests/e2e/chat-acp-inline-timeline.spec.ts 中的测试session plan replay isolates session A through an A-to-B-to-A switch构造了 A、B 两个会话各自独立的回放数据:

  • 会话 A 回放session-a-plan(Todo items: 1 / 2),会话 B 回放session-b-plan(Todo items: 0 / 1);
  • 从 A 切到 B:指示器文本变为Todo items: 0 / 1,展开后显示Keep session B separate;
  • 从 B 切回 A:指示器恢复为Todo items: 1 / 2,且重新挂载后默认折叠(aria-expanded="false",面板数量为 0),展开后显示的是 A 会话的最新回放内容(Load fresh session A replay等)。

测试还通过记录 ACP 加载会话键的顺序([MAIN_SESSION_KEY, sessionBKey, MAIN_SESSION_KEY])确认每次切换都触发了正确的会话加载,而非使用任何内存残留。

重载后的恢复路径

测试session plan replay restores a collapsed plan after renderer reload验证了规格中的另一条行为:渲染器page.reload()后,指示器从 ACP 的session/load回放(rawInput.plan)中重新恢复计划,且恢复后默认折叠。也就是说,计划的"持久化"完全依赖 ACP 回放本身,渲染器内存不跨重载存活。

UI 实现:AcpSessionPlan 组件的只读指示器

组件职责

AcpSessionPlan.tsx 是实际的 UI 组件,接收plan、sessionKey、isExpanded与onExpandedChange四个属性。它有以下几个关键设计:

  • 无计划时不渲染:if (!plan) return null;,保证空状态零 DOM 残留;
  • 默认折叠:组件内部useState初始化expanded: false,每次挂载都从折叠开始;
  • 展开状态按"计划身份 + 会话键"记忆:getPlanIdentity将[completedCount, totalCount, steps]序列化为 JSON 字符串作为身份指纹。只有当身份未变且会话键未变时,用户展开的面板才会保持打开;一旦计划内容更新或切换会话,面板自动收起(对应单元测试closes an expanded panel when its plan or session identity changes)。

进度胶囊(Collapsed 状态)

折叠时,组件渲染一个胶囊形按钮:

  • 左侧是ListChecks图标(lucide-react);
  • 中间是本地化进度文本Todo items: {{completed}} / {{total}}(使用tabular-nums等宽数字);
  • 全部完成时呈现绿色主题样式(border-green-500/20 bg-green-500/10 text-green-700);
  • 按钮带完整的aria-expanded、aria-controls、aria-label语义,支持键盘焦点(focus-visible:ring-2)。

展开面板(Expanded 状态)

展开后组件渲染一个上浮面板(absolute bottom-full right-0,悬浮于输入区上方):

  • 面板内是<ol>有序列表,每个步骤为<li>;
  • 三种状态对应三种图标:completed→CheckCircle2(绿色文本)、in_progress→CircleEllipsis、pending→Circle;
  • 步骤文本使用break-words允许长文本换行而非截断(单元测试专门断言了break-words类);
  • 面板内没有任何 button、input、checkbox 等交互控件(单元测试断言panel.querySelectorAll('button, input[type="checkbox"]')长度为 0),从结构上保证只读;
  • max-h-48 overflow-y-auto限制面板高度,步骤多时可滚动;
  • 步骤文本不附加 "Running/Pending/Completed" 文字标签,完全靠图标与颜色表达状态。

在 ChatInput 中的挂载与联动

状态行集成

ChatInput.tsx 将指示器放进 Composer 上方的工作状态行:

const showStatusRow = showWorkingIndicator || showSubagentControl || currentPlan != null;

状态行同时容纳"思考中/子代理工作"指示、子代理会话控件(AcpSubagentSessions)与计划指示器(AcpSessionPlan)。currentPlan != null是显示状态行的充分条件之一,与发送中/子代理忙碌状态互不干扰。

面板互斥与受控展开

ChatInput内部维护expandedComposerPanel状态,类型为{ sessionKey: string; panel: 'subagents' | 'plan' | null },用于控制子代理面板与计划面板的互斥展开:

const composerSessionKey = draftKey ?? ''; const activeComposerPanel = expandedComposerPanel.sessionKey === composerSessionKey ? expandedComposerPanel.panel : null;

AcpSessionPlan的isExpanded由activeComposerPanel === 'plan'驱动,onExpandedChange回调负责写回状态行。这里同样以composerSessionKey(即草稿键/会话键)作为展开状态的会话边界——切换会话后,旧会话的展开状态不会残留在新会话上。

E2E 对布局位置的断言

E2E 测试对指示器的实际布局提出了硬性要求:

  • 指示器 x 坐标 + 宽度应大于输入框右边界 - 100(贴近输入框右缘);
  • 指示器的 y 坐标小于输入框的 y 坐标(位于输入框上方);
  • 展开面板底部不超过指示器按钮底部(panelBox.y + panelBox.height <= toggleBox.y),且不遮挡输入框(< expandedComposerBox.y)。

这些断言确保指示器作为"输入区上方的一行附属状态"存在,不会遮挡或干扰正文输入。

国际化(i18n)与多语言支持

语言包结构

规格要求新增 UI 文案必须同步覆盖英文、中文、日文、俄文四种语言。实际语言包位于 shared/i18n/locales 下的en/chat.json、zh/chat.json、ja/chat.json、ru/chat.json,四个文件在acp.sessionPlan键下保持一致的四条文案:

keyenzhjaru
progressTodo items: {{completed}} / {{total}}待办项 {{completed}} / {{total}}タスク {{completed}} / {{total}}Задачи: {{completed}} / {{total}}
expandExpand plan展开计划計画を展開Развернуть план
collapseCollapse plan折叠计划計画を折りたたむСвернуть план
tasksPlan tasks计划任务計画のタスクЗадачи плана

组件通过useTranslation('chat')读取这些文案,进度文本用{{completed}}/{{total}}插值。仓库另有i18n-locale-parity.test.ts单元测试保障各语言包键的一致性。

相关 Harness 规格与验证方式

关联文档

  • 场景规格:acp-chat-experience.md 将该任务列为 ACP 聊天体验场景的组成部分;
  • 权威规则:acp-chat-state-and-history.md 第 35 段专门规范了计划指示器的数据边界(只能从结构化输入派生、只能选最新非失败有效计划、会话作用域内回退、无持久化/全局缓存);
  • 参考文档:harness/reference/acp-chat.md 说明持久的 ACP 回放与权威边界。

规格自带的验证命令

任务规格requiredTests提供了完整的验证命令集,可用于复现与回归:

# 单元测试:投影算法 pnpm exec vitest run tests/unit/acp-current-plan.test.ts # 单元测试:组件渲染、键盘交互、只读性 pnpm exec vitest run tests/unit/acp-session-plan.test.tsx tests/unit/chat-input.test.tsx tests/unit/chat-acp-inline-timeline.test.tsx # Electron E2E:实时计划显示、会话切换、重载回放恢复 pnpm exec playwright test tests/e2e/chat-acp-inline-timeline.spec.ts -g "session plan|plan indicator" # Harness 规格校验与干跑 pnpm harness validate --spec harness/specs/tasks/acp-session-plan-indicator.md pnpm harness run --spec harness/specs/tasks/acp-session-plan-indicator.md --dry-run

这些命令覆盖了从纯函数到 UI 组件再到真实 Electron 应用的全链路验证。

验收标准与实现要点总结

任务规格acceptance一节给出了可逐条核对的验收标准,结合源码可以归纳为实现要点:

验收条目实现位置
校验结构化ToolCallItem.input为非空有序计划,条目含非空 step、合法状态、至多一个 in_progresscurrent-plan.ts 的projectPlan
选取最新非失败有效 update_plan,新计划失败时回退到前一个有效计划current-plan.ts 的getCurrentAcpPlan
计划恢复限定于活动 ACP 会话的回放时间线,时间线变化时重算src/pages/Chat/index.tsx 的useMemo组合
不新增持久化、缓存、传输、后端端点、IPC 通道或计划变更控制全链路仅使用useMemo派生与内存useState,无任何写路径
指示器只读、默认折叠、键盘可达、展开仅显示规范化计划详情AcpSessionPlan.tsx 的按钮语义与面板结构
新 UI 文案覆盖英/中/日/俄四种语言shared/i18n/locales 四份 chat.json 的acp.sessionPlan
E2E 覆盖实时显示、会话切换、重载后从rawInput.plan恢复tests/e2e/chat-acp-inline-timeline.spec.ts 三个专项测试

总结

ClawX 的 ACP 会话计划指示器是一个典型的"纯渲染层投影"范例:它不拥有数据、不写入数据,只对 ACP 回放时间线中经过严格结构校验的update_plan工具输入做一次只读投影,并以无障碍友好的进度胶囊形式呈现在输入区上方。其设计精髓在于三点:校验严格(任何一项不合规即放弃该候选计划)、回退保守(新计划失败只回退到上一个有效结构化输入)、边界清晰(会话隔离、无持久化、无计划变更能力)。理解这套实现,对于在 ClawX 中扩展其他基于 ACP 时间线的只读派生 UI(如文件活动、子代理会话状态)同样具有直接的参考价值。

  • 人工智能
  • AI 应用
  • 桌面应用
  • 交互助手

【免费下载链接】ClawX

ClawX is a desktop app that provides a graphical interface for OpenClaw AI agents. It turns CLI-based AI orchestration into a desktop experience without using the terminal. China website is https://clawx.com.cn.

项目地址:https://gitcode.com/gh_mirrors/cl/ClawX
点击查看免费下载
上一篇:什么是SOCD?一文看懂格斗游戏神器Hitboxer(socd):键位重映射与方向冲突终极解决方案
下一篇:Web Starter Kit与Riot.js集成:轻量级组件化多设备开发

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 2:29:53

基于CNN的人脸表情识别完整项目:从数据到UI的实战指南

简介&#xff1a;这份资源是面向计算机相关专业学生与项目实战学习者的Python期末大作业完整源码&#xff0c;主题为基于CNN的人脸表情识别系统&#xff0c;适合正在准备课程设计、需要中等难度实战案例的人群参考与二次开发。压缩包共23个文件&#xff0c;约19.17MB&#xff0…

作者头像 李华
网站建设 2026/9/28 2:29:41

多尺度多数据融合:遥感图像检测与融合的工程化实践

简介&#xff1a;本资源是一套面向遥感图像处理初学者与科研实践者的MATLAB代码包&#xff0c;聚焦NASA遥感数据的多尺度分析、多源数据融合及地物检测任务&#xff0c;适用于环境监测、灾害评估与土地覆盖分类等实际应用场景。压缩包共5个.m文件&#xff0c;总大小仅3KB&#…

作者头像 李华
网站建设 2026/9/28 2:28:44

C++扫雷可视化实战:SFML图形界面开发入门

简介&#xff1a;本资源是一份面向C初学者与高校课程设计学生的可视化扫雷小程序完整实现源码&#xff0c;适用于《C程序设计》大作业实践与图形界面编程入门学习。项目基于Qt框架开发&#xff0c;包含15个核心文件&#xff1a;4个cpp实现逻辑与界面交互&#xff0c;3个h头文件…

作者头像 李华
网站建设 2026/9/28 2:28:40

Java网上花店系统实战:从部署到二次开发全解析

简介&#xff1a;这份资源是面向Java初学者与毕业设计学生的网上花店系统完整实战包&#xff0c;围绕Java Web开发全流程展开&#xff0c;帮助读者理解Servlet、JSP、JDBC与MVC模式在实际项目中的落地方式。压缩包共5个文件&#xff0c;包含2个zip源码包、2个mp4部署视频和1个s…

作者头像 李华
网站建设 2026/9/28 2:27:50

POE端口浪涌防护与EMC设计工程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华