diff --git a/README.md b/README.md index 446e207f0..c70d3df33 100644 --- a/README.md +++ b/README.md @@ -308,7 +308,7 @@ Full reference: [Interaction and commands](docs/interaction.en.md). `/resume` · `/home` · `/agentview` · `/bg` · `⌸` open the same session manager: workspace rail, live state, filter, ★ pins. Also `/model` `/new` `/compact` `/export` `/btw` `/tree` `/fork` `/rewind` `/settings` `/setup` `/status` `/cost` `/jobs` `/skills` `/mcp` `/provider` `/auth` `/login` `/update`. -`/model` shows models and reasoning effort on one page. DSH has provider tabs with **Recently used** first, moving the current model to the top; `Tab` / `Shift+Tab` select the next / previous provider. Codex and Claude open their model catalogs directly, without provider or recents tabs. `↑/↓` select a model, `←/→` adjust its effort, `Enter` applies the model and any explicit effort draft, and `Esc` cancels. Changing only the model preserves the backend's preference handling. Mouse users can click the available tabs, models, effort levels, and select/cancel hints; the wheel moves through models. +`/model` shows models and reasoning effort on one page. DSH has provider tabs with **Recently used** first, moving the current model to the top; `Tab` / `Shift+Tab` select the next / previous provider. Codex and Claude open their model catalogs directly, without provider or recents tabs. `↑/↓` select a model, `←/→` adjust its effort, `Enter` applies the model and any explicit effort draft, and `Esc` cancels. Changing only the model preserves the backend's preference handling. Mouse users can click the available tabs, models, effort levels, and select/cancel hints; clicking an effort level immediately applies the focused model and that effort and closes the picker. The wheel moves through models. Short terminals keep the focused model and select/cancel actions visible, hiding descriptions and neighboring models first, then the effort row when only two rows remain. Shortcuts sit below the title and effort levels have their own section; DSH, Claude, and Codex use the terminal background while covering the text underneath. diff --git a/README_ZH.md b/README_ZH.md index e29c0f569..d28119737 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -262,7 +262,7 @@ OpenAI 条款约束。完整操作与当前边界:[Codex 后端](docs/codex-ba `/resume` · `/home` · `/agentview` · `/bg` · `⌸` 打开同一个会话管理界面:工作区栏、实时状态、筛选、★ 固定。另有 `/model` `/new` `/compact` `/export` `/btw` `/tree` `/fork` `/rewind` `/settings` `/setup` `/status` `/cost` `/jobs` `/skills` `/mcp` `/provider` `/auth` `/login` `/update`。 -`/model` 同页选择模型与推理等级。DSH 顶部显示提供商标签,**最近使用**排在第一位,打开时将当前模型去重置顶;`Tab` / `Shift+Tab` 切换下一个 / 上一个提供商。Codex 与 Claude 直接展示模型目录,不显示提供商或最近使用标签。`↑/↓` 选择模型,`←/→` 调整推理等级,`Enter` 应用模型及显式调整的推理草稿,`Esc` 取消;只换模型时沿用后端既有偏好处理。鼠标可点击可用的标签、模型、档位及选择/取消提示,滚轮切换模型。 +`/model` 同页选择模型与推理等级。DSH 顶部显示提供商标签,**最近使用**排在第一位,打开时将当前模型去重置顶;`Tab` / `Shift+Tab` 切换下一个 / 上一个提供商。Codex 与 Claude 直接展示模型目录,不显示提供商或最近使用标签。`↑/↓` 选择模型,`←/→` 调整推理等级,`Enter` 应用模型及显式调整的推理草稿,`Esc` 取消;只换模型时沿用后端既有偏好处理。鼠标可点击可用的标签、模型、档位及选择/取消提示;点击推理档位会立即应用当前聚焦模型与该强度,并关闭选择器。滚轮切换模型。 短终端优先保留聚焦模型与选择/取消入口,先隐藏描述和邻项,仅剩两行时再隐藏推理行。 操作提示位于标题下,推理档位独立成区;DSH、Claude、Codex 均沿用终端背景,并遮住底层文字。 diff --git a/docs/user-guide.en.md b/docs/user-guide.en.md index c1b133753..428eee99b 100644 --- a/docs/user-guide.en.md +++ b/docs/user-guide.en.md @@ -481,7 +481,7 @@ Full-screen view of the whole session timeline (doesn't pollute scrollback); key - `/model`: selector. **Switching = fork the session** (history kept, only routing changes, the old session stays in `/resume`); persisted to `~/.dsh-tui/model.json` and reused by the next launch (a `provider`/`model` pair in cordis.yml/profile is only the first-run deployment default). -- DSH provider tabs start with **Recently used**, and `Tab` / `Shift+Tab` switch providers; Codex and Claude show their catalogs directly without these tabs. All backends use `↑/↓` to select a model, `←/→` to adjust its effort, `Enter` to apply both, and `Esc` to cancel the draft. Click the available tabs, models, effort levels, or select/cancel hints; the wheel moves model focus. +- DSH provider tabs start with **Recently used**, and `Tab` / `Shift+Tab` switch providers; Codex and Claude show their catalogs directly without these tabs. All backends use `↑/↓` to select a model, `←/→` to adjust its effort, `Enter` to apply both, and `Esc` to cancel the draft. Click the available tabs, models, effort levels, or select/cancel hints; clicking an effort level immediately applies the focused model and that effort and closes the picker. The wheel moves model focus. - Switching is rejected mid-turn. - `/preset` options: `standard` (default full features), `ptc`, `minimal` (the kernel's Minimal preset: one persistent-shell tool only, no compaction, no plan mode — and therefore no compaction and no tool-result pruning, so a long session can hit the context limit and `/compact` plus questions are unavailable; Help and `/` completion mark the command, and entering the preset says so once), `cordis`, `liangshen` (Liangshen mode). diff --git a/docs/user-guide.md b/docs/user-guide.md index 76fecb026..d8702e5f7 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -443,7 +443,7 @@ dsh-TUI 不预装通用技能;`/skills` 浏览 DSH 发现的技能,可直调 - `/model`:选择器。**切换 = fork 会话续聊**(历史保留、仅换路由,旧会话留在 `/resume`); 持久化 `~/.dsh-tui/model.json`,下次启动直接沿用它(cordis.yml/profile 中的 `provider`/`model` 只是首次启动的部署默认值)。 -- DSH 的顶部标签以「最近使用」开头,再列各提供商,`Tab` / `Shift+Tab` 切换提供商;Codex 与 Claude 直接显示模型目录,不显示这些标签。所有后端均用 `↑/↓` 选模型、`←/→` 调推理等级、`Enter` 一并应用、`Esc` 取消草稿,支持点击可用的标签、模型、档位与选择/取消提示,以及滚轮移动模型焦点。 +- DSH 的顶部标签以「最近使用」开头,再列各提供商,`Tab` / `Shift+Tab` 切换提供商;Codex 与 Claude 直接显示模型目录,不显示这些标签。所有后端均用 `↑/↓` 选模型、`←/→` 调推理等级、`Enter` 一并应用、`Esc` 取消草稿,支持点击可用的标签、模型、档位与选择/取消提示,以及滚轮移动模型焦点。点击推理档位会立即应用当前聚焦模型与该强度,并关闭选择器。 - 回合运行中切换会被拒绝。 - `/preset` 可选:`standard`(默认全功能)、`ptc`、`minimal`(内核「极简模式」:只暴露一个持久 shell 工具,无 compaction、无计划模式;也因此没有压缩、工具结果不剪枝——长会话可能撞上下文上限,`/compact` 与问卷不可用,Help 与 `/` 补全会标注,进入该预设时提示一次)、 `cordis`、`liangshen`(梁神模式)。 diff --git a/guide/dsh-tui-guide/user-guide.en.md b/guide/dsh-tui-guide/user-guide.en.md index c1b133753..428eee99b 100644 --- a/guide/dsh-tui-guide/user-guide.en.md +++ b/guide/dsh-tui-guide/user-guide.en.md @@ -481,7 +481,7 @@ Full-screen view of the whole session timeline (doesn't pollute scrollback); key - `/model`: selector. **Switching = fork the session** (history kept, only routing changes, the old session stays in `/resume`); persisted to `~/.dsh-tui/model.json` and reused by the next launch (a `provider`/`model` pair in cordis.yml/profile is only the first-run deployment default). -- DSH provider tabs start with **Recently used**, and `Tab` / `Shift+Tab` switch providers; Codex and Claude show their catalogs directly without these tabs. All backends use `↑/↓` to select a model, `←/→` to adjust its effort, `Enter` to apply both, and `Esc` to cancel the draft. Click the available tabs, models, effort levels, or select/cancel hints; the wheel moves model focus. +- DSH provider tabs start with **Recently used**, and `Tab` / `Shift+Tab` switch providers; Codex and Claude show their catalogs directly without these tabs. All backends use `↑/↓` to select a model, `←/→` to adjust its effort, `Enter` to apply both, and `Esc` to cancel the draft. Click the available tabs, models, effort levels, or select/cancel hints; clicking an effort level immediately applies the focused model and that effort and closes the picker. The wheel moves model focus. - Switching is rejected mid-turn. - `/preset` options: `standard` (default full features), `ptc`, `minimal` (the kernel's Minimal preset: one persistent-shell tool only, no compaction, no plan mode — and therefore no compaction and no tool-result pruning, so a long session can hit the context limit and `/compact` plus questions are unavailable; Help and `/` completion mark the command, and entering the preset says so once), `cordis`, `liangshen` (Liangshen mode). diff --git a/guide/dsh-tui-guide/user-guide.md b/guide/dsh-tui-guide/user-guide.md index 76fecb026..d8702e5f7 100644 --- a/guide/dsh-tui-guide/user-guide.md +++ b/guide/dsh-tui-guide/user-guide.md @@ -443,7 +443,7 @@ dsh-TUI 不预装通用技能;`/skills` 浏览 DSH 发现的技能,可直调 - `/model`:选择器。**切换 = fork 会话续聊**(历史保留、仅换路由,旧会话留在 `/resume`); 持久化 `~/.dsh-tui/model.json`,下次启动直接沿用它(cordis.yml/profile 中的 `provider`/`model` 只是首次启动的部署默认值)。 -- DSH 的顶部标签以「最近使用」开头,再列各提供商,`Tab` / `Shift+Tab` 切换提供商;Codex 与 Claude 直接显示模型目录,不显示这些标签。所有后端均用 `↑/↓` 选模型、`←/→` 调推理等级、`Enter` 一并应用、`Esc` 取消草稿,支持点击可用的标签、模型、档位与选择/取消提示,以及滚轮移动模型焦点。 +- DSH 的顶部标签以「最近使用」开头,再列各提供商,`Tab` / `Shift+Tab` 切换提供商;Codex 与 Claude 直接显示模型目录,不显示这些标签。所有后端均用 `↑/↓` 选模型、`←/→` 调推理等级、`Enter` 一并应用、`Esc` 取消草稿,支持点击可用的标签、模型、档位与选择/取消提示,以及滚轮移动模型焦点。点击推理档位会立即应用当前聚焦模型与该强度,并关闭选择器。 - 回合运行中切换会被拒绝。 - `/preset` 可选:`standard`(默认全功能)、`ptc`、`minimal`(内核「极简模式」:只暴露一个持久 shell 工具,无 compaction、无计划模式;也因此没有压缩、工具结果不剪枝——长会话可能撞上下文上限,`/compact` 与问卷不可用,Help 与 `/` 补全会标注,进入该预设时提示一次)、 `cordis`、`liangshen`(梁神模式)。 diff --git a/scripts/verify-model-picker-ui.tsx b/scripts/verify-model-picker-ui.tsx index a37ab3dbf..0e5c0f5b6 100644 --- a/scripts/verify-model-picker-ui.tsx +++ b/scripts/verify-model-picker-ui.tsx @@ -1,7 +1,7 @@ /** * /model through the real Chat/channel/rendering path, with a fake LLM catalog: * recents-first tabs, current-model promotion, forward/backward wrapping, independent model/effort drafts, - * same-batch navigation/confirmation, cancellation, mouse picks and wheel, + * same-batch navigation/confirmation, cancellation, single-click effort confirmation and wheel, * focus windowing and resize in inline/fullscreen at 100 and 36 columns; * header shortcuts, readable effort colors and the original terminal background. * Claude/Codex use a flat catalog, focus the current model after loading, @@ -329,10 +329,19 @@ async function scenario(fullscreen: boolean, columns: number): Promise { if (fullscreen) { await click('Beta 01') await check('mouse focuses a model', () => focused('Beta 01') && inverse('OFF')) + assert.deepEqual(switches, ['alpha/a1'], 'clicking a model only updates its draft') await click('MAX') - await check('mouse selects effort draft', () => inverse('MAX')) - await click('选择') - await check('mouse applies model and effort', () => channel.model === 'b1' && channel.reasoningEffort === 'max' && hit('最近使用') === undefined) + await check('one effort click applies the focused model and effort and closes the picker', () => channel.model === 'b1' && channel.reasoningEffort === 'max' && hit('最近使用') === undefined) + assert.deepEqual(switches, ['alpha/a1', 'beta/b1']) + assert.deepEqual(effortPicks, ['high', 'max']) + assert.equal(JSON.parse(readFileSync(join(prefsDir, 'effort.json'), 'utf8')).effort, 'max') + await open('Beta 01', 'MAX') + await click('beta / Beta 01') + await check('clicking the current model keeps the picker open', () => focused('Beta 01') && inverse('MAX')) + await click('MAX') + await check('clicking the selected effort also confirms and closes', () => hit('最近使用') === undefined && switches.length === 3 && effortPicks.length === 3) + assert.deepEqual(switches, ['alpha/a1', 'beta/b1', 'beta/b1']) + assert.deepEqual(effortPicks, ['high', 'max', 'max']) stdin.write('/model') await check('mouse follow-up command', () => hit('/model') !== undefined) stdin.write('\r') @@ -441,9 +450,9 @@ async function backendSurface(backendId: 'claude' | 'codex', fullscreen: boolean await click('Model 01') assert.ok(await settled(() => focused('Model 01') && panelUsesDefaultBackground(term)), `${label}: mouse selects a model`) await click('HIGH') - await click('选择') - assert.ok(await settled(() => !text().includes('推理强度') && picks.length === 2 && effortPicks.at(-1) === 'high'), `${label}: mouse applies model and reasoning`) + assert.ok(await settled(() => !text().includes('推理强度') && picks.length === 2 && effortPicks.at(-1) === 'high'), `${label}: one effort click applies model and reasoning and closes the picker`) assert.deepEqual(picks, ['m0', 'm1']) + assert.deepEqual(effortPicks, ['high']) } else { assert.deepEqual(effortPicks, [], `${label}: no reasoning changes without an explicit draft`) } diff --git a/src/components/ModelPicker.tsx b/src/components/ModelPicker.tsx index 40a75510e..bd36b025a 100644 --- a/src/components/ModelPicker.tsx +++ b/src/components/ModelPicker.tsx @@ -184,6 +184,7 @@ export function ModelPicker({ muted={false} cursor={caretZone === 'effort'} onPick={onEffort} + onConfirm={onConfirm} /> )} @@ -197,7 +198,7 @@ export function ModelPicker({ } /** Keep the active cell visible when a provider or effort strip exceeds its width. */ -function PickerTabs({ labels, focusIndex, width, muted = true, cursor = false, onPick }: { +function PickerTabs({ labels, focusIndex, width, muted = true, cursor = false, onPick, onConfirm }: { labels: readonly string[] focusIndex: number width: number @@ -205,6 +206,8 @@ function PickerTabs({ labels, focusIndex, width, muted = true, cursor = false, o /** This strip owns the native caret — its region was the last one used. */ cursor?: boolean onPick(index: number): void + /** Confirm a clicked label; overflow arrows only navigate. */ + onConfirm?(): void }): React.ReactNode { const singleLines = labels.map(label => label.replace(/[\r\n]+/g, ' ')) const allFit = singleLines.reduce((sum, label) => sum + stringWidth(label) + 2, 0) @@ -225,7 +228,10 @@ function PickerTabs({ labels, focusIndex, width, muted = true, cursor = false, o isFocused={start + index === focusIndex} muted={muted} cursor={cursor} - onPick={() => onPick(start + index)} + onPick={() => { + onPick(start + index) + onConfirm?.() + }} /> ))} {end < cells.length ? { event.stopImmediatePropagation(); onPick(end) }}>› : null}