@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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The stop-everything confirmation dialog — the gate behind the double-Esc
3
+ * stop gesture.
4
+ *
5
+ * "Stop all LLM work" is the widest lever the TUI has: it cancels the main
6
+ * turn AND every live subagent (background/continuable children survive a
7
+ * plain parent cancel, so the stop enumerates and cancels each). That reach
8
+ — and the fact that the gesture is two Esc presses, the same key users
9
+ * mash to close popups — is exactly why the second Esc opens THIS dialog
10
+ * instead of firing: the arm notice plus an explicit, read-before-confirm
11
+ * panel is the misfire guard. The old 200ms auto-fire window confirmed the
12
+ * *timing* of the press, not the *intent*; a dialog states what will die
13
+ * and waits for Enter.
14
+ *
15
+ * Mirrors the preset-switch dialog's pure-reducer split (src/preset-dialog.ts,
16
+ * itself modeled on src/repair-dialog.ts) so the decision matrix stays
17
+ * unit-testable without a terminal. Two ways out: STOP (Enter on the
18
+ * preselected row, or `1`) and CANCEL (Esc, `2`, or Enter on row 2). While
19
+ * the overlay is open the keymap yields every app key to it, so a third Esc
20
+ * lands in the dialog (cancel), never straight at the task. Framing/focus
21
+ * follow the shared overlay contract: PanelHost framing, close re-focuses
22
+ * the CURRENT editor instance through `restoreFocus`.
23
+ */
24
+ import { type Component, type TUI } from '@earendil-works/pi-tui';
25
+ import { type TuiTheme } from './theme/index.ts';
26
+ /** The two choices in display order; index 0 (STOP) is preselected. */
27
+ export declare const STOP_CONFIRM_OPTION_IDS: ReadonlyArray<'stop' | 'cancel'>;
28
+ /** What the dialog says: the title plus the "what is running" body points. */
29
+ export interface StopConfirmWording {
30
+ title: string;
31
+ body: readonly string[];
32
+ }
33
+ /**
34
+ * The dialog wording for one stop moment. The body names exactly what will
35
+ * be cancelled — the main turn when mid-turn, the running-subagent count —
36
+ * because the dialog is the only place the blast radius is stated before
37
+ * the user commits. Always ends with the reassurance that queued prompts
38
+ * survive (`keepInbox`) and the session stays resumable.
39
+ */
40
+ export declare function stopConfirmWording(mainRunning: boolean, runningChildren: number): StopConfirmWording;
41
+ /** The fixed option rows for one wording. */
42
+ export declare function stopConfirmOptions(): ReadonlyArray<{
43
+ id: 'stop' | 'cancel';
44
+ text: string;
45
+ }>;
46
+ /** Accent-BOLD dialog title. */
47
+ export declare function stopConfirmTitle(wording: StopConfirmWording): string;
48
+ /** Footer hint — hardcoded like every other panel footer (English-only). */
49
+ export declare const STOP_CONFIRM_FOOTER = "\u2191\u2193 select \u00B7 1/2 pick \u00B7 Enter confirm \u00B7 Esc keep running";
50
+ /**
51
+ * Pure dialog state: which row is highlighted, and the terminal outcome.
52
+ * Same shape as the preset dialog's state.
53
+ */
54
+ export interface StopConfirmState {
55
+ selected: number;
56
+ /**
57
+ * Set once by a terminal key: `'confirm'` (Enter on a selection) or
58
+ * `'cancel'` (Esc). Further input is ignored afterwards.
59
+ */
60
+ settled?: 'confirm' | 'cancel';
61
+ }
62
+ export declare function initialStopConfirmState(): StopConfirmState;
63
+ /**
64
+ * Apply one raw key sequence to the dialog state. Unknown keys are no-ops;
65
+ * anything after a settle is ignored (single terminal outcome guard).
66
+ */
67
+ export declare function updateStopConfirm(state: StopConfirmState, data: string): StopConfirmState;
68
+ /** Resolved dialog outcome: `'stop'`, or undefined on cancel. */
69
+ export declare function stopConfirmOutcome(state: StopConfirmState): 'stop' | undefined;
70
+ /**
71
+ * The framed overlay component. Renders the title, the wrapped body points
72
+ * and the two option rows; every key goes through
73
+ * {@link updateStopConfirm}, and the first terminal key fires `onFinish`
74
+ * exactly once.
75
+ */
76
+ export declare class StopConfirmPanel implements Component {
77
+ private readonly theme;
78
+ private readonly wording;
79
+ private readonly onFinish;
80
+ private readonly requestRenderFn;
81
+ private state;
82
+ constructor(theme: TuiTheme, wording: StopConfirmWording, onFinish: (outcome: 'stop' | undefined) => void, requestRender: () => void);
83
+ invalidate(): void;
84
+ render(width: number): string[];
85
+ handleInput(data: string): void;
86
+ }
87
+ /**
88
+ * Open the stop-everything confirmation dialog. Resolves `'stop'` when the
89
+ * user confirmed, or `'cancelled'` (Esc / row 2 / an overlay that failed to
90
+ * mount — treated as cancel so a half-mounted dialog can never imply
91
+ * consent). Closing always hands focus back through `restoreFocus` before
92
+ * the promise settles.
93
+ */
94
+ export declare function openStopConfirmDialog(tui: TUI, theme: TuiTheme, mainRunning: boolean, runningChildren: number, restoreFocus: () => void): Promise<'stop' | 'cancelled'>;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The stop-everything confirmation dialog — the gate behind the double-Esc
3
+ * stop gesture.
4
+ *
5
+ * "Stop all LLM work" is the widest lever the TUI has: it cancels the main
6
+ * turn AND every live subagent (background/continuable children survive a
7
+ * plain parent cancel, so the stop enumerates and cancels each). That reach
8
+ — and the fact that the gesture is two Esc presses, the same key users
9
+ * mash to close popups — is exactly why the second Esc opens THIS dialog
10
+ * instead of firing: the arm notice plus an explicit, read-before-confirm
11
+ * panel is the misfire guard. The old 200ms auto-fire window confirmed the
12
+ * *timing* of the press, not the *intent*; a dialog states what will die
13
+ * and waits for Enter.
14
+ *
15
+ * Mirrors the preset-switch dialog's pure-reducer split (src/preset-dialog.ts,
16
+ * itself modeled on src/repair-dialog.ts) so the decision matrix stays
17
+ * unit-testable without a terminal. Two ways out: STOP (Enter on the
18
+ * preselected row, or `1`) and CANCEL (Esc, `2`, or Enter on row 2). While
19
+ * the overlay is open the keymap yields every app key to it, so a third Esc
20
+ * lands in the dialog (cancel), never straight at the task. Framing/focus
21
+ * follow the shared overlay contract: PanelHost framing, close re-focuses
22
+ * the CURRENT editor instance through `restoreFocus`.
23
+ */
24
+ import { getKeybindings } from '@earendil-works/pi-tui';
25
+ import { PanelHost, panelThemeFns } from "./panels.js";
26
+ import { BOLD, RESET, ansiFg } from "./theme/index.js";
27
+ import { clipToWidth, wrapText } from "./text.js";
28
+ /** The two choices in display order; index 0 (STOP) is preselected. */
29
+ export const STOP_CONFIRM_OPTION_IDS = ['stop', 'cancel'];
30
+ /**
31
+ * The dialog wording for one stop moment. The body names exactly what will
32
+ * be cancelled — the main turn when mid-turn, the running-subagent count —
33
+ * because the dialog is the only place the blast radius is stated before
34
+ * the user commits. Always ends with the reassurance that queued prompts
35
+ * survive (`keepInbox`) and the session stays resumable.
36
+ */
37
+ export function stopConfirmWording(mainRunning, runningChildren) {
38
+ const running = [];
39
+ if (mainRunning)
40
+ running.push('the main turn is generating');
41
+ if (runningChildren > 0) {
42
+ running.push(`${runningChildren} subagent${runningChildren === 1 ? ' is' : 's are'} running`);
43
+ }
44
+ const situation = running.length > 0
45
+ ? `Right now ${running.join(' and ')}.`
46
+ : 'Nothing is running right now.';
47
+ return {
48
+ title: '● Stop all LLM work?',
49
+ body: [
50
+ situation,
51
+ 'Confirming cancels the main turn and every running subagent. '
52
+ + 'Queued messages are kept and the session stays resumable.',
53
+ ],
54
+ };
55
+ }
56
+ /** The fixed option rows for one wording. */
57
+ export function stopConfirmOptions() {
58
+ return [
59
+ { id: 'stop', text: 'Stop everything — cancel the main turn and all subagents' },
60
+ { id: 'cancel', text: 'Cancel — keep everything running' },
61
+ ];
62
+ }
63
+ /** Accent-BOLD dialog title. */
64
+ export function stopConfirmTitle(wording) {
65
+ return wording.title;
66
+ }
67
+ /** Footer hint — hardcoded like every other panel footer (English-only). */
68
+ export const STOP_CONFIRM_FOOTER = '↑↓ select · 1/2 pick · Enter confirm · Esc keep running';
69
+ export function initialStopConfirmState() {
70
+ return { selected: 0 };
71
+ }
72
+ /**
73
+ * Apply one raw key sequence to the dialog state. Unknown keys are no-ops;
74
+ * anything after a settle is ignored (single terminal outcome guard).
75
+ */
76
+ export function updateStopConfirm(state, data) {
77
+ if (state.settled !== undefined)
78
+ return state;
79
+ const kb = getKeybindings();
80
+ if (kb.matches(data, 'tui.select.cancel'))
81
+ return { ...state, settled: 'cancel' };
82
+ if (kb.matches(data, 'tui.input.submit'))
83
+ return { ...state, settled: 'confirm' };
84
+ if (kb.matches(data, 'tui.select.up')) {
85
+ return { ...state, selected: Math.max(0, state.selected - 1) };
86
+ }
87
+ if (kb.matches(data, 'tui.select.down')) {
88
+ return { ...state, selected: Math.min(STOP_CONFIRM_OPTION_IDS.length - 1, state.selected + 1) };
89
+ }
90
+ // Digit direct-select (1-based): selects the row, Enter still confirms —
91
+ // same select-then-confirm split as the preset dialog.
92
+ const digit = /^([1-9])$/.exec(data);
93
+ if (digit !== null) {
94
+ const index = Number(digit[1]) - 1;
95
+ if (index < STOP_CONFIRM_OPTION_IDS.length)
96
+ return { ...state, selected: index };
97
+ }
98
+ return state;
99
+ }
100
+ /** Resolved dialog outcome: `'stop'`, or undefined on cancel. */
101
+ export function stopConfirmOutcome(state) {
102
+ if (state.settled !== 'confirm')
103
+ return undefined;
104
+ return STOP_CONFIRM_OPTION_IDS[state.selected] === 'stop' ? 'stop' : undefined;
105
+ }
106
+ /**
107
+ * The framed overlay component. Renders the title, the wrapped body points
108
+ * and the two option rows; every key goes through
109
+ * {@link updateStopConfirm}, and the first terminal key fires `onFinish`
110
+ * exactly once.
111
+ */
112
+ export class StopConfirmPanel {
113
+ theme;
114
+ wording;
115
+ onFinish;
116
+ requestRenderFn;
117
+ state = initialStopConfirmState();
118
+ constructor(theme, wording, onFinish, requestRender) {
119
+ this.theme = theme;
120
+ this.wording = wording;
121
+ this.onFinish = onFinish;
122
+ this.requestRenderFn = requestRender;
123
+ }
124
+ invalidate() { }
125
+ render(width) {
126
+ const fns = panelThemeFns(this.theme);
127
+ const wrap = Math.max(2, width - 2);
128
+ // Word-wrap the body FIRST, then paint (iron rule: width math runs on
129
+ // plain text, ANSI goes on after clipping).
130
+ const lines = [
131
+ fns.accent(BOLD + clipToWidth(stopConfirmTitle(this.wording), wrap) + RESET),
132
+ ...this.wording.body.flatMap(point => wrapText(point, wrap).map(segment => fns.muted(clipToWidth(segment, wrap)))),
133
+ '',
134
+ ];
135
+ const options = stopConfirmOptions();
136
+ for (let i = 0; i < options.length; i++) {
137
+ const option = options[i];
138
+ const marker = i === this.state.selected ? '▸' : ' ';
139
+ const row = clipToWidth(`${marker} ${i + 1}. ${option.text}`, wrap);
140
+ lines.push(i === this.state.selected
141
+ ? ansiFg(this.theme.palette.accent) + BOLD + row + RESET
142
+ : fns.muted(row));
143
+ }
144
+ lines.push('');
145
+ lines.push(fns.subtle(clipToWidth(STOP_CONFIRM_FOOTER, wrap)));
146
+ return lines;
147
+ }
148
+ handleInput(data) {
149
+ const previous = this.state;
150
+ this.state = updateStopConfirm(this.state, data);
151
+ if (this.state === previous)
152
+ return;
153
+ if (this.state.settled === undefined) {
154
+ this.requestRenderFn();
155
+ return;
156
+ }
157
+ this.onFinish(stopConfirmOutcome(this.state));
158
+ }
159
+ }
160
+ /**
161
+ * Open the stop-everything confirmation dialog. Resolves `'stop'` when the
162
+ * user confirmed, or `'cancelled'` (Esc / row 2 / an overlay that failed to
163
+ * mount — treated as cancel so a half-mounted dialog can never imply
164
+ * consent). Closing always hands focus back through `restoreFocus` before
165
+ * the promise settles.
166
+ */
167
+ export function openStopConfirmDialog(tui, theme, mainRunning, runningChildren, restoreFocus) {
168
+ return new Promise(resolve => {
169
+ let settled = false;
170
+ const settle = (outcome) => {
171
+ if (settled)
172
+ return;
173
+ settled = true;
174
+ resolve(outcome);
175
+ };
176
+ // A half-mounted overlay must not strand the keyboard: PanelHost's
177
+ // onError closes + calls restoreFocus, then we settle as cancelled.
178
+ const host = new PanelHost(tui, theme, () => {
179
+ restoreFocus();
180
+ settle('cancelled');
181
+ });
182
+ const finish = (outcome) => {
183
+ host.close();
184
+ restoreFocus();
185
+ settle(outcome === 'stop' ? 'stop' : 'cancelled');
186
+ };
187
+ const panel = new StopConfirmPanel(theme, stopConfirmWording(mainRunning, runningChildren), outcome => finish(outcome), () => tui.requestRender());
188
+ // maxHeight is a hard slice in pi-tui: title + 2 wrapped body points +
189
+ // 2 options + footer ≈ 8 content rows + 4 frame rows; the preset
190
+ // dialog's 75%-of-24-rows headroom covers it with room to spare.
191
+ host.open(panel, '70%', '75%');
192
+ });
193
+ }
194
+ //# sourceMappingURL=stop-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stop-dialog.js","sourceRoot":"","sources":["../src/stop-dialog.ts"],"names":[],"mappings":"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"}
@@ -7,28 +7,32 @@
7
7
  * - `maxAgents` caps concurrent live children. A `tools.guard` registered on
