@antst/dashi 0.1.0-alpha.10

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/lib/application.d.ts +43 -0
  4. package/lib/application.js +273 -0
  5. package/lib/at-trigger.d.ts +8 -0
  6. package/lib/at-trigger.js +13 -0
  7. package/lib/catalogs.d.ts +25 -0
  8. package/lib/catalogs.js +235 -0
  9. package/lib/clipboard-image.d.ts +3 -0
  10. package/lib/clipboard-image.js +74 -0
  11. package/lib/completion-trigger.d.ts +2 -0
  12. package/lib/completion-trigger.js +7 -0
  13. package/lib/diff-view.d.ts +5 -0
  14. package/lib/diff-view.js +24 -0
  15. package/lib/external-editor.d.ts +3 -0
  16. package/lib/external-editor.js +39 -0
  17. package/lib/help.d.ts +2 -0
  18. package/lib/help.js +6 -0
  19. package/lib/history-records.d.ts +4 -0
  20. package/lib/history-records.js +14 -0
  21. package/lib/image-input.d.ts +8 -0
  22. package/lib/image-input.js +37 -0
  23. package/lib/index.d.ts +4 -0
  24. package/lib/index.js +301 -0
  25. package/lib/input.d.ts +10 -0
  26. package/lib/input.js +222 -0
  27. package/lib/memory.d.ts +3 -0
  28. package/lib/memory.js +27 -0
  29. package/lib/mouse-input.d.ts +1 -0
  30. package/lib/mouse-input.js +6 -0
  31. package/lib/presentation.d.ts +9 -0
  32. package/lib/presentation.js +54 -0
  33. package/lib/renderer.d.ts +20 -0
  34. package/lib/renderer.js +435 -0
  35. package/lib/rewind.d.ts +18 -0
  36. package/lib/rewind.js +84 -0
  37. package/lib/session-export.d.ts +3 -0
  38. package/lib/session-export.js +27 -0
  39. package/lib/session-list.d.ts +15 -0
  40. package/lib/session-list.js +65 -0
  41. package/lib/session-runtime.d.ts +46 -0
  42. package/lib/session-runtime.js +1293 -0
  43. package/lib/shell-command.d.ts +10 -0
  44. package/lib/shell-command.js +65 -0
  45. package/lib/state.d.ts +508 -0
  46. package/lib/state.js +667 -0
  47. package/lib/terminal-guard.d.ts +14 -0
  48. package/lib/terminal-guard.js +76 -0
  49. package/lib/tool-presentation.d.ts +14 -0
  50. package/lib/tool-presentation.js +156 -0
  51. package/lib/transcript.d.ts +12 -0
  52. package/lib/transcript.js +323 -0
  53. package/lib/tui-root.d.ts +20 -0
  54. package/lib/tui-root.js +25 -0
  55. package/package.json +73 -0
  56. package/validated-dsh-versions.json +3 -0
