@antst/dashi 0.1.0-alpha.19 → 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,45 @@
1
+ import { type Renderer } from './renderer.js';
2
+ import { type ActivatableOverlayValue, type DecisionAnswer, type DraftAttachment, type PendingDecision, type OverlayOption, type UiAction, type ViewState } from './state.js';
3
+ export interface ShellBindings {
4
+ readonly dispatch: (action: UiAction) => void;
5
+ readonly readState: () => ViewState;
6
+ }
7
+ export interface TerminalShellOptions {
8
+ readonly accessible?: boolean;
9
+ readonly beforeExit?: () => Promise<void>;
10
+ readonly activateOverlay?: (value: ActivatableOverlayValue) => Promise<void>;
11
+ readonly attach?: (path: string) => Promise<DraftAttachment | undefined>;
12
+ readonly complete?: (query: string, caret: number) => Promise<readonly OverlayOption[]>;
13
+ readonly createView?: (bindings: ShellBindings) => Renderer;
14
+ readonly cyclePermission?: () => Promise<void>;
15
+ readonly cwd: string;
16
+ readonly exit: (code: number) => void;
17
+ readonly externalEdit?: (text: string, cwd: string) => Promise<string>;
18
+ readonly initialAttachments?: readonly DraftAttachment[];
19
+ readonly initialCells?: ViewState['cells'];
20
+ readonly initialHasMore?: boolean;
21
+ readonly initialPrompts?: ViewState['prompts'];
22
+ readonly initialRoot?: ViewState['root'];
23
+ readonly initialToolMode?: ViewState['toolMode'];
24
+ readonly interrupt?: () => void;
25
+ readonly inline: boolean;
26
+ readonly loadHistory?: (rootId: string) => Promise<void>;
27
+ readonly onReleased?: () => void;
28
+ readonly pasteImage?: () => Promise<DraftAttachment>;
29
+ readonly search?: (query: string, rootId: string) => Promise<void>;
30
+ readonly statusLine?: () => string | undefined;
31
+ readonly submit?: (text: string, mode: 'next-turn' | 'steer', attachments: readonly DraftAttachment[]) => Promise<void> | void;
32
+ readonly suspend?: () => Promise<void>;
33
+ readonly writeError?: (message: string) => void;
34
+ }
35
+ export interface TerminalShell {
36
+ cancelDecision(id: string): void;
37
+ dispatch(action: UiAction): void;
38
+ dispose(): Promise<void>;
39
+ fail(error: unknown): Promise<void>;
40
+ readState(): ViewState;
41
+ requestDecision(decision: PendingDecision): Promise<DecisionAnswer>;
42
+ start(): void;
43
+ whenIdle(): Promise<void>;
44
+ }
45
+ export declare function createTerminalShell(options: TerminalShellOptions): TerminalShell;
@@ -0,0 +1,273 @@
1
+ import { createRenderer } from './renderer.js';
2
+ import { runEditor } from './external-editor.js';
3
+ import { initialViewState, reduce, } from './state.js';
4
+ import { createTerminalGuard } from './terminal-guard.js';
5
+ function suspendProcess() {
6
+ return new Promise((resolve, reject) => {
7
+ const resumed = () => { resolve(); };
8
+ process.once('SIGCONT', resumed);
9
+ try {
10
+ process.kill(process.pid, 'SIGTSTP');
11
+ }
12
+ catch (error) {
13
+ process.off('SIGCONT', resumed);
14
+ reject(error);
15
+ }
16
+ });
17
+ }
18
+ export function createTerminalShell(options) {
19
+ let state = initialViewState(options.cwd, options.inline, options.initialRoot, options.initialCells, options.initialPrompts, options.initialHasMore, options.initialAttachments, options.initialToolMode);
20
+ let accepting = true;
21
+ let failed = false;
22
+ let queue = Promise.resolve();
23
+ const pending = new Map();
24
+ const bindings = {
25
+ dispatch: action => { dispatch(action); },
26
+ readState: () => state,
27
+ };
28
+ const renderer = options.createView?.(bindings) ?? createRenderer({
29
+ ...bindings, ...(options.accessible === undefined ? {} : { accessible: options.accessible }), ...(options.statusLine === undefined ? {} : { statusLine: options.statusLine }), inline: options.inline,
30
+ });
31
+ const guard = createTerminalGuard(renderer);
32
+ const writeError = options.writeError ?? (message => { process.stderr.write(message); });
33
+ let exitTimer;
34
+ let released = false;
35
+ const release = async () => {
36
+ if (exitTimer !== undefined)
37
+ clearTimeout(exitTimer);
38
+ exitTimer = undefined;
39
+ await guard.dispose();
40
+ if (released)
41
+ return;
42
+ released = true;
43
+ options.onReleased?.();
44
+ };
45
+ async function run(effect) {
46
+ switch (effect.type) {
47
+ case 'activate-overlay':
48
+ if (effect.value.kind === 'open-file') {
49
+ const path = effect.value.path;
50
+ await guard.withCookedTerminal(() => runEditor(process.env.EDITOR?.trim() || 'vi', path, options.cwd)).catch(error => {
51
+ dispatch({ type: 'runtime-error', message: error instanceof Error ? error.message : String(error) });
52
+ });
53
+ }
54
+ else
55
+ await options.activateOverlay?.(effect.value);
56
+ return;
57
+ case 'attach': {
58
+ try {
59
+ const attachment = await options.attach?.(effect.path);
60
+ if (attachment !== undefined)
61
+ dispatch({
62
+ type: 'attachment-added', attachment, source: effect.source, text: effect.text,
63
+ });
64
+ }
65
+ catch (error) {
66
+ dispatch({ type: 'runtime-error', message: error instanceof Error ? error.message : String(error) });
67
+ }
68
+ return;
69
+ }
70
+ case 'bell':
71
+ if (options.accessible !== true)
72
+ renderer.bell();
73
+ return;
74
+ case 'clipboard-image': {
75
+ try {
76
+ const attachment = await options.pasteImage?.();
77
+ if (attachment !== undefined)
78
+ dispatch({ type: 'attachment-added', attachment });
79
+ }
80
+ catch (error) {
81
+ dispatch({ type: 'runtime-error', message: error instanceof Error ? error.message : String(error) });
82
+ }
83
+ return;
84
+ }
85
+ case 'complete': {
86
+ const items = await options.complete?.(effect.query, effect.caret) ?? [];
87
+ dispatch({ type: 'completion-ready', options: items, query: effect.query });
88
+ return;
89
+ }
90
+ case 'copy': {
91
+ const result = renderer.copy(effect.text);
92
+ if (result === 'too-large')
93
+ dispatch({ type: 'runtime-error', message: 'Copy is limited to 64 KiB.' });
94
+ else if (result === 'unsupported')
95
+ dispatch({ type: 'runtime-error', message: 'This terminal does not support OSC 52 copy.' });
96
+ return;
97
+ }
98
+ case 'cycle-permission':
99
+ await options.cyclePermission?.();
100
+ return;
101
+ case 'exit':
102
+ accepting = false;
103
+ await options.beforeExit?.();
104
+ await release();
105
+ options.exit(0);
106
+ return;
107
+ case 'exit-timer':
108
+ if (exitTimer !== undefined)
109
+ clearTimeout(exitTimer);
110
+ exitTimer = effect.armed ? setTimeout(() => { dispatch({ type: 'disarm-exit' }); }, 1_500) : undefined;
111
+ return;
112
+ case 'external-edit': {
113
+ try {
114
+ let edited;
115
+ await guard.withCookedTerminal(async () => {
116
+ edited = await options.externalEdit?.(effect.text, effect.cwd);
117
+ });
118
+ if (edited !== undefined)
119
+ dispatch({ type: 'composer-set', text: edited });
120
+ }
121
+ catch (error) {
122
+ dispatch({ type: 'runtime-error', message: error instanceof Error ? error.message : String(error) });
123
+ }
124
+ return;
125
+ }
126
+ case 'interrupt':
127
+ options.interrupt?.();
128
+ return;
129
+ case 'load-history':
130
+ await options.loadHistory?.(effect.rootId);
131
+ return;
132
+ case 'redraw':
133
+ renderer.render(effect.force);
134
+ return;
135
+ case 'reject-decision': {
136
+ const settlement = pending.get(effect.id);
137
+ if (settlement === undefined)
138
+ return;
139
+ pending.delete(effect.id);
140
+ if (settlement.kind === 'approval') {
141
+ settlement.resolve(effect.reason === 'terminal' ? 'rejected' : 'cancelled');
142
+ }
143
+ else {
144
+ const error = new Error(effect.reason === 'terminal'
145
+ ? 'the terminal closed before the user answered'
146
+ : 'the question was withdrawn before the user answered');
147
+ error.name = 'UserQuestionError';
148
+ error.code = effect.reason === 'terminal' ? 'ASK_CANCELLED' : 'ASK_ABORTED';
149
+ settlement.reject(error);
150
+ }
151
+ return;
152
+ }
153
+ case 'set-composer':
154
+ renderer.setComposer(effect.text, effect.cursor);
155
+ return;
156
+ case 'search':
157
+ await options.search?.(effect.query, effect.rootId);
158
+ return;
159
+ case 'settle-decision': {
160
+ const settlement = pending.get(effect.id);
161
+ if (settlement === undefined)
162
+ return;
163
+ pending.delete(effect.id);
164
+ settlement.resolve(effect.answer);
165
+ return;
166
+ }
167
+ case 'submit':
168
+ await options.submit?.(effect.text, effect.mode, effect.attachments);
169
+ return;
170
+ case 'suspend':
171
+ await guard.withCookedTerminal(options.suspend ?? suspendProcess);
172
+ }
173
+ }
174
+ async function fail(error) {
175
+ if (failed)
176
+ return;
177
+ failed = true;
178
+ accepting = false;
179
+ renderer.discardSecretComposer();
180
+ const detail = error instanceof Error ? error.stack ?? error.message : String(error);
181
+ writeError(`dashi: terminal failure\n${detail}\n`);
182
+ try {
183
+ const [next, effects] = reduce(state, { type: 'terminal-lost' });
184
+ state = next;
185
+ for (const effect of effects)
186
+ await run(effect);
187
+ try {
188
+ await options.beforeExit?.();
189
+ }
190
+ catch (cleanupError) {
191
+ const cleanupDetail = cleanupError instanceof Error
192
+ ? cleanupError.stack ?? cleanupError.message
193
+ : String(cleanupError);
194
+ writeError(`dashi: cleanup failure\n${cleanupDetail}\n`);
195
+ }
196
+ await release();
197
+ }
198
+ finally {
199
+ options.exit(1);
200
+ }
201
+ }
202
+ function dispatch(action) {
203
+ if (!accepting)
204
+ return;
205
+ const question = state.decisions[0]?.kind === 'question' ? state.decisions[0].questions[state.decisions[0].index] : undefined;
206
+ if (question?.secret === true && ['decision-submit', 'decision-cancel', 'decision-withdrawn', 'escape', 'terminal-lost'].includes(action.type))
207
+ renderer.discardSecretComposer();
208
+ const [next, effects] = reduce(state, action);
209
+ state = next;
210
+ if (effects.length === 0)
211
+ return;
212
+ queue = queue.then(async () => {
213
+ for (const effect of effects)
214
+ await run(effect);
215
+ }).catch(fail);
216
+ }
217
+ async function whenIdle() {
218
+ for (;;) {
219
+ const observed = queue;
220
+ await observed;
221
+ if (queue === observed)
222
+ return;
223
+ }
224
+ }
225
+ return {
226
+ cancelDecision(id) {
227
+ dispatch({ type: 'decision-withdrawn', id });
228
+ },
229
+ dispatch,
230
+ async dispose() {
231
+ renderer.discardSecretComposer();
232
+ const [next, effects] = reduce(state, { type: 'terminal-lost' });
233
+ state = next;
234
+ for (const effect of effects)
235
+ await run(effect);
236
+ accepting = false;
237
+ await queue;
238
+ try {
239
+ await options.beforeExit?.();
240
+ }
241
+ finally {
242
+ await release();
243
+ }
244
+ },
245
+ fail,
246
+ readState: () => state,
247
+ requestDecision(decision) {
248
+ if (!accepting)
249
+ return Promise.reject(new Error('dashi terminal is closing'));
250
+ if (pending.has(decision.id))
251
+ return Promise.reject(new Error(`duplicate decision ${decision.id}`));
252
+ let resolve;
253
+ let reject;
254
+ const promise = new Promise((accept, decline) => {
255
+ resolve = accept;
256
+ reject = decline;
257
+ });
258
+ pending.set(decision.id, { kind: decision.kind, reject, resolve });
259
+ dispatch({ type: 'decision-enqueued', decision });
260
+ return promise;
261
+ },
262
+ start() {
263
+ try {
264
+ guard.start();
265
+ renderer.render(true);
266
+ }
267
+ catch (error) {
268
+ void fail(error);
269
+ }
270
+ },
271
+ whenIdle,
272
+ };
273
+ }
@@ -0,0 +1,8 @@
1
+ export interface AtTrigger {
2
+ readonly end: number;
3
+ readonly query: string;
4
+ readonly quoted: boolean;
5
+ readonly start: number;
6
+ }
7
+ /** Terminal copy of the @ branch in DSH ui-input-trigger core/detect. */
8
+ export declare function detectAtTrigger(draft: string, caret: number): AtTrigger | undefined;
@@ -0,0 +1,13 @@
1
+ import { activeAtToken } from '@deepseek-ai/dsh-file-reference/grammar';
2
+ /** Terminal copy of the @ branch in DSH ui-input-trigger core/detect. */
3
+ export function detectAtTrigger(draft, caret) {
4
+ const token = activeAtToken(draft, caret);
5
+ if (token === undefined)
6
+ return undefined;
7
+ return {
8
+ end: caret,
9
+ query: token.query,
10
+ quoted: token.quoted,
11
+ start: caret - token.prefix.length,
12
+ };
13
+ }
@@ -0,0 +1,28 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ import type { AgentPreset } from '@deepseek-ai/dsh-agent-presets';
4
+ import type { ModelCatalog, ModelSelection, SessionSummary } from '@deepseek-ai/dsh-api-session-controller';
5
+ import type { SessionSearchHit } from '@deepseek-ai/dsh-session-query';
6
+ import type { Overlay, OverlayOption, RootView } from './state.js';
7
+ /** Build command and skill completion rows for one exact root. */
8
+ export declare function completionOptions(ctx: Context, agent: Agent, input: string, caret?: number, signal?: AbortSignal): Promise<readonly OverlayOption[]>;
9
+ /** Build a filtered view of the skills resolved for one live session. */
10
+ export declare function skillOverlay(ctx: Context, agent: Agent, filter: string, signal?: AbortSignal): Promise<Overlay>;
11
+ /** Build the cold session picker without adopting any candidate. */
12
+ export declare function sessionOverlay(items: readonly SessionSummary[], currentId: string, cwd?: string): Overlay;
13
+ /** Flatten DSH's provider/model/effort catalog into one terminal picker. */
14
+ export declare function modelOverlay(catalog: ModelCatalog, selected?: ModelSelection): Overlay;
15
+ /** Build the preset picker from the native preset service. */
16
+ export declare function permissionOverlay(ctx: Context, agent: Agent): Overlay;
17
+ export declare function sessionStatusLine(ctx: Context, agent: Agent, root: RootView, repo?: string): string;
18
+ /** Read DSH-owned root facts for /status in one projection cut. */
19
+ export declare function statusOverlay(ctx: Context, agent: Agent, root: RootView): Overlay;
20
+ /** Show DSH's heuristic next-request composition without treating it as an exact total. */
21
+ export declare function contextOverlay(ctx: Context, agent: Agent): Overlay;
22
+ /** Read DSH's whole-log token and timing projections without folding session history. */
23
+ export declare function usageOverlay(ctx: Context, agent: Agent): Overlay;
24
+ /** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
25
+ export declare function agentPresetOverlay(presets: readonly AgentPreset[], current: string, blank: boolean): Overlay;
26
+ /** Build bounded same-cwd prompt-search rows from DSH query hits. */
27
+ export declare function searchOverlay(items: readonly SessionSearchHit[], rootId: string): Overlay;
28
+ export declare function currentModel(ctx: Context, agent: Agent): ModelSelection | undefined;