@antst/dashi 0.1.0-alpha.17 → 0.1.0-alpha.19

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