@antst/dashi 0.1.0-alpha.20 → 0.1.0-alpha.21

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 (53) hide show
  1. package/lib/application.d.ts +45 -0
  2. package/lib/application.js +273 -0
  3. package/lib/at-trigger.d.ts +8 -0
  4. package/lib/at-trigger.js +13 -0
  5. package/lib/catalogs.d.ts +28 -0
  6. package/lib/catalogs.js +308 -0
  7. package/lib/clipboard-image.d.ts +3 -0
  8. package/lib/clipboard-image.js +74 -0
  9. package/lib/completion-trigger.d.ts +6 -0
  10. package/lib/completion-trigger.js +19 -0
  11. package/lib/diff-view.d.ts +5 -0
  12. package/lib/diff-view.js +30 -0
  13. package/lib/external-editor.d.ts +3 -0
  14. package/lib/external-editor.js +39 -0
  15. package/lib/help.d.ts +2 -0
  16. package/lib/help.js +6 -0
  17. package/lib/history-records.d.ts +4 -0
  18. package/lib/history-records.js +4 -0
  19. package/lib/image-input.d.ts +8 -0
  20. package/lib/image-input.js +37 -0
  21. package/lib/index.d.ts +4 -0
  22. package/lib/index.js +325 -0
  23. package/lib/input.d.ts +17 -0
  24. package/lib/input.js +267 -0
  25. package/lib/memory.d.ts +3 -0
  26. package/lib/memory.js +27 -0
  27. package/lib/mouse-input.d.ts +1 -0
  28. package/lib/mouse-input.js +6 -0
  29. package/lib/presentation.d.ts +9 -0
  30. package/lib/presentation.js +54 -0
  31. package/lib/renderer.d.ts +21 -0
  32. package/lib/renderer.js +491 -0
  33. package/lib/rewind.d.ts +31 -0
  34. package/lib/rewind.js +114 -0
  35. package/lib/session-export.d.ts +3 -0
  36. package/lib/session-export.js +27 -0
  37. package/lib/session-list.d.ts +15 -0
  38. package/lib/session-list.js +65 -0
  39. package/lib/session-runtime.d.ts +49 -0
  40. package/lib/session-runtime.js +1492 -0
  41. package/lib/shell-command.d.ts +10 -0
  42. package/lib/shell-command.js +66 -0
  43. package/lib/state.d.ts +517 -0
  44. package/lib/state.js +685 -0
  45. package/lib/terminal-guard.d.ts +14 -0
  46. package/lib/terminal-guard.js +76 -0
  47. package/lib/tool-presentation.d.ts +14 -0
  48. package/lib/tool-presentation.js +156 -0
  49. package/lib/transcript.d.ts +14 -0
  50. package/lib/transcript.js +336 -0
  51. package/lib/tui-root.d.ts +20 -0
  52. package/lib/tui-root.js +25 -0
  53. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ import { type Terminal } from '@earendil-works/pi-tui';
