From 58142eb717000296ff37b838ea2413cd3f68a416 Mon Sep 17 00:00:00 2001 From: Gargantua <22532097@zju.edu.cn> Date: Tue, 15 Sep 2026 19:23:19 +0800 Subject: [PATCH] fix(settings): open the AI tab pickers as in-app menus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → 全局 AI rendered the permission mode and the command shell as native `` remains. Refs #405 --- .../settings/SettingsMenuSelect.tsx | 158 ++++++++++++++++++ .../src/features/settings/SettingsPage.tsx | 27 +-- .../src/features/settings/primitives.tsx | 31 ++-- apps/desktop/src/styles/settings.css | 74 ++++++-- .../test/fixed-dropdown-surfaces.test.mjs | 1 + .../test/plan-approval-settings.test.mjs | 2 +- apps/desktop/test/settings-general.test.mjs | 6 +- docs/spec/04-ux/06-settings-ia.md | 6 +- docs/spec/06-delivery/04-e2e-test-plan.md | 10 ++ docs/zh-CN/spec/04-ux/06-settings-ia.md | 6 +- .../spec/06-delivery/04-e2e-test-plan.md | 16 ++ 11 files changed, 289 insertions(+), 48 deletions(-) create mode 100644 apps/desktop/src/components/settings/SettingsMenuSelect.tsx diff --git a/apps/desktop/src/components/settings/SettingsMenuSelect.tsx b/apps/desktop/src/components/settings/SettingsMenuSelect.tsx new file mode 100644 index 000000000..86bec5848 --- /dev/null +++ b/apps/desktop/src/components/settings/SettingsMenuSelect.tsx @@ -0,0 +1,158 @@ +/** + * A menu select for a Settings row. + * + * Settings cards clip their overflow (`.settings-panel` draws the frame with + * `overflow: hidden`), so the option list opens through `AnchoredMenu`. A + * native ` + onChange={(mode) => void saveSettings({ - defaultPermissionMode: e.target.value as GlobalPermissionMode, + defaultPermissionMode: mode as GlobalPermissionMode, }) } - > - - - - + options={[ + { id: "ask", label: t("settings.permissionModeAsk") }, + { + id: "accept-edits", + label: t("settings.permissionModeAcceptEdits"), + }, + { id: "auto", label: t("settings.permissionModeAuto") }, + ]} + /> diff --git a/apps/desktop/src/features/settings/primitives.tsx b/apps/desktop/src/features/settings/primitives.tsx index b6e2ced3c..d9d4c063f 100644 --- a/apps/desktop/src/features/settings/primitives.tsx +++ b/apps/desktop/src/features/settings/primitives.tsx @@ -12,7 +12,8 @@ import { } from "@pi-desktop/shared"; import { api } from "../../lib/api"; import { resolveContextUsageDisplay } from "../../lib/context-usage"; -import { Input, Select, cx } from "../../components/ui"; +import { Input, cx } from "../../components/ui"; +import { SettingsMenuSelect } from "../../components/settings/SettingsMenuSelect"; export function SettingsRow({ title, @@ -157,22 +158,22 @@ export function CommandShellRow({ {t("settings.commandShellNoChoices")} ) : ( - + busy={saving} + onChange={(value) => void onChange(value)} + options={catalog.choices.map((choice) => ({ + id: choice.id, + label: `${choice.label}${ + choice.available + ? "" + : ` - ${t("settings.commandShellUnavailable")}` + }`, + disabled: !choice.available, + }))} + /> )} {effectiveStatus ? ( {effectiveStatus} diff --git a/apps/desktop/src/styles/settings.css b/apps/desktop/src/styles/settings.css index 8392bb7f6..f8348789d 100644 --- a/apps/desktop/src/styles/settings.css +++ b/apps/desktop/src/styles/settings.css @@ -909,17 +909,21 @@ text-align: center; } -/* Language and theme pickers (Appearance). Pill trigger matches the font - picker; the menu is an AnchoredMenu so the settings card cannot clip it. */ +/* Language and theme pickers (Appearance) plus the short-list menu selects on + the remaining tabs. Pill trigger matches the font picker; the menu is an + AnchoredMenu so the settings card cannot clip it, and one definition here + means a design change reaches every Settings picker together. */ .settings-language-anchor, -.settings-theme-anchor { +.settings-theme-anchor, +.settings-menu-select-anchor { position: relative; width: 100%; min-width: 0; } .settings-language-trigger, -.settings-theme-trigger { +.settings-theme-trigger, +.settings-menu-select-trigger { display: flex; align-items: center; gap: 8px; @@ -935,12 +939,20 @@ } .settings-language-trigger:hover, -.settings-theme-trigger:hover { +.settings-theme-trigger:hover, +.settings-menu-select-trigger:hover { background: var(--ds-bg-hover); } +/* A write in flight keeps the trigger readable but not re-openable. */ +.settings-menu-select-trigger:disabled { + color: var(--ds-text-secondary); + cursor: default; +} + .settings-language-trigger-label, -.settings-theme-trigger-label { +.settings-theme-trigger-label, +.settings-menu-select-trigger-label { min-width: 0; flex: 1; overflow: hidden; @@ -950,13 +962,15 @@ } .settings-language-trigger > svg, -.settings-theme-trigger > svg { +.settings-theme-trigger > svg, +.settings-menu-select-trigger > svg { flex: 0 0 auto; color: var(--ds-text-muted); } .settings-language-menu, -.settings-theme-menu { +.settings-theme-menu, +.settings-menu-select-menu { position: fixed; z-index: 60; width: min(240px, calc(100vw - 48px)); @@ -972,7 +986,8 @@ } .settings-language-menu.is-open, -.settings-theme-menu.is-open { +.settings-theme-menu.is-open, +.settings-menu-select-menu.is-open { opacity: 1; pointer-events: auto; visibility: visible; @@ -1017,7 +1032,8 @@ } .settings-language-results, -.settings-theme-results { +.settings-theme-results, +.settings-menu-select-results { min-height: 0; max-height: min(360px, calc(100vh - 220px)); overflow-y: auto; @@ -1026,14 +1042,16 @@ } .settings-language-list, -.settings-theme-list { +.settings-theme-list, +.settings-menu-select-list { margin: 0; padding: 0; list-style: none; } .settings-language-option, -.settings-theme-option { +.settings-theme-option, +.settings-menu-select-option { display: flex; align-items: center; gap: 8px; @@ -1048,12 +1066,25 @@ .settings-language-option:hover, .settings-language-option.is-active, .settings-theme-option:hover, -.settings-theme-option.is-active { +.settings-theme-option.is-active, +.settings-menu-select-option:hover, +.settings-menu-select-option.is-active { background: var(--ds-bg-hover); } +/* A host-reported unavailable shell stays listed but cannot be chosen. */ +.settings-menu-select-option:disabled { + color: var(--ds-text-muted); + cursor: default; +} + +.settings-menu-select-option:disabled:hover { + background: transparent; +} + .settings-language-option.is-current .settings-language-option-native, -.settings-theme-option.is-current .settings-theme-option-title { +.settings-theme-option.is-current .settings-theme-option-title, +.settings-menu-select-option.is-current .settings-menu-select-option-label { color: var(--ds-text-primary); } @@ -1067,7 +1098,8 @@ } .settings-language-option-native, -.settings-theme-option-title { +.settings-theme-option-title, +.settings-menu-select-option-label { overflow: hidden; color: var(--ds-text-primary); font-size: var(--text-sm-plus); @@ -1075,6 +1107,15 @@ white-space: nowrap; } +.settings-menu-select-option:disabled .settings-menu-select-option-label { + color: var(--ds-text-muted); +} + +.settings-menu-select-option-label { + min-width: 0; + flex: 1; +} + .settings-language-option-english, .settings-theme-option-hint { overflow: hidden; @@ -1086,7 +1127,8 @@ } .settings-language-check, -.settings-theme-check { +.settings-theme-check, +.settings-menu-select-check { flex: 0 0 auto; color: var(--ds-accent); } diff --git a/apps/desktop/test/fixed-dropdown-surfaces.test.mjs b/apps/desktop/test/fixed-dropdown-surfaces.test.mjs index b6008372e..a30528e58 100644 --- a/apps/desktop/test/fixed-dropdown-surfaces.test.mjs +++ b/apps/desktop/test/fixed-dropdown-surfaces.test.mjs @@ -29,6 +29,7 @@ const anchoredSurfaceSources = await Promise.all( const dropdownSurfaces = [ "settings-font-menu", "settings-theme-menu", + "settings-menu-select-menu", "provider-service-menu", "model-default-menu", "provider-model-multi-menu", diff --git a/apps/desktop/test/plan-approval-settings.test.mjs b/apps/desktop/test/plan-approval-settings.test.mjs index 873a7ec63..796761690 100644 --- a/apps/desktop/test/plan-approval-settings.test.mjs +++ b/apps/desktop/test/plan-approval-settings.test.mjs @@ -109,7 +109,7 @@ test("command-shell settings are catalog-driven and use the existing save flow", assert.match(settingsPage, /api\s*\.\s*listCommandShells\(\)/s); assert.match(settingsPage, /settings\.defaultCommandShell/); assert.match(settingsPage, /catalog\.choices\.map/); - assert.match(settingsPage, /disabled=\{!choice\.available\}/); + assert.match(settingsPage, /disabled: !choice\.available/); assert.match(settingsPage, /saveSettings\(\{ defaultCommandShell: choice\.id \}\)/); assert.match(settingsPage, /catalog\.configuredId/); assert.match(settingsPage, /catalog\??\.effective/); diff --git a/apps/desktop/test/settings-general.test.mjs b/apps/desktop/test/settings-general.test.mjs index db51c3751..07c090369 100644 --- a/apps/desktop/test/settings-general.test.mjs +++ b/apps/desktop/test/settings-general.test.mjs @@ -131,9 +131,13 @@ test("Basics and AI tabs expose their respective app and AI controls", () => { assert.doesNotMatch(settingsPageSource, /commandShellConfigured/); assert.match( aiSource, - /defaultPermissionMode: e\.target\.value as GlobalPermissionMode/, + /defaultPermissionMode: mode as GlobalPermissionMode/, ); + assert.match(aiSource, / popup is + // platform-drawn and cannot carry the shared menu surface or its check mark. + assert.doesNotMatch(aiSource, /` popup, so every Settings picker opens the same + way. - **Defaults** card: the host-backed default operating mode (Agent / Plan / Goal), command shell selection, Link open destination, context usage display (remaining or used), Enter-to-send control, and the large text paste @@ -137,7 +140,8 @@ Settings is a **full-window page** that replaces the app sidebar + main chrome ( and the fallback state is shown. When the selected shell is available, the selector is the only configured-state indicator; status text is reserved for the default, fallback, and no-effective-shell cases. A Bash turn verifies its - pinned ID/dialect before execution. + pinned ID/dialect before execution. The row renders the same menu select as + the Permissions card and the Appearance pickers. - Context management has **no card and no controls** (D200 / ADR 0061, kept by D203 / ADR 0064). Automatic protection is always on and its budgets and retention limits are derived from the active model's window, so there is diff --git a/docs/spec/06-delivery/04-e2e-test-plan.md b/docs/spec/06-delivery/04-e2e-test-plan.md index 5d1739308..b6a00aa0c 100644 --- a/docs/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/spec/06-delivery/04-e2e-test-plan.md @@ -2148,6 +2148,16 @@ and identify the platform validation still needed. - **Milestone**: M5 - **Status**: Documented; native Windows validation pending +#### E2E-SETTINGS-ai-tab-pickers-use-in-app-menus + +- **Preconditions**: App running with a host that reports at least one configured command shell and at least one catalog shell that is unavailable on this platform. +- **Steps**: 1) Open Settings → General and open the Theme and Language pickers; note the pill trigger and the opened surface. 2) Open 全局 AI. 3) Open the Permissions card's permission-mode control and select ask, accept-edits, and auto in turn. 4) Open the Defaults card's Command shell control; inspect the unavailable entries and select an available shell. 5) Dismiss each open menu with Escape and then with an outside press. 6) Close Settings, reopen it, and read both rows. +- **Expected**: Both rows open the same anchored menu surface as the Appearance pickers — the app-drawn frame with the shared radius, elevation, border, and theme tokens, a check mark on the current option, and a hover/keyboard highlight — and never a platform-drawn `` 弹层,因此设置里的每个选择器 + 都以相同方式展开。 - **默认项**卡:主机支持的默认运行模式(Agent / Plan / Goal)、 命令 Shell 选择、链接打开目标、上下文用量显示(剩余或已用)、 回车发送控制和大段文本粘贴阈值。链接打开目标默认使用工作面板浏览器, @@ -87,7 +89,7 @@ 选择稍后变得不可用,使用第一个可用的平台 shell 并显示回退状态。选定的 Shell 可用时,选择器就是唯一的配置状态提示; 只有默认、回退和没有实际可用 Shell 时才显示状态文案。Bash 回合验证其固定的 - ID/dialect 后再执行。 + ID/dialect 后再执行。该行渲染与权限卡和外观选择器相同的菜单选择器。 - 上下文管理**无卡且无控件**(D200 / ADR 0061,由 D203 / ADR 0064)。自动保护始终开启,其预算和 保留限制源自活动模型的窗口,因此有 diff --git a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md index 8c26755a1..c678eacfe 100644 --- a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md @@ -1002,6 +1002,22 @@ unit/integration 测试;代码 pull request 使用有选择且高价值的 E2E - **里程碑**:M4 - **状态**:已记录 +#### E2E-SETTINGS-ai-tab-pickers-use-in-app-menus:全局 AI 下拉使用应用内菜单 + +- **先决条件**:应用正在运行,主机至少报告一个已配置的命令 Shell,以及至少一个在当前平台不可用的目录内 Shell。 +- **步骤**: + 1) 打开设置 → 常规,展开主题和语言选择器,记下药丸触发器与展开后的表面。 + 2) 打开全局 AI。 + 3) 展开权限卡的权限模式控件,依次选择询问、接受编辑、自动。 + 4) 展开默认项卡的命令 Shell 控件;查看不可用条目并选择一个可用的 Shell。 + 5) 先用 Escape、再用外部点击分别关闭打开的菜单。 + 6) 关闭设置后重新打开,读取这两行。 +- **预期**:两行都展开与外观选择器相同的锚定菜单表面 —— 应用绘制的边框,使用共享的圆角、阴影、描边和主题 token,当前选项带勾选标记,并有悬浮/键盘高亮 —— 而不是平台绘制的 `