8
8
  * the plugin root ctx denies model-facing spawn tools once the bridge's
9
9
  * live child count meets the cap. The live-child COUNT covers every child
10
- * in the process, but like `disableSubagent` the DENIAL is scoped to
11
- * sessions this bridge created or resumed (`TUI_SURFACE_KEY`): unmarked
12
- * callers fail open. The workflow/ralph fan-out bypasses the tool pipeline (its worker thread
13
- * spawns through the subagent provider directly), so a `subagent/start`
14
- * listener prunes any newcomer that slips past the guard.
10
+ * in the process, but the DENIAL scope is "belongs to this TUI": a caller
11
+ * carrying the surface marker, or any live descendant of a marked root
12
+ * (the ancestor walk closes the host-created-children hole). Foreign
13
+ * roots fail open. The workflow/ralph fan-out bypasses the tool pipeline
14
+ * (its worker thread spawns through the subagent provider directly), so a
15
+ * `subagent/start` listener prunes any newcomer that slips past the guard
16
+ * — same ownership scope, decided by the child's parent ancestry.
15
17
  * - `disableSubagent` disables the plain native `subagent` tool: its calls
16
- * are denied for every TUI session (and it is hidden from the main
18
+ * are denied for every TUI-scoped caller (and it is hidden from the main
17
19
  * agent's catalog), so delegation goes through registered agent
18
20
  * definitions (`~/.dsh/agents/*.md` via the registry's `use_agent`).
19
- * `subagent_fork`, `workflow` and `ralph` stay available. Enforcement is
20
- * scoped to sessions this bridge created or resumed (see
21
- * `TUI_SURFACE_KEY`): the guard reads the calling agent's surface marker
22
- * and FAILS OPEN for everything else, so a future web-profile deployment
23
- * of this plugin never disables the native tool inside Web UI sessions.
21
+ * `subagent_fork`, `workflow` and `ralph` stay available.
24
22
  * - `maxRounds` caps a child's assistant messages (each LLM round-trip is
25
- * one "round"): on the bridge's `onRoundCount` the policy injects one
26
- * plugin-sourced user message telling the child to wrap up — via `steer()`
27
- * while the child runs (consumed at the next STEP boundary, the very next
28
- * LLM round-trip) or `followup()` when idle, mirroring the Ctrl+G steer
29
- * routing. The child's log shows the injection with a `⚡` marker on the
30
- * compact line and in the subagent viewer, so an ignored wrap-up is
31
- * visible, not silent.
23
+ * one "round") through a TWO-STAGE ladder. Stage 1: at the cap — the
24
+ * per-agent tier when the child's label resolves to an agent .md
25
+ * `maxRounds` frontmatter key, else the global setting — the policy
26
+ * injects one plugin-sourced user message telling the child to wrap up
27
+ * (`steer()` while running, `followup()` when idle). Stage 2: after
28
+ * `maxRoundsGrace` further rounds, `state.cancelChild` force-stops the
29
+ * run — code, not persuasion; a one-shot run settles `aborted` with its
30
+ * partial output in the parent's tool result, a continuable child's
31
+ * session and inbox survive for resume. `grace: 0` collapses the ladder
32
+ * to the historical warn-only behavior. Every injection carries a `⚡`
33
+ * marker in the compact line and the subagent viewer; every hard stop is
34
+ * reported to the bridge for the `⏻` marker — an ignored wrap-up is
35
+ * visible, and so is its enforcement.
32
36
  */
33
37
  import type { Context } from '@deepseek-ai/cordis';
34
38
  /**
@@ -93,9 +97,11 @@ export declare function markTuiSurface(agentCtx: Context): void;
93
97
  * English and directive on purpose: it is a policy instruction to the child
94
98
  * LLM, and a soft "please summarize" (the earlier one-line Chinese request)
95
99
  * was routinely ignored while the child kept calling tools. It names the
96
- * limit, forbids further tool calls, and demands a final-answer summary.
100
+ * limit, forbids further tool calls, and demands a final-answer summary —
101
+ * and, with a positive grace window, warns that the run is force-stopped
102
+ * if the summary does not land within it.
97
103
  */
98
- export declare function wrapupMessage(maxRounds: number): string;
104
+ export declare function wrapupMessage(maxRounds: number, grace: number): string;
99
105
  /** The policy's live backstop — the bridge's per-child accessors. */
100
106
  export interface SubagentPolicyState {
101
107
  /** Current live (not settled) children — the count the `maxAgents` guard caps. */
@@ -107,11 +113,38 @@ export interface SubagentPolicyState {
107
113
  getRoundCount(childId: string): number;
108
114
  /** Whether one child already settled — a settled child is never re-awakened. */
109
115
  isSettled(childId: string): boolean;
116
+ /**
117
+ * Forcibly stop one live child (the everything-stop's per-child idiom,
118
+ * `cancel({kind:'user'}, {keepInbox:true})`): a one-shot run settles
119
+ * `aborted` (the parent's tool call reports the cancellation with the
120
+ * partial output), a continuable child's run stops while its session and
121
+ * inbox survive for later resume. `false` when nothing live was found.
122
+ */
123
+ cancelChild(childId: string): boolean;
124
+ }
125
+ /**
126
+ * A hard stop the policy executed on one child, surfaced through the policy
127
+ * handle for the bridge to fold into the child's view (the `⏻` marker).
128
+ */
129
+ export interface HardStopRecord {
130
+ /** The child session id the stop was issued to. */
131
+ readonly childId: string;
132
+ /** Round count at which the stop fired (`cap + grace`). */
133
+ readonly round: number;
134
+ /** The cap that was exceeded (per-agent when one resolved, else global). */
135
+ readonly cap: number;
136
+ /** The grace window that was exhausted. */
137
+ readonly grace: number;
110
138
  }
111
139
  /** The running policy: the bridge's `onRoundCount` sink plus the teardown. */
112
140
  export interface SubagentPolicy {
113
141
  /** Called by the bridge whenever one child produced another assistant message. */
114
142
  onRoundCount(childId: string, count: number): void;
143
+ /**
144
+ * Report sink for hard stops: wired by the host to the bridge's fold so a
145
+ * force-stopped child shows its `⏻` marker everywhere the view renders.
146
+ */
147
+ onHardStop?(record: HardStopRecord): void;
115
148
  /** Unwind the guard and event listeners. */
116
149
  dispose(): void;
117
150
  }
@@ -122,8 +155,12 @@ export interface SubagentPolicy {
122
155
  * is defensive: a settings-less deployment resolves the defaults, so the
123
156
  * policy still enforces its documented caps.
124
157
  * @param state - the host's live view (bridge.getLiveChildren /
125
- * bridge.getRoundCount). The guard and the round injection read it at every
158
+ * bridge.getRoundCount). The guard and the round ladder read it at every
126
159
  * decision — no snapshot, no watch.
160
+ * @param resolveAgentCap - the per-agent round cap lookup (agent .md
161
+ * frontmatter `maxRounds` via the registry contract). `undefined`/throwing
162
+ * resolutions fall back to the global cap, never widen it. Omitted = the
163
+ * global cap always applies.
127
164
  * @returns the policy handle wired to the bridge's `onRoundCount`.
128
165
  */
129
- export declare function applySubagentPolicy(ctx: Context, state: SubagentPolicyState): SubagentPolicy;
166
+ export declare function applySubagentPolicy(ctx: Context, state: SubagentPolicyState, resolveAgentCap?: (label: string) => number | undefined): SubagentPolicy;