2
+ export interface TerminalSession {
3
+ drainInput(): Promise<void>;
4
+ render(force: boolean): void;
5
+ start(): void;
6
+ stop(preserveScreen: boolean): void;
7
+ }
8
+ export interface TerminalGuard {
9
+ dispose(): Promise<void>;
10
+ start(): void;
11
+ withCookedTerminal(operation: () => Promise<void>): Promise<void>;
12
+ }
13
+ export declare function createProcessTerminal(onWheel?: (lines: number) => void): Terminal;
14
+ export declare function createTerminalGuard(session: TerminalSession): TerminalGuard;
@@ -0,0 +1,76 @@
1
+ import { ProcessTerminal } from '@earendil-works/pi-tui';
2
+ import { filterWheelInput } from './mouse-input.js';
3
+ export function createProcessTerminal(onWheel) {
4
+ const terminal = new ProcessTerminal();
5
+ if (onWheel === undefined)
6
+ return terminal;
7
+ return {
8
+ get columns() { return terminal.columns; },
9
+ get kittyProtocolActive() { return terminal.kittyProtocolActive; },
10
+ get rows() { return terminal.rows; },
11
+ clearFromCursor: () => { terminal.clearFromCursor(); },
12
+ clearLine: () => { terminal.clearLine(); },
13
+ clearScreen: () => { terminal.clearScreen(); },
14
+ drainInput: (maxMs, idleMs) => terminal.drainInput(maxMs, idleMs),
15
+ hideCursor: () => { terminal.hideCursor(); },
16
+ moveBy: lines => { terminal.moveBy(lines); },
17
+ setProgress: active => { terminal.setProgress(active); },
18
+ setTitle: title => { terminal.setTitle(title); },
19
+ showCursor: () => { terminal.showCursor(); },
20
+ start(onInput, onResize) {
21
+ terminal.start(data => {
22
+ const filtered = filterWheelInput(data);
23
+ if (typeof filtered === 'string')
24
+ onInput(filtered);
25
+ else
26
+ onWheel(filtered);
27
+ }, onResize);
28
+ },
29
+ stop: () => { terminal.stop(); },
30
+ write: data => { terminal.write(data); },
31
+ };
32
+ }
33
+ export function createTerminalGuard(session) {
34
+ let phase = 'fresh';
35
+ let disposal;
36
+ const stopActive = async () => {
37
+ try {
38
+ await session.drainInput();
39
+ }
40
+ finally {
41
+ session.stop(true);
42
+ }
43
+ };
44
+ return {
45
+ start() {
46
+ if (phase !== 'fresh')
47
+ return;
48
+ phase = 'active';
49
+ session.start();
50
+ },
51
+ dispose() {
52
+ if (disposal !== undefined)
53
+ return disposal;
54
+ const wasActive = phase === 'active';
55
+ phase = 'released';
56
+ disposal = wasActive ? stopActive() : Promise.resolve();
57
+ return disposal;
58
+ },
59
+ async withCookedTerminal(operation) {
60
+ if (phase !== 'active')
61
+ return;
62
+ phase = 'cooked';
63
+ await stopActive();
64
+ try {
65
+ await operation();
66
+ }
67
+ finally {
68
+ if (phase === 'cooked') {
69
+ phase = 'active';
70
+ session.start();
71
+ session.render(true);
72
+ }
73
+ }
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,14 @@
1
+ import type { ToolDefinition, ToolResult } from '@deepseek-ai/dsh-tools';
2
+ import type { ToolCard } from './state.js';
3
+ export interface PresentedCall {
4
+ readonly args: unknown;
5
+ readonly card: ToolCard;
6
+ }
7
+ export interface ToolPresenter {
8
+ call(name: string, rawArguments: string): PresentedCall;
9
+ result(name: string, args: unknown, result: ToolResult, prior: ToolCard): ToolCard;
10
+ }
11
+ /** Bound a card body without hiding how much was omitted. */
12
+ export declare function boundedBody(text: string): string;
13
+ /** Bind DSH's live tool definitions to a pure, bounded terminal card projection. */
14
+ export declare function createToolPresenter(get: (name: string) => ToolDefinition | undefined): ToolPresenter;
@@ -0,0 +1,156 @@
1
+ const HEAD_LINES = 12;
2
+ const TAIL_LINES = 8;
3
+ const MAX_LINE_CHARS = 2_000;
4
+ function short(value) {
5
+ const chars = [...value];
6
+ return chars.length <= MAX_LINE_CHARS ? value : `${chars.slice(0, MAX_LINE_CHARS).join('')}…`;
7
+ }
8
+ /** Bound a card body without hiding how much was omitted. */
9
+ export function boundedBody(text) {
10
+ const lines = text.replaceAll('\r\n', '\n').split('\n').map(short);
11
+ if (lines.length <= HEAD_LINES + TAIL_LINES)
12
+ return lines.join('\n');
13
+ const omitted = lines.length - HEAD_LINES - TAIL_LINES;
14
+ return [...lines.slice(0, HEAD_LINES), `… ${omitted} lines omitted …`, ...lines.slice(-TAIL_LINES)].join('\n');
15
+ }
16
+ function blockText(blocks) {
17
+ return blocks.map((block) => {
18
+ if (block.type === 'text' || block.type === 'reasoning')
19
+ return block.text;
20
+ if (block.type === 'image')
21
+ return '[image]';
22
+ return `[${String(block.type ?? 'content')}]`;
23
+ }).join('\n');
24
+ }
25
+ function displayValue(value) {
26
+ if (typeof value === 'string')
27
+ return value;
28
+ try {
29
+ return JSON.stringify(value, undefined, 2) ?? '';
30
+ }
31
+ catch {
32
+ return '[unrenderable input]';
33
+ }
34
+ }
35
+ function decode(raw) {
36
+ try {
37
+ return raw === '' ? {} : JSON.parse(raw);
38
+ }
39
+ catch {
40
+ return raw;
41
+ }
42
+ }
43
+ function generic(title, body = '') {
44
+ return { card: 'generic', title: short(title), ...(body === '' ? {} : { body: boundedBody(body) }) };
45
+ }
46
+ function lines(text) {
47
+ const normalized = text.replaceAll('\r\n', '\n');
48
+ const values = normalized.split('\n');
49
+ if (values.at(-1) === '')
50
+ values.pop();
51
+ return values;
52
+ }
53
+ function compactDiff(path, oldText, newText) {
54
+ const before = oldText === null ? [] : lines(oldText);
55
+ const after = lines(newText);
56
+ let prefix = 0;
57
+ while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix])
58
+ prefix++;
59
+ let suffix = 0;
60
+ while (suffix < before.length - prefix && suffix < after.length - prefix
61
+ && before[before.length - suffix - 1] === after[after.length - suffix - 1])
62
+ suffix++;
63
+ const removed = before.length - prefix - suffix;
64
+ const added = after.length - prefix - suffix;
65
+ const contextBefore = before.slice(Math.max(0, prefix - 3), prefix).map(value => ` ${value}`);
66
+ const removedLines = before.slice(prefix, before.length - suffix).map(value => `- ${value}`);
67
+ const addedLines = after.slice(prefix, after.length - suffix).map(value => `+ ${value}`);
68
+ const contextAfter = before.slice(before.length - suffix, before.length - suffix + 3).map(value => ` ${value}`);
69
+ return {
70
+ added,
71
+ lines: boundedBody([...contextBefore, ...removedLines, ...addedLines, ...contextAfter].join('\n')).split('\n'),
72
+ path: short(path),
73
+ removed,
74
+ };
75
+ }
76
+ function diffs(view) {
77
+ return view.diffs.slice(0, 50).map(diff => compactDiff(diff.path, diff.oldText, diff.newText));
78
+ }
79
+ function callCard(view) {
80
+ switch (view.card) {
81
+ case 'generic': {
82
+ const body = [view.content === undefined ? '' : blockText(view.content),
83
+ view.rawInput === undefined ? '' : displayValue(view.rawInput)].filter(Boolean).join('\n');
84
+ return { ...generic(view.title, body), card: 'generic' };
85
+ }
86
+ case 'terminal':
87
+ return {
88
+ card: 'terminal', title: short(view.title),
89
+ ...(view.description === undefined ? {} : { body: boundedBody(view.description) }),
90
+ ...(view.cwd === undefined ? {} : { status: `cwd ${short(view.cwd)}` }),
91
+ };
92
+ case 'diff': return { card: 'diff', title: short(view.title), diffs: diffs(view) };
93
+ }
94
+ }
95
+ function searchBody(view) {
96
+ if (view.shape === 'paths')
97
+ return view.paths.join('\n');
98
+ return view.files.flatMap(file => [file.path, ...file.matches.map(match => `${match.lineNumber}: ${match.line}`)]).join('\n');
99
+ }
100
+ function resultCard(view, prior, raw) {
101
+ const title = short(view.title ?? prior.title);
102
+ switch (view.card) {
103
+ case 'generic': return generic(title, view.content === undefined ? raw : blockText(view.content));
104
+ case 'terminal':
105
+ return {
106
+ card: 'terminal', title,
107
+ ...((view.output ?? raw) === '' ? {} : { body: boundedBody(view.output ?? raw) }),
108
+ ...(view.exitCode === undefined && view.signal === undefined ? {} : {
109
+ status: view.exitCode === undefined ? `signal ${short(view.signal ?? '')}` : `exit ${String(view.exitCode)}`,
110
+ }),
111
+ };
112
+ case 'diff': return { card: 'diff', title, diffs: diffs(view) };
113
+ case 'search': return {
114
+ card: 'search', title, body: boundedBody(searchBody(view)),
115
+ status: `${String(view.total)} ${view.shape === 'paths' ? 'paths' : 'matches'}${view.truncated ? ' · truncated' : ''}`,
116
+ };
117
+ case 'read': return {
118
+ card: 'read', title, body: boundedBody(view.lines.map(line => `${line.number} ${line.text}`).join('\n')),
119
+ status: `${short(view.path)} · ${String(view.lines.length)}/${String(view.totalLines)} lines`,
120
+ };
121
+ case 'web': {
122
+ const body = view.kind === 'search'
123
+ ? [view.answer ?? '', ...view.sources.map(source => `${source.title ?? source.url} · ${source.url}`)].filter(Boolean).join('\n')
124
+ : raw;
125
+ const status = view.kind === 'search'
126
+ ? `${String(view.sources.length)} sources${view.truncated ? ' · truncated' : ''}`
127
+ : `${String(view.statusCode)} · ${short(view.url)}${view.truncated ? ' · truncated' : ''}`;
128
+ return { card: 'web', title, ...(body === '' ? {} : { body: boundedBody(body) }), status };
129
+ }
130
+ }
131
+ }
132
+ /** Bind DSH's live tool definitions to a pure, bounded terminal card projection. */
133
+ export function createToolPresenter(get) {
134
+ return {
135
+ call(name, rawArguments) {
136
+ const args = decode(rawArguments);
137
+ try {
138
+ const view = get(name)?.presentCall?.(args);
139
+ return { args, card: view === undefined ? generic(name, displayValue(args)) : callCard(view) };
140
+ }
141
+ catch {
142
+ return { args, card: generic(name, displayValue(args)) };
143
+ }
144
+ },
145
+ result(name, args, result, prior) {
146
+ const raw = blockText(result.content);
147
+ try {
148
+ const view = get(name)?.presentResult?.(args, result);
149
+ return view === undefined ? generic(prior.title, raw) : resultCard(view, prior, raw);
150
+ }
151
+ catch {
152
+ return generic(prior.title, raw);
153
+ }
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,14 @@
1
+ import type { StreamChunk, UserMessage } from '@deepseek-ai/dsh-llm';
2
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
3
+ import type { TerminalCell } from './state.js';
4
+ import { type ToolPresenter } from './tool-presentation.js';
5
+ interface FoldOptions {
6
+ readonly truncatedStart?: boolean;
7
+ }
8
+ /** Project DSH-owned pending injections beside the durable transcript. */
9
+ export declare function pendingShellCells(messages: readonly UserMessage[]): readonly TerminalCell[];
10
+ /** Fold one transient DSH Assistant frame into the current pending cells. */
11
+ export declare function foldAssistantStream(current: readonly TerminalCell[], turn: number, step: number, chunk: StreamChunk): readonly TerminalCell[];
12
+ /** Fold one chronological DSH event slice into terminal-owned presentation cells. */
13
+ export declare function foldCells(events: readonly SessionEvent[], presenter?: ToolPresenter | undefined, options?: FoldOptions): readonly TerminalCell[];
14
+ export {};
@@ -0,0 +1,336 @@
1
+ import { createToolPresenter } from './tool-presentation.js';
2
+ function blockText(blocks, kind) {
3
+ return blocks.filter((block) => block.type === kind)
4
+ .map(block => block.text).join('');
5
+ }
6
+ function messageText(blocks) {
7
+ const parts = [];
8
+ for (const block of blocks) {
9
+ if (block.type === 'text')
10
+ parts.push(block.text);
11
+ else if (block.type === 'image')
12
+ parts.push('[image]');
13
+ else if (block.type !== 'reasoning' && block.type !== 'tool-call' && block.type !== 'tool-result') {
14
+ parts.push(`[${String(block.type ?? 'content')}]`);
15
+ }
16
+ }
17
+ return parts.join('');
18
+ }
19
+ function isDashiShell(message) {
20
+ const source = message.source;
21
+ return source.kind === 'plugin' && source.plugin === 'dashi' && source.form === 'notice'
22
+ && messageText(message.content).startsWith('$ ');
23
+ }
24
+ function shellCell(message, pending) {
25
+ return {
26
+ key: `shell:${String(message.id)}`, kind: 'shell', pending, text: messageText(message.content),
27
+ };
28
+ }
29
+ function collapsedContextCell(message, key) {
30
+ const source = message.source;
31
+ const form = source.form;
32
+ if (source.kind !== 'agent-instructions'
33
+ && (source.kind !== 'plugin' || !['instructions', 'catalog', 'snapshot', 'notice', 'recall'].includes(String(form)))) {
34
+ return undefined;
35
+ }
36
+ const text = messageText(message.content);
37
+ const lines = text.split(/\r?\n/u);
38
+ const first = form === 'notice' && typeof source.summary === 'string' ? source.summary : lines[0] ?? '';
39
+ return {
40
+ collapsed: true,
41
+ detail: `${String(form)} · ${first.trim()} · ${String(lines.length)} lines`,
42
+ key,
43
+ kind: 'context',
44
+ text,
45
+ };
46
+ }
47
+ /** Project DSH-owned pending injections beside the durable transcript. */
48
+ export function pendingShellCells(messages) {
49
+ return messages.filter(isDashiShell).map(message => shellCell(message, true));
50
+ }
51
+ function outcome(reason) {
52
+ switch (reason.kind) {
53
+ case 'completed': return 'completed';
54
+ case 'aborted': return `interrupted (${reason.reason.kind})`;
55
+ case 'blocked': return 'blocked';
56
+ case 'error': return `${reason.error.message} [${reason.error.code}]`;
57
+ case 'max-tokens': return 'maximum output reached';
58
+ case 'interrupted': return 'interrupted by an earlier crash';
59
+ default: return String(reason.kind ?? 'unknown outcome');
60
+ }
61
+ }
62
+ function todoText(event) {
63
+ const glyph = { completed: '✓', in_progress: '●', pending: '○' };
64
+ const lines = event.data.todos.map(todo => `${glyph[todo.status]} ${todo.content}`);
65
+ return `Todos\n${lines.join('\n')}`;
66
+ }
67
+ function compactText(summary) {
68
+ if (summary === undefined)
69
+ return 'Conversation compacted';
70
+ const items = summary.data.shadowedSeqs.length;
71
+ return `Compacted ${String(items)} item${items === 1 ? '' : 's'} · ${String(summary.data.shadowedTokenCount)} tokens`;
72
+ }
73
+ function compactSource(event) {
74
+ if (event.surfaceOp === undefined || event.surfaceOp === 'append' || event.surfaceOp.op !== 'replace')
75
+ return undefined;
76
+ const source = event.data.source;
77
+ if (source.kind !== 'plugin' || source.plugin !== 'compact' || typeof source.compactionId !== 'string')
78
+ return undefined;
79
+ return {
80
+ compactionId: source.compactionId,
81
+ ...(typeof source.sourceCommandId === 'string' ? { sourceCommandId: source.sourceCommandId } : {}),
82
+ };
83
+ }
84
+ function putAssistant(cells, assistants, turn, step, kind, text, complete) {
85
+ const key = `${turn}:${step}`;
86
+ const pair = assistants.get(key) ?? {};
87
+ const slot = kind === 'text' ? pair.text : pair.reasoning;
88
+ const cellKind = kind === 'text' ? 'assistant' : 'reasoning';
89
+ if (slot === undefined) {
90
+ const next = cells.length;
91
+ cells.push({
92
+ key: `assistant:${key}:${kind}`,
93
+ kind: cellKind,
94
+ ...(!complete && kind === 'text' ? { pending: true } : {}),
95
+ text,
96
+ ...(complete && kind === 'reasoning' ? { collapsed: true } : {}),
97
+ });
98
+ if (kind === 'text')
99
+ pair.text = next;
100
+ else
101
+ pair.reasoning = next;
102
+ assistants.set(key, pair);
103
+ return;
104
+ }
105
+ const previous = cells[slot];
106
+ if (previous === undefined)
107
+ return;
108
+ const { pending: _pending, ...settled } = previous;
109
+ cells[slot] = {
110
+ ...(complete ? settled : previous),
111
+ text: complete ? text : previous.text + text,
112
+ ...(complete && kind === 'reasoning' ? { collapsed: true } : {}),
113
+ };
114
+ }
115
+ /** Fold one transient DSH Assistant frame into the current pending cells. */
116
+ export function foldAssistantStream(current, turn, step, chunk) {
117
+ const cells = [...current];
118
+ const pair = {};
119
+ cells.forEach((cell, slot) => {
120
+ if (cell.kind === 'assistant')
121
+ pair.text = slot;
122
+ else if (cell.kind === 'reasoning')
123
+ pair.reasoning = slot;
124
+ });
125
+ const assistants = new Map([[`${turn}:${step}`, pair]]);
126
+ if (chunk.type === 'text-delta')
127
+ putAssistant(cells, assistants, turn, step, 'text', chunk.text, false);
128
+ else if (chunk.type === 'reasoning-delta')
129
+ putAssistant(cells, assistants, turn, step, 'reasoning', chunk.text, false);
130
+ else if (chunk.type === 'block-end' && (chunk.block.type === 'text' || chunk.block.type === 'reasoning')) {
131
+ putAssistant(cells, assistants, turn, step, chunk.block.type, chunk.block.text, true);
132
+ }
133
+ return cells;
134
+ }
135
+ /** Fold one chronological DSH event slice into terminal-owned presentation cells. */
136
+ export function foldCells(events, presenter = createToolPresenter(() => undefined), options = {}) {
137
+ const toolPresenter = presenter ?? createToolPresenter(() => undefined);
138
+ const cells = [];
139
+ const assistants = new Map();
140
+ const calls = new Map();
141
+ const commands = new Map();
142
+ const inheritedPendingCommands = new Set();
143
+ const compactions = new Map();
144
+ const openTurns = new Set();
145
+ let planSlot;
146
+ let todoSlot;
147
+ let truncatedPrefix = options.truncatedStart === true;
148
+ for (const event of events) {
149
+ switch (event.type) {
150
+ case 'system/message':
151
+ break;
152
+ case 'turn/start':
153
+ truncatedPrefix = false;
154
+ openTurns.add(event.data.turn);
155
+ if (todoSlot !== undefined) {
156
+ const previous = cells[todoSlot];
157
+ if (previous !== undefined)
158
+ cells[todoSlot] = { ...previous, collapsed: true, text: 'Todos · previous revision' };
159
+ todoSlot = undefined;
160
+ }
161
+ break;
162
+ case 'user/message': {
163
+ const compact = compactSource(event);
164
+ if (compact !== undefined) {
165
+ const text = compactText(compactions.get(compact.compactionId));
166
+ const slot = compact.sourceCommandId === undefined ? undefined : commands.get(compact.sourceCommandId);
167
+ const command = slot === undefined ? undefined : cells[slot];
168
+ if (slot !== undefined && command?.kind === 'command')
169
+ cells[slot] = { ...command, text: `${command.text}\n${text}` };
170
+ else
171
+ cells.push({ key: `${event.seq}:compaction`, kind: 'compaction', text });
172
+ break;
173
+ }
174
+ const text = messageText(event.data.content);
175
+ const context = collapsedContextCell(event.data, `${event.seq}:context`);
176
+ cells.push(isDashiShell(event.data)
177
+ ? shellCell(event.data, false)
178
+ : context ?? {
179
+ key: `${event.seq}:user`,
180
+ kind: event.data.source.kind === 'user' ? 'user' : 'context',
181
+ text,
182
+ });
183
+ break;
184
+ }
185
+ case 'assistant/message': {
186
+ const { message, step, turn } = event.data;
187
+ const text = blockText(message.content, 'text');
188
+ const reasoning = blockText(message.content, 'reasoning');
189
+ if (reasoning !== '')
190
+ putAssistant(cells, assistants, turn, step, 'reasoning', reasoning, true);
191
+ if (text !== '')
192
+ putAssistant(cells, assistants, turn, step, 'text', text, true);
193
+ break;
194
+ }
195
+ case 'tool/call': {
196
+ const id = String(event.data.callId);
197
+ if (calls.has(id)) {
198
+ cells.push({ key: `${event.seq}:error`, kind: 'error', text: `duplicate tool call ${id}` });
199
+ break;
200
+ }
201
+ const presented = toolPresenter.call(event.data.name, event.data.arguments);
202
+ if (event.data.name === 'exit_plan_mode') {
203
+ const previous = planSlot === undefined ? undefined : cells[planSlot];
204
+ if (planSlot !== undefined && previous !== undefined)
205
+ cells[planSlot] = { ...previous, collapsed: true };
206
+ planSlot = cells.length;
207
+ }
208
+ calls.set(id, { args: presented.args, name: event.data.name, slot: cells.length });
209
+ cells.push({
210
+ key: `tool:${id}`,
211
+ kind: 'tool',
212
+ pending: true,
213
+ text: presented.card.title,
214
+ tool: presented.card,
215
+ });
216
+ break;
217
+ }
218
+ case 'todo/write': {
219
+ const previous = todoSlot === undefined ? undefined : cells[todoSlot];
220
+ if (todoSlot !== undefined && previous !== undefined)
221
+ cells[todoSlot] = { ...previous, collapsed: true, text: 'Todos · previous revision' };
222
+ todoSlot = cells.length;
223
+ cells.push({ key: `${event.seq}:todos`, kind: 'todo', text: todoText(event) });
224
+ break;
225
+ }
226
+ case 'compaction/summary':
227
+ compactions.set(String(event.data.compactionId), event);
228
+ break;
229
+ case 'tool/result': {
230
+ const id = String(event.data.message.source.callId);
231
+ const call = calls.get(id);
232
+ if (call === undefined || cells[call.slot] === undefined) {
233
+ if (truncatedPrefix)
234
+ break;
235
+ cells.push({ key: `${event.seq}:error`, kind: 'error', text: `tool result ${id} has no matching call` });
236
+ break;
237
+ }
238
+ const block = event.data.message.content[0];
239
+ if (block === undefined) {
240
+ cells.push({ key: `${event.seq}:error`, kind: 'error', text: `tool result ${id} has no content` });
241
+ break;
242
+ }
243
+ const result = {
244
+ content: block.content,
245
+ isError: block.isError || event.data.error !== undefined,
246
+ ...(event.data.meta === undefined ? {} : { meta: event.data.meta }),
247
+ };
248
+ const previous = cells[call.slot];
249
+ if (previous === undefined)
250
+ break;
251
+ const card = toolPresenter.result(call.name, call.args, result, previous.tool ?? {
252
+ card: 'generic', title: call.name,
253
+ });
254
+ cells[call.slot] = {
255
+ ...previous,
256
+ text: card.title,
257
+ tool: card,
258
+ pending: false,
259
+ ...(result.isError ? { kind: 'error' } : {}),
260
+ };
261
+ break;
262
+ }
263
+ case 'command/run': {
264
+ const id = String(event.data.commandId);
265
+ if (commands.has(id)) {
266
+ cells.push({ key: `${event.seq}:error`, kind: 'error', text: `duplicate command ${id}` });
267
+ break;
268
+ }
269
+ commands.set(id, cells.length);
270
+ cells.push({
271
+ key: `command:${id}`,
272
+ kind: 'command',
273
+ pending: true,
274
+ startedAt: event.time,
275
+ text: `/${event.data.name}${event.data.args ?? ''}`,
276
+ });
277
+ break;
278
+ }
279
+ case 'session/end-seed':
280
+ // Fork includes post-turn log events through the next turn/start
281
+ // (api/session-controller/src/commands.ts:229-231). A command/run is
282
+ // appended before its handler (interaction/commands/src/index.ts:345-350),
283
+ // so the child can inherit that run but never its parent-only done event.
284
+ for (const slot of commands.values()) {
285
+ if (cells[slot]?.pending === true)
286
+ inheritedPendingCommands.add(slot);
287
+ }
288
+ break;
289
+ case 'command/done': {
290
+ const id = String(event.data.commandId);
291
+ const slot = commands.get(id);
292
+ if (slot === undefined) {
293
+ if (truncatedPrefix)
294
+ break;
295
+ cells.push({ key: `${event.seq}:error`, kind: 'error', text: `command result ${id} has no matching run` });
296
+ break;
297
+ }
298
+ const previous = cells[slot];
299
+ if (previous === undefined || previous.kind !== 'command') {
300
+ cells.push({ key: `${event.seq}:error`, kind: 'error', text: `command result ${id} has no matching run` });
301
+ break;
302
+ }
303
+ inheritedPendingCommands.delete(slot);
304
+ cells[slot] = {
305
+ ...previous,
306
+ ...(previous.startedAt === undefined ? {} : { elapsedMs: Math.max(0, event.time - previous.startedAt) }),
307
+ pending: false,
308
+ text: `${previous.text}${event.data.text === undefined ? '' : `\n${event.data.text}`}${event.data.kind === 'error' ? '\nfailed' : ''}`,
309
+ };
310
+ break;
311
+ }
312
+ case 'turn/end':
313
+ if (event.data.reason.kind === 'interrupted')
314
+ break;
315
+ if (!openTurns.delete(event.data.turn)) {
316
+ if (!truncatedPrefix) {
317
+ cells.push({ key: `${event.seq}:error`, kind: 'error', text: `turn ${event.data.turn} ended without starting` });
318
+ }
319
+ }
320
+ else {
321
+ cells.push({
322
+ key: `${event.seq}:outcome`,
323
+ kind: event.data.reason.kind === 'error' ? 'error' : 'outcome',
324
+ text: outcome(event.data.reason),
325
+ });
326
+ }
327
+ truncatedPrefix = false;
328
+ break;
329
+ default:
330
+ break;
331
+ }
332
+ }
333
+ return inheritedPendingCommands.size === 0
334
+ ? cells
335
+ : cells.filter((_cell, slot) => !inheritedPendingCommands.has(slot));
336
+ }
@@ -0,0 +1,20 @@
1
+ import { Service, type Context } from '@deepseek-ai/cordis';
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ export interface TuiRootContext {
4
+ current(): Agent | undefined;
5
+ }
6
+ declare module '@deepseek-ai/cordis' {
7
+ interface Context {
8
+ tuiRoot: TuiRootContext;
9
+ }
10
+ interface Events {
11
+ 'tui/root-changed'(previous: Agent | undefined, current: Agent | undefined): void;
12
+ }
13
+ }
14
+ export declare class TuiRoot extends Service implements TuiRootContext {
15
+ private agent;
16
+ constructor(ctx: Context);
17
+ current(): Agent | undefined;
18
+ bind(agent: Agent): void;
19
+ clear(expected?: Agent): void;
20
+ }
@@ -0,0 +1,25 @@
1
+ import { Service } from '@deepseek-ai/cordis';
2
+ export class TuiRoot extends Service {
3
+ agent;
4
+ constructor(ctx) {
5
+ super(ctx, 'tuiRoot');
6
+ ctx.on('agent/disposed', ({ agent }) => { this.clear(agent); }, { global: true });
7
+ }
8
+ current() {
9
+ return this.agent;
10
+ }
11
+ bind(agent) {
12
+ if (this.agent === agent)
13
+ return;
14
+ const previous = this.agent;
15
+ this.agent = agent;
16
+ this.ctx.emit('tui/root-changed', previous, agent);
17
+ }
18
+ clear(expected) {
19
+ if (this.agent === undefined || (expected !== undefined && this.agent !== expected))
20
+ return;
21
+ const previous = this.agent;
22
+ this.agent = undefined;
23
+ this.ctx.emit('tui/root-changed', previous, undefined);
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antst/dashi",
3
- "version": "0.1.0-alpha.20",
3
+ "version": "0.1.0-alpha.21",
4
4
  "description": "A terminal UI for DeepSeek Harness",
5
5
  "publishConfig": {
6
6
  "access": "public"