@aiwayds/dsh-tui-pi 2.6.0 → 2.7.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,254 @@
1
+ /**
2
+ * The preset-switch confirmation dialog + the switch orchestration.
3
+ *
4
+ * Switching presets is an EXPLICIT action with a visible cost: the preset is
5
+ * baked into a session at creation time, so applying a new selection means
6
+ * starting a NEW session — the current one is detached (and stays resumable
7
+ * via /resume). Whenever a live session exists, the switch therefore goes
8
+ * through this confirmation gate first (mirroring the /resume repair
9
+ * dialog's pure-reducer split, src/repair-dialog.ts, so the decision matrix
10
+ * stays unit-testable without a terminal); on a fresh TUI without a session
11
+ * there is nothing to leave behind and the selection applies directly.
12
+ *
13
+ * The dialog offers exactly three ways out: FORK the current conversation
14
+ * into the new session on the preset (context weight included), start FRESH
15
+ * (an empty session on the preset), or CANCEL. Title/body wording
16
+ * distinguishes a switch to a different preset from a restart on the
17
+ * current one. ↑↓ moves the selection, `1`/`2`/`3` select directly, Enter
18
+ * confirms, Esc = cancel. Framing/focus follow the shared overlay contract:
19
+ * PanelHost framing, close re-focuses the CURRENT editor instance through
20
+ * `restoreFocus`.
21
+ */
22
+ import { getKeybindings } from '@earendil-works/pi-tui';
23
+ import { PanelHost, panelThemeFns } from "./panels.js";
24
+ import { BOLD, RESET, ansiFg } from "./theme/index.js";
25
+ import { clipToWidth, wrapText } from "./text.js";
26
+ /** The three choices in display order; index 0 is preselected. */
27
+ export const PRESET_CONFIRM_OPTION_IDS = ['fork', 'fresh', 'cancel'];
28
+ /** The dialog wording for `name`: `restart` marks a re-selection of the current preset. */
29
+ export function presetConfirmWording(name, restart) {
30
+ return restart
31
+ ? {
32
+ title: `● Restart session on ${name}?`,
33
+ fork: `Fork & restart — new session on ${name}, carrying this conversation`,
34
+ fresh: `Restart now — new empty session on ${name}`,
35
+ firstPoint: `Restarting starts a NEW session on ${name}.`,
36
+ }
37
+ : {
38
+ title: `● Switch preset to ${name}?`,
39
+ fork: `Fork & switch — new session on ${name}, carrying this conversation`,
40
+ fresh: `Fresh start — new empty session on ${name}`,
41
+ firstPoint: `Switching starts a NEW session on ${name}.`,
42
+ };
43
+ }
44
+ /** The fixed option rows for one wording. */
45
+ export function presetConfirmOptions(wording) {
46
+ return [
47
+ { id: 'fork', text: wording.fork },
48
+ { id: 'fresh', text: wording.fresh },
49
+ { id: 'cancel', text: 'Cancel — stay on the current session' },
50
+ ];
51
+ }
52
+ /** Accent-BOLD dialog title. */
53
+ export function presetConfirmTitle(wording) {
54
+ return wording.title;
55
+ }
56
+ /**
57
+ * The fixed body points, one entry per bullet — each wrapped to the terminal
58
+ * width by the panel before painting.
59
+ */
60
+ export function presetConfirmBody(wording) {
61
+ return [
62
+ wording.firstPoint,
63
+ 'Fork carries this conversation into the new session (compacted context included) — fresh starts empty.',
64
+ ];
65
+ }
66
+ /** Footer hint — hardcoded like every other panel footer (English-only). */
67
+ export const PRESET_CONFIRM_FOOTER = '↑↓ select · 1/2/3 pick · Enter confirm · Esc cancel';
68
+ export function initialPresetConfirmState() {
69
+ return { selected: 0 };
70
+ }
71
+ /**
72
+ * Apply one raw key sequence to the dialog state. Unknown keys are no-ops;
73
+ * anything after a settle is ignored (single terminal outcome guard).
74
+ */
75
+ export function updatePresetConfirm(state, data) {
76
+ if (state.settled !== undefined)
77
+ return state;
78
+ const kb = getKeybindings();
79
+ if (kb.matches(data, 'tui.select.cancel'))
80
+ return { ...state, settled: 'cancel' };
81
+ if (kb.matches(data, 'tui.input.submit'))
82
+ return { ...state, settled: 'confirm' };
83
+ if (kb.matches(data, 'tui.select.up')) {
84
+ return { ...state, selected: Math.max(0, state.selected - 1) };
85
+ }
86
+ if (kb.matches(data, 'tui.select.down')) {
87
+ return { ...state, selected: Math.min(PRESET_CONFIRM_OPTION_IDS.length - 1, state.selected + 1) };
88
+ }
89
+ // Digit direct-select (1-based): selects the row, Enter still confirms —
90
+ // same select-then-confirm split as the routing dialog.
91
+ const digit = /^([1-9])$/.exec(data);
92
+ if (digit !== null) {
93
+ const index = Number(digit[1]) - 1;
94
+ if (index < PRESET_CONFIRM_OPTION_IDS.length)
95
+ return { ...state, selected: index };
96
+ }
97
+ return state;
98
+ }
99
+ /** Resolved dialog outcome: the chosen action, or undefined on cancel. */
100
+ export function presetConfirmOutcome(state) {
101
+ if (state.settled !== 'confirm')
102
+ return undefined;
103
+ return PRESET_CONFIRM_OPTION_IDS[state.selected];
104
+ }
105
+ /**
106
+ * The framed overlay component. Renders the title, the wrapped body points
107
+ * and the three option rows; every key goes through
108
+ * {@link updatePresetConfirm}, and the first terminal key fires `onFinish`
109
+ * exactly once.
110
+ */
111
+ export class PresetConfirmPanel {
112
+ theme;
113
+ wording;
114
+ onFinish;
115
+ requestRenderFn;
116
+ state = initialPresetConfirmState();
117
+ constructor(theme, wording, onFinish, requestRender) {
118
+ this.theme = theme;
119
+ this.wording = wording;
120
+ this.onFinish = onFinish;
121
+ this.requestRenderFn = requestRender;
122
+ }
123
+ invalidate() { }
124
+ render(width) {
125
+ const fns = panelThemeFns(this.theme);
126
+ const wrap = Math.max(2, width - 2);
127
+ // Word-wrap the body FIRST, then paint (iron rule: width math runs on
128
+ // plain text, ANSI goes on after clipping).
129
+ const lines = [
130
+ fns.accent(BOLD + clipToWidth(presetConfirmTitle(this.wording), wrap) + RESET),
131
+ ...presetConfirmBody(this.wording).flatMap(point => wrapText(point, wrap).map(segment => fns.muted(clipToWidth(segment, wrap)))),
132
+ '',
133
+ ];
134
+ const options = presetConfirmOptions(this.wording);
135
+ for (let i = 0; i < options.length; i++) {
136
+ const option = options[i];
137
+ const marker = i === this.state.selected ? '▸' : ' ';
138
+ const row = clipToWidth(`${marker} ${i + 1}. ${option.text}`, wrap);
139
+ lines.push(i === this.state.selected
140
+ ? ansiFg(this.theme.palette.accent) + BOLD + row + RESET
141
+ : fns.muted(row));
142
+ }
143
+ lines.push('');
144
+ lines.push(fns.subtle(clipToWidth(PRESET_CONFIRM_FOOTER, wrap)));
145
+ return lines;
146
+ }
147
+ handleInput(data) {
148
+ const previous = this.state;
149
+ this.state = updatePresetConfirm(this.state, data);
150
+ if (this.state === previous)
151
+ return;
152
+ if (this.state.settled === undefined) {
153
+ this.requestRenderFn();
154
+ return;
155
+ }
156
+ this.onFinish(presetConfirmOutcome(this.state));
157
+ }
158
+ }
159
+ /**
160
+ * Open the preset-switch confirmation dialog for one target preset.
161
+ * Resolves the chosen action (`'fork'` / `'fresh'`) or `'cancelled'` (Esc,
162
+ * or an overlay that failed to mount — treated as cancel so a half-mounted
163
+ * dialog can never imply consent). Closing always hands focus back through
164
+ * `restoreFocus` before the promise settles.
165
+ */
166
+ export function openPresetConfirmDialog(tui, theme, presetName, restart, restoreFocus) {
167
+ return new Promise(resolve => {
168
+ let settled = false;
169
+ const settle = (outcome) => {
170
+ if (settled)
171
+ return;
172
+ settled = true;
173
+ resolve(outcome);
174
+ };
175
+ // A half-mounted overlay must not strand the keyboard: PanelHost's
176
+ // onError closes + calls restoreFocus, then we settle as cancelled.
177
+ const host = new PanelHost(tui, theme, () => {
178
+ restoreFocus();
179
+ settle('cancelled');
180
+ });
181
+ const finish = (outcome) => {
182
+ host.close();
183
+ restoreFocus();
184
+ settle(outcome === 'fork' || outcome === 'fresh' ? outcome : 'cancelled');
185
+ };
186
+ const panel = new PresetConfirmPanel(theme, presetConfirmWording(presetName, restart), outcome => finish(outcome), () => tui.requestRender());
187
+ // maxHeight is a hard slice in pi-tui: title + 2 wrapped body points +
188
+ // 3 options + footer ≈ 9 content rows + 4 frame rows; 75% of the 24-row
189
+ // e2e floor is 18 (the /resume picker's headroom).
190
+ host.open(panel, '70%', '75%');
191
+ });
192
+ }
193
+ /**
194
+ * One preset switch, from any entry path (picker Enter, `/preset <name>`,
195
+ * `/preset next`): with a live session the confirmation dialog gates the
196
+ * switch — cancelling changes NOTHING (selection index untouched, no
197
+ * detach); `fresh` commits an empty new session (the /new path); `fork`
198
+ * commits a new session seeded with the current conversation. Without a
199
+ * live session the selection applies directly (the first submit already
200
+ * creates the session on it). Pure with respect to `state` apart from the
201
+ * commit-index write, and fully injectable.
202
+ */
203
+ export async function performPresetSwitch(state, target, deps) {
204
+ if (deps.hasLiveSession()) {
205
+ const restart = state.roster[state.index]?.id === target.id;
206
+ const action = await deps.confirmSwitch(target.name, restart);
207
+ if (action === 'cancel') {
208
+ return { switched: false, message: 'Preset unchanged — still on the current session.' };
209
+ }
210
+ // Snapshot BEFORE the index write: a commit that throws must not leave
211
+ // the selection (and with it the footer) advertising a preset the live
212
+ // session is not on.
213
+ const previousIndex = state.index;
214
+ const index = state.roster.findIndex(preset => preset.id === target.id);
215
+ try {
216
+ if (index >= 0)
217
+ state.index = index;
218
+ if (action === 'fork') {
219
+ await deps.forkCommit(target.id);
220
+ return { switched: true, message: `Preset → ${target.name} — new session forked from this conversation.` };
221
+ }
222
+ await deps.commit(target.id);
223
+ return { switched: true, message: `Preset → ${target.name} — new empty session started on it.` };
224
+ }
225
+ catch (error) {
226
+ state.index = previousIndex;
227
+ throw error;
228
+ }
229
+ }
230
+ const index = state.roster.findIndex(preset => preset.id === target.id);
231
+ if (index >= 0)
232
+ state.index = index;
233
+ await deps.commit(target.id);
234
+ return { switched: true, message: `Preset → ${target.name} — new session started on it.` };
235
+ }
236
+ /**
237
+ * The balanced completed-turn prefix of a session snapshot: every event up
238
+ * to and including the last `turn/end` — the in-flight turn is unbalanced
239
+ * and cannot replay as a valid seed. Contiguous from seq 0 (live sequence
240
+ * numbers equal array indexes) and empty before any completed turn, in which
241
+ * case a fork degrades to a fresh session. The same slice the host's fork
242
+ * subagent backend (`dsh-subagent-fork-in-process`) takes.
243
+ */
244
+ export function completedTurnSeed(events) {
245
+ let lastEnd = -1;
246
+ for (let i = events.length - 1; i >= 0; i--) {
247
+ if (events[i].type === 'turn/end') {
248
+ lastEnd = i;
249
+ break;
250
+ }
251
+ }
252
+ return lastEnd < 0 ? [] : events.slice(0, lastEnd + 1);
253
+ }
254
+ //# sourceMappingURL=preset-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preset-dialog.js","sourceRoot":"","sources":["../src/preset-dialog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,cAAc,EAA4B,MAAM,wBAAwB,CAAA;AAGjF,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAiB,MAAM,kBAAkB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEjD,kEAAkE;AAClE,MAAM,CAAC,MAAM,yBAAyB,GAA+C,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAA;AAiBhH,2FAA2F;AAC3F,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,OAAgB;IACjE,OAAO,OAAO;QACZ,CAAC,CAAC;YACE,KAAK,EAAE,wBAAwB,IAAI,GAAG;YACtC,IAAI,EAAE,mCAAmC,IAAI,8BAA8B;YAC3E,KAAK,EAAE,sCAAsC,IAAI,EAAE;YACnD,UAAU,EAAE,sCAAsC,IAAI,GAAG;SAC1D;QACH,CAAC,CAAC;YACE,KAAK,EAAE,sBAAsB,IAAI,GAAG;YACpC,IAAI,EAAE,kCAAkC,IAAI,8BAA8B;YAC1E,KAAK,EAAE,sCAAsC,IAAI,EAAE;YACnD,UAAU,EAAE,qCAAqC,IAAI,GAAG;SACzD,CAAA;AACP,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,oBAAoB,CAAC,OAA6B;IAChE,OAAO;QACL,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE;QAClC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE;QACpC,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,sCAAsC,EAAE;KAC/D,CAAA;AACH,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC9D,OAAO,OAAO,CAAC,KAAK,CAAA;AACtB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA6B;IAC7D,OAAO;QACL,OAAO,CAAC,UAAU;QAClB,wGAAwG;KACzG,CAAA;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qBAAqB,GAAG,qDAAqD,CAAA;AAY1F,MAAM,UAAU,yBAAyB;IACvC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAA;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAyB,EAAE,IAAY;IACzE,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IAC7C,MAAM,EAAE,GAAG,cAAc,EAAE,CAAA;IAC3B,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,mBAAmB,CAAC;QAAE,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;IACjF,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAAE,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IACjF,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC;QACtC,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAA;IAChE,CAAC;IACD,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,yBAAyB,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAA;IACnG,CAAC;IACD,yEAAyE;IACzE,wDAAwD;IACxD,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,KAAK,GAAG,yBAAyB,CAAC,MAAM;YAAE,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;IACpF,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB,CAAC,KAAyB;IAC5D,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACjD,OAAO,yBAAyB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,kBAAkB;IACZ,KAAK,CAAU;IACf,OAAO,CAAsB;IAC7B,QAAQ,CAA4D;IACpE,eAAe,CAAY;IACpC,KAAK,GAAuB,yBAAyB,EAAE,CAAA;IAE/D,YACE,KAAe,EACf,OAA6B,EAC7B,QAAoE,EACpE,aAAyB;QAEzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,eAAe,GAAG,aAAa,CAAA;IACtC,CAAC;IAED,UAAU,KAAU,CAAC;IAErB,MAAM,CAAC,KAAa;QAClB,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;QACnC,sEAAsE;QACtE,4CAA4C;QAC5C,MAAM,KAAK,GAAa;YACtB,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,WAAW,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC;YAC9E,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CACjD,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;YAC9E,EAAE;SACH,CAAA;QACD,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAE,CAAA;YAC1B,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;YACpD,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,CAAA;YACnE,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ;gBAClC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,GAAG,GAAG,KAAK;gBACxD,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;QACrB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACd,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;QAChE,OAAO,KAAK,CAAA;IACd,CAAC;IAED,WAAW,CAAC,IAAY;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAA;QAC3B,IAAI,CAAC,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QAClD,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAM;QACnC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,eAAe,EAAE,CAAA;YACtB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IACjD,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,GAAQ,EACR,KAAe,EACf,UAAkB,EAClB,OAAgB,EAChB,YAAwB;IAExB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,MAAM,MAAM,GAAG,CAAC,OAAuC,EAAQ,EAAE;YAC/D,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,OAAO,CAAC,OAAO,CAAC,CAAA;QAClB,CAAC,CAAA;QACD,mEAAmE;QACnE,oEAAoE;QACpE,MAAM,IAAI,GAAG,IAAI,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;YAC1C,YAAY,EAAE,CAAA;YACd,MAAM,CAAC,WAAW,CAAC,CAAA;QACrB,CAAC,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,CAAC,OAAgD,EAAQ,EAAE;YACxE,IAAI,CAAC,KAAK,EAAE,CAAA;YACZ,YAAY,EAAE,CAAA;YACd,MAAM,CAAC,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;QAC3E,CAAC,CAAA;QACD,MAAM,KAAK,GAAG,IAAI,kBAAkB,CAClC,KAAK,EACL,oBAAoB,CAAC,UAAU,EAAE,OAAO,CAAC,EACzC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,EAC1B,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,EAAE,CAC1B,CAAA;QACD,uEAAuE;QACvE,wEAAwE;QACxE,mDAAmD;QACnD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IAChC,CAAC,CAAC,CAAA;AACJ,CAAC;AAuCD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,KAAkB,EAClB,MAAmB,EACnB,IAAsB;IAEtB,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,MAAM,CAAC,EAAE,CAAA;QAC3D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QAC7D,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,kDAAkD,EAAE,CAAA;QACzF,CAAC;QACD,uEAAuE;QACvE,uEAAuE;QACvE,qBAAqB;QACrB,MAAM,aAAa,GAAG,KAAK,CAAC,KAAK,CAAA;QACjC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC,CAAA;QACvE,IAAI,CAAC;YACH,IAAI,KAAK,IAAI,CAAC;gBAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAA;YACnC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;gBACtB,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;gBAChC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,MAAM,CAAC,IAAI,+CAA+C,EAAE,CAAA;YAC5G,CAAC;YACD,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YAC5B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,MAAM,CAAC,IAAI,qCAAqC,EAAE,CAAA;QAClG,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,KAAK,GAAG,aAAa,CAAA;YAC3B,MAAM,KAAK,CAAA;QACb,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC,CAAA;IACvE,IAAI,KAAK,IAAI,CAAC;QAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAA;IACnC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC5B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,MAAM,CAAC,IAAI,+BAA+B,EAAE,CAAA;AAC5F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAA+B;IAC/D,IAAI,OAAO,GAAG,CAAC,CAAC,CAAA;IAChB,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,IAAI,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACnC,OAAO,GAAG,CAAC,CAAA;YACX,MAAK;QACP,CAAC;IACH,CAAC;IACD,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAA;AACxD,CAAC"}
package/lib/preset.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Agent-preset state manager — scans the filesystem for preset directories
3
3
  * (same discovery logic as @deepseek-ai/dsh-agent-presets), tracks the user's
