fix(web): make plan transitions admission-safe

This commit is contained in:
fz
2026-07-24 16:55:04 +08:00
parent aed744dbbe
commit e90b3bc5a2
18 changed files with 214 additions and 61 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-24-web-plan-mode.md: 8ed22fa3833b3903dfaeb34c8c7810267aef9930
2026-07-24-web-plan-mode.zh.md: 9647e3d71bcc9132bc0990e7c296a63cdab55812
2026-07-24-web-plan-mode.md: 395ef2a3afcdac53c5d97b5555c2ffd635497db2
2026-07-24-web-plan-mode.zh.md: 21727052c284805b38d411e0215af876b2986e35

View File

@@ -14,7 +14,7 @@ Plan selection is not a stop command. The plan service deliberately queues the l
`@deepseek-ai/dsh-client-ui-plan` is one Web feature plugin with lifecycle-coupled host and browser entries. Its host entry mounts `@deepseek-ai/dsh-plan-mode` with the Web product's complete planning policy. Its browser entry registers `PlanModeControl` into the session-scoped `conversation.composer.controls` list slot. The `dsh web` roster selects the package once; plugin discovery loads the browser entry while the same roster mount supplies the host behavior.
The policy is product-owned configuration at this composition boundary. It tells the model to inspect before planning, avoid mutation while planning, resolve discoverable facts without asking, make the plan decision-complete, and submit it as the only final `exit_plan_mode` call. The plan package continues to own logged state, boundary timing, prompt-section activation, the stable exit-tool schema, and review semantics. The Web plugin does not copy any of those mechanisms.
The policy is product-owned configuration at this composition boundary. Its active section tells the model to inspect before planning, avoid mutation while planning, resolve discoverable facts without asking, make the plan decision-complete, and submit it as the only final `exit_plan_mode` call. Its complementary default section explicitly says that the session is no longer in plan mode, permits normal implementation work, and forbids calling the still-registered exit tool. This current-state statement prevents earlier conversation text about plan mode from confusing a later default request. The plan package continues to own logged state, boundary timing, the active prompt-section decision, the stable exit-tool schema, and review semantics. The Web plugin does not copy those mechanisms.
`ui-conversation` owns and renders the new additive controls slot to the left of the primary composer action. It provides no business payload; entries receive the standard session kit. Whole-composer replacements remain on the separate selector-routed `conversation.composer` chain, so a pending question replaces the InputBar and its controls without either feature importing the other.
@@ -32,6 +32,8 @@ The control is absent when `planMode` is `null`. Otherwise, its selected value i
Business and transport failures leave the confirmed snapshot unchanged, re-enable the selector, and render a compact visible failure beside it. The component guards asynchronous completion after unmount so switching sessions cannot update a retired control.
Sending waits for the latest selector request and any newer selection that supersedes it. During that admission-only interval, the composer clears its draft and disables duplicate sends; it does not wait for model generation. Host acceptance releases the admission lock, after which the ordinary running state keeps Stop available. A selection or admission failure sends no prompt and restores the submitted draft only when replacement text has not appeared.
## Exit review
`exit_plan_mode` remains registered in both modes for request-cache stability. In plan mode the model submits the complete Markdown plan through that tool. The plan service asks through `ctx.userInteraction`, and the already-composed Web question plugin presents the plan detail with Approve, Keep planning, and the free-text answer channel. Question detail reuses the assistant-output Markdown primitive and its untrusted-content policy. The capped question card keeps its title, navigation, and submission actions fixed while the complete plan and choices share an internal scroll region. The chat flow omits its generic pending-question placeholder because the composer takeover is the sole presentation of that wait.
@@ -40,7 +42,7 @@ Approval queues inactive mode for the next step; it does not rewrite the current
## Product composition and evidence
Fixture mode implements the same pending and boundary behavior in memory so browser acceptance tests exercise the assembled product without a key. The keyless browser flow selects Plan, commits it with a prompt, selects Default during generation, stops without losing the pending target, and commits Default with the next prompt. A file snapshot records each user-visible state. A real `dsh web` process with a mock provider additionally proves that the roster mounts plan mode, the state RPC reports capability, and the active Web policy reaches the provider request alongside workspace instructions.
Fixture mode implements the same pending and boundary behavior in memory so browser acceptance tests exercise the assembled product without a key. The keyless browser flow selects Plan, commits it with a prompt, selects Default during generation, stops without losing the pending target, and commits Default with the next prompt. A file snapshot records each user-visible state. A real `dsh web` process with a mock provider additionally proves that the roster mounts plan mode, the state RPC reports capability, and both exact-mode Web policies reach their corresponding provider requests alongside workspace instructions.
## Alternatives considered
@@ -56,4 +58,4 @@ Fixture mode implements the same pending and boundary behavior in memory so brow
## Consequences
The Web product now exposes the same plan interaction model as the current terminal and ACP compositions while retaining its plugin boundaries. Selecting the feature adds one host policy/tool owner and one browser slot entry; removing its fiber removes both. The model tool catalog stays stable across mode changes, but the active system-prompt section changes at a plan boundary and therefore changes the request prefix. Plan mode remains guidance, not an execution sandbox: deployments that require enforced read-only planning still compose the independent sandbox and approval policies.
The Web product now exposes the same plan interaction model as the current terminal and ACP compositions while retaining its plugin boundaries. Selecting the feature adds one host policy/tool owner and one browser slot entry; removing its fiber removes both. The model tool catalog stays stable across mode changes, but the active Web mode section changes at a plan boundary and therefore changes the request prefix. Plan mode remains guidance, not an execution sandbox: deployments that require enforced read-only planning still compose the independent sandbox and approval policies.

