- 人工智能
- 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.
导读
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:什么不该做
规格明确列出了三个"绝不越界"的行为,实现时严禁触碰:
- 禁止从工具标题、工具输出或助手文本中推断计划步骤—— 计划内容只能来自经过校验的结构化
ToolCallItem.input; - 禁止在 ACP 回放不再提供有效结构化输入时保留计划—— 计划指示器必须隐藏而非"回忆";
- 禁止编辑、完成、删除或以任何方式变更 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; }算法行为
- 从尾部向前遍历:最新的工具调用优先被检查;
- 三重过滤:跳过非
tool-call项目、failed状态的项目、非update_plan标题的项目; - 即时生效:状态为
running/in_progress的运行中计划调用,只要结构化输入有效,会立即显示(测试shows a valid running plan update immediately验证了这一点); - 失败回退:当更新的
update_plan调用状态为failed或输入校验失败时,继续向前寻找上一个有效计划(测试falls back to the prior valid plan after a newer update fails与skips a newer candidate with %s验证了这一点); - 无计划返回 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键下保持一致的四条文案:
| key | en | zh | ja | ru |
|---|---|---|---|---|
progress | Todo items: {{completed}} / {{total}} | 待办项 {{completed}} / {{total}} | タスク {{completed}} / {{total}} | Задачи: {{completed}} / {{total}} |
expand | Expand plan | 展开计划 | 計画を展開 | Развернуть план |
collapse | Collapse plan | 折叠计划 | 計画を折りたたむ | Свернуть план |
tasks | Plan 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_progress | current-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.
相关推荐
three.js XRManager 完全指南:基于 WebXR Device API 的会话管理与渲染层架构
three.js XRManager 完全指南:基于 WebXR Device API 的会话管理与渲染层架构 XRManager 是 three.js 通用渲
前端3D渲染图形学Android NDK 之 Hello Vulkan:基于 GameActivity 的三角形渲染、校验层与预旋转实战
Android NDK 之 Hello Vulkan:基于 GameActivity 的三角形渲染、校验层与预旋转实战 本指南以 ndk samples 仓库中
示例工程移动开发微信聊天记录导出:WeChatMsg 快速导出 HTML/Word/CSV,还能生成聊天年度报告
微信聊天记录导出:WeChatMsg 快速导出 HTML/Word/CSV,还能生成聊天年度报告 WeChatMsg 是一款开源的微信聊天记录导出工具:它读取微
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考