4
- * Tab selection, and formats the footer label.
4
+ * /preset selection, and formats the footer label.
5
5
  *
6
6
  * Presets are a dsh deployment concept: each preset composes a session's agent
7
7
  * from a different set of plugins/tools. The TUI scans the preset roots once
8
- * at startup and lets the user cycle through them with Tab; the chosen preset
9
- * is applied to the next blank session on first submit.
8
+ * at startup and lets the user switch through /preset; a switch is an explicit,
9
+ * confirmed action that starts a NEW session on the chosen preset (a fresh TUI
10
+ * without a live session applies the selection directly).
10
11
  *
11
- * The roster is O(1) to read (an in-memory array); cycle is O(1) mutation of
12
- * the index. The module is pure except for the async `fetchPresetRoster` which
13
- * scans the filesystem.
12
+ * The roster is O(1) to read (an in-memory array). The module is pure except
13
+ * for the async `fetchPresetRoster` which scans the filesystem.
14
14
  */
15
15
  /** One preset entry from the deployment roster. */
16
16
  export interface PresetEntry {
@@ -55,8 +55,15 @@ export declare function resolvePresetRoots(): PresetRoot[];
55
55
  * presets are found.
56
56
  */
57
57
  export declare function fetchPresetRoster(): Promise<PresetEntry[]>;
58
- /** Cycle the preset index forward (default) or backward, wrapping around. */
59
- export declare function cyclePreset(state: PresetState, direction?: 1 | -1): void;
58
+ /**
59
+ * The entry `/preset next` would switch to — the roster item after the
60
+ * current selection, wrapping around. PURE (no index mutation): the switch
61
+ * only commits through the confirmation flow, so `next` must be able to
62
+ * preview its target first. With 0 or 1 entries there is nothing to switch
63
+ * to (the caller short-circuits); with ≥2 the result always differs from
64
+ * the current selection.
65
+ */
66
+ export declare function peekNextPreset(state: PresetState): PresetEntry | undefined;
60
67
  /** Return the currently selected preset, or undefined when the roster is empty. */
61
68
  export declare function currentPreset(state: PresetState): PresetEntry | undefined;
62
69
  /**
@@ -67,7 +74,7 @@ export declare function currentPreset(state: PresetState): PresetEntry | undefin
67
74
  export declare function findPresetByName(state: PresetState, name: string): PresetEntry | undefined;
68
75
  /** The preset id selected out of the box when the roster supplies it. It
69
76
  * mirrors the dsh shipped default, but there is no mechanical binding:
70
- * until the user interacts with /preset or Tab, the server-side
77
+ * until the user switches via /preset, the server-side
71
78
  * `agent-presets.default` setting still governs session creation. */
72
79
  export declare const DEFAULT_PRESET_ID = "standard";
73
80
  /**
package/lib/preset.js CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Agent-preset state manager — scans the filesystem for preset directories
3
3
  * (same discovery logic as @deepseek-ai/dsh-agent-presets), tracks the user's
4
- * Tab selection, and formats the footer label.
4
+ * /preset selection, and formats the footer label.
5
5
  *
6
6
  * Presets are a dsh deployment concept: each preset composes a session's agent
7
7
  * from a different set of plugins/tools. The TUI scans the preset roots once
8
- * at startup and lets the user cycle through them with Tab; the chosen preset
9
- * is applied to the next blank session on first submit.
8
+ * at startup and lets the user switch through /preset; a switch is an explicit,
9
+ * confirmed action that starts a NEW session on the chosen preset (a fresh TUI
10
+ * without a live session applies the selection directly).
10
11
  *
11
- * The roster is O(1) to read (an in-memory array); cycle is O(1) mutation of
12
- * the index. The module is pure except for the async `fetchPresetRoster` which
13
- * scans the filesystem.
12
+ * The roster is O(1) to read (an in-memory array). The module is pure except
13
+ * for the async `fetchPresetRoster` which scans the filesystem.
14
14
  */
15
15
  import { access, readdir, readFile } from 'node:fs/promises';
16
16
  import { join, resolve } from 'node:path';
@@ -151,11 +151,18 @@ export async function fetchPresetRoster() {
151
151
  }
152
152
  return roster;
153
153
  }