View File

@@ -14,7 +14,7 @@ Web 宿主可以通过[可选会话 RPC 契约](../architecture/2026-07-24-web-p
`@deepseek-ai/dsh-client-ui-plan` 是一个 Web 功能插件,包含生命周期耦合的宿主入口和浏览器入口。宿主入口使用 Web 产品的完整 plan 策略挂载 `@deepseek-ai/dsh-plan-mode`。浏览器入口则把 `PlanModeControl` 注册到会话作用域的 `conversation.composer.controls` 列表槽。`dsh web` 的插件清单只需选择该包一次:插件发现机制会加载浏览器入口,同一次清单挂载则提供宿主行为。
该策略是此组合边界上由产品拥有的配置。它要求模型先检查再规划、规划期间避免修改、无需询问即可自行查明能够发现的事实、使 plan 包含完成决策所需的全部信息,并把 plan 作为唯一且最终的 `exit_plan_mode` 调用提交。plan 包(package)继续拥有已记录的状态、边界时序、提示词段激活、稳定的退出工具 schema 和评审语义。Web 插件不会复制其中任何机制。
该策略是此组合边界上由产品拥有的配置。其激活段要求模型先检查再规划、规划期间避免修改、无需询问即可自行查明能够发现的事实、使 plan 包含完成决策所需的全部信息,并把 plan 作为唯一且最终的 `exit_plan_mode` 调用提交。与之配套的默认段会明确说明会话已不处于 plan mode,允许正常实施工作,并禁止调用仍保持注册的退出工具。此项当前状态声明可避免对话中较早出现的 plan mode 文本干扰之后的默认模式请求。plan 包(package)继续拥有已记录的状态、边界时序、提示词段激活判定、稳定的退出工具 schema 和评审语义。Web 插件不会复制这些机制。
`ui-conversation` 拥有并渲染新增的可叠加控件槽,其位置在 composer 主操作左侧。该槽不提供业务载荷;各入口接收标准会话注入项。替换整个 composer 的功能仍走另一条由选择器路由的 `conversation.composer` 链,因此待处理的问题会替换 InputBar 及其控件,两个功能均无需导入对方。
@@ -32,6 +32,8 @@ Web 宿主可以通过[可选会话 RPC 契约](../architecture/2026-07-24-web-p
业务故障和传输故障都会保持已确认的快照不变,重新启用选择器,并在其旁边渲染一条紧凑且可见的失败信息。组件会防止卸载后完成的异步操作继续更新状态,因此切换会话不会更新已经退出使用的控件。
发送操作会等待最近一次选择器请求,以及此后任何取代该请求的更新选择。在这个仅涵盖受理阶段的区间内,composer 会清空草稿并禁用重复发送;它不会等待模型生成。宿主受理请求即释放受理锁,此后普通的运行状态仍会使「停止」可用。选择或受理失败时不会发送提示词,并且仅当输入框尚未出现替代文本时,才会恢复已提交的草稿。
## 退出评审
`exit_plan_mode` 在两种模式下都会保持注册,以维持请求缓存稳定性。在 plan mode 中,模型通过该工具提交完整的 Markdown plan。plan 服务经由 `ctx.userInteraction` 发起询问,已组合进 Web 的问题插件会展示 plan 详情,并提供「批准」、「继续规划」和自由文本回答渠道。问题详情会复用 assistant 输出所用的 Markdown 基础组件及其不受信任内容策略。设有高度上限的问题卡片会固定显示标题、导航操作和提交操作,而完整 plan 与选项共享同一个内部滚动区域。由于 composer 接管是该等待状态的唯一呈现方式,聊天流程不会渲染通用的待处理问题占位块。
@@ -40,7 +42,7 @@ Web 宿主可以通过[可选会话 RPC 契约](../architecture/2026-07-24-web-p
## 产品组合与证据
fixture(测试前置数据)模式在内存中实现相同的待生效与边界行为,因此浏览器验收测试无需密钥即可覆盖组合后的产品。无密钥浏览器流程依次选择「计划」、通过提示词提交该模式、在生成期间选择「默认」、停止生成且不丢失待生效目标,再通过下一条提示词提交「默认」。文件快照记录每个用户可见状态。另一个使用 mock 提供方的真实 `dsh web` 进程还证明:插件清单会挂载 plan mode,状态 RPC 会报告该功能,并且激活的 Web 策略会随工作区指令一同进入提供方请求。
fixture(测试前置数据)模式在内存中实现相同的待生效与边界行为,因此浏览器验收测试无需密钥即可覆盖组合后的产品。无密钥浏览器流程依次选择「计划」、通过提示词提交该模式、在生成期间选择「默认」、停止生成且不丢失待生效目标,再通过下一条提示词提交「默认」。文件快照记录每个用户可见状态。另一个使用 mock 提供方的真实 `dsh web` 进程还证明:插件清单会挂载 plan mode,状态 RPC 会报告该功能,并且 Web 针对两种确切模式的策略会随工作区指令分别进入对应的提供方请求。
## 考虑过的替代方案
@@ -56,4 +58,4 @@ fixture(测试前置数据)模式在内存中实现相同的待生效与边
## 后果
Web 产品现在会在保留插件边界的同时,公开与现有终端和 ACP(Agent Client Protocol)产品组合相同的 plan 交互模型。选择该功能会增加一个宿主策略/工具所有者和一个浏览器槽入口;移除其 fiber 会同时移除二者。模型工具目录在模式切换期间保持稳定,但激活的系统提示词段会在 plan 边界发生变化,因此请求前缀也会改变。Plan mode 仍是引导机制,而非执行沙箱:需要强制只读规划的部署仍需组合彼此独立的沙箱策略和审批策略。
Web 产品现在会在保留插件边界的同时,公开与现有终端和 ACP(Agent Client Protocol)产品组合相同的 plan 交互模型。选择该功能会增加一个宿主策略/工具所有者和一个浏览器槽入口;移除其 fiber 会同时移除二者。模型工具目录在模式切换期间保持稳定,但激活的 Web 模式段会在 plan 边界发生变化,因此请求前缀也会改变。Plan mode 仍是引导机制,而非执行沙箱:需要强制只读规划的部署仍需组合彼此独立的沙箱策略和审批策略。