@aiwayds/dsh-tui-pi 2.9.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/README.zh-CN.md +4 -3
- package/lib/index.js +127 -83
- package/lib/index.js.map +1 -1
- package/lib/keymap.d.ts +11 -4
- package/lib/keymap.js +20 -9
- package/lib/keymap.js.map +1 -1
- package/lib/preset.d.ts +11 -5
- package/lib/preset.js +16 -5
- package/lib/preset.js.map +1 -1
- package/lib/session.d.ts +32 -0
- package/lib/session.js +58 -0
- package/lib/session.js.map +1 -1
- package/lib/stop-dialog.d.ts +94 -0
- package/lib/stop-dialog.js +194 -0
- package/lib/stop-dialog.js.map +1 -0
- package/lib/subagent-viewer.d.ts +17 -5
- package/lib/subagent-viewer.js +36 -8
- package/lib/subagent-viewer.js.map +1 -1
- package/lib/theme-settings.d.ts +18 -0
- package/lib/theme-settings.js +38 -0
- package/lib/theme-settings.js.map +1 -1
- package/lib/workspace-presets.d.ts +61 -0
- package/lib/workspace-presets.js +110 -0
- package/lib/workspace-presets.js.map +1 -0
- package/package.json +1 -1
- package/skills/dsh-tui-pi-config/SKILL.md +8 -2
package/README.md
CHANGED
|
@@ -105,11 +105,11 @@ dsh plugin --profile tui add @aiwayds/dsh-topics-memory
|
|
|
105
105
|
| Key | Action |
|
|
106
106
|
|---|---|
|
|
107
107
|
| `Enter` | Send the prompt |
|
|
108
|
-
| `Esc` | **Double-press to stop**
|
|
109
|
-
| `Ctrl+C` | Mid-
|
|
108
|
+
| `Esc` | **Double-press to stop everything** — the first press arms, the second opens a confirmation dialog naming what is running (main turn + subagent count); `Enter` there stops the main turn AND every running subagent, `Esc` keeps everything running. A popup open closes it instead. Works while only background subagents run. |
|
|
109
|
+
| `Ctrl+C` | Mid-task: first press stops (same everything-stop, no dialog), second quits; idle: clears editor / quits. Held-key auto-repeat never quits. |
|
|
110
110
|
| `Ctrl+D` | Quit (only when the editor is empty) |
|
|
111
111
|
| `Ctrl+L` | Open the model/think picker |
|
|
112
|
-
| `Ctrl+G` | Open the subagent picker (viewer `Enter` opens steer) |
|
|
112
|
+
| `Ctrl+G` | Open the subagent picker (viewer `Enter` opens steer, `x ×2` stops that subagent while it runs / closes when settled) |
|
|
113
113
|
| `Ctrl+O` | Pending-message queue (s steer now · d remove) |
|
|
114
114
|
| `Ctrl+Shift+F` | Transcript search (`Enter`/`Ctrl+G` next · `Shift+Enter`/`Ctrl+Shift+G` previous · `Esc` close) |
|
|
115
115
|
| `↑` / `↓` | Browse submitted-message history |
|
|
@@ -132,6 +132,7 @@ Every knob lives under the `dsh-tui` settings namespace in `~/.dsh/settings.yaml
|
|
|
132
132
|
| `footerHints` | all `true` | Per-segment footer hint toggles: `send`/`stop`/`quit`/`quitEmpty`/`subagents`/`search`/`history` |
|
|
133
133
|
| `cacheHitMode` | `lastMessage` | Footer CH segment scope: `lastMessage` — the latest assistant message's cache-hit rate, matching the pi-tui footer (default); `session` — cumulative over the whole session |
|
|
134
134
|
| `iconSet` | `auto` | `auto`/`nerdfont`/`plain` — powerline glyphs adapt to your font; install a Nerd Font with `node scripts/install-font.mjs` |
|
|
135
|
+
| `rememberPreset` | `true` | Remember the last `/preset` selection **per workspace** (keyed by directory) and start the next launch there — the first session composes under it instead of the server-side default. `false` always starts on the server default. The memory itself lives in `$DSH_HOME/workspace-presets.json` |
|
|
135
136
|
| `favoriteModels` | `[]` | Favorite models (`provider/id`), pinned to the top of the `/model` picker |
|
|
136
137
|
| `hiddenModels` | `[]` | Hidden models (`provider/id`), moved to the picker's Hidden section (`f` favorites / `h` hides inside the picker — both persist to these keys) |
|
|
137
138
|
|
package/README.zh-CN.md
CHANGED
|
@@ -102,11 +102,11 @@ dsh plugin --profile tui add @aiwayds/dsh-topics-memory
|
|
|
102
102
|
| 按键 | 功能 |
|
|
103
103
|
|---|---|
|
|
104
104
|
| `Enter` | 发送 prompt |
|
|
105
|
-
| `Esc` |
|
|
106
|
-
| `Ctrl+C` |
|
|
105
|
+
| `Esc` | **双击停止一切 LLM 工作**——第一击进入待发状态,第二击弹出确认框(列明正在运行的主 turn 与子代理数);`Enter` 确认后停止主 turn **和**所有运行中的子代理,`Esc` 维持运行。仅有后台子代理在跑时同样有效。有 popup 打开时双击改为关闭它 |
|
|
106
|
+
| `Ctrl+C` | 任务中:第一次停止(同一"停止一切",无确认框),第二次退出;空闲时:清空编辑器 / 退出。长按自动重复绝不会触发退出。 |
|
|
107
107
|
| `Ctrl+D` | 退出(仅在编辑器为空时) |
|
|
108
108
|
| `Ctrl+L` | 打开 model/think 选择器 |
|
|
109
|
-
| `Ctrl+G` | 打开 subagent 选择器(查看器内 `Enter` 打开 steer
|
|
109
|
+
| `Ctrl+G` | 打开 subagent 选择器(查看器内 `Enter` 打开 steer;子代理运行中 `x ×2` 停止它,已结束后 `x ×2` 关闭面板) |
|
|
110
110
|
| `Ctrl+O` | 待发消息队列(`s` 立即 steer · `d` 移除) |
|
|
111
111
|
| `Ctrl+Shift+F` | 全文搜索(`Enter`/`Ctrl+G` 下一个 · `Shift+Enter`/`Ctrl+Shift+G` 上一个 · `Esc` 关闭) |
|
|
112
112
|
| `↑` / `↓` | 浏览已提交消息历史 |
|
|
@@ -129,6 +129,7 @@ dsh plugin --profile tui add @aiwayds/dsh-topics-memory
|
|
|
129
129
|
| `footerHints` | 全 `true` | footer 快捷键提示分段开关:`send`/`stop`/`quit`/`quitEmpty`/`subagents`/`search`/`history` |
|
|
130
130
|
| `cacheHitMode` | `lastMessage` | footer CH 段口径:`lastMessage`——最新一条 assistant 消息的缓存命中率(与 pi-tui footer 一致,默认);`session`——全会话累计 |
|
|
131
131
|
| `iconSet` | `auto` | `auto`/`nerdfont`/`plain`——powerline 字形自适应你的字体;用 `node scripts/install-font.mjs` 安装 Nerd Font |
|
|
132
|
+
| `rememberPreset` | `true` | 按**工作区**(目录)记住最后一次 `/preset` 的选择,下次同目录启动直接用它——首个会话按它组合,而不是服务端默认。`false` 则始终用服务端默认。记忆本体存于 `$DSH_HOME/workspace-presets.json` |
|
|
132
133
|
| `favoriteModels` | `[]` | 收藏模型(`provider/id`),钉在 `/model` 选择器顶部 |
|
|
133
134
|
| `hiddenModels` | `[]` | 隐藏模型(`provider/id`),移入 `/model` 的 Hidden 区(选择器内 `f` 收藏 / `h` 隐藏,即写这两个键) |
|
|
134
135
|
|
package/lib/index.js
CHANGED
|
@@ -27,7 +27,8 @@ import { AGENT_TICK_MS, LiveWidgets } from "./live-widgets.js";
|
|
|
27
27
|
import { displayPermissionPreset } from "./permission.js";
|
|
28
28
|
import { pickEffort, pickModel, pickPermission, pickPreset, pickTheme } from "./selectors.js";
|
|
29
29
|
import { DshSessionBridge, persistDefaultModel, stashSessionIdForReload, takeStashedSessionId } from "./session.js";
|
|
30
|
-
import { currentCacheHitMode, currentThemePreference, readFooterHintsPreference, readIconSetPreference, readPanelHeightPreference, readSessionManagementExplicit, readSubagentLimits, readThemePreference, registerThemeSettings, writeThemePreference, } from "./theme-settings.js";
|
|
30
|
+
import { currentCacheHitMode, currentRememberPreset, currentThemePreference, readFooterHintsPreference, readIconSetPreference, readPanelHeightPreference, readRememberPreset, readSessionManagementExplicit, readSubagentLimits, readThemePreference, registerThemeSettings, writeThemePreference, } from "./theme-settings.js";
|
|
31
|
+
import { loadWorkspacePresets, rememberedPresetFor, saveWorkspacePresets, withRememberedPreset, workspacePresetsPath, } from "./workspace-presets.js";
|
|
31
32
|
import { applyIconSet, resolveIconSet, stopIcon } from "./icons.js";
|
|
32
33
|
import { detectNerdFontAvailable } from "./font-detect.js";
|
|
33
34
|
import { openAgentManager } from "./agents.js";
|
|
@@ -42,7 +43,7 @@ import { collectStartupSummary, formatResumeCommand, parseResumeArg, resolveProf
|
|
|
42
43
|
import { inspectPersistedSession, pickPersistedSession, sessionLogRoot, showSessionInfo } from "./sessions.js";
|
|
43
44
|
import { isCorruptLogError, locateSessionLog, repairFailureNotice, repairSessionLog, } from "./log-repair.js";
|
|
44
45
|
import { openRepairConfirmDialog } from "./repair-dialog.js";
|
|
45
|
-
import { WriterLockedError } from "./writer-lock.js";
|
|
46
|
+
import { WriterLockedError, projectKeyFor } from "./writer-lock.js";
|
|
46
47
|
import { emitNotice } from "./notice-bridge.js";
|
|
47
48
|
import { applySubagentPolicy } from "./subagent-policy.js";
|
|
48
49
|
import { openSubagentViewer } from "./subagent-viewer.js";
|
|
@@ -62,6 +63,7 @@ import { keybindingsPath, loadKeyBindings, openHotkeysManager } from "./hotkeys.
|
|
|
62
63
|
import { startTui } from "./tui.js";
|
|
63
64
|
import { currentPreset, fetchPresetRoster, findPresetByName, formatPresetLabel, initialPresetIndex, peekNextPreset } from "./preset.js";
|
|
64
65
|
import { completedTurnSeed, openPresetConfirmDialog, performPresetSwitch } from "./preset-dialog.js";
|
|
66
|
+
import { openStopConfirmDialog } from "./stop-dialog.js";
|
|
65
67
|
import { registerAskUserProvider } from "./ask-user.js";
|
|
66
68
|
import { checkHostSupport } from "./host-version.js";
|
|
67
69
|
export const name = 'dsh-tui-pi';
|
|
@@ -72,15 +74,6 @@ export const name = 'dsh-tui-pi';
|
|
|
72
74
|
* ~30-50ms) aborts the quit before this timer ever fires.
|
|
73
75
|
*/
|
|
74
76
|
const QUIT_CONFIRM_MS = 200;
|
|
75
|
-
/**
|
|
76
|
-
* Delay (ms) before the double-Esc stop actually fires — the same held-key
|
|
77
|
-
* defence as the Ctrl+C quit: the OS repeat delay (~183ms-2s) puts the first
|
|
78
|
-
* auto-repeat of a held Esc exactly at the 500ms stop-window boundary; the
|
|
79
|
-
* stop is confirmed for this long and a follow-up `key-repeat` (betraying
|
|
80
|
-
* the held key) aborts it. A second HUMAN-speed press during the window
|
|
81
|
-
* fires the stop immediately (see `interrupt-cancel`).
|
|
82
|
-
*/
|
|
83
|
-
const STOP_CONFIRM_MS = 200;
|
|
84
77
|
/**
|
|
85
78
|
* The TUI drives the agent factory and registers slash commands. The render
|
|
86
79
|
* effect also touches `ctx.systemPrompt` (ask-user guidance section) and the
|
|
@@ -103,7 +96,7 @@ const SKILL_RESOURCE_BASE = {
|
|
|
103
96
|
};
|
|
104
97
|
const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true };
|
|
105
98
|
/** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
|
|
106
|
-
const SKILL_DESCRIPTION = 'dsh TUI 增强套件(@aiwayds/dsh-tui-pi)使用与配置指南。凡涉及 TUI 主题/面板/footer、子代理并发与轮数限制、模型收藏与隐藏、会话保留清理与 /resume
|
|
99
|
+
const SKILL_DESCRIPTION = 'dsh TUI 增强套件(@aiwayds/dsh-tui-pi)使用与配置指南。凡涉及 TUI 主题/面板/footer、子代理并发与轮数限制、模型收藏与隐藏、会话保留清理与 /resume 过滤、preset 记忆,或要配置 dsh-tui 段时先读本指南:settings.yaml 顶层 `dsh-tui:` 段 13 键(theme/panelHeight/maxAgents/maxRounds/disableSubagent/footerHints/cacheHitMode/iconSet/rememberPreset/favoriteModels/hiddenModels/retention/resume)、DSH_TUI_* 环境变量、ask_user_question 快速上手向导、keybindings.json 与 /hotkeys。触发词:tui、主题、theme、面板、footer、收藏模型、隐藏模型、保留策略、panelHeight、resume、preset。';
|
|
107
100
|
const SKILL_CANDIDATE = {
|
|
108
101
|
name: SKILL_PROVIDER_NAME,
|
|
109
102
|
description: SKILL_DESCRIPTION,
|
|
@@ -290,6 +283,17 @@ export function apply(ctx) {
|
|
|
290
283
|
const nerdfontAvailable = await detectNerdFontAvailable();
|
|
291
284
|
applyIconSet(resolveIconSet(iconSetPreference, nerdfontAvailable));
|
|
292
285
|
const presetRoster = await fetchPresetRoster();
|
|
286
|
+
// Preset memory (`dsh-tui.rememberPreset`, default ON): the last /preset
|
|
287
|
+
// selection committed in THIS workspace (keyed by the session backend's
|
|
288
|
+
// project key of the cwd) becomes the launch selection — runTui seeds it
|
|
289
|
+
// into both the footer state AND `bridge.setAgentPreset`, so the FIRST
|
|
290
|
+
// session of the day composes under the remembered preset instead of the
|
|
291
|
+
// server default. A stale id (preset renamed/removed upstream) degrades
|
|
292
|
+
// to the stock default-selection behavior; memory off skips the read
|
|
293
|
+
// entirely.
|
|
294
|
+
const rememberedPresetId = (await readRememberPreset(ctx))
|
|
295
|
+
? rememberedPresetFor(loadWorkspacePresets(workspacePresetsPath()), projectKeyFor(process.cwd()))
|
|
296
|
+
: undefined;
|
|
293
297
|
// Startup configuration snapshot (mcp/skills/plugins readout under the
|
|
294
298
|
// welcome banner): best-effort by contract — no loader service or a
|
|
295
299
|
// throwing entry walk degrades to undefined and the banner renders
|
|
@@ -302,7 +306,7 @@ export function apply(ctx) {
|
|
|
302
306
|
const cmdlineArgs = ctx.get('cmdlineArgs')?.get();
|
|
303
307
|
let disposer;
|
|
304
308
|
try {
|
|
305
|
-
disposer = runTui(themePreference, panelHeight, footerHints, nerdfontAvailable, presetRoster, startupInfo, cmdlineArgs);
|
|
309
|
+
disposer = runTui(themePreference, panelHeight, footerHints, nerdfontAvailable, presetRoster, startupInfo, cmdlineArgs, rememberedPresetId);
|
|
306
310
|
}
|
|
307
311
|
catch (error) {
|
|
308
312
|
// An async-effect failure after startTui would otherwise orphan the
|
|
@@ -323,7 +327,7 @@ export function apply(ctx) {
|
|
|
323
327
|
* reaches cordis. Returns the effect disposer handed back to cordis on
|
|
324
328
|
* teardown.
|
|
325
329
|
*/
|
|
326
|
-
function runTui(themePreference, panelHeight, footerHints, nerdfontAvailable, presetRoster, startupInfo, cmdlineArgs) {
|
|
330
|
+
function runTui(themePreference, panelHeight, footerHints, nerdfontAvailable, presetRoster, startupInfo, cmdlineArgs, rememberedPresetId) {
|
|
327
331
|
// User keybindings (`~/.dsh/keybindings.json`): a partial map of the app
|
|
328
332
|
// keys, read once per TUI start — `/reload` re-runs apply() and re-reads
|
|
329
333
|
// it. Broken entries surface as notices instead of breaking startup.
|
|
@@ -333,16 +337,23 @@ export function apply(ctx) {
|
|
|
333
337
|
// switching is an explicit, confirmed action). The roster is fetched once
|
|
334
338
|
// at startup from the api-proxy service; an empty roster (no service / no
|
|
335
339
|
// presets) disables the feature gracefully (/preset errors, footer shows
|
|
336
|
-
// plain "dsh").
|
|
337
|
-
// preset when
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
340
|
+
// plain "dsh"). The initial selection is the workspace's REMEMBERED
|
|
341
|
+
// preset when preset memory is on and the id is still on the roster
|
|
342
|
+
// (initialPresetIndex prefers it), else the `standard` entry, else the
|
|
343
|
+
// first-scanned one. Without memory the launch selection is a local
|
|
344
|
+
// display only: before the user switches via /preset, NO
|
|
345
|
+
// `meta.agentPreset` is sent at session create, so the server-side
|
|
346
|
+
// default (`agent-presets.default` settings / deployment config)
|
|
347
|
+
// governs. WITH memory the remembered id is ALSO seeded into the bridge
|
|
348
|
+
// below (bridge.setAgentPreset), which is the point: the first session
|
|
349
|
+
// of the launch composes under the remembered preset, not the server
|
|
350
|
+
// default. We deliberately seed ONLY a remembered id, never
|
|
351
|
+
// DEFAULT_PRESET_ID — that would override the server-side default with
|
|
352
|
+
// a client-side assumption.
|
|
353
|
+
const rememberedPresetEntry = rememberedPresetId !== undefined
|
|
354
|
+
? presetRoster.find(preset => preset.id === rememberedPresetId)
|
|
355
|
+
: undefined;
|
|
356
|
+
const presetState = { roster: presetRoster, index: initialPresetIndex(presetRoster, rememberedPresetEntry?.id) };
|
|
346
357
|
// Docked-modal liveness (the ask-user questions panel): while a question
|
|
347
358
|
// is pending the panel owns the keyboard — the keymap treats it like an
|
|
348
359
|
// open overlay (see tui.ts), and refocusEditor must not steal focus from
|
|
@@ -353,51 +364,44 @@ export function apply(ctx) {
|
|
|
353
364
|
void submit(text);
|
|
354
365
|
},
|
|
355
366
|
keyBindings: keyBindings.bindings,
|
|
356
|
-
// App-level keys (pi interrupt chain): Esc
|
|
367
|
+
// App-level keys (pi interrupt chain): Esc while LLM work runs is a
|
|
357
368
|
// deliberate double-press — the first press arms the window (with
|
|
358
|
-
// feedback), the second within 500ms
|
|
359
|
-
//
|
|
360
|
-
// is
|
|
361
|
-
//
|
|
369
|
+
// feedback), the second within 500ms opens the stop-everything
|
|
370
|
+
// CONFIRMATION dialog (main turn + every running subagent); stopping
|
|
371
|
+
// everything is a read-before-confirm action, not a timer race.
|
|
372
|
+
// popup/autocomplete first, and an idle Esc (any editor) is an
|
|
373
|
+
// anti-misfire no-op. "Running" covers background subagents too —
|
|
374
|
+
// children keep working after the parent turn ends, and the stop must
|
|
375
|
+
// still reach them (bridge.hasRunningWork).
|
|
376
|
+
// Ctrl+C cancels a running task or clears the editor — a second press
|
|
362
377
|
// within 500ms quits — and Ctrl+D quits only on an empty editor.
|
|
363
378
|
// Ctrl+G opens the subagent picker while children run. All decisions
|
|
364
379
|
// live in keymap.ts.
|
|
365
|
-
isRunning: () => bridge.
|
|
380
|
+
isRunning: () => bridge.hasRunningWork(),
|
|
366
381
|
getRunningAgents: () => bridge.getLiveChildren().length,
|
|
367
382
|
hasSession: () => bridge.getSessionId() !== undefined,
|
|
368
383
|
dockedModalActive: () => askUserActive,
|
|
369
384
|
onKeyAction: (action) => {
|
|
370
385
|
switch (action.kind) {
|
|
371
386
|
case 'interrupt-arm-stop':
|
|
372
|
-
// First Esc while
|
|
373
|
-
// a second Esc within the window
|
|
374
|
-
// never kill a running
|
|
375
|
-
// press from feeling dead and states the
|
|
376
|
-
|
|
387
|
+
// First Esc while LLM work runs: the stop is ARMED, not fired —
|
|
388
|
+
// a second Esc within the window opens the confirmation dialog,
|
|
389
|
+
// so a stray Esc can never kill a running task. The notice keeps
|
|
390
|
+
// the armed first press from feeling dead and states the
|
|
391
|
+
// contract explicitly.
|
|
392
|
+
renderer.renderNotice('Press Esc again to stop all LLM work', 'info');
|
|
377
393
|
break;
|
|
378
394
|
case 'interrupt-cancel': {
|
|
379
|
-
// Second Esc while
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
// deferred by the confirm window.
|
|
390
|
-
if (stopConfirmTimer !== undefined) {
|
|
391
|
-
clearTimeout(stopConfirmTimer);
|
|
392
|
-
stopConfirmTimer = undefined;
|
|
393
|
-
void stopTask();
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
renderer.renderNotice(`${stopIcon()} stopping — Esc was double-pressed`, 'info');
|
|
397
|
-
stopConfirmTimer = setTimeout(() => {
|
|
398
|
-
stopConfirmTimer = undefined;
|
|
399
|
-
void stopTask();
|
|
400
|
-
}, STOP_CONFIRM_MS);
|
|
395
|
+
// Second Esc while LLM work runs (main turn and/or subagents):
|
|
396
|
+
// open the stop-everything CONFIRMATION dialog. The old flow
|
|
397
|
+
// auto-fired the stop after a 200ms window — that confirmed the
|
|
398
|
+
// press timing, never the intent, and the gesture's reach had
|
|
399
|
+
// grown to every running subagent. The dialog states the blast
|
|
400
|
+
// radius (what is running right now) and waits for Enter; Esc
|
|
401
|
+
// inside it cancels and keeps everything running. While the
|
|
402
|
+
// overlay is up the keymap yields it every app key, so further
|
|
403
|
+
// Esc presses land in the dialog, never straight at the task.
|
|
404
|
+
void confirmAndStop();
|
|
401
405
|
break;
|
|
402
406
|
}
|
|
403
407
|
case 'ctrl-c-cancel': {
|
|
@@ -437,18 +441,14 @@ export function apply(ctx) {
|
|
|
437
441
|
}
|
|
438
442
|
case 'key-repeat':
|
|
439
443
|
// Auto-repeat of a held key — betrays a hold, never a double:
|
|
440
|
-
// abort a pending Ctrl+C quit confirmation
|
|
441
|
-
//
|
|
444
|
+
// abort a pending Ctrl+C quit confirmation. (The double-Esc stop
|
|
445
|
+
// has no timer anymore — its confirmation is the dialog, which
|
|
446
|
+
// ignores held keys through the keymap's repeat filter.)
|
|
442
447
|
if (action.key === 'ctrl-c' && quitConfirmTimer !== undefined) {
|
|
443
448
|
clearTimeout(quitConfirmTimer);
|
|
444
449
|
quitConfirmTimer = undefined;
|
|
445
450
|
renderer.renderNotice('quit aborted — Ctrl+C was held, not double-pressed', 'info');
|
|
446
451
|
}
|
|
447
|
-
if (action.key === 'escape' && stopConfirmTimer !== undefined) {
|
|
448
|
-
clearTimeout(stopConfirmTimer);
|
|
449
|
-
stopConfirmTimer = undefined;
|
|
450
|
-
renderer.renderNotice('stop aborted — Esc was held, not double-pressed', 'info');
|
|
451
|
-
}
|
|
452
452
|
break;
|
|
453
453
|
case 'ctrl-d-quit':
|
|
454
454
|
void disposeAndExit(0);
|
|
@@ -459,7 +459,8 @@ export function apply(ctx) {
|
|
|
459
459
|
break;
|
|
460
460
|
case 'subagent-viewer':
|
|
461
461
|
// Ctrl+G: open the subagent picker → transcript viewer while
|
|
462
|
-
// children run (modal overlay; Esc
|
|
462
|
+
// children run (modal overlay; Esc closes, x ×2 stops the child
|
|
463
|
+
// while it runs / closes when settled).
|
|
463
464
|
void openSubagentViewer(ctx, ui.tui, ui.theme, bridge, refocusEditor);
|
|
464
465
|
break;
|
|
465
466
|
case 'queue-panel': {
|
|
@@ -551,27 +552,35 @@ export function apply(ctx) {
|
|
|
551
552
|
*/
|
|
552
553
|
let quitConfirmTimer;
|
|
553
554
|
/**
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
* could theoretically be in flight.
|
|
555
|
+
* The double-Esc stop flow: the confirmation dialog first (it states
|
|
556
|
+
* what is running and waits for Enter), then the everything-stop. The
|
|
557
|
+
* dialog is modal, so a mashing user cannot double-commit — every key
|
|
558
|
+
* while it is up goes to the dialog, and a cancel simply returns.
|
|
559
559
|
*/
|
|
560
|
-
|
|
560
|
+
const confirmAndStop = async () => {
|
|
561
|
+
const outcome = await openStopConfirmDialog(ui.tui, ui.theme, bridge.isRunning(), bridge.getLiveChildren().length, refocusEditor);
|
|
562
|
+
if (outcome === 'stop')
|
|
563
|
+
await stopTask();
|
|
564
|
+
};
|
|
561
565
|
/**
|
|
562
|
-
* Stop the whole task
|
|
563
|
-
* client's stop button
|
|
564
|
-
* true })` via the bridge
|
|
565
|
-
*
|
|
566
|
+
* Stop the whole task — the everything-stop: the main turn (mirroring
|
|
567
|
+
* the web client's stop button, `agent.cancel({ kind: 'user' }, {
|
|
568
|
+
* keepInbox: true })` via the bridge), btw side calls, AND every live
|
|
569
|
+
* subagent. The children leg is what makes this "everything": cancelling
|
|
570
|
+
* the parent only kills foreground children (they ride the tool call's
|
|
571
|
+
* abort signal) — background/continuable children survive it and would
|
|
572
|
+
* keep burning LLM rounds (bridge.cancelAllChildren). Fired by the
|
|
573
|
+
* dialog's Stop choice (double-Esc) and the first Ctrl+C mid-task.
|
|
566
574
|
*/
|
|
567
575
|
const stopTask = async () => {
|
|
568
|
-
renderer.renderNotice(`${stopIcon()}
|
|
576
|
+
renderer.renderNotice(`${stopIcon()} stopping all LLM work…`, 'info');
|
|
569
577
|
// The stop gesture is the everything-stop: side calls die with the turn.
|
|
570
578
|
btwController.cancelAll();
|
|
571
579
|
const cancelled = await bridge.cancelActiveTurn();
|
|
572
|
-
|
|
573
|
-
//
|
|
574
|
-
|
|
580
|
+
const stoppedChildren = bridge.cancelAllChildren();
|
|
581
|
+
// State raced idle between the decision and the cancel calls — nothing
|
|
582
|
+
// to cancel (e.g. the task settled while the dialog was up).
|
|
583
|
+
if (!cancelled && stoppedChildren === 0)
|
|
575
584
|
renderer.renderNotice('Nothing running to cancel.', 'info');
|
|
576
585
|
};
|
|
577
586
|
// Arm the settings watch sink now that the renderer exists (see apply()).
|
|
@@ -715,6 +724,13 @@ export function apply(ctx) {
|
|
|
715
724
|
};
|
|
716
725
|
const bridge = new DshSessionBridge(ctx, bridgeCallbacksWithTakeover);
|
|
717
726
|
bridgeRef = bridge;
|
|
727
|
+
// Preset memory's creation-time leg: seed the remembered selection so the
|
|
728
|
+
// FIRST session creation (the lazy create on the first submitted prompt)
|
|
729
|
+
// records `meta.agentPreset` — without this the launch selection would be
|
|
730
|
+
// footer cosmetics while the server default composed the session. Only a
|
|
731
|
+
// roster-validated id is ever seeded (see presetState above).
|
|
732
|
+
if (rememberedPresetEntry !== undefined)
|
|
733
|
+
bridge.setAgentPreset(rememberedPresetEntry.id);
|
|
718
734
|
// Subagent fine-grained control, all in-process (see subagent-policy.ts):
|
|
719
735
|
// a tools.guard denies spawn-tool calls once `maxAgents` children run
|
|
720
736
|
// (workflow fan-out, which bypasses the tool pipeline, is pruned on
|
|
@@ -998,6 +1014,29 @@ export function apply(ctx) {
|
|
|
998
1014
|
description: 'Switch the current model\'s think (reasoning) level',
|
|
999
1015
|
handler: invocation => thinkHandler(invocation.rawInput, invocation.signal),
|
|
1000
1016
|
}), 'dsh-tui-pi: /think');
|
|
1017
|
+
/**
|
|
1018
|
+
* Record one committed preset as THIS workspace's remembered selection
|
|
1019
|
+
* (`$DSH_HOME/workspace-presets.json`, keyed by the session backend's
|
|
1020
|
+
* project key of the cwd — the same grouping /resume and the writer lock
|
|
1021
|
+
* use). Honors the LIVE `dsh-tui.rememberPreset` toggle (off = no read at
|
|
1022
|
+
* startup, no write here; the file is kept so flipping back restores the
|
|
1023
|
+
* last choices). Never throws: memory is best-effort, a failed save is a
|
|
1024
|
+
* notice.
|
|
1025
|
+
*/
|
|
1026
|
+
const persistRememberedPreset = (presetId) => {
|
|
1027
|
+
try {
|
|
1028
|
+
if (!currentRememberPreset(ctx))
|
|
1029
|
+
return;
|
|
1030
|
+
const path = workspacePresetsPath();
|
|
1031
|
+
const doc = withRememberedPreset(loadWorkspacePresets(path), projectKeyFor(process.cwd()), presetId);
|
|
1032
|
+
const error = saveWorkspacePresets(path, doc);
|
|
1033
|
+
if (error !== undefined)
|
|
1034
|
+
renderer.renderNotice(`preset memory not saved: ${error}`, 'error');
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
// A broken store must never fail the switch itself.
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1001
1040
|
// /preset: switch the agent preset — an EXPLICIT action that takes effect
|
|
1002
1041
|
// immediately by starting a NEW session on the chosen preset. Every entry
|
|
1003
1042
|
// path (picker Enter, `/preset <name>`, `/preset next`) funnels into
|
|
@@ -1030,8 +1069,7 @@ export function apply(ctx) {
|
|
|
1030
1069
|
}
|
|
1031
1070
|
if (target === undefined)
|
|
1032
1071
|
return { kind: 'success', text: 'Preset unchanged.' };
|
|
1033
|
-
const outcome = await performPresetSwitch(presetState, target, {
|
|
1034
|
-
hasLiveSession: () => bridge.getAgent() !== undefined,
|
|
1072
|
+
const outcome = await performPresetSwitch(presetState, target, { hasLiveSession: () => bridge.getAgent() !== undefined,
|
|
1035
1073
|
confirmSwitch: (name, restart) => openPresetConfirmDialog(ui.tui, ui.theme, name, restart, refocusEditor)
|
|
1036
1074
|
.then(outcome => (outcome === 'cancelled' ? 'cancel' : outcome)),
|
|
1037
1075
|
commit: async (presetId) => {
|
|
@@ -1102,6 +1140,14 @@ export function apply(ctx) {
|
|
|
1102
1140
|
}
|
|
1103
1141
|
},
|
|
1104
1142
|
});
|
|
1143
|
+
// Preset memory's recording leg: a COMMITTED switch (fresh, fork, or a
|
|
1144
|
+
// degraded fork — every switched path above seeds the same id) is the
|
|
1145
|
+
// moment the workspace's memory is written. Lives AFTER the switch so
|
|
1146
|
+
// a failed commit never records; the toggle is read LIVE, so a
|
|
1147
|
+
// /settings change applies without a restart. Best-effort: a failed
|
|
1148
|
+
// save is a notice, never a failed switch.
|
|
1149
|
+
if (outcome.switched)
|
|
1150
|
+
persistRememberedPreset(target.id);
|
|
1105
1151
|
return { kind: 'success', text: outcome.message };
|
|
1106
1152
|
};
|
|
1107
1153
|
commands.registerLocal('preset', presetHandler);
|
|
@@ -2102,8 +2148,6 @@ export function apply(ctx) {
|
|
|
2102
2148
|
stashSessionIdForReload(bridge.getSessionId());
|
|
2103
2149
|
if (quitConfirmTimer !== undefined)
|
|
2104
2150
|
clearTimeout(quitConfirmTimer);
|
|
2105
|
-
if (stopConfirmTimer !== undefined)
|
|
2106
|
-
clearTimeout(stopConfirmTimer);
|
|
2107
2151
|
subagentPolicy.dispose();
|
|
2108
2152
|
try {
|
|
2109
2153
|
await bridge.dispose();
|