154
- /** Cycle the preset index forward (default) or backward, wrapping around. */
155
- export function cyclePreset(state, direction = 1) {
154
+ /**
155
+ * The entry `/preset next` would switch to — the roster item after the
156
+ * current selection, wrapping around. PURE (no index mutation): the switch
157
+ * only commits through the confirmation flow, so `next` must be able to
158
+ * preview its target first. With 0 or 1 entries there is nothing to switch
159
+ * to (the caller short-circuits); with ≥2 the result always differs from
160
+ * the current selection.
161
+ */
162
+ export function peekNextPreset(state) {
156
163
  if (state.roster.length <= 1)
157
- return;
158
- state.index = (state.index + direction + state.roster.length) % state.roster.length;
164
+ return undefined;
165
+ return state.roster[(state.index + 1) % state.roster.length];
159
166
  }
160
167
  /** Return the currently selected preset, or undefined when the roster is empty. */
161
168
  export function currentPreset(state) {
@@ -174,7 +181,7 @@ export function findPresetByName(state, name) {
174
181
  }
175
182
  /** The preset id selected out of the box when the roster supplies it. It
176
183
  * mirrors the dsh shipped default, but there is no mechanical binding:
177
- * until the user interacts with /preset or Tab, the server-side
184
+ * until the user switches via /preset, the server-side
178
185
  * `agent-presets.default` setting still governs session creation. */
179
186
  export const DEFAULT_PRESET_ID = 'standard';
180
187
  /**
package/lib/preset.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"preset.js","sourceRoot":"","sources":["../src/preset.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"preset.js","sourceRoot":"","sources":["../src/preset.ts"],"names":[],"mappings":"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"}
package/lib/session.d.ts CHANGED
@@ -334,6 +334,37 @@ export declare class DshSessionBridge {
334
334
  * point; switching away or exiting releases what we established.
335
335
  */
336
336
  private ownWithLock;
337
+ /**
338
+ * Tear down the current live binding — the shared prelude of every session
339
+ * switch (resume, fork): stop the remote watch, dispose the handle, clear
340
+ * the subagent tracker and re-seed the model selection (a live /model or
341
+ * /think choice survives the switch; otherwise the composed default
342
+ * applies). The renderer keeps its live snapshot across the teardown (it
343
+ * must survive theme-switch rebuilds), so an empty `onLive` emit is
344
+ * required to drop the previous session's agents block before the next
345
+ * session's events rebuild it.
346
+ */
347
+ private teardownLiveBinding;
348
+ /**
349
+ * Fork-and-switch: create a NEW session seeded with a balanced
350
+ * completed-turn prefix of the current session's log, then rebind this
351
+ * bridge to it — the resume lifecycle (teardown, tracker clear, rebind)
352
+ * over `agents.create` instead of `agents.resume`. The new session carries
353
+ * durable fork lineage (`parentSession` + `isSeeded` + the inherited
354
+ * prefix length) and composes under `presetId` (`meta.agentPreset`) — a
355
+ * seeded USER session, deliberately NOT a subagent child (no
356
+ * `origin`/delegationDepth, so /resume keeps it and the tracker ignores
357
+ * it). The seed must be sliced from the live session BEFORE calling this —
358
+ * the in-memory snapshot is the stable source. The teardown runs FIRST:
359
+ * `markTuiSurface` provides a cordis service on the agent scope and two
360
+ * live agents cannot coexist on it (the /new cycle only works because
361
+ * dispose reclaims the service before the next create). A failed create
362
+ * therefore leaves the old session DETACHED but fully resumable.
363
+ * @param seed contiguous-from-0 events ending at a `turn/end` (non-empty).
364
+ * @param parentSessionId the session the seed came from (durable lineage).
365
+ * @param presetId the preset id the new session composes under.
366
+ */
367
+ forkCurrentSession(seed: readonly SessionEvent[], parentSessionId: SessionId, presetId: string | undefined): Promise<AgentHandle>;
337
368
  /**
338
369
  * Resume a persisted session: tear down the live agent (disposers are kept —
339
370
  * the session-id filter re-binds to the resumed id) and load the persisted
@@ -397,6 +428,16 @@ export declare class DshSessionBridge {
397
428
  /** The composed global default (`agent-default-model`) as a plain selection. */
398
429
  private composedDefaultSelection;
399
430
  /** Create the agent with the composed default model selection. */
431
+ /**
432
+ * Create the agent with the composed default model selection. `fork`
433
+ * seeds the new session with a balanced completed-turn prefix of another
434
+ * session's log (the preset-switch "Fork & switch" path): durable lineage
435
+ * (`parentSession` + `isSeeded` + the inherited prefix length) plus the
436
+ * fork's own `agentPreset` — recording the preset is what keeps the
437
+ * session's history reconstructable (a cold read that dropped it would
438
+ * rebuild turns under the wrong composition, the same rule
439
+ * `childSessionMeta` documents for subagent children).
440
+ */
400
441
  private createSession;
401
442
  /** The model selection shown in the footer (live value after `/model`). */
402
443
  getSelection(): ModelSelection | undefined;
package/lib/session.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { installModelSelection } from '@deepseek-ai/dsh-agent';
13
13
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
14
14
  import { SettingsConflictError } from '@deepseek-ai/dsh-settings';
15
- import { SessionId } from '@deepseek-ai/dsh-session';
15
+ import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
16
16
  import { readAppendSystem } from "./append-system.js";
17
17
  import { join } from 'node:path';
18
18
  import { isAgentEnd, isAgentStart, isDcpCompactionNotice, isLlmRetry, isSubagentDescriptor, isTuiPluginInjection } from "./dsh-events.js";
@@ -880,6 +880,60 @@ export class DshSessionBridge {
880
880
  };
881
881
  return handle;
882
882
  }
883
+ /**
884
+ * Tear down the current live binding — the shared prelude of every session
885
+ * switch (resume, fork): stop the remote watch, dispose the handle, clear
886
+ * the subagent tracker and re-seed the model selection (a live /model or
887
+ * /think choice survives the switch; otherwise the composed default
888
+ * applies). The renderer keeps its live snapshot across the teardown (it
889
+ * must survive theme-switch rebuilds), so an empty `onLive` emit is
890
+ * required to drop the previous session's agents block before the next
891
+ * session's events rebuild it.
892
+ */
893
+ async teardownLiveBinding() {
894
+ await this.stopRemoteWatch();
895
+ const handle = this.handle;
896
+ this.handle = undefined;
897
+ this.sessionId = undefined;
898
+ this.running = false;
899
+ this.agentViews.clear();
900
+ this.childSessions.clear();
901
+ this.trackedSessions.clear();
902
+ this.runSeqToChild.clear();
903
+ this.emitLive();
904
+ if (handle !== undefined)
905
+ await handle.dispose();
906
+ this.seedSelectionFromDefault();
907
+ }
908
+ /**
909
+ * Fork-and-switch: create a NEW session seeded with a balanced
910
+ * completed-turn prefix of the current session's log, then rebind this
911
+ * bridge to it — the resume lifecycle (teardown, tracker clear, rebind)
912
+ * over `agents.create` instead of `agents.resume`. The new session carries
913
+ * durable fork lineage (`parentSession` + `isSeeded` + the inherited
914
+ * prefix length) and composes under `presetId` (`meta.agentPreset`) — a
915
+ * seeded USER session, deliberately NOT a subagent child (no
916
+ * `origin`/delegationDepth, so /resume keeps it and the tracker ignores
917
+ * it). The seed must be sliced from the live session BEFORE calling this —
918
+ * the in-memory snapshot is the stable source. The teardown runs FIRST:
919
+ * `markTuiSurface` provides a cordis service on the agent scope and two
920
+ * live agents cannot coexist on it (the /new cycle only works because
921
+ * dispose reclaims the service before the next create). A failed create
922
+ * therefore leaves the old session DETACHED but fully resumable.
923
+ * @param seed contiguous-from-0 events ending at a `turn/end` (non-empty).
924
+ * @param parentSessionId the session the seed came from (durable lineage).
925
+ * @param presetId the preset id the new session composes under.
926
+ */
927
+ async forkCurrentSession(seed, parentSessionId, presetId) {
928
+ // Teardown FIRST (see docblock): the old agent's scope holds the TUI
929
+ // surface service the new agent's setup must provide.
930
+ await this.teardownLiveBinding();
931
+ const handle = await this.createSession({ seed, parentSessionId, presetId });
932
+ this.handle = handle;
933
+ this.sessionId = handle.agent.session.id;
934
+ this.trackedSessions.add(String(this.sessionId));
935
+ return handle;
936
+ }
883
937
  /**
884
938
  * Resume a persisted session: tear down the live agent (disposers are kept —
885
939
  * the session-id filter re-binds to the resumed id) and load the persisted
@@ -900,25 +954,7 @@ export class DshSessionBridge {
900
954
  // touching this field, so it always names the id actually loading.
901
955
  this.resumeTargetId = sessionId;
902
956
  const task = (async () => {
903
- await this.stopRemoteWatch();
904
- const handle = this.handle;
905
- this.handle = undefined;
906
- this.sessionId = undefined;
907
- this.running = false;
908
- this.agentViews.clear();
909
- this.childSessions.clear();
910
- this.trackedSessions.clear();
911
- this.runSeqToChild.clear();
912
- // The renderer keeps its live snapshot across the resume teardown (it
913
- // must survive theme-switch rebuilds), so an empty emit is required to
914
- // drop the previous session's agents block before the resumed log's
915
- // workflow events rebuild it.
916
- this.emitLive();
917
- if (handle !== undefined)
918
- await handle.dispose();
919
- // Same seeding rule as createSession: a live /model or /think choice
920
- // survives the resume; otherwise the composed default applies.
921
- this.seedSelectionFromDefault();
957
+ await this.teardownLiveBinding();
922
958
  // Attach arm: the session may already be LIVE in this process — e.g.
923
959
  // created by the feishu bot's /new or resumed by it. agents.resume
924
960
  // refuses live sessions ("cannot prepare session while it is live"),
@@ -1478,21 +1514,42 @@ export class DshSessionBridge {
1478
1514
  };
1479
1515
  }
1480
1516
  /** Create the agent with the composed default model selection. */
1481
- async createSession() {
1517
+ /**
1518
+ * Create the agent with the composed default model selection. `fork`
1519
+ * seeds the new session with a balanced completed-turn prefix of another
1520
+ * session's log (the preset-switch "Fork & switch" path): durable lineage
1521
+ * (`parentSession` + `isSeeded` + the inherited prefix length) plus the
1522
+ * fork's own `agentPreset` — recording the preset is what keeps the
1523
+ * session's history reconstructable (a cold read that dropped it would
1524
+ * rebuild turns under the wrong composition, the same rule
1525
+ * `childSessionMeta` documents for subagent children).
1526
+ */
1527
+ async createSession(fork) {
1482
1528
  this.seedSelectionFromDefault();
1483
1529
  // Mint the id HERE so the pre-create lock covers exactly the directory
1484
1530
  // agents.create is about to persist into (same cold-arm guard as resume).
1485
1531
  const sessionId = crypto.randomUUID();
1486
1532
  const cwd = process.cwd();
1487
1533
  const lock = await this.acquireSessionLock(sessionId, cwd);
1534
+ const meta = {
1535
+ cwd,
1536
+ ...this.agentPreset !== undefined ? { agentPreset: this.agentPreset } : {},
1537
+ };
1538
+ if (fork !== undefined) {
1539
+ if (fork.presetId !== undefined)
1540
+ meta.agentPreset = fork.presetId;
1541
+ meta.parentSession = fork.parentSessionId;
1542
+ meta.isSeeded = true;
1543
+ }
1488
1544
  let handle;
1489
1545
  try {
1490
1546
  handle = await this.ctx.agents.create({
1491
1547
  sessionId: SessionId(sessionId),
1492
- meta: {
1493
- cwd,
1494
- ...this.agentPreset !== undefined ? { agentPreset: this.agentPreset } : {},
1495
- },
1548
+ meta,
1549
+ ...(fork === undefined ? {} : {
1550
+ seed: fork.seed,
1551
+ inheritedEventCount: SessionLogOffset(fork.seed.length),
1552
+ }),
1496
1553
  agentOptions: this.selection ?? {},
1497
1554
  // Install the mutable selection so `/model` can live-switch the route,
1498
1555
  // the APPEND_SYSTEM.md section on this agent ONLY (never its