package/lib/state.js ADDED
@@ -0,0 +1,667 @@
1
+ import { FLAG_HELP, VERSION_LINE } from './help.js';
2
+ export function initialViewState(cwd, inline, root, cells = [], prompts = [], historyHasMore = false, attachments = []) {
3
+ return {
4
+ attachments, cells, composer: '', cwd, decisions: [], exitArmed: false, historyHasMore, inline,
5
+ newOutput: 0, prompts, rewindArmed: false, scrollOffset: 0,
6
+ sendMode: 'steer', toolMode: 'collapsed',
7
+ ...(root === undefined ? {} : { root }),
8
+ };
9
+ }
10
+ export const TURN_BELL_THRESHOLD_MS = 10_000;
11
+ function redraw() {
12
+ return [{ type: 'redraw', force: false }];
13
+ }
14
+ function focusAfter(state, decisions) {
15
+ const next = decisions[0];
16
+ return { type: 'set-composer', text: next?.kind === 'question' ? next.custom : state.composer };
17
+ }
18
+ function removeHead(state, effect) {
19
+ const decisions = state.decisions.slice(1);
20
+ return [{ ...state, decisions }, [effect, focusAfter(state, decisions), ...redraw()]];
21
+ }
22
+ function moveCursor(decision, cursor) {
23
+ if (decision.kind === 'approval')
24
+ return { ...decision, cursor: Math.max(0, Math.min(2, cursor)) };
25
+ const question = decision.questions[decision.index];
26
+ if (question === undefined)
27
+ return decision;
28
+ return { ...decision, cursor: Math.max(0, Math.min(question.options.length - (question.allowCustom === false ? 1 : 0), cursor)) };
29
+ }
30
+ function help() {
31
+ return {
32
+ kind: 'info',
33
+ title: 'Help',
34
+ lines: [
35
+ VERSION_LINE,
36
+ 'Enter send · Ctrl+J newline · Ctrl+T steer/next turn',
37
+ 'Esc interrupt · Ctrl+C clear/interrupt · Ctrl+C/D again within 2s to exit',
38
+ '/ and @ complete live · Tab insert · Shift+Tab permission · Ctrl+O cards',
39
+ '↑↓ prompt recall · Ctrl+R recall search · Ctrl+F transcript search',
40
+ 'PageUp/PageDown scroll · Ctrl+Home/Ctrl+End ends · Esc Esc rewind/draft recall',
41
+ 'Ctrl+B activity details for jobs and subagents',
42
+ '@ paths require DSH file-reference · Ctrl+V pastes an image · Backspace removes it',
43
+ 'Ctrl+S stash · Ctrl+G editor · F1 help · Ctrl+L redraw · Ctrl+Z suspend',
44
+ '/new /clear /reset /resume /continue [--all] [NAME|UUID] /fork /branch /rewind /rename /model /effort /permission /agents /queue /status /context',
45
+ '/init /memory /skills /config /login /logout /diff /plugins /plugin /tasks /bashes /subtask /history /export /copy /exit /quit',
46
+ ...FLAG_HELP,
47
+ ],
48
+ };
49
+ }
50
+ function moveOverlay(overlay, cursor, detailsCount = 0) {
51
+ if (overlay.kind === 'info' || overlay.kind === 'history')
52
+ return overlay;
53
+ const limit = overlay.kind === 'confirm' ? 1
54
+ : overlay.kind === 'details' ? Math.max(0, detailsCount - 1)
55
+ : Math.max(0, overlay.options.length - 1);
56
+ return { ...overlay, cursor: Math.max(0, Math.min(limit, cursor)) };
57
+ }
58
+ const PAGE_CELLS = 8;
59
+ function matches(cells, query) {
60
+ const needle = query.trim().toLocaleLowerCase();
61
+ if (needle === '')
62
+ return [];
63
+ const found = [];
64
+ cells.forEach((cell, index) => {
65
+ const tool = cell.tool;
66
+ const haystack = [cell.text, cell.detail, tool?.title, tool?.body,
67
+ ...(tool?.diffs?.flatMap(diff => [diff.path, ...diff.lines]) ?? [])]
68
+ .filter((value) => value !== undefined).join('\n').toLocaleLowerCase();
69
+ if (haystack.includes(needle))
70
+ found.push(index);
71
+ });
72
+ return found;
73
+ }
74
+ function copyText(cell) {
75
+ const card = cell.tool;
76
+ if (card === undefined)
77
+ return cell.text;
78
+ const detail = card.diffs === undefined
79
+ ? card.body
80
+ : card.diffs.flatMap(diff => [diff.path, ...diff.lines]).join('\n');
81
+ return detail === undefined || detail === '' ? card.title : `${card.title}\n${detail}`;
82
+ }
83
+ function refreshSearch(search, cells) {
84
+ if (search === undefined)
85
+ return undefined;
86
+ const found = matches(cells, search.query);
87
+ return { ...search, cursor: Math.min(search.cursor, Math.max(0, found.length - 1)), matches: found };
88
+ }
89
+ function completedAssistant(cells, nth) {
90
+ let seen = 0;
91
+ for (let index = cells.length - 1; index >= 0; index--) {
92
+ const cell = cells[index];
93
+ if (cell?.kind === 'assistant' && cell.pending !== true && ++seen === nth)
94
+ return cell;
95
+ }
96
+ }
97
+ function codeBlockOptions(text) {
98
+ const fence = /(?:^|\r?\n)(`{3,}|~{3,})([^\r\n]*)\r?\n([\s\S]*?)\r?\n\1[ \t]*(?=\r?\n|$)/gu;
99
+ return [...text.matchAll(fence)].map(match => {
100
+ const code = match[3] ?? '';
101
+ return {
102
+ label: `${match[2]?.trim() || 'text'} · ${code.split(/\r?\n/u)[0] ?? ''}`,
103
+ value: { kind: 'copy', text: code },
104
+ };
105
+ });
106
+ }
107
+ export function reduce(state, action) {
108
+ const head = state.decisions[0];
109
+ switch (action.type) {
110
+ case 'attachment-added': {
111
+ const replace = action.source !== undefined && action.text !== undefined && state.composer === action.source;
112
+ return [{
113
+ ...state, attachments: [...state.attachments, action.attachment],
114
+ ...(replace ? { composer: action.text } : {}),
115
+ }, [...(replace ? [{ type: 'set-composer', text: action.text }] : []), ...redraw()]];
116
+ }
117
+ case 'attachment-remove': {
118
+ if (action.index < 0 || action.index >= state.attachments.length)
119
+ return [state, []];
120
+ return [{ ...state, attachments: state.attachments.filter((_item, index) => index !== action.index) }, redraw()];
121
+ }
122
+ case 'composer-changed': {
123
+ if (head?.kind === 'question') {
124
+ if (head.custom === action.text)
125
+ return [state, []];
126
+ return [{ ...state, decisions: [{ ...head, custom: action.text }, ...state.decisions.slice(1)] }, redraw()];
127
+ }
128
+ if (state.search !== undefined) {
129
+ const found = matches(state.cells, action.text);
130
+ const cursor = found.length === 0 ? 0 : Math.min(state.search.cursor, found.length - 1);
131
+ const index = found[cursor];
132
+ return [{
133
+ ...state,
134
+ search: { ...state.search, cursor, matches: found, query: action.text },
135
+ ...(index === undefined ? {} : state.search.scope === 'history'
136
+ ? { overlay: { cursor: index, expanded: false, kind: 'history' } }
137
+ : { scrollOffset: Math.max(0, state.cells.length - 1 - index) }),
138
+ }, redraw()];
139
+ }
140
+ if (action.text === state.composer && !state.exitArmed && !state.rewindArmed)
141
+ return [state, []];
142
+ const next = {
143
+ ...state, composer: action.text, exitArmed: false, recall: undefined, rewindArmed: false,
144
+ };
145
+ if (action.completion === true)
146
+ return [next, [
147
+ { type: 'complete', caret: action.caret ?? action.text.length, query: action.text }, ...redraw(),
148
+ ]];
149
+ if (state.overlay?.kind === 'list' && state.overlay.purpose === 'completion') {
150
+ const { overlay: _overlay, ...withoutOverlay } = next;
151
+ return [withoutOverlay, redraw()];
152
+ }
153
+ return [next, redraw()];
154
+ }
155
+ case 'composer-set':
156
+ return [{ ...state, composer: action.text, exitArmed: false, rewindArmed: false }, [
157
+ { type: 'set-composer', text: action.text, ...(action.cursor === undefined ? {} : { cursor: action.cursor }) }, ...redraw(),
158
+ ]];
159
+ case 'clipboard-paste':
160
+ return head === undefined && state.overlay === undefined && state.search === undefined
161
+ ? [state, [{ type: 'clipboard-image' }]] : [state, []];
162
+ case 'ctrl-c': {
163
+ if (state.root?.status === 'running')
164
+ return [state, [{ type: 'interrupt' }]];
165
+ if (state.composer !== '' || state.attachments.length > 0) {
166
+ return [{
167
+ ...state, attachments: [], composer: '', exitArmed: false, recall: undefined, rewindArmed: false,
168
+ }, [{ type: 'set-composer', text: '' }, ...redraw()]];
169
+ }
170
+ if (state.exitArmed)
171
+ return reduce(state, { type: 'exit' });
172
+ return [{ ...state, exitArmed: true, rewindArmed: false }, [{ type: 'exit-timer', armed: true }, ...redraw()]];
173
+ }
174
+ case 'ctrl-d':
175
+ if (state.composer !== '')
176
+ return [state, []];
177
+ if (state.root?.status !== 'idle' || state.attachments.length > 0)
178
+ return reduce(state, { type: 'exit' });
179
+ return reduce(state, { type: 'ctrl-c' });
180
+ case 'decision-cancel': {
181
+ if (head === undefined)
182
+ return [state, []];
183
+ return head.kind === 'approval'
184
+ ? removeHead(state, { type: 'settle-decision', id: head.id, answer: 'cancelled' })
185
+ : removeHead(state, { type: 'reject-decision', id: head.id, reason: 'withdrawn' });
186
+ }
187
+ case 'decision-enqueued': {
188
+ if (state.decisions.some(decision => decision.id === action.decision.id))
189
+ return [state, []];
190
+ const decisions = [...state.decisions, action.decision];
191
+ const effects = [
192
+ ...(state.root?.status === 'running' ? [{ type: 'bell' }] : []),
193
+ ...(head === undefined ? [focusAfter(state, decisions), ...redraw()] : redraw()),
194
+ ];
195
+ const { overlay: _overlay, search: _search, ...withoutOverlay } = state;
196
+ return [{ ...withoutOverlay, decisions, exitArmed: false, rewindArmed: false }, effects];
197
+ }
198
+ case 'decision-move': {
199
+ if (head === undefined)
200
+ return [state, []];
201
+ const next = moveCursor(head, head.cursor + action.offset);
202
+ return [{ ...state, decisions: [next, ...state.decisions.slice(1)] }, redraw()];
203
+ }
204
+ case 'decision-number': {
205
+ if (head === undefined || action.number < 1)
206
+ return [state, []];
207
+ const next = moveCursor(head, action.number - 1);
208
+ return [{ ...state, decisions: [next, ...state.decisions.slice(1)] }, redraw()];
209
+ }
210
+ case 'decision-toggle': {
211
+ if (head?.kind !== 'question')
212
+ return [state, []];
213
+ const question = head.questions[head.index];
214
+ const option = question?.options[head.cursor];
215
+ if (question?.multiSelect !== true || option === undefined)
216
+ return [state, []];
217
+ const selected = head.selected.includes(option.label)
218
+ ? head.selected.filter(label => label !== option.label)
219
+ : [...head.selected, option.label];
220
+ return [{ ...state, decisions: [{ ...head, selected }, ...state.decisions.slice(1)] }, redraw()];
221
+ }
222
+ case 'decision-submit': {
223
+ if (head === undefined)
224
+ return [state, []];
225
+ if (head.kind === 'approval') {
226
+ const answer = head.cursor === 0 ? 'allowed-once' : head.cursor === 1 ? 'rejected' : 'cancelled';
227
+ return removeHead(state, { type: 'settle-decision', id: head.id, answer });
228
+ }
229
+ const question = head.questions[head.index];
230
+ if (question === undefined)
231
+ return [state, []];
232
+ const custom = question.allowCustom !== false && head.cursor === question.options.length ? question.secret === true ? head.custom : head.custom.trim() : undefined;
233
+ if (custom !== undefined && custom === '')
234
+ return [state, []];
235
+ const option = question.options[head.cursor];
236
+ const selected = question.multiSelect
237
+ ? head.selected.length === 0 && option !== undefined ? [option.label] : head.selected
238
+ : option === undefined ? [] : [option.label];
239
+ const answer = { id: question.id, selected, ...(custom === undefined ? {} : { custom }) };
240
+ const answers = [...head.answers, answer];
241
+ if (head.index + 1 < head.questions.length) {
242
+ const next = { ...head, answers, cursor: 0, custom: '', index: head.index + 1, selected: [] };
243
+ return [{ ...state, decisions: [next, ...state.decisions.slice(1)] }, [{ type: 'set-composer', text: '' }, ...redraw()]];
244
+ }
245
+ return removeHead(state, { type: 'settle-decision', id: head.id, answer: { answers } });
246
+ }
247
+ case 'decision-withdrawn': {
248
+ const index = state.decisions.findIndex(decision => decision.id === action.id);
249
+ if (index === -1)
250
+ return [state, []];
251
+ const decisions = state.decisions.filter(decision => decision.id !== action.id);
252
+ const effects = [{ type: 'reject-decision', id: action.id, reason: 'withdrawn' }];
253
+ if (index === 0)
254
+ effects.push(focusAfter(state, decisions), ...redraw());
255
+ return [{ ...state, decisions }, effects];
256
+ }
257
+ case 'disarm-exit':
258
+ return state.exitArmed || state.rewindArmed
259
+ ? [{ ...state, exitArmed: false, rewindArmed: false }, [
260
+ { type: 'exit-timer', armed: false }, ...redraw(),
261
+ ]] : [state, []];
262
+ case 'escape':
263
+ if (head !== undefined)
264
+ return reduce(state, { type: 'decision-cancel' });
265
+ if (state.search !== undefined)
266
+ return reduce(state, { type: 'search-close' });
267
+ if (state.overlay !== undefined)
268
+ return reduce(state, { type: 'overlay-close' });
269
+ if (state.root?.status === 'running') {
270
+ return [{ ...state, rewindArmed: false }, [{ type: 'interrupt' }]];
271
+ }
272
+ if (state.root !== undefined) {
273
+ if (state.rewindArmed) {
274
+ if (state.composer !== '')
275
+ return [{
276
+ ...state, attachments: [], composer: '', prompts: [...state.prompts, state.composer],
277
+ recall: undefined, rewindArmed: false,
278
+ }, [{ type: 'set-composer', text: '' }, ...redraw()]];
279
+ return [{ ...state, rewindArmed: false }, [
280
+ { type: 'activate-overlay', value: { kind: 'open-rewind' } }, ...redraw(),
281
+ ]];
282
+ }
283
+ return [{ ...state, rewindArmed: true }, redraw()];
284
+ }
285
+ return [state, []];
286
+ case 'exit': {
287
+ const effects = state.decisions.map(decision => ({ type: 'reject-decision', id: decision.id, reason: 'terminal' }));
288
+ return [{ ...state, decisions: [] }, [...effects, { type: 'exit' }]];
289
+ }
290
+ case 'external-edit':
291
+ return head === undefined && state.overlay === undefined && state.search === undefined
292
+ ? [state, [{ type: 'external-edit', cwd: state.root?.cwd ?? state.cwd, text: state.composer }]]
293
+ : [state, []];
294
+ case 'help':
295
+ return head === undefined
296
+ ? [{ ...state, overlay: help(), search: undefined, exitArmed: false, rewindArmed: false }, [
297
+ ...(state.search === undefined ? [] : [{ type: 'set-composer', text: state.composer }]),
298
+ ...redraw(),
299
+ ]]
300
+ : [state, []];
301
+ case 'history-copy': {
302
+ if (state.overlay?.kind !== 'history')
303
+ return [state, []];
304
+ const cell = state.cells[state.overlay.cursor];
305
+ return cell === undefined ? [state, []] : [state, [{ type: 'copy', text: copyText(cell) }]];
306
+ }
307
+ case 'history-move': {
308
+ if (state.overlay?.kind !== 'history' || state.cells.length === 0)
309
+ return [state, []];
310
+ const cursor = Math.max(0, Math.min(state.cells.length - 1, state.overlay.cursor + action.offset));
311
+ const load = cursor === 0 && action.offset < 0 && state.historyHasMore && state.root !== undefined
312
+ ? [{ type: 'load-history', rootId: state.root.id }] : [];
313
+ return [{ ...state, overlay: { ...state.overlay, cursor, expanded: false } }, [...load, ...redraw()]];
314
+ }
315
+ case 'history-page': {
316
+ if (state.root?.id !== action.rootId)
317
+ return [state, []];
318
+ const added = Math.max(0, action.cells.length - state.cells.length);
319
+ let overlay = state.overlay?.kind === 'history'
320
+ ? { ...state.overlay, cursor: Math.max(0, added - 1) }
321
+ : state.overlay;
322
+ const search = refreshSearch(state.search, action.cells);
323
+ if (search?.scope === 'history' && overlay?.kind === 'history') {
324
+ overlay = { ...overlay, cursor: search.matches[search.cursor] ?? overlay.cursor };
325
+ }
326
+ return [{
327
+ ...state, cells: action.cells, historyHasMore: action.hasMore,
328
+ scrollOffset: action.cells.length === 0 ? 0 : action.cells.length - 1,
329
+ ...(overlay === undefined ? {} : { overlay }),
330
+ ...(search === undefined ? {} : { search }),
331
+ }, redraw()];
332
+ }
333
+ case 'history-toggle':
334
+ return state.overlay?.kind !== 'history' ? [state, []] : [{
335
+ ...state, overlay: { ...state.overlay, expanded: !state.overlay.expanded },
336
+ }, redraw()];
337
+ case 'open-history':
338
+ return head !== undefined ? [state, []] : [{
339
+ ...state, overlay: {
340
+ cursor: Math.max(0, state.cells.length - 1), expanded: false, kind: 'history',
341
+ }, search: undefined, exitArmed: false, rewindArmed: false,
342
+ }, redraw()];
343
+ case 'open-details': {
344
+ if (head !== undefined || state.root === undefined)
345
+ return [state, []];
346
+ const count = (state.root.jobs?.length ?? 0) + (state.root.subagents?.length ?? 0);
347
+ return count === 0 ? [state, []] : [{
348
+ ...state, overlay: { cursor: 0, expanded: false, kind: 'details' },
349
+ search: undefined, exitArmed: false, rewindArmed: false,
350
+ }, redraw()];
351
+ }
352
+ case 'completion-ready': {
353
+ if (head !== undefined || state.composer !== action.query)
354
+ return [state, []];
355
+ if (action.options.length === 0) {
356
+ if (state.overlay?.kind !== 'list' || state.overlay.purpose !== 'completion')
357
+ return [state, []];
358
+ const { overlay: _overlay, ...withoutOverlay } = state;
359
+ return [withoutOverlay, redraw()];
360
+ }
361
+ return [{ ...state, overlay: {
362
+ cursor: 0, kind: 'list', options: action.options, purpose: 'completion', title: 'Complete',
363
+ } }, redraw()];
364
+ }
365
+ case 'open-overlay':
366
+ return head === undefined
367
+ ? [{ ...state, overlay: action.overlay, exitArmed: false, rewindArmed: false }, redraw()]
368
+ : [state, []];
369
+ case 'overlay-close': {
370
+ if (state.overlay === undefined)
371
+ return [state, []];
372
+ const { overlay: _overlay, ...withoutOverlay } = state;
373
+ return [withoutOverlay, redraw()];
374
+ }
375
+ case 'overlay-move': {
376
+ if (state.overlay === undefined)
377
+ return [state, []];
378
+ const count = (state.root?.jobs?.length ?? 0) + (state.root?.subagents?.length ?? 0);
379
+ const overlay = moveOverlay(state.overlay, state.overlay.kind === 'info' ? 0 : state.overlay.cursor + action.offset, count);
380
+ return overlay === state.overlay ? [state, []] : [{ ...state, overlay }, redraw()];
381
+ }
382
+ case 'overlay-number': {
383
+ if (state.overlay === undefined || state.overlay.kind === 'info' || action.number < 1)
384
+ return [state, []];
385
+ const count = (state.root?.jobs?.length ?? 0) + (state.root?.subagents?.length ?? 0);
386
+ return [{ ...state, overlay: moveOverlay(state.overlay, action.number - 1, count) }, redraw()];
387
+ }
388
+ case 'overlay-submit': {
389
+ const overlay = state.overlay;
390
+ if (overlay === undefined)
391
+ return [state, []];
392
+ if (overlay.kind === 'history')
393
+ return reduce(state, { type: 'history-toggle' });
394
+ if (overlay.kind === 'info')
395
+ return reduce(state, { type: 'overlay-close' });
396
+ if (overlay.kind === 'details') {
397
+ const job = state.root?.jobs?.[overlay.cursor - (state.root?.subagents?.length ?? 0)];
398
+ if (job !== undefined)
399
+ return [state, [{ type: 'activate-overlay', value: { jobId: job.id, kind: 'job-output' } }]];
400
+ return [{ ...state, overlay: { ...overlay, expanded: !overlay.expanded } }, redraw()];
401
+ }
402
+ if (overlay.kind === 'confirm') {
403
+ if (overlay.cursor !== 0)
404
+ return reduce(state, { type: 'overlay-close' });
405
+ const { overlay: _overlay, ...withoutOverlay } = state;
406
+ return [withoutOverlay, [{ type: 'activate-overlay', value: overlay.value }, ...redraw()]];
407
+ }
408
+ const option = overlay.options[overlay.cursor];
409
+ if (option === undefined)
410
+ return [state, []];
411
+ if (option.value.kind === 'insert') {
412
+ const { overlay: _overlay, ...withoutOverlay } = state;
413
+ if (action.execute === true && option.submitOnEnter === true) {
414
+ return reduce({ ...withoutOverlay, composer: option.value.text.trimEnd() }, { type: 'submit' });
415
+ }
416
+ return [{ ...withoutOverlay, composer: option.value.text, recall: undefined }, [
417
+ { type: 'set-composer', text: option.value.text }, ...redraw(),
418
+ ]];
419
+ }
420
+ if (option.value.kind === 'attach') {
421
+ const { overlay: _overlay, ...withoutOverlay } = state;
422
+ return [withoutOverlay, [{
423
+ type: 'attach', path: option.value.path, source: option.value.source, text: option.value.text,
424
+ }, ...redraw()]];
425
+ }
426
+ if (option.value.kind === 'copy') {
427
+ const { overlay: _overlay, ...withoutOverlay } = state;
428
+ return [withoutOverlay, [{ type: 'copy', text: option.value.text }, ...redraw()]];
429
+ }
430
+ if (option.value.kind === 'permission' && option.danger === true) {
431
+ return [{ ...state, overlay: {
432
+ acceptLabel: 'Enable',
433
+ cursor: 1,
434
+ detail: option.confirmDetail ?? ['This preset disables approval prompts.'],
435
+ kind: 'confirm',
436
+ title: `Enable ${option.label}?`,
437
+ value: option.value,
438
+ } }, redraw()];
439
+ }
440
+ const { overlay: _overlay, ...withoutOverlay } = state;
441
+ return [withoutOverlay, [{ type: 'activate-overlay', value: option.value }, ...redraw()]];
442
+ }
443
+ case 'redraw': return [state, [{ type: 'redraw', force: true }]];
444
+ case 'prompt-recorded':
445
+ return state.root?.id !== action.rootId
446
+ ? [state, []] : [{ ...state, prompts: [...state.prompts, action.text], recall: undefined }, []];
447
+ case 'recall-move': {
448
+ if (state.prompts.length === 0)
449
+ return [state, []];
450
+ const current = state.recall?.cursor ?? state.prompts.length;
451
+ const cursor = Math.max(0, Math.min(state.prompts.length, current + action.offset));
452
+ const draft = state.recall?.draft ?? state.composer;
453
+ const text = cursor === state.prompts.length ? draft : state.prompts[cursor];
454
+ if (text === undefined)
455
+ return [state, []];
456
+ return [{
457
+ ...state, composer: text,
458
+ ...(cursor === state.prompts.length ? { recall: undefined } : { recall: { cursor, draft } }),
459
+ }, [{ type: 'set-composer', text }, ...redraw()]];
460
+ }
461
+ case 'request-completion':
462
+ return head === undefined && state.overlay === undefined && state.composer !== ''
463
+ ? [state, [{ type: 'complete', caret: action.caret, query: state.composer }]] : [state, []];
464
+ case 'request-search':
465
+ return head === undefined && state.overlay === undefined && state.root !== undefined
466
+ && state.composer.trim() !== ''
467
+ ? [state, [{ type: 'search', query: state.composer, rootId: state.root.id }]] : [state, []];
468
+ case 'scroll': {
469
+ if (state.cells.length === 0)
470
+ return [state, []];
471
+ const limit = state.cells.length - 1;
472
+ const scrollOffset = action.direction === 'end' ? 0
473
+ : action.direction === 'home' ? limit
474
+ : Math.max(0, Math.min(limit, state.scrollOffset
475
+ + (action.direction === 'page-up' ? 1 : -1) * (action.lines ?? PAGE_CELLS)));
476
+ const load = state.root !== undefined && state.historyHasMore
477
+ && (action.direction === 'home' || action.direction === 'page-up' && scrollOffset === limit)
478
+ ? [{ type: 'load-history', rootId: state.root.id }] : [];
479
+ return [{
480
+ ...state, scrollOffset,
481
+ ...(scrollOffset === 0 ? { newOutput: 0 } : {}),
482
+ }, [...load, ...redraw()]];
483
+ }
484
+ case 'search-close': {
485
+ if (state.search === undefined)
486
+ return [state, []];
487
+ const { search: _search, ...withoutSearch } = state;
488
+ return [withoutSearch, [{ type: 'set-composer', text: state.composer }, ...redraw()]];
489
+ }
490
+ case 'search-move': {
491
+ const search = state.search;
492
+ if (search === undefined || search.matches.length === 0)
493
+ return [state, []];
494
+ const cursor = (search.cursor + action.offset + search.matches.length) % search.matches.length;
495
+ const index = search.matches[cursor];
496
+ if (index === undefined)
497
+ return [state, []];
498
+ return [{
499
+ ...state, search: { ...search, cursor },
500
+ ...(search.scope === 'history'
501
+ ? { overlay: { cursor: index, expanded: false, kind: 'history' } }
502
+ : { scrollOffset: Math.max(0, state.cells.length - 1 - index) }),
503
+ }, redraw()];
504
+ }
505
+ case 'search-open': {
506
+ if (head !== undefined || action.scope === 'history' && state.overlay?.kind !== 'history')
507
+ return [state, []];
508
+ const found = matches(state.cells, '');
509
+ const next = { ...state, search: { cursor: 0, matches: found, query: '', scope: action.scope } };
510
+ if (action.scope === 'history')
511
+ return [next, [{ type: 'set-composer', text: '' }, ...redraw()]];
512
+ const { overlay: _overlay, ...withoutOverlay } = next;
513
+ return [withoutOverlay, [{ type: 'set-composer', text: '' }, ...redraw()]];
514
+ }
515
+ case 'suspend': return [state, [{ type: 'suspend' }]];
516
+ case 'stash-toggle': {
517
+ const current = { attachments: state.attachments, composer: state.composer, cursor: action.cursor };
518
+ if (state.stash === undefined) {
519
+ if (current.composer === '' && current.attachments.length === 0)
520
+ return [state, []];
521
+ return [{ ...state, attachments: [], composer: '', stash: current }, [
522
+ { type: 'set-composer', text: '' }, ...redraw(),
523
+ ]];
524
+ }
525
+ const restored = state.stash;
526
+ if (current.composer === '' && current.attachments.length === 0) {
527
+ const { stash: _stash, ...withoutStash } = state;
528
+ return [{ ...withoutStash, attachments: restored.attachments, composer: restored.composer }, [
529
+ { type: 'set-composer', text: restored.composer, cursor: restored.cursor }, ...redraw(),
530
+ ]];
531
+ }
532
+ return [{
533
+ ...state, attachments: restored.attachments, composer: restored.composer, stash: current,
534
+ }, [{ type: 'set-composer', text: restored.composer, cursor: restored.cursor }, ...redraw()]];
535
+ }
536
+ case 'terminal-lost':
537
+ return state.decisions.length === 0 ? [state, []] : [
538
+ { ...state, decisions: [] },
539
+ state.decisions.map(decision => ({ type: 'reject-decision', id: decision.id, reason: 'terminal' })),
540
+ ];
541
+ case 'cycle-permission':
542
+ if (head !== undefined)
543
+ return [state, []];
544
+ if (state.overlay === undefined)
545
+ return [state, [{ type: 'cycle-permission' }]];
546
+ {
547
+ const { overlay: _overlay, ...withoutOverlay } = state;
548
+ return [withoutOverlay, [{ type: 'cycle-permission' }]];
549
+ }
550
+ case 'toggle-send-mode': {
551
+ if (state.root?.status !== 'running')
552
+ return [state, []];
553
+ const sendMode = state.sendMode === 'steer' ? 'next-turn' : 'steer';
554
+ return [{ ...state, sendMode }, redraw()];
555
+ }
556
+ case 'toggle-tool-mode': {
557
+ const toolMode = state.toolMode === 'collapsed' ? 'expanded' : state.toolMode === 'expanded' ? 'hidden' : 'collapsed';
558
+ return [{ ...state, toolMode }, redraw()];
559
+ }
560
+ case 'turn-ended':
561
+ return state.root?.id === action.rootId && action.durationMs >= TURN_BELL_THRESHOLD_MS
562
+ ? [state, [{ type: 'bell' }]] : [state, []];
563
+ case 'submit': {
564
+ if (head !== undefined)
565
+ return reduce(state, { type: 'decision-submit' });
566
+ if (state.root === undefined || state.composer === '' && state.attachments.length === 0)
567
+ return [state, []];
568
+ const mode = state.root.status === 'running' ? state.sendMode : 'next-turn';
569
+ return [{
570
+ ...state, attachments: [], composer: '', exitArmed: false, recall: undefined, rewindArmed: false,
571
+ }, [
572
+ { type: 'set-composer', text: '' }, {
573
+ type: 'submit', attachments: state.attachments, text: state.composer, mode,
574
+ }, ...redraw(),
575
+ ]];
576
+ }
577
+ case 'root-bound':
578
+ {
579
+ const { overlay: _overlay, ...withoutOverlay } = state;
580
+ return [{
581
+ ...withoutOverlay, cells: action.cells, historyHasMore: action.hasMore,
582
+ newOutput: 0, prompts: action.prompts, recall: undefined,
583
+ rewindArmed: false, root: action.root, scrollOffset: 0, search: undefined,
584
+ }, redraw()];
585
+ }
586
+ case 'root-cleared': {
587
+ if (state.root === undefined)
588
+ return [state, []];
589
+ const { root: _root, ...withoutRoot } = state;
590
+ return [{
591
+ ...withoutRoot, historyHasMore: false, newOutput: 0, prompts: [],
592
+ recall: undefined, rewindArmed: false, scrollOffset: 0, search: undefined,
593
+ }, redraw()];
594
+ }
595
+ case 'root-model':
596
+ if (state.root?.id !== action.rootId)
597
+ return [state, []];
598
+ {
599
+ const { effort: _effort, ...root } = state.root;
600
+ return [{ ...state, root: {
601
+ ...root,
602
+ model: action.model,
603
+ provider: action.provider,
604
+ ...(action.effort === undefined ? {} : { effort: action.effort }),
605
+ } }, redraw()];
606
+ }
607
+ case 'root-presentation': {
608
+ if (state.root?.id !== action.rootId)
609
+ return [state, []];
610
+ const { contextPercent: _contextPercent, ...root } = state.root;
611
+ const contextPercent = action.contextPercent === null
612
+ ? undefined
613
+ : action.contextPercent ?? state.root.contextPercent;
614
+ return [{ ...state, root: {
615
+ ...root,
616
+ ...(action.jobs === undefined && state.root.jobs === undefined ? {} : { jobs: action.jobs ?? state.root.jobs }),
617
+ ...(action.subagents === undefined && state.root.subagents === undefined
618
+ ? {} : { subagents: action.subagents ?? state.root.subagents }),
619
+ ...(contextPercent === undefined ? {} : { contextPercent }),
620
+ } }, redraw()];
621
+ }
622
+ case 'root-status':
623
+ return state.root?.id !== action.rootId || state.root.status === action.status
624
+ ? [state, []] : [{ ...state, root: { ...state.root, status: action.status } }, redraw()];
625
+ case 'root-permission':
626
+ return state.root?.id !== action.rootId || state.root.permission === action.preset
627
+ ? [state, []] : [{ ...state, root: { ...state.root, permission: action.preset } }, redraw()];
628
+ case 'root-title':
629
+ return state.root?.id !== action.rootId || state.root.title === action.title
630
+ ? [state, []] : [{ ...state, root: { ...state.root, title: action.title } }, redraw()];
631
+ case 'runtime-error':
632
+ if (action.rootId !== undefined && state.root?.id !== action.rootId)
633
+ return [state, []];
634
+ return [{
635
+ ...state, cells: [...state.cells, {
636
+ key: `runtime-error:${state.cells.length}`, kind: 'error', text: action.message,
637
+ }],
638
+ ...(state.scrollOffset === 0 ? {} : {
639
+ newOutput: state.newOutput + 1, scrollOffset: state.scrollOffset + 1,
640
+ }),
641
+ }, redraw()];
642
+ case 'transcript-changed': {
643
+ if (action.rootId !== undefined && state.root?.id !== action.rootId)
644
+ return [state, []];
645
+ const added = Math.max(0, action.cells.length - state.cells.length);
646
+ const search = refreshSearch(state.search, action.cells);
647
+ return [{
648
+ ...state, cells: action.cells,
649
+ ...(action.hasMore === undefined ? {} : { historyHasMore: action.hasMore }),
650
+ ...(search === undefined ? {} : { search }),
651
+ ...(state.scrollOffset === 0 ? { newOutput: 0 } : {
652
+ newOutput: state.newOutput + 1, scrollOffset: state.scrollOffset + added,
653
+ }),
654
+ }, redraw()];
655
+ }
656
+ case 'copy-assistant': {
657
+ const cell = completedAssistant(state.cells, action.selection === 'code' ? 1 : action.selection);
658
+ if (cell === undefined)
659
+ return [state, []];
660
+ if (action.selection !== 'code')
661
+ return [state, [{ type: 'copy', text: cell.text }]];
662
+ return [{ ...state, overlay: {
663
+ cursor: 0, kind: 'list', options: codeBlockOptions(cell.text), purpose: 'copy', title: 'Copy code block',
664
+ } }, redraw()];
665
+ }
666
+ }
667
+ }