From b03b1f2e7ba5e82d36617d82a58ef989103e2c6d Mon Sep 17 00:00:00 2001 From: Kaige-Gao Date: Mon, 17 Aug 2026 17:04:17 +0800 Subject: [PATCH 01/33] fix(web): improve permission labels and blank defaults --- ...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 +- apps/web/tests/access-confirmation.e2e.ts | 10 ++--- apps/web/tests/settings-chrome.e2e.ts | 18 ++++----- .../access-confirmation/ui.expected.md | 8 ++-- .../settings-chrome/dialog.expected.md | 4 +- docs/config-catalog.i18n.yaml | 2 +- docs/config-catalog.md | 2 +- docs/subsystems/permission-presets.i18n.yaml | 4 +- docs/subsystems/permission-presets.md | 2 +- docs/subsystems/permission-presets.zh.md | 2 +- .../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 | 27 ++++++++----- .../tests/input-bar.client.spec.tsx | 34 ++++++++-------- .../ui-permission-presets/README.i18n.yaml | 4 +- .../client/ui-permission-presets/README.md | 4 +- .../client/ui-permission-presets/README.zh.md | 4 +- .../src/client/PermissionRow.tsx | 9 +++-- .../ui-permission-presets/src/client/index.ts | 8 +++- .../src/client/locales.ts | 24 ++++++++--- .../src/client/presentation.ts | 31 ++++++++++++-- .../tests/browser-plugin.client.spec.ts | 10 +++-- .../permission-presets-row.client.spec.tsx | 40 +++++++++---------- .../tests/settings-store.client.spec.ts | 4 +- .../permission-presets/README.i18n.yaml | 4 +- .../interaction/permission-presets/README.md | 2 +- .../permission-presets/README.zh.md | 2 +- .../permission-presets/src/index.ts | 30 ++++++++++++-- .../tests/permission-presets.spec.ts | 39 ++++++++++++++++++ .../tests/subagent-codex.spec.ts | 2 +- 40 files changed, 313 insertions(+), 125 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.md create 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 new file mode 100644 index 0000000000..ff3289544e --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.i18n.yaml @@ -0,0 +1,6 @@ +# 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: 2765462385f6a0e41bcdda8fd4a1ed532f38be2d +2026-08-17-blank-permission-default-refresh.zh.md: 8619b9f2aed01b56347af5f99439e5f356067935 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 new file mode 100644 index 0000000000..2765462385 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.md @@ -0,0 +1,29 @@ +# 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 + +`dsh-permission-presets` treats a settings change as a chance to advance reusable blank placeholders. When `defaultPreset` changes, the service scans live sessions, finds sessions that have not started a turn, and switches only those whose effective permission still equals the previous default. Sessions that have started a turn are never changed. Blank sessions the user already switched away from the previous default are also left alone. + +This keeps the existing Web blank-session reuse policy intact while making the reused placeholder observe the same default a freshly created session would receive. The update goes through the normal preset setter, so the durable `permission/preset`, `sandbox/mode`, and `approval/policy` facts remain the single 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: started sessions and seeded resumes remain pinned, while unseeded blank placeholders may advance because the Web treats them as New Session reuse targets. + +## 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 or add a cross-plugin hook solely for this case. The permission service already owns the default and can repair its own blank placeholders. + +**Update every blank session unconditionally.** Rejected because a user may deliberately switch the current blank session's permission before sending the first prompt. Matching only the previous default updates stale placeholders without overwriting an explicit blank-session selection. + +## Consequences + +A settings change may append permission facts to unseeded blank sessions, but those sessions remain blank because blankness is defined by the absence of `turn/start`. Started conversations, seeded resumes, and blank sessions with an explicit user-selected preset keep their original 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 new file mode 100644 index 0000000000..8619b9f2ae --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-17-blank-permission-default-refresh.zh.md @@ -0,0 +1,29 @@ +# Agent Note: Refresh blank session permission defaults + +Status: implemented + +[English](2026-08-17-blank-permission-default-refresh.md) | 中文 + +## Problem + +Web 新会话流程会复用工作区中的空白会话,而不是不断创建隐藏占位会话。权限默认值在会话创建时被固定到该会话中,因此当某个空白占位会话已经存在后,用户再修改「通用设置」里的权限默认值,这个占位会话仍会保留旧预设。下一次“新”对话复用它时,权限 chip 就会和刚保存的默认设置不一致。 + +## Decision + +`dsh-permission-presets` 将设置变更视为推进可复用空白占位会话的时机。当 `defaultPreset` 变化时,服务会扫描 live sessions,找到尚未开始过轮次的会话,并且只切换那些有效权限仍等于旧默认值的会话。已经开始过轮次的会话绝不会被改变。用户已经在空白会话中手动切离旧默认值的会话也会保持原样。 + +这样既保留了既有的 Web 空白会话复用策略,也让被复用的占位会话观察到与真正新建会话相同的默认值。更新仍走常规 preset setter,因此持久的 `permission/preset`、`sandbox/mode` 与 `approval/policy` 事实继续作为投影和执行的单一来源。 + +这项修复部分细化了较早的[新会话权限默认值](../feature/2026-07-31-permission-default-for-new-sessions.md)决策:已经开始的会话和带 seed 的恢复仍保持固定,而未带 seed 的空白占位会话可以推进,因为 Web 会将它们作为新会话复用目标。 + +## Alternatives considered + +**权限设置变化后禁用空白会话复用。** 拒绝,因为这会留下额外的隐藏占位会话,并让新会话行为更不确定。既有复用策略有价值;错误只在于权限默认值过期。 + +**让客户端比较空白会话的权限投影和 Settings 行。** 拒绝,因为 workspace runtime 需要理解 permission settings namespace,或为这个场景新增跨插件 hook。权限服务已经拥有默认值,也能修复自己的空白占位会话。 + +**无条件更新所有空白会话。** 拒绝,因为用户可能在发送第一条 prompt 前,刻意切换当前空白会话的权限。只匹配旧默认值可以更新过期占位会话,同时避免覆盖明确的空白会话选择。 + +## Consequences + +设置变更可能向未带 seed 的空白会话追加权限事实,但这些会话仍保持 blank,因为 blankness 由是否缺少 `turn/start` 定义。已经开始的对话、带 seed 的恢复,以及已有用户显式选择预设的空白会话都会保留原权限。 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 b82a2e1fa9..67d3267414 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: f63502cd3e2306f36b136e6ed8543641449c3d83 -2026-07-31-gui-full-access-confirmation.zh.md: f4b3686d1e1ad9e51a08e513a7dd5930d311582d +2026-07-31-gui-full-access-confirmation.md: c8920981ee09c0496f045962e0736bf0cc39c59b +2026-07-31-gui-full-access-confirmation.zh.md: 8c453a3cb9f4efce079115ff5cf5d4a44dc06796 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 f63502cd3e..c8920981ee 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 product label `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 locale product label for 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. -- `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. +- The full-access product label 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. Later, the same localized built-in-label rule was extended to the safer shipped presets while preserving the same wire names ([blank permission default refresh](../bug-fix/2026-08-17-blank-permission-default-refresh.md)). ## 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 f4b3686d1e..8c453a3cb9 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` 对话框:启用按钮在用户勾选明确的风险确认复选框前保持禁用;预设以产品标签 `Full access` 展示;所有取消路径都不作任何提交。** +**每个权限选择器都把 `danger-full-access` 关进共享的页面内 `RiskConfirmation` 对话框:启用按钮在用户勾选明确的风险确认复选框前保持禁用;预设以完全权限的本地化产品标签展示;所有取消路径都不作任何提交。** - `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、关闭与点击遮罩均不会改动已存默认值。 -- `Full access` 在每个选择器中都有意覆盖 kebab 转 Title Case 的显示变换;命令与 Settings 写入在 wire 上保留机器名,每份警示正文都保持中英文 locale 感知。 +- 完全权限产品标签在每个选择器中都有意覆盖 kebab 转 Title Case 的显示变换;命令与 Settings 写入在 wire 上保留机器名,每份警示正文都保持中英文 locale 感知。后来,同一个本地化内置标签规则扩展到了更安全的随附预设,同时仍保留相同的 wire 名称([空白权限默认值刷新](../bug-fix/2026-08-17-blank-permission-default-refresh.md))。 ## 考虑过的替代方案 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 c28df0e4cf..c5a2281059 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: ebf7fe39712d64c18e12b9b26d86201a61ad6cfd -2026-07-31-permission-default-for-new-sessions.zh.md: c56a1b4ac3a6bc88a489dd7e945fa1c11581e7a6 +2026-07-31-permission-default-for-new-sessions.md: 7383d4b0e51f6c5570818e1b0c3163b7c2f26146 +2026-07-31-permission-default-for-new-sessions.zh.md: 398bb668cc9b08cc989ad099897d4aab17844015 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 ebf7fe3971..7383d4b0e5 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`. 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 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 does not change started conversations. Reusable, unseeded blank placeholders that still carry the previous default advance to the new default before Web New Session reuses them; this later bug fix is recorded in [blank permission default refresh](../bug-fix/2026-08-17-blank-permission-default-refresh.md). 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 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. +Changing Permission in Settings updates `settings.yaml` and the selector immediately, but does not alter a started 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. Reusable blank placeholders may receive a new pinned triplet when they still reflect the previous default. 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 c56a1b4ac3..398bb668cc 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`。这些事实将创建时选中的权限固定下来,因此后续 Settings 变更只影响之后的会话。带 seed 或只完成部分初始化的会话会保留其有效调节项,只补齐缺失的事实;恢复时绝不会采用最新的用户默认值。`Session` 甚至会用 `session/end-seed` 标记显式为空的构造器 seed,因此不能把空的持久化日志误认为新会话。 +服务会在 `session/created` 时同步读取当前 Settings 值。真正的新会话会收到三个显式事件:`permission/preset`、`sandbox/mode` 和 `approval/policy`。这些事实将创建时选中的权限固定下来,因此后续 Settings 变更不会改变已经开始的对话。仍可复用、未带 seed、且还保持旧默认值的空白占位会话会在 Web 新会话复用前推进到新默认值;这项后续 bug 修复记录在[空白权限默认值刷新](../bug-fix/2026-08-17-blank-permission-default-refresh.md)。带 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` 和选择器,但不会改变已打开的会话。之后的每个会话都可以从三个已固定的权限事实中重建,即使用户再次更改默认值或进程重启也不受影响。如果部署中组合后的沙箱和审批默认值与任何 preset 都不匹配,则必须显式配置 `defaultPreset`。 +在 Settings 中更改「权限」会立即更新 `settings.yaml` 和选择器,但不会改变已经开始的已打开会话。之后的每个会话都可以从三个已固定的权限事实中重建,即使用户再次更改默认值或进程重启也不受影响。可复用空白占位会话若仍反映旧默认值,则可能收到新的固定三元组。如果部署中组合后的沙箱和审批默认值与任何 preset 都不匹配,则必须显式配置 `defaultPreset`。 组装后的 Web 快照包含功能完整的「权限」选择器。其无密钥浏览器场景会写入 `read-only`,验证现有的 `workspace-write` 会话保持不变,并验证随后创建的会话以 read-only 事件三元组启动。 diff --git a/apps/web/tests/access-confirmation.e2e.ts b/apps/web/tests/access-confirmation.e2e.ts index aea33f14f1..4c13748b37 100644 --- a/apps/web/tests/access-confirmation.e2e.ts +++ b/apps/web/tests/access-confirmation.e2e.ts @@ -49,13 +49,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('访问模式,当前:Workspace Write') + expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:可写入工作区') await access.click() - await page.getByRole('menuitem', { name: 'Full access' }).click() - const dialog = page.getByRole('dialog', { name: '确认启用 Full access?' }) + await page.getByRole('menuitem', { name: '完全权限' }).click() + const dialog = page.getByRole('dialog', { name: '确认启用完全权限?' }) await dialog.waitFor({ timeout: 10_000 }) - const enable = dialog.getByRole('button', { name: '启用 Full access' }) + const enable = dialog.getByRole('button', { name: '启用完全权限' }) expect(await enable.isDisabled()).toBe(true) // The modal is in this page's body (not a native/new window) and escapes @@ -68,7 +68,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('访问模式,当前:Full access') + .toBe('访问模式,当前:完全权限') 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 c5162e7b19..078f5e8777 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -60,7 +60,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: 'Workspace Write' }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: '可写入工作区' }).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: '打开配置文件' }) @@ -138,12 +138,12 @@ describe('web e2e: settings modal and General preferences', () => { 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: 'Workspace Write' }) + const selector = dialog.getByRole('button', { name: '可写入工作区' }) await selector.waitFor({ timeout: 10_000 }) await expect.poll(() => selector.isEnabled(), { timeout: 5_000 }).toBe(true) await selector.click() - await page.getByRole('menuitem', { name: 'Read Only' }).click() - await dialog.getByRole('button', { name: 'Read Only' }).waitFor({ timeout: 10_000 }) + await page.getByRole('menuitem', { name: '仅可查看' }).click() + await dialog.getByRole('button', { name: '仅可查看' }).waitFor({ timeout: 10_000 }) const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') expect(document).toContain('permission:') @@ -158,14 +158,14 @@ describe('web e2e: settings modal and General preferences', () => { ['approval/policy', { policy: 'ask' }], ]) - 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' }) + await dialog.getByRole('button', { name: '仅可查看' }).click() + await page.getByRole('menuitem', { name: '完全权限' }).click() + const confirmation = page.getByRole('dialog', { name: '确认启用完全权限?' }) + const enable = confirmation.getByRole('button', { name: '启用完全权限' }) expect(await enable.isDisabled()).toBe(true) await confirmation.getByRole('checkbox').click() await enable.click() - await dialog.getByRole('button', { name: 'Full access' }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: '完全权限' }).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')) diff --git a/apps/web/tests/snapshots/access-confirmation/ui.expected.md b/apps/web/tests/snapshots/access-confirmation/ui.expected.md index 1287e6e565..306ad17f07 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 "确认启用 Full access?": - - heading "确认启用 Full access?" [level=2] +- dialog "确认启用完全权限?": + - heading "确认启用完全权限?" [level=2] - button "Close": - img - img - - paragraph: 启用 Full access 后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。 + - paragraph: 启用完全权限后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。 - checkbox "我已了解风险,并愿意继续" - text: 我已了解风险,并愿意继续 - button "取消" - - button "启用 Full access" [disabled] + - button "启用完全权限" [disabled] diff --git a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md index 89cff5df3f..00dcd44f53 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 "Workspace Write": - - text: Workspace Write + - button "可写入工作区": + - text: 可写入工作区 - img - text: 语言 - button "中文": diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index f7735c282f..2e99c8713a 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: 1676a7e042348bbac8eca478cabcc76c22770af5 +config-catalog.md: 646db6e251fdcbfcccea354de97f8db394abc8c2 config-catalog.zh.md: 0b32dd18e1d8ab1ca24ce3e5f144402d0d3fad76 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 1676a7e042..646db6e251 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1339,7 +1339,7 @@ export interface PresetSpec { Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md) -Source: [`packages/interaction/permission-presets/src/index.ts:140`](../packages/interaction/permission-presets/src/index.ts) +Source: [`packages/interaction/permission-presets/src/index.ts:150`](../packages/interaction/permission-presets/src/index.ts) diff --git a/docs/subsystems/permission-presets.i18n.yaml b/docs/subsystems/permission-presets.i18n.yaml index 127988f5c1..44c1f00455 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: 16ce29a4c3b00fece089ebcdc959e57f419d35c9 -permission-presets.zh.md: d2e5eff6696d5dc6f925032d7eaafb7b19774f4e +permission-presets.md: e16a0f11cb3df48d54387ffd39157887d41764ff +permission-presets.zh.md: 989d6033fff49b62cb17968dc5f5deb15f690c84 diff --git a/docs/subsystems/permission-presets.md b/docs/subsystems/permission-presets.md index 16ce29a4c3..e16a0f11cb 100644 --- a/docs/subsystems/permission-presets.md +++ b/docs/subsystems/permission-presets.md @@ -127,5 +127,5 @@ set(session: Session, name: string): void Types: [Session](session.md) · [SessionEvent](session.md) -Source: [`packages/interaction/permission-presets/src/index.ts:159`](../../packages/interaction/permission-presets/src/index.ts) +Source: [`packages/interaction/permission-presets/src/index.ts:169`](../../packages/interaction/permission-presets/src/index.ts) diff --git a/docs/subsystems/permission-presets.zh.md b/docs/subsystems/permission-presets.zh.md index d2e5eff669..989d6033ff 100644 --- a/docs/subsystems/permission-presets.zh.md +++ b/docs/subsystems/permission-presets.zh.md @@ -127,5 +127,5 @@ set(session: Session, name: string): void Types: [Session](session.md) · [SessionEvent](session.md) -Source: [`packages/interaction/permission-presets/src/index.ts:159`](../../packages/interaction/permission-presets/src/index.ts) +Source: [`packages/interaction/permission-presets/src/index.ts:169`](../../packages/interaction/permission-presets/src/index.ts) diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 6d866e05e7..3c60cc8dec 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: d1a265b5789d9f1d9b5e630e0548ae5f619eebbf -README.zh.md: 3f303391d39bc040b4a6a5a2d1f6a34fe8891919 +README.md: e3540fcb3e866beac7a63bbdac20a7b1200510a4 +README.zh.md: 554f7b42d08b1873100641cabe63d04547128c4d diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index d1a265b578..e3540fcb3e 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 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. +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 and unknown kebab-case names render as title-case labels. 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. The session header renders the session-scoped `'conversation.session.header.actions'` list beside the title and the independent `'conversation.session.header.utilities'` list at the right edge. Session context and lineage controls remain in `actions`; optional Session utilities cannot reorder or move them. 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 3f303391d3..554f7b42d0 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 原语下拉,其中 kebab-case 预设名渲染为 Title Case 标签;普通安全预设会立即经输入栏注入的 `command` 回调提交 `/permission `,而 `danger-full-access` 在界面中显示为 `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 原语下拉,其中内置预设 id 渲染为本地化产品标签,未知 kebab-case 预设名仍渲染为 Title Case 标签;普通安全预设会立即经输入栏注入的 `command` 回调提交 `/permission `,而 `danger-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.md)。 diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 368475c583..d82e36b935 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -66,11 +66,14 @@ export const zh = { 'settings.enter.description': '仅在智能体运行时生效;Cmd/Ctrl+Enter 使用另一行为', 'settings.enter.queue': '排队发送', 'settings.enter.steer': '插话发送', - 'access.confirm.title': '确认启用 Full access?', - 'access.confirm.description': '启用 Full access 后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。', + 'access.preset.readOnly': '仅可查看', + 'access.preset.workspaceWrite': '可写入工作区', + 'access.preset.fullAccess': '完全权限', + 'access.confirm.title': '确认启用完全权限?', + 'access.confirm.description': '启用完全权限后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。', 'access.confirm.acknowledge': '我已了解风险,并愿意继续', 'access.confirm.cancel': '取消', - 'access.confirm.enable': '启用 Full access', + 'access.confirm.enable': '启用完全权限', 'hero.headline': '探索未至之境', 'hero.preview': '预览版', 'hero.chooseWorkspace': '选择工作区', @@ -236,6 +239,9 @@ 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 5230f59835..c95065d11d 100644 --- a/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx @@ -46,19 +46,19 @@ function permissionGlyph(value: string): ReactNode | undefined { } /** - * 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. + * Display transform: built-in machine names render as locale product labels; + * non-kebab host-configured names pass through. */ 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(' ') } -function optionLabel(option: PermissionSelectValue['options'][number]): string { - return option.value === FULL_ACCESS ? 'Full access' : displayName(option.name) +function permissionLabel(value: string, name: string, t: ComposerBarProps['t']): string { + 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) } export interface PermissionSelectProps { @@ -86,13 +86,20 @@ 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: optionLabel(option), ...icon === undefined ? {} : { icon } } + return { + id: option.value, + label: permissionLabel(option.value, option.name, t), + ...icon === undefined ? {} : { icon }, + } }) const submit = (id: string): void => { @@ -138,7 +145,7 @@ export function PermissionSelect({ value, locked, command, t }: PermissionSelect