From 7ce85283b5af14837fc4447f63684b9c6bb7dc55 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:21:13 +0800 Subject: [PATCH 1/2] Revert "Merge pull request #2608 from deepseek-harness/fix/permission-copy-and-default" This reverts commit d51f4106a2b0669d33e0f5dc1d5dcf21a764d313, reversing changes made to 69ace51625b6ac665b2f2ec1e81005d5b055f152. --- ...blank-permission-default-refresh.i18n.yaml | 6 -- ...-08-17-blank-permission-default-refresh.md | 29 ------ ...-17-blank-permission-default-refresh.zh.md | 29 ------ ...-31-gui-full-access-confirmation.i18n.yaml | 4 +- ...2026-07-31-gui-full-access-confirmation.md | 4 +- ...6-07-31-gui-full-access-confirmation.zh.md | 4 +- ...mission-default-for-new-sessions.i18n.yaml | 4 +- ...-31-permission-default-for-new-sessions.md | 4 +- ...-permission-default-for-new-sessions.zh.md | 4 +- ...-workspace-write-surface-default.i18n.yaml | 4 +- ...6-07-31-workspace-write-surface-default.md | 4 +- ...7-31-workspace-write-surface-default.zh.md | 4 +- apps/web/tests/access-confirmation.e2e.ts | 10 +- apps/web/tests/settings-chrome.e2e.ts | 26 +++--- .../access-confirmation/ui.expected.md | 8 +- .../settings-chrome/dialog.expected.md | 4 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 7 +- docs/config-catalog.zh.md | 8 +- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 3 +- docs/module-graph.zh.md | 3 +- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 11 +-- docs/persistence-catalog.zh.md | 11 +-- docs/subsystems/permission-presets.i18n.yaml | 4 +- docs/subsystems/permission-presets.md | 16 +--- docs/subsystems/permission-presets.zh.md | 16 +--- .../src/client/contract/sessions-port.ts | 12 +-- .../runtime/src/client/sessions/manager.ts | 12 +-- .../runtime/src/client/sessions/service.ts | 7 +- .../runtime/src/client/workspaces/service.ts | 18 +--- .../tests/workspaces-service.client.spec.ts | 29 ++---- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 2 +- packages/client/ui-conversation/README.zh.md | 2 +- .../ui-conversation/src/client/locales.ts | 12 +-- .../src/client/skeleton/PermissionSelect.tsx | 54 ++++------- .../tests/input-bar.client.spec.tsx | 53 ++++------- .../ui-permission-presets/README.i18n.yaml | 4 +- .../client/ui-permission-presets/README.md | 5 +- .../client/ui-permission-presets/README.zh.md | 5 +- .../src/client/PermissionRow.tsx | 9 +- .../ui-permission-presets/src/client/index.ts | 12 +-- .../src/client/locales.ts | 24 ++--- .../src/client/presentation.ts | 33 +------ .../src/client/settings-store.ts | 5 +- .../tests/browser-plugin.client.spec.ts | 14 +-- .../permission-presets-row.client.spec.tsx | 40 ++++---- .../tests/settings-store.client.spec.ts | 2 +- .../extensions/tool-cordis/src/api-catalog.ts | 5 - packages/host/apiproxy/package.json | 4 +- packages/host/apiproxy/src/api-proxy.ts | 20 +--- .../host/apiproxy/src/api/sessions.schema.ts | 5 - packages/host/apiproxy/src/api/sessions.ts | 13 +-- .../apiproxy/tests/api-proxy-cold.spec.ts | 58 +----------- .../tests/api-proxy-workspace.spec.ts | 63 ------------- .../host/apiproxy/tests/rpc-schemas.spec.ts | 10 +- packages/host/apiproxy/tsconfig.json | 3 - .../permission-presets/README.i18n.yaml | 4 +- .../interaction/permission-presets/README.md | 10 +- .../permission-presets/README.zh.md | 10 +- .../permission-presets/src/index.ts | 72 ++++----------- .../tests/permission-presets.spec.ts | 91 +------------------ .../tests/subagent-codex.spec.ts | 1 - pnpm-lock.yaml | 3 - 66 files changed, 218 insertions(+), 751 deletions(-) delete mode 100644 .agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.i18n.yaml delete mode 100644 .agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.md delete mode 100644 .agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.zh.md diff --git a/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.i18n.yaml deleted file mode 100644 index f19a710287..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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 .agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.md -2026-08-17-blank-permission-default-refresh.md: 6e54d030eed4523974c9ca55ff8a621e107704a9 -2026-08-17-blank-permission-default-refresh.zh.md: 171395ade710082029f874da127b8934406ce6ac diff --git a/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.md b/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.md deleted file mode 100644 index 6e54d030ee..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: Refresh blank session permission defaults - -Status: implemented - -English | [中文](2026-08-17-blank-permission-default-refresh.zh.md) - -## Problem - -The Web New Session flow reuses a workspace's blank session instead of minting another hidden placeholder. Permission defaults are pinned into a session at creation time, so changing the General settings permission row after a blank placeholder already existed left that placeholder on the previous preset. The next "new" conversation could therefore reuse a blank session whose permission chip contradicted the newly saved default. - -## Decision - -The Web workspace runtime owns candidate selection: a reusable session must be blank, belong to the selected Workspace, match its canonical cwd, and not be archived. Instead of returning that id directly, `WorkspaceRuntime.connectWorkspace` explicitly adopts it through `session.create` with `reuseWorkspaceBlank: true`. The host rechecks blankness, Workspace membership, cwd, and archive state before notification, and can resume a cold persisted placeholder before notifying optional default owners about the exact eligible session. - -`dsh-permission-presets` records each `permission/preset` origin as `default`, `selection`, or `inferred`. On confirmed reuse, it advances the session to the current `defaultPreset` only when no turn has started, the latest selection is default-origin, and the effective sandbox and approval knobs still match that selection. Explicit picks, inferred or origin-less legacy selections, and independently changed knobs remain pinned. The update goes through the normal preset writer, so durable `permission/preset`, `sandbox/mode`, and `approval/policy` facts remain the source for projections and execution. - -This partially refines the earlier [permission default for new sessions](../feature/2026-07-31-permission-default-for-new-sessions.md) decision: a settings write alone does not mutate an existing session, while the later confirmed reuse of a default-origin Workspace blank may advance it after live or cold adoption. - -## Alternatives considered - -**Disable blank-session reuse after any permission settings change.** Rejected because it would leave extra hidden placeholders and make New Session less deterministic. The existing reuse policy is valuable; only stale permission defaults were wrong. - -**Have the client compare a blank session's permission projection with the Settings row.** Rejected because the workspace runtime would need to understand the permission settings namespace. The client reports only its reuse decision; the permission service owns the default-origin test and update. - -**Scan every live blank session when Settings changes.** Rejected because the live store omits cold persisted placeholders and includes blank sessions that Web cannot reuse, such as archived or non-member sessions. It also cannot distinguish an old default from an explicit selection after restart without a durable origin. - -## Consequences - -A Settings change does not rewrite an existing session. Confirmed New Session reuse may append permission facts to a live or cold default-origin placeholder, which remains blank because blankness is defined by the absence of `turn/start`. Started conversations, ordinary seeded resumes, explicit selections, and sessions outside the Web reuse decision keep their permission. diff --git a/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.zh.md b/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.zh.md deleted file mode 100644 index 171395ade7..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.zh.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: Refresh blank session permission defaults - -Status: implemented - -[English](2026-08-17-blank-permission-default-refresh.md) | 中文 - -## Problem - -Web 新会话流程会复用工作区中的空白会话,而不是不断创建隐藏占位会话。权限默认值在会话创建时被固定到该会话中,因此当某个空白占位会话已经存在后,用户再修改「通用设置」里的权限默认值,这个占位会话仍会保留旧预设。下一次“新”对话复用它时,权限 chip 就会和刚保存的默认设置不一致。 - -## Decision - -Web workspace runtime 负责选择候选会话:可复用会话必须保持空白、属于所选 Workspace、匹配其规范 cwd,并且未归档。`WorkspaceRuntime.connectWorkspace` 不再直接返回该 id,而是通过带 `reuseWorkspaceBlank: true` 的 `session.create` 显式接纳它。host 会在通知前重新检查空白状态、Workspace 成员关系、cwd 与归档状态,并且可以先恢复冷存储中的持久占位会话,再向可选的默认值所有者通知确实符合资格的会话。 - -`dsh-permission-presets` 会把每条 `permission/preset` 的来源记录为 `default`、`selection` 或 `inferred`。复用确认后,只有在会话尚未开始轮次、最近选择来自默认值,并且有效沙箱与审批旋钮仍匹配该选择时,服务才会将它推进到当前 `defaultPreset`。显式选择、由旧旋钮推断或没有来源标记的旧选择,以及独立变更的旋钮都会保持固定。更新仍走常规 preset writer,因此持久的 `permission/preset`、`sandbox/mode` 与 `approval/policy` 事实继续作为投影和执行的来源。 - -这项修复部分细化了较早的[新会话权限默认值](../feature/2026-07-31-permission-default-for-new-sessions.zh.md)决策:单独写入设置不会改变既有会话,而 Web 之后确认复用、且权限来自默认值的 Workspace 空白会话可以在 live 或冷接纳后推进。 - -## Alternatives considered - -**权限设置变化后禁用空白会话复用。** 拒绝,因为这会留下额外的隐藏占位会话,并让新会话行为更不确定。既有复用策略有价值;错误只在于权限默认值过期。 - -**让客户端比较空白会话的权限投影和 Settings 行。** 拒绝,因为 workspace runtime 需要理解 permission settings namespace。客户端只报告自己的复用决定;权限服务拥有默认来源检查和更新。 - -**Settings 变化时扫描所有 live 空白会话。** 拒绝,因为 live store 会漏掉冷存储中的持久占位会话,同时包含 Web 无法复用的空白会话,例如已归档或不属于 Workspace 的会话;重启后若没有持久来源,也无法区分旧默认值与显式选择。 - -## Consequences - -Settings 变更不会改写既有会话。确认的新会话复用可能向 live 或冷存储中、权限来自默认值的占位会话追加权限事实;该会话仍保持 blank,因为 blankness 由是否缺少 `turn/start` 定义。已经开始的对话、普通 seed 恢复、显式选择,以及不在 Web 复用决定中的会话都会保留原权限。 diff --git a/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.i18n.yaml index ac34aaa150..b82a2e1fa9 100644 --- a/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.md -2026-07-31-gui-full-access-confirmation.md: 5822ae26d36329b865a05c59dd672daa55fb6224 -2026-07-31-gui-full-access-confirmation.zh.md: 612300dccaf8aa5d75b223767350f769037ea0f1 +2026-07-31-gui-full-access-confirmation.md: f63502cd3e2306f36b136e6ed8543641449c3d83 +2026-07-31-gui-full-access-confirmation.zh.md: f4b3686d1e1ad9e51a08e513a7dd5930d311582d diff --git a/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.md b/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.md index 5822ae26d3..f63502cd3e 100644 --- a/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.md +++ b/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.md @@ -10,13 +10,13 @@ Switching the web client to `danger-full-access` was a single click on a permiss ## Decision -**Every permission picker gates `danger-full-access` behind the shared in-page `RiskConfirmation` dialog whose enabling action stays disabled until an explicit acknowledgement checkbox is checked; the preset renders under the locale product label for full access; every dismissal path submits nothing.** +**Every permission picker gates `danger-full-access` behind the shared in-page `RiskConfirmation` dialog whose enabling action stays disabled until an explicit acknowledgement checkbox is checked; the preset renders under the product label `Full access`; every dismissal path submits nothing.** - `RiskConfirmation` (ui-primitives) is a controlled Modal composition: title, description, acknowledgement checkbox, cancel, and a confirm button disabled until `acknowledged`. It stays an in-page dialog — the Modal portals to this document's body and never opens a native or separate browser window that could land on another display. `Modal` gains a `contentClassName` seat so the warning body scrolls inside constrained mobile/landscape viewports while the action row stays fixed. - The composer chip (`PermissionSelect`, ui-conversation) intercepts a Full-access pick before the `/permission` submit: `confirmation`/`acknowledged` component state opens the dialog, confirm submits `/permission danger-full-access` through the same injected `command` path as every other pick, and cancel/Escape/close/mask leave the current preset untouched with the checkbox reset. The confirmation revokes itself when the session locks (`locked`/value-absent effect) and resets across task switches (`key={sessionId}` remount). Copy rides the standard `conversation` locale seat as `access.confirm.*` keys. - The `/permission` popup (ui-permission over the ui-commands shell) gates through data, not a second dialog implementation: `SelectOption` grows an optional `confirmation` payload, the popup controller owns the `confirming`/`acknowledged` state transitions, and `PopupSelectView` swaps the picker card for the same `RiskConfirmation` while a gated option is pending. - The General-settings Permission row uses the same controlled `RiskConfirmation` before persisting Full access as the default for later sessions. Its warning names that future-session lifetime; cancel, Escape, close, and mask dismissal leave the stored default untouched. -- The full-access product label overrides the kebab-to-title display transform when the host keeps the canonical built-in name; an explicit host label wins. Command and Settings writes keep the machine name on the wire, and each warning body remains locale-aware in Chinese and English. The same conditional localization applies to the safer shipped presets ([blank permission default refresh](../bug-fix/2026-08-17-blank-permission-default-refresh.md)). +- `Full access` intentionally overrides the kebab-to-title display transform in every picker; command and Settings writes keep the machine name on the wire, and each warning body remains locale-aware in Chinese and English. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.zh.md b/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.zh.md index 612300dcca..f4b3686d1e 100644 --- a/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-gui-full-access-confirmation.zh.md @@ -10,13 +10,13 @@ Status: implemented ## 决策 -**每个权限选择器都把 `danger-full-access` 关进共享的页面内 `RiskConfirmation` 对话框:启用按钮在用户勾选明确的风险确认复选框前保持禁用;预设以完全权限的本地化产品标签展示;所有取消路径都不作任何提交。** +**每个权限选择器都把 `danger-full-access` 关进共享的页面内 `RiskConfirmation` 对话框:启用按钮在用户勾选明确的风险确认复选框前保持禁用;预设以产品标签 `Full access` 展示;所有取消路径都不作任何提交。** - `RiskConfirmation`(ui-primitives)是受控的 Modal 组合:标题、说明、确认复选框、取消,以及 `acknowledged` 勾选前禁用的确认按钮。它始终是页面内对话框——Modal portal 到本文档 body,绝不打开可能落在另一块显示器上的原生或独立浏览器窗口。`Modal` 新增 `contentClassName` slot,令警示正文在受限的移动端/横屏视口内滚动,动作行保持固定。 - composer chip(ui-conversation 的 `PermissionSelect`)在 `/permission` 提交前拦截 Full-access 选择:`confirmation`/`acknowledged` 组件状态打开对话框,确认后经与其他选择完全相同的注入 `command` 通道提交 `/permission danger-full-access`;取消、Escape、关闭与遮罩点击均保持当前预设不变并重置复选框。会话锁定时确认自行撤销(`locked`/值缺席 effect),切换任务时随 `key={sessionId}` 重挂载而重置。文案经标准 `conversation` locale slot 以 `access.confirm.*` 键供给。 - `/permission` popup(ui-permission 构建于 ui-commands 外壳之上)以数据而非第二套对话框实现完成把关:`SelectOption` 新增可选的 `confirmation` 载荷,popup 控制器拥有 `confirming`/`acknowledged` 状态迁移,`PopupSelectView` 在门控选项未决期间把选择卡换成同一个 `RiskConfirmation`。 - 「通用」设置中的「权限」行在把 Full access 持久化为后续会话的默认值前,也使用同一个受控 `RiskConfirmation`。警示会明确说明该设置只影响后续会话;取消、Escape、关闭与点击遮罩均不会改动已存默认值。 -- 当 host 保留内置预设的规范名称时,完全权限产品标签会覆盖 kebab 转 Title Case 的显示变换;显式 host 标签优先。命令与 Settings 写入在 wire 上保留机器名,每份警示正文都保持中英文 locale 感知。同一个条件式本地化规则也适用于更安全的随附预设([空白权限默认值刷新](../bug-fix/2026-08-17-blank-permission-default-refresh.zh.md))。 +- `Full access` 在每个选择器中都有意覆盖 kebab 转 Title Case 的显示变换;命令与 Settings 写入在 wire 上保留机器名,每份警示正文都保持中英文 locale 感知。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml index 4b56c3fa98..aaa3cc3cc8 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md -2026-07-31-permission-default-for-new-sessions.md: 818c105ce880f7b67bfd28030502bf6775e1a039 -2026-07-31-permission-default-for-new-sessions.zh.md: 980b007a4d246da36446e79cd089a7c513f22c85 +2026-07-31-permission-default-for-new-sessions.md: ebf7fe39712d64c18e12b9b26d86201a61ad6cfd +2026-07-31-permission-default-for-new-sessions.zh.md: c0f450c8efca8647e3058fb305724cf0a554cc8d diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md index 818c105ce8..ebf7fe3971 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md @@ -12,7 +12,7 @@ The Web General-settings page displayed Permission as a disabled skeleton even t `dsh-permission-presets` owns a `permission` Settings namespace with one `defaultPreset` field. Its base value is `Config.defaultPreset`, or the preset matching the composed sandbox and approval defaults when the config omits it. The schema derives its enum from the configured preset table, so Settings validates stored values and the Web client discovers the deployment's actual choices without duplicating them. -The service reads the current Settings value synchronously at `session/created`. A genuinely fresh session receives three explicit events: `permission/preset`, `sandbox/mode`, and `approval/policy`; the preset fact records that the selection came from the default. Those facts pin the permission selected at creation, so a later Settings change does not change started conversations. When Web later confirms a Workspace blank for New Session reuse, the host explicitly adopts that live or cold session and advances it only if its latest selection remains default-origin and effective; this refinement is recorded in [blank permission default refresh](../bug-fix/2026-08-17-blank-permission-default-refresh.md). An ordinary seeded or partially initialized session preserves its effective knobs and receives only missing facts; it never adopts the latest user default merely by resuming. `Session` marks even an explicitly empty constructor seed with `session/end-seed`, so an empty persisted log cannot be mistaken for a fresh session. +The service reads the current Settings value synchronously at `session/created`. A genuinely fresh session receives three explicit events: `permission/preset`, `sandbox/mode`, and `approval/policy`. Those facts pin the permission selected at creation, so a later Settings change affects only later sessions. A seeded or partially initialized session preserves its effective knobs and receives only missing facts; it never adopts the latest user default while resuming. `Session` marks even an explicitly empty constructor seed with `session/end-seed`, so an empty persisted log cannot be mistaken for a fresh session. The existing `/permission` command and `permissions` projection remain the current-session path. The browser plugin now contributes the Permission row to `settings.general.item`, reads the dynamic enum from the redacted Settings descriptor, and writes only `defaultPreset` through a revision-checked `settings.mutate`. The row injects its observable through the slot `hooks` compartment instead of binding a renderer-specific hook, and the Permission service sweeps already-live sessions when it mounts so HMR cannot leave an unpinned session. The ownerless General-settings package contributes no placeholder rows. @@ -20,7 +20,7 @@ ApiProxy explicitly adds `permission` to its Web settings allowlist beside the c ## Consequences -Changing Permission in Settings updates `settings.yaml` and the selector immediately, but does not alter an existing session by itself. Every later session is reconstructable from its three pinned permission facts, including after the user changes the default again or the process restarts. A Workspace blank may receive a new pinned triplet only when Web confirms it as the reuse target and its effective selection is still default-origin. Deployments whose composed sandbox and approval defaults match no preset must configure `defaultPreset` explicitly. +Changing Permission in Settings updates `settings.yaml` and the selector immediately, but does not alter the open session. Every later session is reconstructable from its three pinned permission facts, including after the user changes the default again or the process restarts. Deployments whose composed sandbox and approval defaults match no preset must configure `defaultPreset` explicitly. The assembled Web snapshot contains a functional Permission selector. Its keyless browser scenario writes `read-only`, verifies an existing `workspace-write` session is unchanged, and verifies a subsequently created session starts with the read-only event triplet. diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md index 980b007a4d..c0f450c8ef 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md @@ -12,7 +12,7 @@ Web「通用」设置页将「权限」显示为禁用的骨架控件,尽管 ` `dsh-permission-presets` 拥有一个 `permission` Settings namespace,其中只有 `defaultPreset` 字段。它的基础值是 `Config.defaultPreset`;省略该配置时,则使用与组合后的沙箱和审批默认值匹配的 preset。schema 的 enum 从已配置的 preset 表派生,因此 Settings 既能校验已存储的值,Web 客户端也能发现部署中的实际选项,而无需重复定义。 -服务会在 `session/created` 时同步读取当前 Settings 值。真正的新会话会收到三个显式事件:`permission/preset`、`sandbox/mode` 和 `approval/policy`;preset 事实会记录该选择来自默认值。这些事实将创建时选中的权限固定下来,因此后续 Settings 变更不会改变已经开始的对话。Web 之后把某个 Workspace 空白会话确认为新会话复用目标时,host 会显式接纳这个 live 或冷会话,并且只在其最近选择仍来自默认值且继续有效时推进;这项细化记录在[空白权限默认值刷新](../bug-fix/2026-08-17-blank-permission-default-refresh.zh.md)。普通的带 seed 或只完成部分初始化的会话会保留其有效调节项,只补齐缺失的事实;仅仅恢复会话时绝不会采用最新的用户默认值。`Session` 甚至会用 `session/end-seed` 标记显式为空的构造器 seed,因此不能把空的持久化日志误认为新会话。 +服务会在 `session/created` 时同步读取当前 Settings 值。真正的新会话会收到三个显式事件:`permission/preset`、`sandbox/mode` 和 `approval/policy`。这些事实将创建时选中的权限固定下来,因此后续 Settings 变更只影响之后的会话。带 seed 或只完成部分初始化的会话会保留其有效调节项,只补齐缺失的事实;恢复时绝不会采用最新的用户默认值。`Session` 甚至会用 `session/end-seed` 标记显式为空的构造器 seed,因此不能把空的持久化日志误认为新会话。 现有 `/permission` 命令和 `permissions` 投影仍是当前会话的操作路径。浏览器插件现在向 `settings.general.item` 贡献「权限」行,从脱敏后的 Settings 描述符读取动态 enum,并只通过经过 revision 校验的 `settings.mutate` 写入 `defaultPreset`。该行通过 slot 的 `hooks` 格注入 observable,而不是绑定渲染器专用钩子;权限服务挂载时会遍历并固定所有已存活会话,因此 HMR(热模块替换)不会遗留未固定的会话。无归属的「通用」设置包不贡献任何占位行。 @@ -20,7 +20,7 @@ ApiProxy 在可配置提供方 namespace 之外,将 `permission` 显式加入 ## 后果 -在 Settings 中更改「权限」会立即更新 `settings.yaml` 和选择器,但单凭该操作不会改变既有会话。之后的每个会话都可以从三个已固定的权限事实中重建,即使用户再次更改默认值或进程重启也不受影响。只有当 Web 把某个 Workspace 空白会话确认为复用目标,并且其有效选择仍来自默认值时,该会话才可能收到新的固定三元组。如果部署中组合后的沙箱和审批默认值与任何 preset 都不匹配,则必须显式配置 `defaultPreset`。 +在 Settings 中更改「权限」会立即更新 `settings.yaml` 和选择器,但不会改变已打开的会话。之后的每个会话都可以从三个已固定的权限事实中重建,即使用户再次更改默认值或进程重启也不受影响。如果部署中组合后的沙箱和审批默认值与任何 preset 都不匹配,则必须显式配置 `defaultPreset`。 组装后的 Web 快照包含功能完整的「权限」选择器。其无密钥浏览器场景会写入 `read-only`,验证现有的 `workspace-write` 会话保持不变,并验证随后创建的会话以 read-only 事件三元组启动。 diff --git a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.i18n.yaml index ec1a03cf83..1ba8316340 100644 --- a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md -2026-07-31-workspace-write-surface-default.md: e096028e07f1e7905fef568c435ad7f3d1d19138 -2026-07-31-workspace-write-surface-default.zh.md: d5b1a15ccb0acc1ebfe1df120af57e2c46d5aeb6 +2026-07-31-workspace-write-surface-default.md: 2ce4d7b45ad7058768f412302eb86ac332cabfe9 +2026-07-31-workspace-write-surface-default.zh.md: 40b97812eb17b5c11e422ff23cd718576c9c4364 diff --git a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md index e096028e07..2ce4d7b45a 100644 --- a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md +++ b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md @@ -12,7 +12,7 @@ The shipped terminal and browser surfaces exposed the same coding tools under di [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml) owns one sandbox and permission stack for every shipped TUI, Web, and browser-backed headless session: `dsh-sandbox-local`, `dsh-sandbox-policy`, `dsh-bash-sandbox`, `dsh-fs-sandbox`, `dsh-user-approval`, and `dsh-permission-presets`. The composition fallback is the `workspace-write` preset, which bundles `workspace-write` file effects with the `ask` approval policy. `DSH_PERMISSION_MODE` remains an explicit process override; a stored `permission.defaultPreset` remains the user preference for later sessions and outranks the fallback through the Settings seam. -A genuinely fresh session pins `permission/preset: workspace-write`, `sandbox/mode: workspace-write`, and `approval/policy: ask` before execution. Changing the General-settings default affects fresh sessions, while started and ordinary resumed sessions retain their logged permission. A later Web New Session action may advance a default-origin Workspace blank after confirming and adopting that exact reuse target, as recorded in [blank permission default refresh](../bug-fix/2026-08-17-blank-permission-default-refresh.md). The browser keeps its Access picker, answerable approval cards, and risk confirmation for Full access. The TUI gains the existing `/permission` command because the shared Permission service activates its command child there. +A genuinely fresh session pins `permission/preset: workspace-write`, `sandbox/mode: workspace-write`, and `approval/policy: ask` before execution. Existing and resumed sessions retain their logged permission, and changing the General-settings default affects only sessions created afterward. The browser keeps its Access picker, answerable approval cards, and risk confirmation for Full access. The TUI gains the existing `/permission` command because the shared Permission service activates its command child there. The mode governs file effects only. Sandboxed bash and filesystem mutations admit the session workspace and platform temporary roots; reads, network access, and process visibility remain outside this policy. If no platform runner can enforce a confined bash call, execution fails closed instead of falling through to an unrestricted command. @@ -30,6 +30,6 @@ The keyless shipped-TUI pseudo-terminal smoke boots the real Loader tree, reads ## Consequences -Fresh sessions can modify the active workspace and temporary roots without extra prompts, while an attempted mutation elsewhere is denied before it reaches the target. Full access remains available by explicit selection, and browser selection retains its acknowledgement dialog. A Settings write does not rewrite logged session permissions; confirmed reuse may append a new default-origin permission triplet to the selected blank Workspace session. +Fresh sessions can modify the active workspace and temporary roots without extra prompts, while an attempted mutation elsewhere is denied before it reaches the target. Full access remains available by explicit selection, and browser selection retains its acknowledgement dialog. Stored user defaults and logged session permissions are not rewritten. The browser-backed headless entry inherits the Web composition and therefore the same default. The TUI's missing approval answerer is a deliberate limitation of this change: automatic wider retries fail closed there instead of displaying a permission question. diff --git a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.zh.md b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.zh.md index d5b1a15ccb..40b97812eb 100644 --- a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.zh.md @@ -12,7 +12,7 @@ Status: implemented [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml) 为所有已交付的 TUI、Web 以及由浏览器支撑的无头会话统一持有一套沙箱与权限栈:`dsh-sandbox-local`、`dsh-sandbox-policy`、`dsh-bash-sandbox`、`dsh-fs-sandbox`、`dsh-user-approval` 和 `dsh-permission-presets`。组合回退值为 `workspace-write` preset,其中包含 `workspace-write` 文件效果模式与 `ask` 审批策略。`DSH_PERMISSION_MODE` 仍是显式的进程级覆盖;已存储的 `permission.defaultPreset` 仍是面向后续会话的用户偏好,并通过 Settings seam 优先于该回退值。 -真正的新会话会在执行前固定 `permission/preset: workspace-write`、`sandbox/mode: workspace-write` 和 `approval/policy: ask`。更改「通用」设置中的默认值会影响新建会话,而已经开始的会话和普通恢复会话保留日志中记录的权限。Web 之后执行新会话操作时,可以在确认并接纳确切复用目标后推进权限来自默认值的 Workspace 空白会话;该规则记录在[空白权限默认值刷新](../bug-fix/2026-08-17-blank-permission-default-refresh.zh.md)。浏览器保留 Access 选择器、可应答的审批卡片,以及选择 Full access 时的风险确认。共享 Permission 服务在 TUI 中激活其命令子件,因此 TUI 会获得现有的 `/permission` 命令。 +真正的新会话会在执行前固定 `permission/preset: workspace-write`、`sandbox/mode: workspace-write` 和 `approval/policy: ask`。现有会话和恢复的会话保留日志中记录的权限,更改「通用」设置中的默认值只影响之后创建的会话。浏览器保留 Access 选择器、可应答的审批卡片,以及选择 Full access 时的风险确认。共享 Permission 服务在 TUI 中激活其命令子件,因此 TUI 会获得现有的 `/permission` 命令。 该模式只管辖文件效果。受沙箱约束的 bash 与文件系统修改只允许写入会话工作区和平台临时根目录;读取、网络访问与进程可见性仍不受该策略约束。若没有平台 runner 能强制执行受限的 bash 调用,执行会以拒绝告终,不会退回不受限命令。 @@ -30,6 +30,6 @@ Status: implemented ## 后果 -全新的会话无需额外提示即可修改当前工作区与临时根目录,尝试修改其他位置则会在触及目标前被拒绝。Full access 仍可通过显式选择获得,浏览器选择时也仍会显示确认对话框。Settings 写入不会改写会话日志中的权限;复用确认可以向选中的空白 Workspace 会话追加新的默认来源权限三元组。 +全新的会话无需额外提示即可修改当前工作区与临时根目录,尝试修改其他位置则会在触及目标前被拒绝。Full access 仍可通过显式选择获得,浏览器选择时也仍会显示确认对话框。系统不会重写已存储的用户默认值和会话日志中记录的权限。 由浏览器支撑的无头入口继承 Web 组合,因此默认值相同。TUI 缺少审批应答者是本次变更的明确限制:自动请求更宽权限的重试会在那里以拒绝告终,而不会显示权限询问。 diff --git a/apps/web/tests/access-confirmation.e2e.ts b/apps/web/tests/access-confirmation.e2e.ts index 414aacfb60..a173f8399e 100644 --- a/apps/web/tests/access-confirmation.e2e.ts +++ b/apps/web/tests/access-confirmation.e2e.ts @@ -50,13 +50,13 @@ describe('web e2e: Full access confirmation', () => { const access = page.locator('button[aria-label^="访问模式"]').first() await access.waitFor({ timeout: 10_000 }) - expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:可写入工作区') + expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:Workspace Write') await access.click() - await page.getByRole('menuitem', { name: '完全权限' }).click() - const dialog = page.getByRole('dialog', { name: '确认启用完全权限?' }) + await page.getByRole('menuitem', { name: 'Full access' }).click() + const dialog = page.getByRole('dialog', { name: '确认启用 Full access?' }) await dialog.waitFor({ timeout: 10_000 }) - const enable = dialog.getByRole('button', { name: '启用完全权限' }) + const enable = dialog.getByRole('button', { name: '启用 Full access' }) expect(await enable.isDisabled()).toBe(true) // The modal is in this page's body (not a native/new window) and escapes @@ -69,7 +69,7 @@ describe('web e2e: Full access confirmation', () => { expect(await enable.isEnabled()).toBe(true) await enable.click() await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 }) - .toBe('访问模式,当前:完全权限') + .toBe('访问模式,当前:Full access') expect(await dialog.count()).toBe(0) expect(tripwire.pageErrors).toEqual([]) }, 60_000) diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index cf2e4b9d10..61f5af88c5 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -62,7 +62,7 @@ describe('web e2e: settings modal and General preferences', () => { expect(await trigger.getAttribute('aria-expanded')).toBe('true') // General is active by default; Permission, Language and Appearance are functional. expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBe('true') - await dialog.getByRole('button', { name: '可写入工作区' }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: 'Workspace Write' }).waitFor({ timeout: 10_000 }) await expect.poll(() => dialog.getByText('语言', { exact: true }).count(), { timeout: 5_000 }).toBe(1) await expect.poll(() => dialog.getByText('外观', { exact: true }).count(), { timeout: 5_000 }).toBe(1) const openDocument = dialog.getByRole('button', { name: '打开配置文件' }) @@ -135,44 +135,44 @@ describe('web e2e: settings modal and General preferences', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-permission')) const existing = scaffold.ctx.sessions.create(SessionId('settings-permission-before')) expect(existing.events.find(event => event.type === 'permission/preset')?.data) - .toEqual({ preset: 'workspace-write', origin: 'default' }) + .toEqual({ preset: 'workspace-write' }) await page.getByRole('button', { name: '设置', exact: true }).click() const dialog = page.getByRole('dialog', { name: '设置' }) await dialog.waitFor({ timeout: 10_000 }) - const selector = dialog.getByRole('button', { name: '可写入工作区' }) + const selector = dialog.getByRole('button', { name: 'Workspace Write' }) await selector.waitFor({ timeout: 10_000 }) await expect.poll(() => selector.isEnabled(), { timeout: 5_000 }).toBe(true) await selector.click() - await page.getByRole('menuitem', { name: '仅可查看' }).click() - await dialog.getByRole('button', { name: '仅可查看' }).waitFor({ timeout: 10_000 }) + await page.getByRole('menuitem', { name: 'Read Only' }).click() + await dialog.getByRole('button', { name: 'Read Only' }).waitFor({ timeout: 10_000 }) const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') expect(document).toContain('permission:') expect(document).toContain('defaultPreset: read-only') expect(existing.events.find(event => event.type === 'permission/preset')?.data) - .toEqual({ preset: 'workspace-write', origin: 'default' }) + .toEqual({ preset: 'workspace-write' }) const created = scaffold.ctx.sessions.create(SessionId('settings-permission-after')) expect(created.events.map(event => [event.type, event.data])).toEqual([ - ['permission/preset', { preset: 'read-only', origin: 'default' }], + ['permission/preset', { preset: 'read-only' }], ['sandbox/mode', { mode: 'read-only' }], ['approval/policy', { policy: 'ask' }], ]) - await dialog.getByRole('button', { name: '仅可查看' }).click() - await page.getByRole('menuitem', { name: '完全权限' }).click() - const confirmation = page.getByRole('dialog', { name: '确认启用完全权限?' }) - const enable = confirmation.getByRole('button', { name: '启用完全权限' }) + await dialog.getByRole('button', { name: 'Read Only' }).click() + await page.getByRole('menuitem', { name: 'Full access' }).click() + const confirmation = page.getByRole('dialog', { name: '确认启用 Full access?' }) + const enable = confirmation.getByRole('button', { name: '启用 Full access' }) expect(await enable.isDisabled()).toBe(true) await confirmation.getByRole('checkbox').click() await enable.click() - await dialog.getByRole('button', { name: '完全权限' }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: 'Full access' }).waitFor({ timeout: 10_000 }) const confirmedDocument = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') expect(confirmedDocument).toContain('defaultPreset: danger-full-access') const confirmed = scaffold.ctx.sessions.create(SessionId('settings-permission-confirmed')) expect(confirmed.events.map(event => [event.type, event.data])).toEqual([ - ['permission/preset', { preset: 'danger-full-access', origin: 'default' }], + ['permission/preset', { preset: 'danger-full-access' }], ['sandbox/mode', { mode: 'danger-full-access' }], ['approval/policy', { policy: 'never' }], ]) diff --git a/apps/web/tests/snapshots/access-confirmation/ui.expected.md b/apps/web/tests/snapshots/access-confirmation/ui.expected.md index c083b3aea0..1287e6e565 100644 --- a/apps/web/tests/snapshots/access-confirmation/ui.expected.md +++ b/apps/web/tests/snapshots/access-confirmation/ui.expected.md @@ -1,10 +1,10 @@ -- dialog "确认启用完全权限?": - - heading "确认启用完全权限?" [level=2] +- dialog "确认启用 Full access?": + - heading "确认启用 Full access?" [level=2] - button "Close": - img - img - - paragraph: 启用完全权限后,智能体将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。 + - paragraph: 启用 Full access 后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。 - checkbox "我已了解风险,并愿意继续" - text: 我已了解风险,并愿意继续 - button "取消" - - button "启用完全权限" [disabled] + - button "启用 Full access" [disabled] diff --git a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md index 00dcd44f53..89cff5df3f 100644 --- a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md +++ b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md @@ -22,8 +22,8 @@ - text: 标准模式 - img - text: 权限 选择新会话的默认权限模式 - - button "可写入工作区": - - text: 可写入工作区 + - button "Workspace Write": + - text: Workspace Write - img - text: 语言 - button "中文": diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index c5450730eb..cc74562219 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -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 docs/config-catalog.md -config-catalog.md: fa0e4caa36a3754356876b13507530de82ceb37b -config-catalog.zh.md: 8125a8a80de3f82f6135292f0b834fde2867a840 +config-catalog.md: da7eae6642bf9516755cf4f68268b40b26ac2ee1 +config-catalog.zh.md: bd1903f878aad3071e74d10df8c0782dbd8f3d57 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index fa0e4caa36..da7eae6642 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1437,9 +1437,8 @@ export interface Config { */ presets?: Record /** - * Default for fresh sessions and eligible confirmed blank reuse. When - * omitted, the preset matching the composed sandbox and approval defaults - * is used. + * Default for new sessions. When omitted, the preset matching the composed + * sandbox and approval defaults is used. */ defaultPreset?: string } @@ -1459,7 +1458,7 @@ export interface PresetSpec { Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md) -Source: [`packages/interaction/permission-presets/src/index.ts:168`](../packages/interaction/permission-presets/src/index.ts) +Source: [`packages/interaction/permission-presets/src/index.ts:156`](../packages/interaction/permission-presets/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 8125a8a80d..bd1903f878 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1439,9 +1439,8 @@ export interface Config { */ presets?: Record /** - * Default for fresh sessions and eligible confirmed blank reuse. When - * omitted, the preset matching the composed sandbox and approval defaults - * is used. + * Default for new sessions. When omitted, the preset matching the composed + * sandbox and approval defaults is used. */ defaultPreset?: string } @@ -1461,8 +1460,7 @@ export interface PresetSpec { 依赖:[`ApprovalPolicy`](subsystems/approval.zh.md) · [`SandboxMode`](subsystems/sandbox.zh.md) -来源:[`packages/interaction/permission-presets/src/index.ts:168`](../packages/interaction/permission-presets/src/index.ts) - +来源:[`packages/interaction/permission-presets/src/index.ts:156`](../packages/interaction/permission-presets/src/index.ts) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 4dbba75888..b6b3916d99 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -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 docs/module-graph.md -module-graph.md: f17c65854dbf349adba5ff99288676a4f7eb7402 -module-graph.zh.md: 31608370c550ae7e32e1395e9f6833abf3d096c8 +module-graph.md: b3d061b0b07ec830f88c9fa56fd6921b343d35cc +module-graph.zh.md: b4a748dbe0084afdd554e8295e5ba504e0cf5cea diff --git a/docs/module-graph.md b/docs/module-graph.md index f17c65854d..b3d061b0b0 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -1055,7 +1055,6 @@ flowchart TD pkg_host_apiproxy --> pkg_agent_presets pkg_host_apiproxy --> pkg_cordis_host_runner pkg_host_apiproxy --> pkg_invariants - pkg_host_apiproxy --> pkg_permission_presets pkg_sdk_protocol --> pkg_invariants pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session @@ -1630,7 +1629,7 @@ flowchart TD | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) | | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) | +| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 31608370c5..b4a748dbe0 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -1057,7 +1057,6 @@ flowchart TD pkg_host_apiproxy --> pkg_agent_presets pkg_host_apiproxy --> pkg_cordis_host_runner pkg_host_apiproxy --> pkg_invariants - pkg_host_apiproxy --> pkg_permission_presets pkg_sdk_protocol --> pkg_invariants pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session @@ -1632,7 +1631,7 @@ flowchart TD | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) | | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) | +| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index d2523f7c33..b48d644649 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -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 docs/persistence-catalog.md -persistence-catalog.md: f8c10821e4b6daa8f10b0cff63f838ecb381125c -persistence-catalog.zh.md: 4cefec2eba0a3a3fa7f4c587236fa78e476ad9d7 +persistence-catalog.md: aaedabf93db497dc3e760ea5609497151569a3a4 +persistence-catalog.zh.md: c2810adb5866dafeea4b092794f241c229cd3c9e diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index f8c10821e4..aaedabf93d 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -506,18 +506,15 @@ Source: [`packages/llm/llm-retry/src/types.ts:11`](../packages/llm/llm-retry/src ```ts persistence-catalog /** - * Records the selected preset and whether it came from the session - * default, an explicit selection, or legacy-knob inference. The knob + * Records the selected preset as durable, log-only user intent. The knob * events follow in the same turn and control execution; this event stays * out of the model transcript and lets {@link effectivePermissionPreset} - * preserve a selection when bundles match. `origin` is optional so logs - * written before origin tracking remain readable but are never mistaken - * for refreshable defaults. + * preserve a selection when bundles match. */ -'permission/preset': { preset: string; origin?: 'default' | 'selection' | 'inferred' } +'permission/preset': { preset: string } ``` -Source: [`packages/interaction/permission-presets/src/index.ts:53`](../packages/interaction/permission-presets/src/index.ts) +Source: [`packages/interaction/permission-presets/src/index.ts:50`](../packages/interaction/permission-presets/src/index.ts) ### `plan/*` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 4cefec2eba..c2810adb58 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -508,18 +508,15 @@ export type SessionEvent = { ```ts persistence-catalog /** - * Records the selected preset and whether it came from the session - * default, an explicit selection, or legacy-knob inference. The knob + * Records the selected preset as durable, log-only user intent. The knob * events follow in the same turn and control execution; this event stays * out of the model transcript and lets {@link effectivePermissionPreset} - * preserve a selection when bundles match. `origin` is optional so logs - * written before origin tracking remain readable but are never mistaken - * for refreshable defaults. + * preserve a selection when bundles match. */ -'permission/preset': { preset: string; origin?: 'default' | 'selection' | 'inferred' } +'permission/preset': { preset: string } ``` -来源:[`packages/interaction/permission-presets/src/index.ts:53`](../packages/interaction/permission-presets/src/index.ts) +来源:[`packages/interaction/permission-presets/src/index.ts:50`](../packages/interaction/permission-presets/src/index.ts) ### `plan/*` diff --git a/docs/subsystems/permission-presets.i18n.yaml b/docs/subsystems/permission-presets.i18n.yaml index 31fbe4ceec..4c8afb10ac 100644 --- a/docs/subsystems/permission-presets.i18n.yaml +++ b/docs/subsystems/permission-presets.i18n.yaml @@ -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 docs/subsystems/permission-presets.md -permission-presets.md: fd32572cc6f17c30ea5db8f0fdacdb14cec286ab -permission-presets.zh.md: 0a74303766aeebc0aa55bbdd3098f0bb764e72c2 +permission-presets.md: 4afa5063f2ab107415fd6429cf0cee6a4ecdfe4f +permission-presets.zh.md: 3ec3fde572da32c3276cfcea2db2b256f55c845c diff --git a/docs/subsystems/permission-presets.md b/docs/subsystems/permission-presets.md index fd32572cc6..4afa5063f2 100644 --- a/docs/subsystems/permission-presets.md +++ b/docs/subsystems/permission-presets.md @@ -34,9 +34,8 @@ interface Config { */ presets?: Record /** - * Default for fresh sessions and eligible confirmed blank reuse. When - * omitted, the preset matching the composed sandbox and approval defaults - * is used. + * Default for new sessions. When omitted, the preset matching the composed + * sandbox and approval defaults is used. */ defaultPreset?: string } @@ -92,17 +91,6 @@ Owns the deployment's permission presets and their write path. Requires a confin */ current(events: readonly SessionEvent[]): string -/** - * Advance one blank session after the host has confirmed it as the exact - * Web New Session reuse target. Only a still-effective - * default-origin selection advances; a started session, an explicit pick, - * legacy origin-less data, or independently changed knobs remain pinned. - * This is the permission-side half of the Web candidate selection and the - * host's blankness, membership, cwd, and archive verification. - * @param session - the live session selected for Workspace blank reuse. - */ -refreshDefaultForReuse(session: Session): void - /** * Build the whole select value for one folded knob state: every table * option in declaration order, `custom` appended exactly while derived. diff --git a/docs/subsystems/permission-presets.zh.md b/docs/subsystems/permission-presets.zh.md index 0a74303766..3ec3fde572 100644 --- a/docs/subsystems/permission-presets.zh.md +++ b/docs/subsystems/permission-presets.zh.md @@ -34,9 +34,8 @@ interface Config { */ presets?: Record /** - * Default for fresh sessions and eligible confirmed blank reuse. When - * omitted, the preset matching the composed sandbox and approval defaults - * is used. + * Default for new sessions. When omitted, the preset matching the composed + * sandbox and approval defaults is used. */ defaultPreset?: string } @@ -92,17 +91,6 @@ Owns the deployment's permission presets and their write path. Requires a confin */ current(events: readonly SessionEvent[]): string -/** - * Advance one blank session after the host has confirmed it as the exact - * Web New Session reuse target. Only a still-effective - * default-origin selection advances; a started session, an explicit pick, - * legacy origin-less data, or independently changed knobs remain pinned. - * This is the permission-side half of the Web candidate selection and the - * host's blankness, membership, cwd, and archive verification. - * @param session - the live session selected for Workspace blank reuse. - */ -refreshDefaultForReuse(session: Session): void - /** * Build the whole select value for one folded knob state: every table * option in declaration order, `custom` appended exactly while derived. diff --git a/packages/client/runtime/src/client/contract/sessions-port.ts b/packages/client/runtime/src/client/contract/sessions-port.ts index 8b9bf49229..551c3a16ee 100644 --- a/packages/client/runtime/src/client/contract/sessions-port.ts +++ b/packages/client/runtime/src/client/contract/sessions-port.ts @@ -32,15 +32,11 @@ export interface SessionsPort { /** Observable list snapshot (read face only; writes stay inside the sessions domain). */ readonly list: ObservableSnapshot /** - * Create or explicitly adopt a session on the host. - * @param opts - target workspace and optional confirmed blank-reuse id. - * @returns the created or adopted session id. + * Create a session on the host. + * @param opts - target workspace. + * @returns the new session id. */ - create(opts: { - workspaceId: WorkspaceId - sessionId?: SessionId - reuseWorkspaceBlank?: true - }): Promise + create(opts: { workspaceId: WorkspaceId }): Promise /** * Select a session as current. * @param id - session id (must exist in the list store). diff --git a/packages/client/runtime/src/client/sessions/manager.ts b/packages/client/runtime/src/client/sessions/manager.ts index bb8ed7bd6c..13aa20d1c8 100644 --- a/packages/client/runtime/src/client/sessions/manager.ts +++ b/packages/client/runtime/src/client/sessions/manager.ts @@ -534,18 +534,10 @@ export class SessionManager { * @returns the create result. */ async create( - opts: { - workspaceId?: WorkspaceId - cwd?: string - sessionId?: SessionId - reuseWorkspaceBlank?: true - } = {}, + opts: { workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId } = {}, ): Promise> { try { - const shared = { - ...(opts.sessionId === undefined ? {} : { sessionId: opts.sessionId }), - ...(opts.reuseWorkspaceBlank === undefined ? {} : { reuseWorkspaceBlank: opts.reuseWorkspaceBlank }), - } + const shared = opts.sessionId === undefined ? {} : { sessionId: opts.sessionId } const payload = opts.workspaceId !== undefined ? { workspaceId: opts.workspaceId, ...shared } : { ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }), ...shared } diff --git a/packages/client/runtime/src/client/sessions/service.ts b/packages/client/runtime/src/client/sessions/service.ts index 5e23b3a10a..c66da4e0d3 100644 --- a/packages/client/runtime/src/client/sessions/service.ts +++ b/packages/client/runtime/src/client/sessions/service.ts @@ -482,12 +482,7 @@ export class SessionRuntime implements ISessions { * @returns the new session id. * @throws {SessionCreateError} with the requested id. */ - async create(opts: { - workspaceId?: WorkspaceId - cwd?: string - sessionId?: SessionId - reuseWorkspaceBlank?: true - } = {}): Promise { + async create(opts: { workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId } = {}): Promise { const result = await this.manager.create(opts) if (!result.ok) throw new SessionCreateError(result.error, opts.sessionId) this.projectList() diff --git a/packages/client/runtime/src/client/workspaces/service.ts b/packages/client/runtime/src/client/workspaces/service.ts index bfce0c6e28..c23e3b3a6c 100644 --- a/packages/client/runtime/src/client/workspaces/service.ts +++ b/packages/client/runtime/src/client/workspaces/service.ts @@ -53,7 +53,7 @@ export class WorkspaceRuntime implements IWorkspaces { readonly list: SnapshotStore /** Workspace baseline and frame owner. */ private readonly manager: WorkspaceManager - /** In-flight blank-session connects keyed by workspace (reuse or create). */ + /** In-flight blank-session creates keyed by workspace (connectWorkspace coalescing). */ private readonly connecting = new Map>() /** Guards the runtime-owned one-shot initial-selection subscription. */ private initialSelectionStarted = false @@ -76,11 +76,9 @@ export class WorkspaceRuntime implements IWorkspaces { /** * Resolve the session a New Session flow lands in once this Workspace is - * chosen: explicitly adopt the workspace's existing blank session when one - * is in the list mirror, else create a fresh one on the host - * (`session.create` births or resumes the full Session+Agent — the client - * holds no intermediate state). The adoption tells optional default owners - * that this exact session passed the reuse checks. + * chosen: reuse the workspace's existing blank session when one is in the + * list mirror, else create a fresh one on the host (`session.create` births + * the full Session+Agent — the client holds no intermediate state). The * caller owns navigation: take the returned id to `sessions.open`. * Resolution guarantee (both arms): the returned id is already in the list * store and `sessions.binding(id)` resolves synchronously — draft hand-off @@ -109,13 +107,7 @@ export class WorkspaceRuntime implements IWorkspaces { const summary = sessions.byId[id] if (summary !== undefined && summary.blank && summary.cwd === workspace.path && workspace.sessionIds.includes(summary.id) - && !archived.includes(summary.id)) { - return this.sessions.create({ - workspaceId, - sessionId: summary.id, - reuseWorkspaceBlank: true, - }) - } + && !archived.includes(summary.id)) return summary.id } const attempt = this.sessions.create({ workspaceId }) .finally(() => { this.connecting.delete(workspaceId) }) diff --git a/packages/client/runtime/tests/workspaces-service.client.spec.ts b/packages/client/runtime/tests/workspaces-service.client.spec.ts index 68736f0a58..cfdfa98e9b 100644 --- a/packages/client/runtime/tests/workspaces-service.client.spec.ts +++ b/packages/client/runtime/tests/workspaces-service.client.spec.ts @@ -242,28 +242,20 @@ describe('WorkspaceRuntime', () => { { sessionId: sid('s-stray'), updatedAt: 4, running: false, blank: true, cwd: '/w/gamma' }, ] as never[], })) - api.onCreate = payload => Promise.resolve(ok({ - sessionId: (payload as { sessionId?: SessionId }).sessionId ?? sid('s-unexpected'), - })) await Promise.all([workspaces.refresh(), sessions.refresh()]) await Promise.resolve() - // Hit: same workspace → the parked member blank is explicitly adopted - // after the earlier cwd-matching non-member stray is skipped. + // Hit: same workspace → the parked member blank comes back (the earlier + // cwd-matching non-member stray is skipped), no create RPC. await expect(workspaces.connectWorkspace(wid('alpha'))).resolves.toBe('s-blank') - expect(api.callsOf('session.create')).toEqual([{ - workspaceId: 'alpha', sessionId: 's-blank', reuseWorkspaceBlank: true, - }]) + expect(api.callsOf('session.create')).toEqual([]) // Resolution guarantee: the id is binding-resolvable synchronously. expect(sessions.binding(sid('s-blank'))).toBeDefined() // Miss: beta has only a non-blank session → host create with workspaceId. api.onCreate = () => Promise.resolve(ok({ sessionId: sid('s-fresh') })) await expect(workspaces.connectWorkspace(wid('beta'))).resolves.toBe('s-fresh') - expect(api.callsOf('session.create')).toEqual([ - { workspaceId: 'alpha', sessionId: 's-blank', reuseWorkspaceBlank: true }, - { workspaceId: 'beta' }, - ]) + expect(api.callsOf('session.create')).toEqual([{ workspaceId: 'beta' }]) // Same guarantee on the create arm (draft hand-off writes the machine pre-open). expect(sessions.binding(sid('s-fresh'))).toBeDefined() @@ -271,11 +263,7 @@ describe('WorkspaceRuntime', () => { // never reused, a fresh accounted session is created instead. api.onCreate = () => Promise.resolve(ok({ sessionId: sid('s-fresh-3') })) await expect(workspaces.connectWorkspace(wid('gamma'))).resolves.toBe('s-fresh-3') - expect(api.callsOf('session.create')).toEqual([ - { workspaceId: 'alpha', sessionId: 's-blank', reuseWorkspaceBlank: true }, - { workspaceId: 'beta' }, - { workspaceId: 'gamma' }, - ]) + expect(api.callsOf('session.create')).toEqual([{ workspaceId: 'beta' }, { workspaceId: 'gamma' }]) // Unknown workspace fails loud instead of silently creating in nowhere. await expect(workspaces.connectWorkspace(wid('ghost'))).rejects.toThrow(/unknown workspace ghost/) @@ -296,9 +284,6 @@ describe('WorkspaceRuntime', () => { api.onList = () => Promise.resolve(ok({ items: [{ sessionId: sid('s-blank'), updatedAt: 2, running: false, blank: true, cwd: '/w/alpha' }] as never[], })) - api.onCreate = payload => Promise.resolve(ok({ - sessionId: (payload as { sessionId?: SessionId }).sessionId ?? sid('s-unexpected'), - })) await Promise.all([workspaces.refresh(), sessions.refresh()]) await Promise.resolve() const session = sessions.binding(sid('s-blank'))!.session @@ -307,9 +292,7 @@ describe('WorkspaceRuntime', () => { await Promise.resolve() // Failure leaves blank intact, so the same session is still the reuse hit. await expect(workspaces.connectWorkspace(wid('alpha'))).resolves.toBe('s-blank') - expect(api.callsOf('session.create')).toEqual([{ - workspaceId: 'alpha', sessionId: 's-blank', reuseWorkspaceBlank: true, - }]) + expect(api.callsOf('session.create')).toEqual([]) }) it('returns created Workspaces and preserves Host business errors', async () => { diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index c4fc6a313b..2b24d725a5 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -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 packages/client/ui-conversation/README.md -README.md: 1c2bcf8ea1a1986f5346495404fce3dc2a276044 -README.zh.md: 08bb96c7db8ac85cc0a3748cee754dae9590c15b +README.md: 96f67677f690f12929cf691ab5901ded399504d3 +README.zh.md: 05fdcd7a989fb74eb9f86f11cffb4eb77d38088a diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 1c2bcf8ea1..96f67677f6 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -14,7 +14,7 @@ The view ring is a slot: the strict session-body registration declares the sessi Chat business rows are independent registry contributions rather than a closed built-in union. A client plugin declaration-merges its typed `ChatNodeDataMap` key, registers a `ConversationNodeDefinition` on `ctx.conversationEvents`, and registers the matching keyed renderer on `conversation.chat.node`; it does not modify Session folds or a central renderer switch. The [Conversation Node cookbook](../../../docs/cookbook/adding-a-conversation-node.md) covers stable event ids, append/prepend replay, Location data, and renderer constraints. -Approvals take over the composer through the chain this package declares: `ApprovalPanel` registers as a selector-routed `'conversation.composer'` entry (the ui-user-questions pattern) and occupies the composer in place of the InputBar while an approval wait is pending (amber strip, justification headline, paired command line from the running call's args, one-shot refuse/allow). The `PendingApproval` domain face in `contract/slots.ts` owns the wire encoding — the `ApprovalResponsePayload` value with the audit correlation — over the runtime's `PendingWait` carrier; the broadcast `approval/resolved` frame settles the wait and restores the composer. The runtime manager projects every approval or question wait through `SessionSummary.pendingInteraction`, including sessions never instantiated; `ui-workspace` owns its sidebar presentation. Pending waits leave the message flow entirely: questions (ui-user-questions) and approvals (ApprovalPanel) both answer through the composer takeover, so no display-only placeholder card remains. The composer's bottom-row Access seat mounts `PermissionSelect`, fed by the host-computed `permissions` projection through the standard-kit `useProjection` (key absence hides the chip); the chip opens a Menu-primitive dropdown whose built-in preset ids render as localized product labels only when the host keeps their canonical names, while explicit host labels remain unchanged and unknown kebab-case names render in title case. Safe preset picks submit `/permission ` immediately through the bar's injected `command` callback, while `danger-full-access` first opens an in-page Modal risk confirmation. The enabling action stays disabled until the user checks the acknowledgement; cancel, Escape, close, and mask click submit nothing. +Approvals take over the composer through the chain this package declares: `ApprovalPanel` registers as a selector-routed `'conversation.composer'` entry (the ui-user-questions pattern) and occupies the composer in place of the InputBar while an approval wait is pending (amber strip, justification headline, paired command line from the running call's args, one-shot refuse/allow). The `PendingApproval` domain face in `contract/slots.ts` owns the wire encoding — the `ApprovalResponsePayload` value with the audit correlation — over the runtime's `PendingWait` carrier; the broadcast `approval/resolved` frame settles the wait and restores the composer. The runtime manager projects every approval or question wait through `SessionSummary.pendingInteraction`, including sessions never instantiated; `ui-workspace` owns its sidebar presentation. Pending waits leave the message flow entirely: questions (ui-user-questions) and approvals (ApprovalPanel) both answer through the composer takeover, so no display-only placeholder card remains. The composer's bottom-row Access seat mounts `PermissionSelect`, fed by the host-computed `permissions` projection through the standard-kit `useProjection` (key absence hides the chip); the chip opens a Menu-primitive dropdown whose kebab-case preset names render as title-case labels. Safe preset picks submit `/permission ` immediately through the bar's injected `command` callback, while `danger-full-access` is presented as `Full access` and first opens an in-page Modal risk confirmation. The enabling action stays disabled until the user checks the acknowledgement; cancel, Escape, close, and mask click submit nothing. The session header dispatches each current ordinary title and subagent breadcrumb through the optional session-scoped `'conversation.session.header.lineage'` seat, followed by the `'conversation.session.header.actions'` list and the independent `'conversation.session.header.utilities'` list at the right edge. Each lineage owner supplies plain breadcrumb identity and display text; the render site retains the ordinary title as fallback, and an ancestor also supplies its upward-navigation callback. Removing the occupant restores every title without affecting header actions, and optional Session utilities cannot reorder or move either group. The composer chain currency includes the current conversation `session`; ui-subagent selects one-shot or parent-unavailable addressed sessions for reason-specific read-only copy, while the ordinary InputBar keeps every addressed child Send-only because the continuation service exposes no public per-Activation cancellation operation and `session.cancel` would bypass its ownership. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 08bb96c7db..05fdcd7a98 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -24,7 +24,7 @@ Think 行默认保持折叠,并在不展开思维链的情况下暴露实时 聊天流会把每条生产方关联的重试链投影为一个稳定的弱化状态行,并用最新一次尝试更新该行;每个重试事件仍保留在运行时快照与会话日志中。前端倒计时以客户端收到事件的时刻为计划延迟的起点,避免 Host 与浏览器的时钟偏差;剩余时间向上取整到秒,且下限为 1 秒。最近一次尚未完成的重试会显示从左到右的文字渐变动画。后续轮次事实用于区分已开始的尝试与在退避期间取消的尝试,Host 的 running 位只控制实时动画;随后该行会显示静态的已完成或已取消标签。normal 策略行显示有限重试上限;always 策略行显示 `∞`。激活该行会显示最近一次重试的精确延迟和失败消息。客户端运行时会在相应重试节点到达前移除每次失败尝试的流式输出尾部;后续某次尝试成功后,该状态仍保持可见。终态失败会在其轮次边界渲染为持久的内联状态——重试耗尽后与定格的重试行并列——展示适合显示的持久消息与可选错误码,但不会提供 Host 无法兑现的操作;AUTH 文案绝不会回显提供方给出的凭据片段。 -审批通过本包声明的链条接管编辑器:`ApprovalPanel` 注册为按选择器路由的 `'conversation.composer'` 配置项(ui-user-questions 模式),在审批等待未决期间取代 InputBar 占据编辑器(琥珀色条、理由标题、来自运行中调用参数的配对命令行、一次性的拒绝/允许)。`contract/slots.ts` 中的 `PendingApproval` 领域面在运行时 `PendingWait` 载体之上拥有 wire 编码——带审计关联的 `ApprovalResponsePayload` 值;广播的 `approval/resolved` 帧使等待落定并恢复编辑器。运行时 manager 会将所有审批或问题等待通过 `SessionSummary.pendingInteraction` 投影出来,未实例化的会话也不例外;`ui-workspace` 负责其侧边栏呈现。未决等待完全离开消息流:问题(ui-user-questions)与审批(ApprovalPanel)都经编辑器接管作答,不再保留只读占位卡。编辑器底行的 Access 席位挂载 `PermissionSelect`,由 host 计算的 `permissions` 投影经标准工具包 `useProjection` 供数(key 缺席即隐藏 chip);chip 打开 Menu 原语下拉,其中内置预设 id 仅在 host 保留规范名称时渲染为本地化产品标签,显式 host 标签保持原样,未知 kebab-case 预设名仍渲染为 Title Case 标签;普通安全预设会立即经输入栏注入的 `command` 回调提交 `/permission `,而 `danger-full-access` 选择后先打开页面内的 Modal 风险确认。用户勾选确认项前启用按钮始终不可用;取消、Escape、关闭按钮与点击遮罩都不会提交命令。 +审批通过本包声明的链条接管编辑器:`ApprovalPanel` 注册为按选择器路由的 `'conversation.composer'` 配置项(ui-user-questions 模式),在审批等待未决期间取代 InputBar 占据编辑器(琥珀色条、理由标题、来自运行中调用参数的配对命令行、一次性的拒绝/允许)。`contract/slots.ts` 中的 `PendingApproval` 领域面在运行时 `PendingWait` 载体之上拥有 wire 编码——带审计关联的 `ApprovalResponsePayload` 值;广播的 `approval/resolved` 帧使等待落定并恢复编辑器。运行时 manager 会将所有审批或问题等待通过 `SessionSummary.pendingInteraction` 投影出来,未实例化的会话也不例外;`ui-workspace` 负责其侧边栏呈现。未决等待完全离开消息流:问题(ui-user-questions)与审批(ApprovalPanel)都经编辑器接管作答,不再保留只读占位卡。编辑器底行的 Access 席位挂载 `PermissionSelect`,由 host 计算的 `permissions` 投影经标准工具包 `useProjection` 供数(key 缺席即隐藏 chip);chip 打开 Menu 原语下拉,其中 kebab-case 预设名渲染为 Title Case 标签;普通安全预设会立即经输入栏注入的 `command` 回调提交 `/permission `,而 `danger-full-access` 在界面中显示为 `Full access`,选择后先打开页面内的 Modal 风险确认。用户勾选确认项前启用按钮始终不可用;取消、Escape、关闭按钮与点击遮罩都不会提交命令。 `TodoDock` 以 `order: 0` 占用 `'conversation.input.dock'` 列表 slot(位于 Goal 与 Queue 之前),作为计划条读取 host 计算的 `todos` 投影(当前计划:其后没有更晚 `turn/start` 的最近一次 `todo/write`)并渲染 `TodoPanel`。面板接收纯列表,列表为空时自我隐藏;列表非空时默认折叠,表头显示标题及以 `·` 连接的各状态计数(如 `1 已完成 · 2 进行中 · 1 待处理`,省略零计数)。dock adapter 拥有 selection,因此面板保持为 props 的纯函数。输入区 composer 链隐藏的一切也会隐藏整个 dock。`todo_write` 工具行属于 [`ui-tool`](../ui-tool/README.zh.md)。 diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index db4d218d21..f4e7a7c59a 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -66,14 +66,11 @@ export const zh = { 'settings.enter.description': '仅在智能体运行时生效;Cmd/Ctrl+Enter 使用另一行为', 'settings.enter.queue': '排队发送', 'settings.enter.steer': '插话发送', - 'access.preset.readOnly': '仅可查看', - 'access.preset.workspaceWrite': '可写入工作区', - 'access.preset.fullAccess': '完全权限', - 'access.confirm.title': '确认启用完全权限?', - 'access.confirm.description': '启用完全权限后,智能体将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。', + 'access.confirm.title': '确认启用 Full access?', + 'access.confirm.description': '启用 Full access 后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。', 'access.confirm.acknowledge': '我已了解风险,并愿意继续', 'access.confirm.cancel': '取消', - 'access.confirm.enable': '启用完全权限', + 'access.confirm.enable': '启用 Full access', 'hero.headline': '探索未至之境', 'hero.preview': '预览版', 'hero.chooseWorkspace': '选择工作区', @@ -246,9 +243,6 @@ export const en = { 'settings.enter.description': 'Busy only; Cmd/Ctrl+Enter uses the other behavior', 'settings.enter.queue': 'Queue', 'settings.enter.steer': 'Steer', - 'access.preset.readOnly': 'Read Only', - 'access.preset.workspaceWrite': 'Workspace Write', - 'access.preset.fullAccess': 'Full access', 'access.confirm.title': 'Enable Full access?', 'access.confirm.description': 'Full access reduces confirmation steps and lets the agent perform more actions directly, including sensitive operations, file changes, or external commands. Only use it when you trust the current task.', 'access.confirm.acknowledge': 'I understand the risks and want to continue', diff --git a/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx b/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx index b7ba37a7e0..5230f59835 100644 --- a/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx @@ -15,14 +15,14 @@ const FULL_ACCESS = 'danger-full-access' const shieldOutline = 'M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' -const permissionGlyphs = new Map([ - ['read-only', ( +const permissionGlyphs = { + 'read-only': ( - )], - ['workspace-write', ( + ), + 'workspace-write': ( @@ -30,44 +30,35 @@ const permissionGlyphs = new Map([ - )], - [FULL_ACCESS, ( + ), + [FULL_ACCESS]: ( - )], -]) + ), +} as Record /** Glyph for a permission option value; host-configured names outside the design set get none. */ function permissionGlyph(value: string): ReactNode | undefined { - return permissionGlyphs.get(value) + return permissionGlyphs[value] } /** - * Display transform: built-in machine names render as locale product labels; - * non-kebab host-configured names pass through. + * Display transform: kebab-case machine names render as title-case labels + * (`workspace-write` → `Workspace Write`); non-kebab host-configured names + * pass through. Full access intentionally overrides the machine-name + * transform so both permission surfaces use the product label `Full access`; + * the warning body remains locale-aware. */ function displayName(name: string): string { if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) return name return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') } -const BUILT_IN_PERMISSION_NAMES = new Map([ - ['read-only', 'Read Only'], - ['workspace-write', 'Workspace Write'], - [FULL_ACCESS, 'Full access'], -]) - -function permissionLabel(value: string, name: string, t: ComposerBarProps['t']): string { - const builtInName = BUILT_IN_PERMISSION_NAMES.get(value) - if (builtInName !== undefined && (name === value || name === builtInName)) { - if (value === 'read-only') return t('access.preset.readOnly') - if (value === 'workspace-write') return t('access.preset.workspaceWrite') - if (value === FULL_ACCESS) return t('access.preset.fullAccess') - } - return displayName(name) +function optionLabel(option: PermissionSelectValue['options'][number]): string { + return option.value === FULL_ACCESS ? 'Full access' : displayName(option.name) } export interface PermissionSelectProps { @@ -95,20 +86,13 @@ export function PermissionSelect({ value, locked, command, t }: PermissionSelect const currentValue = pick ?? value.currentValue const current = value.options.find(option => option.value === currentValue) - const currentLabel = current === undefined - ? permissionLabel(currentValue, currentValue, t) - : permissionLabel(current.value, current.name, t) const busy = pick !== null || confirmation !== null const items: MenuEntry[] = value.options .filter(o => o.value !== 'custom') .map((option) => { const icon = permissionGlyph(option.value) - return { - id: option.value, - label: permissionLabel(option.value, option.name, t), - ...icon === undefined ? {} : { icon }, - } + return { id: option.value, label: optionLabel(option), ...icon === undefined ? {} : { icon } } }) const submit = (id: string): void => { @@ -154,7 +138,7 @@ export function PermissionSelect({ value, locked, command, t }: PermissionSelect