@aiwayds/dsh-tui-pi 2.9.1 → 2.11.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 +5 -3
- package/README.zh-CN.md +5 -3
- package/lib/agent-runtime.d.ts +13 -0
- package/lib/agent-runtime.js +26 -0
- package/lib/agent-runtime.js.map +1 -1
- package/lib/agents.js +3 -2
- package/lib/agents.js.map +1 -1
- package/lib/dsh-events.d.ts +36 -0
- package/lib/dsh-events.js +10 -0
- package/lib/dsh-events.js.map +1 -1
- package/lib/index.js +140 -88
- 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/live-widgets.js +4 -0
- package/lib/live-widgets.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 +65 -0
- package/lib/session.js +199 -2
- 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-policy.d.ts +59 -22
- package/lib/subagent-policy.js +231 -40
- package/lib/subagent-policy.js.map +1 -1
- package/lib/subagent-viewer.d.ts +39 -10
- package/lib/subagent-viewer.js +201 -32
- package/lib/subagent-viewer.js.map +1 -1
- package/lib/theme-settings.d.ts +26 -1
- package/lib/theme-settings.js +46 -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 +10 -3
package/lib/index.js
CHANGED
|
@@ -27,8 +27,10 @@ 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";
|
|
33
|
+
import { readAgentMaxRounds } from "./agent-runtime.js";
|
|
32
34
|
import { detectNerdFontAvailable } from "./font-detect.js";
|
|
33
35
|
import { openAgentManager } from "./agents.js";
|
|
34
36
|
import { openProfileManager, openProfileSwitcher } from "./profile.js";
|
|
@@ -42,7 +44,7 @@ import { collectStartupSummary, formatResumeCommand, parseResumeArg, resolveProf
|
|
|
42
44
|
import { inspectPersistedSession, pickPersistedSession, sessionLogRoot, showSessionInfo } from "./sessions.js";
|
|
43
45
|
import { isCorruptLogError, locateSessionLog, repairFailureNotice, repairSessionLog, } from "./log-repair.js";
|
|
44
46
|
import { openRepairConfirmDialog } from "./repair-dialog.js";
|
|
45
|
-
import { WriterLockedError } from "./writer-lock.js";
|
|
47
|
+
import { WriterLockedError, projectKeyFor } from "./writer-lock.js";
|
|
46
48
|
import { emitNotice } from "./notice-bridge.js";
|
|
47
49
|
import { applySubagentPolicy } from "./subagent-policy.js";
|
|
48
50
|
import { openSubagentViewer } from "./subagent-viewer.js";
|
|
@@ -62,6 +64,7 @@ import { keybindingsPath, loadKeyBindings, openHotkeysManager } from "./hotkeys.
|
|
|
62
64
|
import { startTui } from "./tui.js";
|
|
63
65
|
import { currentPreset, fetchPresetRoster, findPresetByName, formatPresetLabel, initialPresetIndex, peekNextPreset } from "./preset.js";
|
|
64
66
|
import { completedTurnSeed, openPresetConfirmDialog, performPresetSwitch } from "./preset-dialog.js";
|
|
67
|
+
import { openStopConfirmDialog } from "./stop-dialog.js";
|
|
65
68
|
import { registerAskUserProvider } from "./ask-user.js";
|
|
66
69
|
import { checkHostSupport } from "./host-version.js";
|
|
67
70
|
export const name = 'dsh-tui-pi';
|
|
@@ -72,15 +75,6 @@ export const name = 'dsh-tui-pi';
|
|
|
72
75
|
* ~30-50ms) aborts the quit before this timer ever fires.
|
|
73
76
|
*/
|
|
74
77
|
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
78
|
/**
|
|
85
79
|
* The TUI drives the agent factory and registers slash commands. The render
|
|
86
80
|
* effect also touches `ctx.systemPrompt` (ask-user guidance section) and the
|
|
@@ -103,7 +97,7 @@ const SKILL_RESOURCE_BASE = {
|
|
|
103
97
|
};
|
|
104
98
|
const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true };
|
|
105
99
|
/** 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
|
|
100
|
+
const SKILL_DESCRIPTION = 'dsh TUI 增强套件(@aiwayds/dsh-tui-pi)使用与配置指南。凡涉及 TUI 主题/面板/footer、子代理并发与轮数限制、模型收藏与隐藏、会话保留清理与 /resume 过滤、preset 记忆,或要配置 dsh-tui 段时先读本指南:settings.yaml 顶层 `dsh-tui:` 段 14 键(theme/panelHeight/maxAgents/maxRounds/maxRoundsGrace/disableSubagent/footerHints/cacheHitMode/iconSet/rememberPreset/favoriteModels/hiddenModels/retention/resume)、DSH_TUI_* 环境变量、ask_user_question 快速上手向导、keybindings.json 与 /hotkeys。触发词:tui、主题、theme、面板、footer、收藏模型、隐藏模型、保留策略、panelHeight、resume、preset。';
|
|
107
101
|
const SKILL_CANDIDATE = {
|
|
108
102
|
name: SKILL_PROVIDER_NAME,
|
|
109
103
|
description: SKILL_DESCRIPTION,
|
|
@@ -290,6 +284,17 @@ export function apply(ctx) {
|
|
|
290
284
|
const nerdfontAvailable = await detectNerdFontAvailable();
|
|
291
285
|
applyIconSet(resolveIconSet(iconSetPreference, nerdfontAvailable));
|
|
292
286
|
const presetRoster = await fetchPresetRoster();
|
|
287
|
+
// Preset memory (`dsh-tui.rememberPreset`, default ON): the last /preset
|
|
288
|
+
// selection committed in THIS workspace (keyed by the session backend's
|
|
289
|
+
// project key of the cwd) becomes the launch selection — runTui seeds it
|
|
290
|
+
// into both the footer state AND `bridge.setAgentPreset`, so the FIRST
|
|
291
|
+
// session of the day composes under the remembered preset instead of the
|
|
292
|
+
// server default. A stale id (preset renamed/removed upstream) degrades
|
|
293
|
+
// to the stock default-selection behavior; memory off skips the read
|
|
294
|
+
// entirely.
|
|
295
|
+
const rememberedPresetId = (await readRememberPreset(ctx))
|
|
296
|
+
? rememberedPresetFor(loadWorkspacePresets(workspacePresetsPath()), projectKeyFor(process.cwd()))
|
|
297
|
+
: undefined;
|
|
293
298
|
// Startup configuration snapshot (mcp/skills/plugins readout under the
|
|
294
299
|
// welcome banner): best-effort by contract — no loader service or a
|
|
295
300
|
// throwing entry walk degrades to undefined and the banner renders
|
|
@@ -302,7 +307,7 @@ export function apply(ctx) {
|
|
|
302
307
|
const cmdlineArgs = ctx.get('cmdlineArgs')?.get();
|
|
303
308
|
let disposer;
|
|
304
309
|
try {
|
|
305
|
-
disposer = runTui(themePreference, panelHeight, footerHints, nerdfontAvailable, presetRoster, startupInfo, cmdlineArgs);
|
|
310
|
+
disposer = runTui(themePreference, panelHeight, footerHints, nerdfontAvailable, presetRoster, startupInfo, cmdlineArgs, rememberedPresetId);
|
|
306
311
|
}
|
|
307
312
|
catch (error) {
|
|
308
313
|
// An async-effect failure after startTui would otherwise orphan the
|
|
@@ -323,7 +328,7 @@ export function apply(ctx) {
|
|
|
323
328
|
* reaches cordis. Returns the effect disposer handed back to cordis on
|
|
324
329
|
* teardown.
|
|
325
330
|
*/
|
|
326
|
-
function runTui(themePreference, panelHeight, footerHints, nerdfontAvailable, presetRoster, startupInfo, cmdlineArgs) {
|
|
331
|
+
function runTui(themePreference, panelHeight, footerHints, nerdfontAvailable, presetRoster, startupInfo, cmdlineArgs, rememberedPresetId) {
|
|
327
332
|
// User keybindings (`~/.dsh/keybindings.json`): a partial map of the app
|
|
328
333
|
// keys, read once per TUI start — `/reload` re-runs apply() and re-reads
|
|
329
334
|
// it. Broken entries surface as notices instead of breaking startup.
|
|
@@ -333,16 +338,23 @@ export function apply(ctx) {
|
|
|
333
338
|
// switching is an explicit, confirmed action). The roster is fetched once
|
|
334
339
|
// at startup from the api-proxy service; an empty roster (no service / no
|
|
335
340
|
// presets) disables the feature gracefully (/preset errors, footer shows
|
|
336
|
-
// plain "dsh").
|
|
337
|
-
// preset when
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
341
|
+
// plain "dsh"). The initial selection is the workspace's REMEMBERED
|
|
342
|
+
// preset when preset memory is on and the id is still on the roster
|
|
343
|
+
// (initialPresetIndex prefers it), else the `standard` entry, else the
|
|
344
|
+
// first-scanned one. Without memory the launch selection is a local
|
|
345
|
+
// display only: before the user switches via /preset, NO
|
|
346
|
+
// `meta.agentPreset` is sent at session create, so the server-side
|
|
347
|
+
// default (`agent-presets.default` settings / deployment config)
|
|
348
|
+
// governs. WITH memory the remembered id is ALSO seeded into the bridge
|
|
349
|
+
// below (bridge.setAgentPreset), which is the point: the first session
|
|
350
|
+
// of the launch composes under the remembered preset, not the server
|
|
351
|
+
// default. We deliberately seed ONLY a remembered id, never
|
|
352
|
+
// DEFAULT_PRESET_ID — that would override the server-side default with
|
|
353
|
+
// a client-side assumption.
|
|
354
|
+
const rememberedPresetEntry = rememberedPresetId !== undefined
|
|
355
|
+
? presetRoster.find(preset => preset.id === rememberedPresetId)
|
|
356
|
+
: undefined;
|
|
357
|
+
const presetState = { roster: presetRoster, index: initialPresetIndex(presetRoster, rememberedPresetEntry?.id) };
|
|
346
358
|
// Docked-modal liveness (the ask-user questions panel): while a question
|
|
347
359
|
// is pending the panel owns the keyboard — the keymap treats it like an
|
|
348
360
|
// open overlay (see tui.ts), and refocusEditor must not steal focus from
|
|
@@ -353,51 +365,44 @@ export function apply(ctx) {
|
|
|
353
365
|
void submit(text);
|
|
354
366
|
},
|
|
355
367
|
keyBindings: keyBindings.bindings,
|
|
356
|
-
// App-level keys (pi interrupt chain): Esc
|
|
368
|
+
// App-level keys (pi interrupt chain): Esc while LLM work runs is a
|
|
357
369
|
// deliberate double-press — the first press arms the window (with
|
|
358
|
-
// feedback), the second within 500ms
|
|
359
|
-
//
|
|
360
|
-
// is
|
|
361
|
-
//
|
|
370
|
+
// feedback), the second within 500ms opens the stop-everything
|
|
371
|
+
// CONFIRMATION dialog (main turn + every running subagent); stopping
|
|
372
|
+
// everything is a read-before-confirm action, not a timer race.
|
|
373
|
+
// popup/autocomplete first, and an idle Esc (any editor) is an
|
|
374
|
+
// anti-misfire no-op. "Running" covers background subagents too —
|
|
375
|
+
// children keep working after the parent turn ends, and the stop must
|
|
376
|
+
// still reach them (bridge.hasRunningWork).
|
|
377
|
+
// Ctrl+C cancels a running task or clears the editor — a second press
|
|
362
378
|
// within 500ms quits — and Ctrl+D quits only on an empty editor.
|
|
363
379
|
// Ctrl+G opens the subagent picker while children run. All decisions
|
|
364
380
|
// live in keymap.ts.
|
|
365
|
-
isRunning: () => bridge.
|
|
381
|
+
isRunning: () => bridge.hasRunningWork(),
|
|
366
382
|
getRunningAgents: () => bridge.getLiveChildren().length,
|
|
367
383
|
hasSession: () => bridge.getSessionId() !== undefined,
|
|
368
384
|
dockedModalActive: () => askUserActive,
|
|
369
385
|
onKeyAction: (action) => {
|
|
370
386
|
switch (action.kind) {
|
|
371
387
|
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
|
-
|
|
388
|
+
// First Esc while LLM work runs: the stop is ARMED, not fired —
|
|
389
|
+
// a second Esc within the window opens the confirmation dialog,
|
|
390
|
+
// so a stray Esc can never kill a running task. The notice keeps
|
|
391
|
+
// the armed first press from feeling dead and states the
|
|
392
|
+
// contract explicitly.
|
|
393
|
+
renderer.renderNotice('Press Esc again to stop all LLM work', 'info');
|
|
377
394
|
break;
|
|
378
395
|
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);
|
|
396
|
+
// Second Esc while LLM work runs (main turn and/or subagents):
|
|
397
|
+
// open the stop-everything CONFIRMATION dialog. The old flow
|
|
398
|
+
// auto-fired the stop after a 200ms window — that confirmed the
|
|
399
|
+
// press timing, never the intent, and the gesture's reach had
|
|
400
|
+
// grown to every running subagent. The dialog states the blast
|
|
401
|
+
// radius (what is running right now) and waits for Enter; Esc
|
|
402
|
+
// inside it cancels and keeps everything running. While the
|
|
403
|
+
// overlay is up the keymap yields it every app key, so further
|
|
404
|
+
// Esc presses land in the dialog, never straight at the task.
|
|
405
|
+
void confirmAndStop();
|
|
401
406
|
break;
|
|
402
407
|
}
|
|
403
408
|
case 'ctrl-c-cancel': {
|
|
@@ -437,18 +442,14 @@ export function apply(ctx) {
|
|
|
437
442
|
}
|
|
438
443
|
case 'key-repeat':
|
|
439
444
|
// Auto-repeat of a held key — betrays a hold, never a double:
|
|
440
|
-
// abort a pending Ctrl+C quit confirmation
|
|
441
|
-
//
|
|
445
|
+
// abort a pending Ctrl+C quit confirmation. (The double-Esc stop
|
|
446
|
+
// has no timer anymore — its confirmation is the dialog, which
|
|
447
|
+
// ignores held keys through the keymap's repeat filter.)
|
|
442
448
|
if (action.key === 'ctrl-c' && quitConfirmTimer !== undefined) {
|
|
443
449
|
clearTimeout(quitConfirmTimer);
|
|
444
450
|
quitConfirmTimer = undefined;
|
|
445
451
|
renderer.renderNotice('quit aborted — Ctrl+C was held, not double-pressed', 'info');
|
|
446
452
|
}
|
|
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
453
|
break;
|
|
453
454
|
case 'ctrl-d-quit':
|
|
454
455
|
void disposeAndExit(0);
|
|
@@ -459,7 +460,8 @@ export function apply(ctx) {
|
|
|
459
460
|
break;
|
|
460
461
|
case 'subagent-viewer':
|
|
461
462
|
// Ctrl+G: open the subagent picker → transcript viewer while
|
|
462
|
-
// children run (modal overlay; Esc
|
|
463
|
+
// children run (modal overlay; Esc closes, x ×2 stops the child
|
|
464
|
+
// while it runs / closes when settled).
|
|
463
465
|
void openSubagentViewer(ctx, ui.tui, ui.theme, bridge, refocusEditor);
|
|
464
466
|
break;
|
|
465
467
|
case 'queue-panel': {
|
|
@@ -551,27 +553,35 @@ export function apply(ctx) {
|
|
|
551
553
|
*/
|
|
552
554
|
let quitConfirmTimer;
|
|
553
555
|
/**
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
* could theoretically be in flight.
|
|
556
|
+
* The double-Esc stop flow: the confirmation dialog first (it states
|
|
557
|
+
* what is running and waits for Enter), then the everything-stop. The
|
|
558
|
+
* dialog is modal, so a mashing user cannot double-commit — every key
|
|
559
|
+
* while it is up goes to the dialog, and a cancel simply returns.
|
|
559
560
|
*/
|
|
560
|
-
|
|
561
|
+
const confirmAndStop = async () => {
|
|
562
|
+
const outcome = await openStopConfirmDialog(ui.tui, ui.theme, bridge.isRunning(), bridge.getLiveChildren().length, refocusEditor);
|
|
563
|
+
if (outcome === 'stop')
|
|
564
|
+
await stopTask();
|
|
565
|
+
};
|
|
561
566
|
/**
|
|
562
|
-
* Stop the whole task
|
|
563
|
-
* client's stop button
|
|
564
|
-
* true })` via the bridge
|
|
565
|
-
*
|
|
567
|
+
* Stop the whole task — the everything-stop: the main turn (mirroring
|
|
568
|
+
* the web client's stop button, `agent.cancel({ kind: 'user' }, {
|
|
569
|
+
* keepInbox: true })` via the bridge), btw side calls, AND every live
|
|
570
|
+
* subagent. The children leg is what makes this "everything": cancelling
|
|
571
|
+
* the parent only kills foreground children (they ride the tool call's
|
|
572
|
+
* abort signal) — background/continuable children survive it and would
|
|
573
|
+
* keep burning LLM rounds (bridge.cancelAllChildren). Fired by the
|
|
574
|
+
* dialog's Stop choice (double-Esc) and the first Ctrl+C mid-task.
|
|
566
575
|
*/
|
|
567
576
|
const stopTask = async () => {
|
|
568
|
-
renderer.renderNotice(`${stopIcon()}
|
|
577
|
+
renderer.renderNotice(`${stopIcon()} stopping all LLM work…`, 'info');
|
|
569
578
|
// The stop gesture is the everything-stop: side calls die with the turn.
|
|
570
579
|
btwController.cancelAll();
|
|
571
580
|
const cancelled = await bridge.cancelActiveTurn();
|
|
572
|
-
|
|
573
|
-
//
|
|
574
|
-
|
|
581
|
+
const stoppedChildren = bridge.cancelAllChildren();
|
|
582
|
+
// State raced idle between the decision and the cancel calls — nothing
|
|
583
|
+
// to cancel (e.g. the task settled while the dialog was up).
|
|
584
|
+
if (!cancelled && stoppedChildren === 0)
|
|
575
585
|
renderer.renderNotice('Nothing running to cancel.', 'info');
|
|
576
586
|
};
|
|
577
587
|
// Arm the settings watch sink now that the renderer exists (see apply()).
|
|
@@ -715,19 +725,33 @@ export function apply(ctx) {
|
|
|
715
725
|
};
|
|
716
726
|
const bridge = new DshSessionBridge(ctx, bridgeCallbacksWithTakeover);
|
|
717
727
|
bridgeRef = bridge;
|
|
728
|
+
// Preset memory's creation-time leg: seed the remembered selection so the
|
|
729
|
+
// FIRST session creation (the lazy create on the first submitted prompt)
|
|
730
|
+
// records `meta.agentPreset` — without this the launch selection would be
|
|
731
|
+
// footer cosmetics while the server default composed the session. Only a
|
|
732
|
+
// roster-validated id is ever seeded (see presetState above).
|
|
733
|
+
if (rememberedPresetEntry !== undefined)
|
|
734
|
+
bridge.setAgentPreset(rememberedPresetEntry.id);
|
|
718
735
|
// Subagent fine-grained control, all in-process (see subagent-policy.ts):
|
|
719
736
|
// a tools.guard denies spawn-tool calls once `maxAgents` children run
|
|
720
737
|
// (workflow fan-out, which bypasses the tool pipeline, is pruned on
|
|
721
|
-
// `subagent/start`), and
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
738
|
+
// `subagent/start`), and the round ladder injects one wrap-up at a
|
|
739
|
+
// child's cap and hard-stops it after the grace window — the per-agent
|
|
740
|
+
// tier reads the agent file's frontmatter `maxRounds` through the
|
|
741
|
+
// registry contract. Limits are read live from the `dsh-tui` settings
|
|
742
|
+
// namespace (/agents → l limits).
|
|
725
743
|
const subagentPolicy = applySubagentPolicy(ctx, {
|
|
726
744
|
getLive: () => bridge.getLiveChildren(),
|
|
727
745
|
getRoundCount: childId => bridge.getRoundCount(childId),
|
|
728
746
|
isSettled: childId => bridge.isChildSettled(childId),
|
|
729
|
-
|
|
747
|
+
cancelChild: childId => bridge.cancelChild(childId),
|
|
748
|
+
}, readAgentMaxRounds);
|
|
730
749
|
bridgeCallbacks.onRoundCount = (childId, count) => subagentPolicy.onRoundCount(childId, count);
|
|
750
|
+
// The `⏻` marker fold: a force-stopped child is policy enforcement, and
|
|
751
|
+
// every render surface shows it as such.
|
|
752
|
+
if (subagentPolicy.onHardStop !== undefined) {
|
|
753
|
+
subagentPolicy.onHardStop = ({ childId, round, cap }) => bridge.markChildHardStopped(childId, round, cap);
|
|
754
|
+
}
|
|
731
755
|
// Ask-user-question provider: the upstream `dsh-tool-ask-user` tool calls
|
|
732
756
|
// `ctx.userQuestions.ask()` while its tool call is pending, and the
|
|
733
757
|
// provider returns a canonical `{ answers: [{ id, selected, custom? }] }`
|
|
@@ -998,6 +1022,29 @@ export function apply(ctx) {
|
|
|
998
1022
|
description: 'Switch the current model\'s think (reasoning) level',
|
|
999
1023
|
handler: invocation => thinkHandler(invocation.rawInput, invocation.signal),
|
|
1000
1024
|
}), 'dsh-tui-pi: /think');
|
|
1025
|
+
/**
|
|
1026
|
+
* Record one committed preset as THIS workspace's remembered selection
|
|
1027
|
+
* (`$DSH_HOME/workspace-presets.json`, keyed by the session backend's
|
|
1028
|
+
* project key of the cwd — the same grouping /resume and the writer lock
|
|
1029
|
+
* use). Honors the LIVE `dsh-tui.rememberPreset` toggle (off = no read at
|
|
1030
|
+
* startup, no write here; the file is kept so flipping back restores the
|
|
1031
|
+
* last choices). Never throws: memory is best-effort, a failed save is a
|
|
1032
|
+
* notice.
|
|
1033
|
+
*/
|
|
1034
|
+
const persistRememberedPreset = (presetId) => {
|
|
1035
|
+
try {
|
|
1036
|
+
if (!currentRememberPreset(ctx))
|
|
1037
|
+
return;
|
|
1038
|
+
const path = workspacePresetsPath();
|
|
1039
|
+
const doc = withRememberedPreset(loadWorkspacePresets(path), projectKeyFor(process.cwd()), presetId);
|
|
1040
|
+
const error = saveWorkspacePresets(path, doc);
|
|
1041
|
+
if (error !== undefined)
|
|
1042
|
+
renderer.renderNotice(`preset memory not saved: ${error}`, 'error');
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
// A broken store must never fail the switch itself.
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1001
1048
|
// /preset: switch the agent preset — an EXPLICIT action that takes effect
|
|
1002
1049
|
// immediately by starting a NEW session on the chosen preset. Every entry
|
|
1003
1050
|
// path (picker Enter, `/preset <name>`, `/preset next`) funnels into
|
|
@@ -1030,8 +1077,7 @@ export function apply(ctx) {
|
|
|
1030
1077
|
}
|
|
1031
1078
|
if (target === undefined)
|
|
1032
1079
|
return { kind: 'success', text: 'Preset unchanged.' };
|
|
1033
|
-
const outcome = await performPresetSwitch(presetState, target, {
|
|
1034
|
-
hasLiveSession: () => bridge.getAgent() !== undefined,
|
|
1080
|
+
const outcome = await performPresetSwitch(presetState, target, { hasLiveSession: () => bridge.getAgent() !== undefined,
|
|
1035
1081
|
confirmSwitch: (name, restart) => openPresetConfirmDialog(ui.tui, ui.theme, name, restart, refocusEditor)
|
|
1036
1082
|
.then(outcome => (outcome === 'cancelled' ? 'cancel' : outcome)),
|
|
1037
1083
|
commit: async (presetId) => {
|
|
@@ -1102,6 +1148,14 @@ export function apply(ctx) {
|
|
|
1102
1148
|
}
|
|
1103
1149
|
},
|
|
1104
1150
|
});
|
|
1151
|
+
// Preset memory's recording leg: a COMMITTED switch (fresh, fork, or a
|
|
1152
|
+
// degraded fork — every switched path above seeds the same id) is the
|
|
1153
|
+
// moment the workspace's memory is written. Lives AFTER the switch so
|
|
1154
|
+
// a failed commit never records; the toggle is read LIVE, so a
|
|
1155
|
+
// /settings change applies without a restart. Best-effort: a failed
|
|
1156
|
+
// save is a notice, never a failed switch.
|
|
1157
|
+
if (outcome.switched)
|
|
1158
|
+
persistRememberedPreset(target.id);
|
|
1105
1159
|
return { kind: 'success', text: outcome.message };
|
|
1106
1160
|
};
|
|
1107
1161
|
commands.registerLocal('preset', presetHandler);
|
|
@@ -2102,8 +2156,6 @@ export function apply(ctx) {
|
|
|
2102
2156
|
stashSessionIdForReload(bridge.getSessionId());
|
|
2103
2157
|
if (quitConfirmTimer !== undefined)
|
|
2104
2158
|
clearTimeout(quitConfirmTimer);
|
|
2105
|
-
if (stopConfirmTimer !== undefined)
|
|
2106
|
-
clearTimeout(stopConfirmTimer);
|
|
2107
2159
|
subagentPolicy.dispose();
|
|
2108
2160
|
try {
|
|
2109
2161
|
await bridge.dispose();
|