@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
@@ -1,54 +0,0 @@
1
- /** Web-equivalent context occupancy; undefined until both DSH projection fields exist. */
2
- export function contextPercent(value) {
3
- if (typeof value !== 'object' || value === null)
4
- return undefined;
5
- const pressure = value;
6
- const used = typeof pressure.projectedTokens === 'number' ? pressure.projectedTokens : pressure.pressureTokens;
7
- return typeof used !== 'number' || typeof pressure.contextWindow !== 'number'
8
- ? undefined
9
- : Math.min(100, Math.round(used / pressure.contextWindow * 100));
10
- }
11
- export function jobViews(jobs) {
12
- return jobs.map(job => ({
13
- id: String(job.id), kind: job.kind, label: job.label, startedAt: job.startedAt, status: job.status,
14
- ...(job.detail === undefined ? {} : { detail: job.detail }),
15
- ...(job.finishedAt === undefined ? {} : { finishedAt: job.finishedAt }),
16
- }));
17
- }
18
- function summaryTitle(summary) {
19
- const title = summary?.projections?.values.title;
20
- return typeof title === 'string' && title !== '' ? title : undefined;
21
- }
22
- function tokenTotal(summary) {
23
- const usage = summary?.projections?.values.tokenUsage;
24
- return usage === undefined ? undefined
25
- : usage.uncachedInputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
26
- }
27
- /** Read DSH's direct-child catalog and projection-backed summary facts. */
28
- export async function subagentViews(ctx, parentId, signal) {
29
- const subagents = ctx.get('subagents');
30
- if (subagents === undefined)
31
- return [];
32
- const [catalog, listed] = await Promise.all([
33
- subagents.remoteExportList(parentId, signal),
34
- ctx.sessionController.list({}, signal),
35
- ]);
36
- const summaries = new Map(listed.items.map(summary => [String(summary.sessionId), summary]));
37
- return catalog.entries.flatMap((entry) => {
38
- if (entry.kind !== 'child')
39
- return [];
40
- const summary = summaries.get(String(entry.id));
41
- const timing = summary?.projections?.values.subagentTiming;
42
- const title = summaryTitle(summary);
43
- const tokens = tokenTotal(summary);
44
- return [{
45
- id: String(entry.id), label: entry.label ?? String(entry.id), mode: entry.mode, state: entry.activity,
46
- ...(timing === undefined ? {} : {
47
- elapsedMs: timing.settledMs,
48
- ...(timing.active === undefined ? {} : { active: timing.active }),
49
- }),
50
- ...(title === undefined ? {} : { summary: title }),
51
- ...(tokens === undefined ? {} : { tokens }),
52
- }];
53
- });
54
- }
package/lib/renderer.d.ts DELETED
@@ -1,21 +0,0 @@
1
- import { type Terminal } from '@earendil-works/pi-tui';
2
- import { type InputBindings } from './input.js';
3
- import type { ComposerCursor } from './state.js';
4
- import { type TerminalSession } from './terminal-guard.js';
5
- export type CopyResult = 'ok' | 'too-large' | 'unsupported';
6
- export interface Renderer extends TerminalSession {
7
- bell(): void;
8
- copy(text: string): CopyResult;
9
- discardSecretComposer(): void;
10
- materializedCells(): number;
11
- setComposer(text: string, cursor?: ComposerCursor): void;
12
- }
13
- export interface RendererOptions extends InputBindings {
14
- readonly accessible?: boolean;
15
- readonly inline: boolean;
16
- readonly statusLine?: () => string | undefined;
17
- readonly terminal?: Terminal;
18
- readonly terminalType?: string;
19
- }
20
- export declare function supportsOsc52(terminalType: string | undefined): boolean;
21
- export declare function createRenderer(options: RendererOptions): Renderer;
package/lib/renderer.js DELETED
@@ -1,491 +0,0 @@
1
- import { Editor, Markdown, TuiAltScreen, TuiMainScreen, stripTerminalSequences, truncateToWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
2
- import { caretOffset, installInput } from './input.js';
3
- import { hasCompletionTrigger } from './completion-trigger.js';
4
- import { createProcessTerminal } from './terminal-guard.js';
5
- const RESET = '\u001B[0m';
6
- const BOLD_CYAN = '\u001B[1;36m';
7
- const DIM = '\u001B[2m';
8
- const FAINT_BORDER = '\u001B[90m';
9
- const GREEN = '\u001B[32m';
10
- const RED = '\u001B[31m';
11
- const YELLOW = '\u001B[33m';
12
- const COPY_LIMIT = 64 * 1024;
13
- const OVERSCAN_CELLS = 8;
14
- const THEME_GENERATION = 1;
15
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
16
- const SPINNER_INTERVAL_MS = 80;
17
- const MASK_TOKEN = new RegExp(String.raw `\u001B(?:\[[0-?]*[ -/]*[@-~]|[_P^X][^\u0007]*(?:\u0007|\u001B\\))|[^\s]`, 'gu');
18
- const markdownTheme = {
19
- heading: text => `\u001B[36m${text}${RESET}`,
20
- link: text => `\u001B[36m${text}${RESET}`,
21
- linkUrl: text => `${DIM}${text}${RESET}`,
22
- code: text => `${YELLOW}${text}${RESET}`,
23
- codeBlock: text => `${GREEN}${text}${RESET}`,
24
- codeBlockBorder: text => `${DIM}${text}${RESET}`,
25
- quote: text => `${DIM}${text}${RESET}`,
26
- quoteBorder: text => `${DIM}${text}${RESET}`,
27
- hr: text => `${DIM}${text}${RESET}`,
28
- listBullet: text => `\u001B[36m${text}${RESET}`,
29
- bold: text => `\u001B[1m${text}\u001B[22m`,
30
- italic: text => `\u001B[3m${text}\u001B[23m`,
31
- strikethrough: text => `\u001B[9m${text}\u001B[29m`,
32
- underline: text => `\u001B[4m${text}\u001B[24m`,
33
- };
34
- export function supportsOsc52(terminalType) {
35
- return terminalType?.toLocaleLowerCase() !== 'dumb';
36
- }
37
- function setEditor(editor, text, cursor) {
38
- editor.setText(text);
39
- if (cursor === undefined)
40
- return;
41
- const lines = text.split('\n');
42
- const lineIndex = Math.max(0, Math.min(cursor.line, lines.length - 1));
43
- const line = lines[lineIndex] ?? '';
44
- const col = Math.max(0, Math.min(cursor.col, line.length));
45
- const suffix = `${line.slice(col)}${lines.slice(lineIndex + 1).map(value => `\n${value}`).join('')}`;
46
- const graphemes = [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(suffix)].length;
47
- for (let step = 0; step < graphemes; step++)
48
- editor.handleInput('\u001B[D');
49
- }
50
- function toolText(cell, mode, accessible) {
51
- const card = cell.tool;
52
- const color = cell.kind === 'error' ? RED : cell.pending ? YELLOW : GREEN;
53
- const marker = accessible
54
- ? `[${cell.kind === 'error' ? 'error' : cell.pending === true ? 'pending' : 'done'}]`
55
- : cell.pending === true ? '○' : '●';
56
- if (card === undefined)
57
- return `${color}${marker} ${plain(cell.text)}${RESET}`;
58
- const status = card.status === undefined ? '' : ` ${DIM}${plain(card.status)}${RESET}`;
59
- const summaries = card.diffs?.map(diff => `${plain(diff.path)} ${GREEN}+${String(diff.added)}${RESET} ${RED}-${String(diff.removed)}${RESET}`) ?? [];
60
- const head = `${color}${marker} ${plain(card.title)}${RESET}${status}${summaries.length === 0 ? '' : ` ${summaries.join(' · ')}`}`;
61
- if (mode !== 'expanded' || cell.collapsed === true)
62
- return head;
63
- const body = card.diffs === undefined
64
- ? card.body
65
- : card.diffs.flatMap(diff => [`${FAINT_BORDER}── ${plain(diff.path)}${RESET}`, ...diff.lines.map(value => plain(value))]).join('\n');
66
- return body === undefined || body === '' ? head : `${head}\n${DIM}${plain(body, true)}${RESET}`;
67
- }
68
- function cellText(cell, toolMode, accessible, now = Date.now()) {
69
- if (cell.tool !== undefined || cell.kind === 'tool')
70
- return toolText(cell, toolMode, accessible);
71
- const raw = accessible && cell.kind === 'todo'
72
- ? cell.text.replace(/^✓ /gmu, '[done] ').replace(/^● /gmu, '[active] ').replace(/^○ /gmu, '[pending] ')
73
- : cell.text;
74
- const safe = plain(raw, true);
75
- switch (cell.kind) {
76
- case 'user': return `${BOLD_CYAN}You${RESET}\n${safe}`;
77
- case 'assistant': return `${BOLD_CYAN}Assistant${RESET}\n${safe}`;
78
- case 'command': {
79
- const elapsed = cell.elapsedMs ?? (cell.startedAt === undefined ? undefined : Math.max(0, now - cell.startedAt));
80
- const marker = cell.pending === true
81
- ? accessible ? '[running]' : SPINNER_FRAMES[Math.floor(now / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length]
82
- : accessible ? '[done]' : '◆';
83
- return `${cell.pending === true ? YELLOW : DIM}${marker} ${safe}${elapsed === undefined ? '' : ` · ${duration(elapsed)}`}${RESET}`;
84
- }
85
- case 'context': {
86
- if (cell.collapsed !== true || cell.detail === undefined)
87
- return `${DIM}Context · ${safe}${RESET}`;
88
- const head = `${DIM}Context · ${plain(cell.detail, true)}${RESET}`;
89
- return toolMode === 'expanded' ? `${head}\n${safe}` : head;
90
- }
91
- case 'shell': return `${DIM}Shell${RESET}\n${safe}`;
92
- case 'compaction': return `${DIM}${accessible ? '[compacted]' : '↻'} ${safe}${RESET}`;
93
- case 'reasoning': return cell.collapsed
94
- ? `${DIM}Thinking · ${[...safe].length} characters${RESET}`
95
- : `${DIM}Thinking${RESET}\n${safe}`;
96
- case 'outcome': return `${DIM}${accessible ? '[done]' : '✓'} ${safe}${RESET}`;
97
- case 'todo': return cell.collapsed ? `${DIM}${safe}${RESET}` : `${BOLD_CYAN}${safe}${RESET}`;
98
- case 'error': return `${RED}Error · ${safe}${RESET}${cell.detail === undefined ? '' : `\n${DIM}${plain(cell.detail)}${RESET}`}`;
99
- }
100
- }
101
- function duration(ms) {
102
- const seconds = Math.max(0, Math.floor(ms / 1_000));
103
- if (seconds < 60)
104
- return `${String(seconds)}s`;
105
- const minutes = Math.floor(seconds / 60);
106
- if (minutes < 60)
107
- return `${String(minutes)}m ${String(seconds % 60)}s`;
108
- return `${String(Math.floor(minutes / 60))}h ${String(minutes % 60)}m`;
109
- }
110
- function jobElapsed(job, now) {
111
- return Math.max(0, (job.finishedAt ?? now) - job.startedAt);
112
- }
113
- function subagentElapsed(subagent, now) {
114
- if (subagent.elapsedMs === undefined)
115
- return undefined;
116
- if (subagent.active === undefined)
117
- return subagent.elapsedMs;
118
- const through = subagent.state === 'running' ? now : subagent.active.through;
119
- return subagent.elapsedMs + Math.max(0, through - subagent.active.since);
120
- }
121
- function activities(state) {
122
- return [
123
- ...(state.root?.subagents ?? []).map(value => ({ kind: 'subagent', value })),
124
- ...(state.root?.jobs ?? []).map(value => ({ kind: 'job', value })),
125
- ];
126
- }
127
- function latestPendingCommand(cells) {
128
- for (let index = cells.length - 1; index >= 0; index--) {
129
- const cell = cells[index];
130
- if (cell?.kind === 'command' && cell.pending === true)
131
- return cell;
132
- }
133
- }
134
- function activityText(activity, now) {
135
- if (activity.kind === 'job') {
136
- const job = activity.value;
137
- return `Job · ${job.label} · ${job.status} · ${duration(jobElapsed(job, now))}${job.detail === undefined ? '' : ` · ${job.detail}`}`;
138
- }
139
- const subagent = activity.value;
140
- const elapsed = subagentElapsed(subagent, now);
141
- return `Subagent · ${subagent.label} · ${subagent.state}${elapsed === undefined ? '' : ` · ${duration(elapsed)}`}${subagent.summary === undefined ? '' : ` · ${subagent.summary}`}`;
142
- }
143
- function plain(text, preserveNewlines = false) {
144
- const withoutC1 = text
145
- .replace(/\u009B[0-?]*[ -/]*[@-~]/gu, '')
146
- .replace(/[\u0080-\u009F]/gu, '');
147
- return [...stripTerminalSequences(withoutC1)].map(character => {
148
- const code = character.codePointAt(0) ?? 0;
149
- if (preserveNewlines && character === '\n')
150
- return character;
151
- return code < 32 || code === 127 ? ' ' : character;
152
- }).join('');
153
- }
154
- function line(text, width) {
155
- return truncateToWidth(text, Math.max(1, width), '…');
156
- }
157
- function windowRows(values, size, position, follow) {
158
- if (values.length <= size)
159
- return [...values];
160
- if (size < 3)
161
- return [values[Math.max(0, Math.min(values.length - 1, position))] ?? ''];
162
- const start = Math.max(0, Math.min(values.length - Math.max(1, size - 1), follow ? position - Math.floor((size - 2) / 2) : position));
163
- const above = start > 0;
164
- let end = Math.min(values.length, start + size - Number(above));
165
- if (end < values.length)
166
- end--;
167
- return [
168
- ...(above ? [`${DIM}↑ ${String(start)} more${RESET}`] : []),
169
- ...values.slice(start, end),
170
- ...(end < values.length ? [`${DIM}↓ ${String(values.length - end)} more${RESET}`] : []),
171
- ];
172
- }
173
- function theme() {
174
- const identity = (text) => text;
175
- return {
176
- borderColor: text => `${FAINT_BORDER}${text}${RESET}`,
177
- selectList: {
178
- selectedPrefix: identity,
179
- selectedText: identity,
180
- description: identity,
181
- scrollInfo: identity,
182
- noMatch: identity,
183
- },
184
- };
185
- }
186
- export function createRenderer(options) {
187
- const terminal = options.terminal ?? createProcessTerminal(options.inline ? undefined : lines => {
188
- options.dispatch({ type: 'scroll', direction: lines > 0 ? 'page-up' : 'page-down', lines: Math.abs(lines) });
189
- });
190
- const terminalType = options.terminalType ?? (options.terminal === undefined ? process.env.TERM : undefined);
191
- const accessible = options.accessible === true || terminalType?.toLocaleLowerCase() === 'dumb';
192
- const osc52 = supportsOsc52(terminalType);
193
- const tui = options.inline
194
- ? new TuiMainScreen(terminal, true)
195
- : new class extends TuiAltScreen {
196
- isOverlayFocused() { return options.readState().overlay !== undefined || super.isOverlayFocused(); }
197
- }(terminal, true, undefined, { mouse: true });
198
- let editor = new Editor(tui, theme(), { paddingX: 1 });
199
- editor.disableSubmit = true;
200
- editor.onChange = text => {
201
- const decision = options.readState().decisions[0];
202
- const caret = caretOffset(text, editor.getCursor());
203
- options.dispatch({
204
- type: 'composer-changed', text: decision?.kind === 'question' && decision.questions[decision.index]?.secret === true ? editor.getExpandedText() : text, caret, completion: hasCompletionTrigger(text, caret),
205
- });
206
- };
207
- const replaceEditor = () => {
208
- const discarded = editor;
209
- editor = new Editor(tui, theme(), { paddingX: 1 });
210
- editor.disableSubmit = true;
211
- editor.onChange = discarded.onChange;
212
- tui.setFocus(editor);
213
- };
214
- const lineCache = new Map();
215
- let animationTimer;
216
- const cellLines = (cell, width, toolMode, now) => {
217
- const key = `${THEME_GENERATION}:${accessible}:${width}:${toolMode}:${cell.key}:${cell.kind}:${cell.pending === true}:${cell.collapsed === true}:${cell.startedAt ?? ''}:${cell.elapsedMs ?? ''}:${cell.text}:${cell.detail ?? ''}:${JSON.stringify(cell.tool)}`;
218
- const cached = cell.pending === true ? undefined : lineCache.get(key);
219
- if (cached !== undefined)
220
- return cached;
221
- const cellWidth = Math.max(1, width - 2);
222
- const lines = !accessible && cell.kind === 'assistant' && cell.pending !== true
223
- ? [`${BOLD_CYAN}Assistant${RESET}`, ...new Markdown(plain(cell.text, true), 0, 0, markdownTheme).render(cellWidth).map(value => value.trimEnd())]
224
- : wrapTextWithAnsi(cellText(cell, toolMode, accessible, now), cellWidth);
225
- const rendered = lines.map(value => ` ${value}`);
226
- if (cell.pending !== true) {
227
- lineCache.set(key, rendered);
228
- if (lineCache.size > 512)
229
- lineCache.delete(lineCache.keys().next().value);
230
- }
231
- return rendered;
232
- };
233
- const optionLine = (label, number, cursor, selected) => `${cursor ? BOLD_CYAN : DIM}${cursor ? accessible ? '>' : '›' : ' '} ${String(number)} ${selected ? '[x]' : '[ ]'} ${plain(label)}${RESET}`;
234
- const announcement = (title, count, selected, cursor = 0) => {
235
- if (!accessible)
236
- return title;
237
- const items = `${String(count)} item${count === 1 ? '' : 's'}`;
238
- return selected === undefined ? `${title} · ${items}`
239
- : `${title} · ${items} · selected ${String(cursor + 1)}: ${selected}`;
240
- };
241
- const decisionPanel = (decision, width, rows) => {
242
- if (decision.kind === 'approval') {
243
- const choices = ['Allow once', 'Reject', 'Cancel'];
244
- const heading = announcement(`Approval · ${plain(decision.toolName)}`, choices.length, choices[decision.cursor], decision.cursor);
245
- const prompt = wrapTextWithAnsi(`${BOLD_CYAN}${heading}${RESET}\n${plain(decision.prompt, true)}`, width);
246
- return [...prompt, ...choices.map((choice, index) => optionLine(choice, index + 1, decision.cursor === index, false))];
247
- }
248
- const question = decision.questions[decision.index];
249
- if (question === undefined)
250
- return [`${RED}Invalid empty question batch${RESET}`];
251
- const planReview = question.intent?.kind === 'plan-review';
252
- const heading = planReview ? 'Plan review' : question.header ?? `Question ${String(decision.index + 1)}/${String(decision.questions.length)}`;
253
- const customAllowed = question.allowCustom !== false;
254
- const selected = question.options[decision.cursor]?.label ?? 'Other (type an answer)';
255
- const announced = announcement(plain(heading), question.options.length + Number(customAllowed), selected, decision.cursor);
256
- const prompt = wrapTextWithAnsi(`${BOLD_CYAN}${announced}${RESET}\n${plain(question.question, true)}`, width);
257
- const choiceRows = question.options.map((option, index) => optionLine(option.description === undefined ? option.label : `${option.label} — ${option.description}`, index + 1, decision.cursor === index, decision.selected.includes(option.label)));
258
- if (customAllowed)
259
- choiceRows.push(optionLine('Other (type an answer)', question.options.length + 1, decision.cursor === question.options.length, false));
260
- const detailLimit = planReview ? Math.max(3, rows - prompt.length - choiceRows.length - 5) : 3;
261
- const detail = question.detail === undefined ? [] : wrapTextWithAnsi(`${DIM}${plain(question.detail, true)}${RESET}`, width).slice(0, detailLimit);
262
- const editorLines = decision.cursor === question.options.length ? editor.render(width) : [];
263
- const custom = question.secret === true
264
- ? editorLines.map((value, index) => index === 0 || index === editorLines.length - 1 ? value : value.replace(MASK_TOKEN, token => token.startsWith('\u001B') ? token : '•')) : editorLines;
265
- const rendered = [...prompt, ...detail, ...choiceRows, ...custom];
266
- return rows < 12 ? rendered.slice(0, Math.max(2, rows - 4)) : rendered;
267
- };
268
- const historyPanel = (overlay, state, width, rows) => {
269
- const selected = state.cells[overlay.cursor];
270
- const radius = Math.max(1, Math.min(4, Math.floor((rows - 6) / 2)));
271
- const start = Math.max(0, overlay.cursor - radius);
272
- const end = Math.min(state.cells.length, overlay.cursor + radius + 1);
273
- const selectedSummary = selected === undefined ? undefined
274
- : `${selected.kind} · ${plain(selected.tool?.title ?? selected.text).split('\n')[0] ?? ''}`;
275
- const result = [`${BOLD_CYAN}${announcement('History', state.cells.length, selectedSummary, overlay.cursor)}${RESET}`];
276
- for (let index = start; index < end; index++) {
277
- const cell = state.cells[index];
278
- if (cell === undefined)
279
- continue;
280
- const summary = plain(cell.tool?.title ?? cell.text).split('\n')[0] ?? '';
281
- result.push(line(`${index === overlay.cursor ? accessible ? '>' : '›' : ' '} ${cell.kind} · ${summary}`, width));
282
- }
283
- if (overlay.expanded && selected !== undefined) {
284
- result.push('', ...wrapTextWithAnsi(cellText(selected, 'expanded', accessible), width).slice(0, Math.max(2, rows - result.length - 4)));
285
- }
286
- result.push(`${DIM}↑↓ select · Enter expand · / search · y copy · Esc close${RESET}`);
287
- return result;
288
- };
289
- const detailsPanel = (overlay, state, width, rows) => {
290
- const items = activities(state);
291
- const cursor = Math.min(overlay.cursor, Math.max(0, items.length - 1));
292
- const radius = Math.max(1, Math.floor((rows - 7) / 2));
293
- const start = Math.max(0, cursor - radius);
294
- const end = Math.min(items.length, cursor + radius + 1);
295
- const now = Date.now();
296
- const selectedItem = items[cursor];
297
- const result = [`${BOLD_CYAN}${announcement('Activity', items.length, selectedItem === undefined ? undefined : activityText(selectedItem, now), cursor)}${RESET}`];
298
- for (let index = start; index < end; index++) {
299
- const item = items[index];
300
- if (item !== undefined)
301
- result.push(line(`${index === cursor ? accessible ? '>' : '›' : ' '} ${activityText(item, now)}`, width));
302
- }
303
- const selected = items[cursor];
304
- if (overlay.expanded && selected !== undefined) {
305
- const detail = selected.kind === 'job'
306
- ? [selected.value.id, `kind: ${selected.value.kind}`, `state: ${selected.value.status}`,
307
- ...(selected.value.detail === undefined ? [] : [selected.value.detail])]
308
- : [selected.value.id, `mode: ${selected.value.mode}`, `state: ${selected.value.state}`,
309
- ...(selected.value.tokens === undefined ? [] : [`tokens: ${String(selected.value.tokens)}`]),
310
- ...(selected.value.summary === undefined ? [] : [selected.value.summary])];
311
- result.push('', ...detail.flatMap(value => wrapTextWithAnsi(`${DIM}${plain(value)}${RESET}`, width)));
312
- }
313
- result.push(`${DIM}↑↓ select · Enter details · Esc close${RESET}`);
314
- return result.slice(0, Math.max(2, rows - 3));
315
- };
316
- const infoRows = (overlay, state, width) => overlay.lines.flatMap(value => wrapTextWithAnsi(plain(value, true), width)).concat(overlay.cells?.flatMap(cell => wrapTextWithAnsi(cellText(cell, state.toolMode, accessible), width)) ?? []);
317
- const overlayPanel = (overlay, state, width, rows) => {
318
- if (overlay.kind === 'history')
319
- return historyPanel(overlay, state, width, rows);
320
- if (overlay.kind === 'details')
321
- return detailsPanel(overlay, state, width, rows);
322
- if (overlay.kind === 'info') {
323
- const body = infoRows(overlay, state, width);
324
- return [
325
- `${BOLD_CYAN}${announcement(plain(overlay.title), overlay.lines.length + (overlay.cells?.length ?? 0))}${RESET}`,
326
- ...windowRows(body, Math.max(1, rows - 5), overlay.scrollOffset ?? 0, false),
327
- `${DIM}↑↓ scroll · PageUp/PageDown · Enter or Esc close${RESET}`,
328
- ];
329
- }
330
- if (overlay.kind === 'confirm') {
331
- const choices = [overlay.acceptLabel, 'Cancel'];
332
- return [
333
- `${YELLOW}${announcement(plain(overlay.title), choices.length, choices[overlay.cursor], overlay.cursor)}${RESET}`,
334
- ...overlay.detail.flatMap(value => wrapTextWithAnsi(plain(value, true), width)),
335
- optionLine(overlay.acceptLabel, 1, overlay.cursor === 0, false),
336
- optionLine('Cancel', 2, overlay.cursor === 1, false),
337
- ].slice(0, Math.max(2, rows - 3));
338
- }
339
- const selected = overlay.options[overlay.cursor];
340
- const result = [
341
- line(`${BOLD_CYAN}${announcement(plain(overlay.title), overlay.options.length, selected === undefined ? undefined : plain(selected.label), overlay.cursor)}${RESET}`, width),
342
- ...(overlay.notice === undefined ? [] : [`${DIM}${plain(overlay.notice)}${RESET}`]),
343
- ];
344
- const body = [];
345
- let selectedRow = 0;
346
- let group;
347
- for (const [index, option] of overlay.options.entries()) {
348
- if (option.group !== undefined && option.group !== group) {
349
- group = option.group;
350
- body.push(`${DIM}${plain(group)}${RESET}`);
351
- }
352
- const detail = option.detail === undefined ? '' : ` — ${plain(option.detail)}`;
353
- const active = option.active === true ? accessible ? ' [active]' : ' ●' : '';
354
- const danger = option.danger === true ? ` ${YELLOW}${accessible ? '[danger]' : '!'}${RESET}` : '';
355
- if (index === overlay.cursor)
356
- selectedRow = body.length;
357
- body.push(line(`${index === overlay.cursor ? BOLD_CYAN : DIM}${index === overlay.cursor ? accessible ? '>' : '›' : ' '} ${String(index + 1)} ${plain(option.label)}${active}${detail}${RESET}${danger}`, width));
358
- }
359
- const limit = Math.max(2, rows - (overlay.purpose === 'completion' ? 6 : 3));
360
- return [...result, ...windowRows(body, Math.max(1, limit - result.length), selectedRow, true)];
361
- };
362
- const document = {
363
- invalidate() { editor.invalidate(); },
364
- render(width) {
365
- const state = options.readState();
366
- const now = Date.now();
367
- const pendingCommand = latestPendingCommand(state.cells);
368
- if (pendingCommand !== undefined && animationTimer === undefined) {
369
- animationTimer = setTimeout(() => {
370
- animationTimer = undefined;
371
- tui.requestRender();
372
- }, accessible ? 1_000 : SPINNER_INTERVAL_MS);
373
- }
374
- else if (pendingCommand === undefined && animationTimer !== undefined) {
375
- clearTimeout(animationTimer);
376
- animationTimer = undefined;
377
- }
378
- const rows = Math.max(1, terminal.rows);
379
- if (width < 32 || rows < 8)
380
- return [line('dashi — enlarge terminal to continue', width)];
381
- const root = state.root;
382
- const cwd = plain(root?.cwd ?? state.cwd);
383
- const title = root === undefined
384
- ? `${BOLD_CYAN}dashi${RESET} ${DIM}no session${RESET}`
385
- : `${BOLD_CYAN}dashi${RESET} ${plain(root.title ?? root.id)}`;
386
- const identity = root === undefined ? cwd : `${plain(root.id)} ${DIM}${cwd}${RESET}`;
387
- const header = width >= 100
388
- ? [line(`${title} ${DIM}${identity}${RESET}`, width)]
389
- : [line(title, width), line(`${DIM}${identity}${RESET}`, width)];
390
- const baseStatusText = state.exitArmed
391
- ? `${DIM}Ctrl+C or Ctrl+D again to exit${RESET}`
392
- : state.decisions.length > 0
393
- ? `${DIM}decision 1/${String(state.decisions.length)} · ↑↓/1-9 choose · Enter answer · Esc cancel${RESET}`
394
- : state.search !== undefined
395
- ? `${DIM}${state.search.scope} search · Enter next · Shift+Enter previous · Esc close${RESET}`
396
- : state.overlay !== undefined
397
- ? `${DIM}${plain(state.overlay.kind === 'list' ? state.overlay.purpose : state.overlay.kind === 'history'
398
- ? 'history' : state.overlay.kind === 'details' ? 'activity' : state.overlay.title)} · ↑↓ choose · Enter select · Esc close${RESET}`
399
- : pendingCommand !== undefined
400
- ? `${DIM}running ${plain(pendingCommand.text).split(/\s/u)[0] ?? 'command'}${pendingCommand.startedAt === undefined ? '' : ` · ${duration(now - pendingCommand.startedAt)}`}${RESET}`
401
- : root === undefined
402
- ? `${DIM}no session · cards ${state.toolMode} · Ctrl+J newline · Ctrl+L redraw${RESET}`
403
- : root.status === 'running'
404
- ? `${DIM}running${accessible ? ` · ${plain(root.model)}` : ''} · Enter ${state.sendMode === 'steer' ? 'steer' : 'queue next turn'} · Ctrl+T toggle · cards ${state.toolMode} · Esc interrupt${RESET}`
405
- : `${DIM}idle${accessible ? ` · ${plain(root.model)}` : ''} · Enter send · cards ${state.toolMode}${RESET}`;
406
- const activityCount = activities(state).length;
407
- const baseStatus = `${baseStatusText}${root?.contextPercent === undefined ? '' : ` · ${String(root.contextPercent)}% context`}${activityCount === 0 ? '' : ' · Ctrl+B activity'}`;
408
- const status = state.newOutput === 0 ? baseStatus
409
- : `${YELLOW}${accessible ? '[new]' : '↓'} ${String(state.newOutput)} new output${state.newOutput === 1 ? '' : 's'} · Ctrl+End${RESET} · ${baseStatus}`;
410
- const decision = state.decisions[0];
411
- const attachmentRows = state.attachments.map((attachment, index) => line(`${YELLOW}[image ${String(index + 1)}]${RESET} ${plain(attachment.name)} · ${attachment.mediaType} · ${String(attachment.bytes)} B · ${plain(attachment.path)}`, width));
412
- const activityRows = decision !== undefined || state.search !== undefined || state.overlay !== undefined
413
- ? [] : activities(state).map(item => line(`${DIM}${activityText(item, now)}${RESET}`, width));
414
- const persistentStatus = accessible || state.exitArmed ? undefined : options.statusLine?.();
415
- const composer = () => [
416
- ...(persistentStatus === undefined ? [] : [line(`${DIM}${plain(persistentStatus)}${RESET}`, width)]),
417
- ...editor.render(width),
418
- ];
419
- const control = decision !== undefined
420
- ? decisionPanel(decision, width, rows)
421
- : state.search !== undefined
422
- ? [
423
- ...(state.search.scope === 'history' && state.overlay?.kind === 'history'
424
- ? historyPanel(state.overlay, state, width, rows).slice(0, Math.max(2, rows - 8)) : []),
425
- `${BOLD_CYAN}${announcement(`Search ${state.search.scope}`, state.search.matches.length, state.search.matches.length === 0 ? undefined : `match ${String(state.search.cursor + 1)}`, state.search.cursor)}${RESET}`,
426
- ...editor.render(width),
427
- `${DIM}${state.search.matches.length === 0 ? 'no matches' : `${String(state.search.cursor + 1)}/${String(state.search.matches.length)}`} · Enter next · Shift+Enter previous${RESET}`,
428
- ]
429
- : state.overlay === undefined
430
- ? [...activityRows, ...attachmentRows, ...composer()]
431
- : [
432
- ...overlayPanel(state.overlay, state, width, rows),
433
- ...(state.overlay.kind === 'list' && state.overlay.purpose === 'completion'
434
- ? [...attachmentRows, ...composer()]
435
- : []),
436
- ];
437
- const fixedRows = header.length + control.length + 2;
438
- const filtered = state.cells
439
- .filter(cell => !(state.toolMode === 'hidden' && (cell.tool !== undefined || cell.kind === 'tool')));
440
- const available = Math.max(0, rows - fixedRows);
441
- const end = Math.max(0, filtered.length - Math.min(state.scrollOffset, Math.max(0, filtered.length - 1)));
442
- const start = Math.max(0, end - available - OVERSCAN_CELLS);
443
- const materialized = options.inline ? filtered : filtered.slice(start, end);
444
- materializedCount = materialized.length;
445
- const transcript = materialized.flatMap(cell => [...cellLines(cell, width, state.toolMode, now), '']);
446
- const visible = options.inline ? transcript : transcript.slice(-available);
447
- const spacerRows = options.inline ? 0 : Math.max(0, available - visible.length);
448
- return [...header, '', ...Array.from({ length: spacerRows }, () => ''), ...visible, ...control, line(status, width)];
449
- },
450
- };
451
- let materializedCount = 0;
452
- tui.addChild(document);
453
- tui.setFocus(editor);
454
- installInput(tui, {
455
- ...options,
456
- dispatch: action => {
457
- const overlay = options.readState().overlay;
458
- const size = Math.max(1, terminal.rows - 5);
459
- options.dispatch(action.type === 'overlay-move' && overlay?.kind === 'info'
460
- ? { ...action, limit: Math.max(0, infoRows(overlay, options.readState(), terminal.columns).length - Math.max(1, size - 1)) }
461
- : action);
462
- },
463
- insertNewline: () => { editor.insertTextAtCursor('\n'); },
464
- insertText: text => { editor.insertTextAtCursor(text); },
465
- readComposerCursor: () => editor.getCursor(),
466
- });
467
- return {
468
- bell: () => { terminal.write('\u0007'); },
469
- copy(text) {
470
- if (!osc52)
471
- return 'unsupported';
472
- const safe = plain(text, true);
473
- if (Buffer.byteLength(safe, 'utf8') > COPY_LIMIT)
474
- return 'too-large';
475
- terminal.write(`\u001B]52;c;${Buffer.from(safe).toString('base64')}\u0007`);
476
- return 'ok';
477
- },
478
- discardSecretComposer: replaceEditor,
479
- drainInput: () => terminal.drainInput(),
480
- materializedCells: () => materializedCount,
481
- render: force => { tui.renderNow(force); },
482
- setComposer: (text, cursor) => { setEditor(editor, text, cursor); },
483
- start: () => { tui.start(); },
484
- stop: preserveScreen => {
485
- if (animationTimer !== undefined)
486
- clearTimeout(animationTimer);
487
- animationTimer = undefined;
488
- tui.stop({ preserveScreen });
489
- },
490
- };
491
- }
package/lib/rewind.d.ts DELETED
@@ -1,26 +0,0 @@
1
- import type { ModelSelection } from '@deepseek-ai/dsh-api-session-controller';
2
- import type { SessionEvent } from '@deepseek-ai/dsh-session';
3
- import type { Overlay, OverlayValue } from './state.js';
4
- export interface RewindBoundary {
5
- readonly atSeq?: number;
6
- readonly label: string;
7
- readonly prompt: string;
8
- }
9
- /** Return only a selection that DSH recorded for the source session. */
10
- export declare function rewindModelSelection(projection: {
11
- readonly lastUsed: ModelSelection | null;
12
- readonly next: ModelSelection | null;
13
- } | undefined): ModelSelection | undefined;
14
- /** Return the text of a direct human message, excluding injected context. */
15
- export declare function humanPrompt(event: SessionEvent): string | undefined;
16
- /** Recover one composer input from DSH's durable log. */
17
- export declare function historyInput(event: SessionEvent): string | undefined;
18
- export declare function historyInputs(events: readonly SessionEvent[]): readonly string[];
19
- /** Derive each recalled input and its controller-valid preceding-turn cut. */
20
- export declare function rewindBoundaries(events: readonly SessionEvent[]): readonly RewindBoundary[];
21
- export declare function rewindOverlay(events: readonly SessionEvent[], roller: boolean): Overlay;
22
- export declare function rewindActionOverlay(boundary: Extract<OverlayValue, {
23
- kind: 'rewind-boundary';
24
- }>): Overlay;
25
- /** Read a fork's durable source boundary without storing lineage separately. */
26
- export declare function inheritedTurn(events: readonly SessionEvent[], inheritedCount: number): number | undefined;