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

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 +5 -5
  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 -30
  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 -4
  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 -31
  36. package/lib/rewind.js +0 -114
  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 -1492
  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 -685
  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 -14
  52. package/lib/transcript.js +0 -336
  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,1492 +0,0 @@
1
- import { basename } from 'node:path';
2
- import { AuthorizationDeclinedError } from '@deepseek-ai/dsh-authorization';
3
- import { parseCredentialKey } from '@deepseek-ai/dsh-credentials';
4
- import { JobId } from '@deepseek-ai/dsh-jobs';
5
- import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm';
6
- import { expandAssistantStream } from '@deepseek-ai/dsh-llm/assistant-stream';
7
- import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
8
- import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionStatusLine, sessionOverlay, skillOverlay, statusOverlay, usageOverlay, } from './catalogs.js';
9
- import { diffOverlay } from './diff-view.js';
10
- import { eventsFromRecords } from './history-records.js';
11
- import { admitDraftImages, encodeDraftImages, readDraftImages } from './image-input.js';
12
- import { contextPercent, jobViews, subagentViews } from './presentation.js';
13
- import { historyInput, historyInputs, humanPrompt, inheritedTurn, latestCompletedTurn, rewindActionOverlay, rewindOverlay, rewindModelSelection, } from './rewind.js';
14
- import { createToolPresenter } from './tool-presentation.js';
15
- import { quoteShellWord, runHumanShell } from './shell-command.js';
16
- import { markdownTranscript } from './session-export.js';
17
- import { isSessionId, sessionMatches, sessionNotFound } from './session-list.js';
18
- import { memoryOverlay } from './memory.js';
19
- import { foldAssistantStream, foldCells, pendingShellCells } from './transcript.js';
20
- export class LaunchArgumentError extends Error {
21
- }
22
- function starterAgents(cwd) {
23
- return `# ${basename(cwd)}\n\n## Working agreement\n\n- Add project-specific architecture guidance.\n- Add required build, test, and lint commands.\n- Add repository conventions and constraints.\n`;
24
- }
25
- const RECAP_PROMPT = 'Summarize this conversation so far in ten lines: goal, decisions, open items.';
26
- function detail(error) {
27
- return error instanceof Error ? error.message : String(error);
28
- }
29
- function continuation(items, cwd) {
30
- const candidates = items.filter(item => item.cwd === cwd
31
- && item.parentSessionId === undefined && item.origin === undefined)
32
- .sort((left, right) => right.updatedAt - left.updatedAt
33
- || String(left.sessionId).localeCompare(String(right.sessionId)));
34
- const chosen = candidates[0];
35
- if (chosen === undefined)
36
- throw new Error(`no resumable DSH session in ${cwd}`);
37
- return chosen.sessionId;
38
- }
39
- function projectionString(projections, key) {
40
- const value = projections.values[key];
41
- return typeof value === 'string' && value !== '' ? value : undefined;
42
- }
43
- function projectionModel(projections) {
44
- const value = projections.values.modelSelection;
45
- return value?.next ?? value?.lastUsed ?? undefined;
46
- }
47
- function modelFromEvent(event) {
48
- if (event.type !== 'request/header')
49
- return undefined;
50
- const config = event.data.header.config;
51
- return {
52
- model: config.model,
53
- provider: config.provider,
54
- ...(config.reasoningEffort === undefined ? {} : { reasoningEffort: String(config.reasoningEffort) }),
55
- };
56
- }
57
- function turnStartTime(events, turn) {
58
- for (let index = events.length - 1; index >= 0; index--) {
59
- const event = events[index];
60
- if (event?.type === 'turn/start' && event.data.turn === turn)
61
- return event.time;
62
- }
63
- }
64
- function latestStep(events) {
65
- for (let index = events.length - 1; index >= 0; index--) {
66
- const event = events[index];
67
- if (event?.type === 'step/start')
68
- return event;
69
- }
70
- }
71
- function openingStream(baseline) {
72
- const attempt = baseline?.activeAttempt;
73
- if (attempt === undefined)
74
- return [];
75
- return expandAssistantStream(attempt.stream).slice(0, attempt.nextIndex)
76
- .reduce((cells, member) => foldAssistantStream(cells, attempt.turn, attempt.step, member.chunk), []);
77
- }
78
- function titleFromEvent(event) {
79
- if (String(event.type) !== 'session/title')
80
- return undefined;
81
- const data = event.data;
82
- return typeof data.title === 'string' ? data.title : undefined;
83
- }
84
- async function firstSnapshot(iterator) {
85
- const first = await iterator.next();
86
- if (first.done === true || first.value.type !== 'snapshot') {
87
- throw new Error('DSH session follow ended before its opening snapshot');
88
- }
89
- return first.value;
90
- }
91
- async function firstControl(iterator) {
92
- const first = await iterator.next();
93
- if (first.done === true || first.value.type !== 'baseline') {
94
- throw new Error('DSH session control ended before its opening baseline');
95
- }
96
- return first.value;
97
- }
98
- async function gitRepo(ctx, agent, signal) {
99
- const fallback = basename(agent.session.header.cwd ?? '');
100
- try {
101
- const result = await ctx.shell.run(ctx.shell.resolve({
102
- command: 'git rev-parse --show-toplevel && git branch --show-current', sandboxPolicy: ctx.sandboxPolicy.resolve({ mode: 'read-only' }),
103
- signal, stdoutMaxBytes: 1_024, timeoutMs: 2_000, workdir: agent.session.header.cwd,
104
- }));
105
- const [root, branch] = result.stdout.text.trim().split(/\r?\n/u);
106
- return result.exitCode === 0 && root !== undefined && branch !== undefined ? `${basename(root)}/${branch}` : fallback;
107
- }
108
- catch {
109
- return fallback;
110
- }
111
- }
112
- function usage(raw, expected) {
113
- return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
114
- }
115
- function loopInterval(seconds) {
116
- if (seconds % 3_600 === 0)
117
- return `${String(seconds / 3_600)}h`;
118
- return seconds % 60 === 0 ? `${String(seconds / 60)}m` : `${String(seconds)}s`;
119
- }
120
- function loopLine(schedule) {
121
- const rule = schedule.kind === 'every' ? `every ${loopInterval(schedule.everySeconds)}`
122
- : schedule.kind === 'after' ? `after ${String(schedule.afterSeconds)}s` : `at ${schedule.scheduledAt}`;
123
- return `${String(schedule.id)} · ${rule} · ${schedule.prompt}`;
124
- }
125
- function configValue(raw) {
126
- try {
127
- return JSON.parse(raw);
128
- }
129
- catch {
130
- return raw;
131
- }
132
- }
133
- function configLayer(value) {
134
- return value === undefined ? '—' : JSON.stringify(value);
135
- }
136
- /** Create or resume the exact current root and own its replaceable terminal binding. */
137
- export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, decisions) {
138
- const lifetime = new AbortController();
139
- let accepting = true;
140
- let started = false;
141
- let shutdown;
142
- let decisionSequence = 0;
143
- let initialId = options.resume === undefined
144
- ? options.continue
145
- ? continuation((await ctx.sessionController.list({}, lifetime.signal)).items, options.cwd)
146
- : (await ctx.sessionController.create({ cwd: options.cwd, ...(options.agentPreset === undefined ? {} : { agentPreset: options.agentPreset }), ...(options.sessionId === undefined ? {} : { sessionId: SessionId(options.sessionId) }) })).sessionId
147
- : SessionId(options.resume);
148
- if (options.forkSession)
149
- initialId = (await ctx.sessionController.fork({ sessionId: initialId })).sessionId;
150
- const prepare = async (sessionId, name) => {
151
- const abort = new AbortController();
152
- try {
153
- const resolved = await ctx.sessionController.resolveAgent(sessionId);
154
- if ('error' in resolved)
155
- throw resolved.error;
156
- const agent = resolved.agent;
157
- const disposers = [];
158
- if (options.tools !== undefined || options.disallowedTools !== undefined) {
159
- try {
160
- disposers.push(agent.ctx.tools.restrict({
161
- ...(options.tools === undefined ? {} : { allow: options.tools }),
162
- ...(options.disallowedTools === undefined ? {} : { deny: options.disallowedTools }),
163
- }));
164
- }
165
- catch (error) {
166
- throw new LaunchArgumentError(detail(error));
167
- }
168
- }
169
- if (options.systemPrompt !== undefined)
170
- disposers.push(agent.ctx.systemPrompt.section({
171
- complete: true, name: 'dashi:system-prompt', order: Number.MAX_SAFE_INTEGER, text: options.systemPrompt,
172
- }));
173
- if (options.appendSystemPrompt !== undefined)
174
- disposers.push(agent.ctx.systemPrompt.section({
175
- name: 'dashi:append-system-prompt', order: Number.MAX_SAFE_INTEGER, text: options.appendSystemPrompt,
176
- }));
177
- let title;
178
- if (name !== undefined)
179
- title = (await ctx.sessionController.rename({ sessionId, title: name })).title;
180
- await ctx.sessions.flush(agent.session);
181
- const iterator = ctx.sessionController.follow({
182
- address: { kind: 'session', sessionId }, assistantStream: true, maxMessages: 50,
183
- }, abort.signal)[Symbol.asyncIterator]();
184
- const controlIterator = ctx.sessionController.control(abort.signal)[Symbol.asyncIterator]();
185
- const [opening, controlOpening, subagents, repo] = await Promise.all([
186
- firstSnapshot(iterator), firstControl(controlIterator), subagentViews(ctx, sessionId, abort.signal),
187
- gitRepo(ctx, agent, abort.signal),
188
- ]);
189
- title ??= projectionString(opening.projections, 'title');
190
- const selected = projectionModel(opening.projections);
191
- const provider = selected?.provider ?? agent.options.provider;
192
- const effort = selected?.reasoningEffort ?? agent.options.reasoningEffort;
193
- const initialContextPercent = contextPercent(controlOpening.value.projections[sessionId]?.values.contextPressure);
194
- const parentTurn = agent.session.header.parentSession === undefined ? undefined
195
- : inheritedTurn(agent.session.snapshotEvents(), agent.session.inheritedEventCount);
196
- return {
197
- abort,
198
- agent,
199
- repo,
200
- controlIterator,
201
- cursor: opening.cursor,
202
- disposers,
203
- events: eventsFromRecords(opening.records),
204
- hasMore: opening.hasMore,
205
- iterator,
206
- presenter: createToolPresenter(tool => ctx.tools.get(tool, agent)),
207
- presentationTimer: undefined,
208
- streamCells: openingStream(opening.assistantStream),
209
- root: {
210
- ...(initialContextPercent === undefined ? {} : { contextPercent: initialContextPercent }),
211
- cwd: opening.header.cwd ?? options.cwd,
212
- id: String(sessionId),
213
- jobs: jobViews(controlOpening.value.jobs[sessionId] ?? []),
214
- model: selected?.model ?? agent.options.model ?? 'default',
215
- permission: ctx.permissionPresets.current(agent.session),
216
- status: agent.status,
217
- subagents,
218
- ...(provider === undefined ? {} : { provider }),
219
- ...(effort === undefined ? {} : { effort: String(effort) }),
220
- ...(agent.session.header.parentSession === undefined
221
- ? {} : { parent: String(agent.session.header.parentSession) }),
222
- ...(parentTurn === undefined ? {} : { parentTurn }),
223
- ...(title === undefined ? {} : { title }),
224
- },
225
- timer: undefined,
226
- };
227
- }
228
- catch (error) {
229
- abort.abort();
230
- throw error;
231
- }
232
- };
233
- let binding = await prepare(initialId, options.name);
234
- const initialAttachments = await readDraftImages(binding.root.cwd, options.images ?? []);
235
- const current = () => binding;
236
- const report = (error, rootId = current().root.id) => {
237
- dispatch({ type: 'runtime-error', message: detail(error), rootId });
238
- };
239
- const executeAccepted = async (bound, line, images, onSettled) => {
240
- const from = bound.agent.session.seq;
241
- const execution = ctx.commands.execute(bound.agent, line, images.map(image => ({ type: 'image', ...image })), lifetime.signal);
242
- const accepted = bound.agent.session.snapshotEvents(from).some(event => event.type === 'command/run');
243
- if (!accepted) {
244
- const result = await execution;
245
- if (result === undefined)
246
- return false;
247
- onSettled?.(result);
248
- return true;
249
- }
250
- void execution.then(result => {
251
- if (result !== undefined)
252
- onSettled?.(result);
253
- }).catch((error) => {
254
- if (!lifetime.signal.aborted)
255
- report(error, bound.root.id);
256
- });
257
- return true;
258
- };
259
- const decide = (decision, signal) => {
260
- if (decisions === undefined)
261
- return Promise.reject(new Error('dashi decision surface is not ready'));
262
- const abortDecision = () => { decisions.cancel(decision.id); };
263
- signal?.addEventListener('abort', abortDecision, { once: true });
264
- return decisions.request(decision).finally(() => {
265
- signal?.removeEventListener('abort', abortDecision);
266
- });
267
- };
268
- const authorizationPrompt = (bound, prompt, detail) => decide({
269
- answers: [], cursor: 0, custom: '', id: `authorization:${String(++decisionSequence)}`, index: 0, kind: 'question', owner: { id: bound.root.id, ...(bound.root.title === undefined ? {} : { label: bound.root.title }) }, selected: [],
270
- questions: [{ allowCustom: prompt.kind !== 'select', ...(detail === undefined ? {} : { detail }), header: 'Login', id: 'authorization', multiSelect: false, options: prompt.kind === 'select' ? prompt.options : [], question: prompt.message, secret: prompt.kind === 'secret' }],
271
- }, prompt.signal).then(result => prompt.kind === 'select' ? prompt.options[prompt.options.findIndex(option => option.label === result.answers[0]?.selected[0])]?.id ?? ''
272
- : result.answers[0]?.custom ?? '').catch(error => {
273
- if (prompt.signal?.aborted !== true && error.code === 'ASK_ABORTED')
274
- throw new AuthorizationDeclinedError();
275
- throw error;
276
- });
277
- const transcriptCells = (bound) => [
278
- ...foldCells(bound.events, bound.presenter, { truncatedStart: bound.hasMore }),
279
- ...bound.streamCells,
280
- ...pendingShellCells(bound.agent.inbox.nextStep),
281
- ];
282
- const publish = (bound) => {
283
- bound.timer = undefined;
284
- dispatch({
285
- type: 'transcript-changed', cells: transcriptCells(bound),
286
- hasMore: bound.hasMore, rootId: bound.root.id,
287
- });
288
- };
289
- const schedule = (bound) => {
290
- if (bound.timer === undefined)
291
- bound.timer = setTimeout(() => { publish(bound); }, 34);
292
- };
293
- const refreshSubagents = async (bound) => {
294
- const subagents = await subagentViews(ctx, bound.agent.id, bound.abort.signal);
295
- if (bound.abort.signal.aborted || current() !== bound)
296
- return;
297
- bound.root = { ...bound.root, subagents };
298
- dispatch({ type: 'root-presentation', rootId: bound.root.id, subagents });
299
- };
300
- const scheduleSubagents = (bound) => {
301
- if (bound.presentationTimer !== undefined)
302
- clearTimeout(bound.presentationTimer);
303
- bound.presentationTimer = setTimeout(() => {
304
- bound.presentationTimer = undefined;
305
- void refreshSubagents(bound).catch((error) => {
306
- if (!bound.abort.signal.aborted)
307
- report(error, bound.root.id);
308
- });
309
- }, 34);
310
- };
311
- const refreshRepo = async (bound) => {
312
- const repo = await gitRepo(ctx, bound.agent, bound.abort.signal);
313
- if (bound.abort.signal.aborted || current() !== bound || bound.repo === repo)
314
- return;
315
- bound.repo = repo;
316
- dispatch({ type: 'redraw' });
317
- };
318
- const consume = (bound, frame) => {
319
- if (frame.type === 'assistant-stream') {
320
- if (frame.frame.type !== 'chunk') {
321
- bound.streamCells = [];
322
- schedule(bound);
323
- return;
324
- }
325
- const step = latestStep(bound.events);
326
- if (step === undefined)
327
- return;
328
- bound.streamCells = foldAssistantStream(bound.streamCells, step.data.turn, step.data.step, frame.frame.chunk);
329
- schedule(bound);
330
- return;
331
- }
332
- const [event] = eventsFromRecords([frame]);
333
- if (event === undefined)
334
- return;
335
- bound.events.push(event);
336
- if (event.type === 'assistant/message' || event.type === 'assistant/attempt')
337
- bound.streamCells = [];
338
- if (event.type === 'turn/end') {
339
- void refreshRepo(bound);
340
- const startedAt = turnStartTime(bound.events, event.data.turn);
341
- if (startedAt !== undefined) {
342
- dispatch({
343
- durationMs: Math.max(0, event.time - startedAt), rootId: bound.root.id, type: 'turn-ended',
344
- });
345
- }
346
- }
347
- const title = titleFromEvent(event);
348
- if (title !== undefined && title !== bound.root.title) {
349
- bound.root = { ...bound.root, title };
350
- dispatch({ type: 'root-title', rootId: bound.root.id, title });
351
- }
352
- const model = modelFromEvent(event);
353
- if (model !== undefined) {
354
- const { effort: _oldEffort, ...root } = bound.root;
355
- bound.root = {
356
- ...root, model: model.model, provider: model.provider,
357
- ...(model.reasoningEffort === undefined ? {} : { effort: model.reasoningEffort }),
358
- };
359
- dispatch({
360
- type: 'root-model', rootId: bound.root.id, model: model.model, provider: model.provider,
361
- ...(model.reasoningEffort === undefined ? {} : { effort: model.reasoningEffort }),
362
- });
363
- }
364
- const prompt = event.type === 'agent/inbox/spliced' ? undefined : historyInput(event);
365
- if (prompt !== undefined)
366
- dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: prompt });
367
- schedule(bound);
368
- };
369
- const follow = async (bound) => {
370
- try {
371
- for (;;) {
372
- const next = await bound.iterator.next();
373
- if (next.done === true)
374
- break;
375
- if (next.value.type === 'snapshot') {
376
- report('DSH session follow emitted a second snapshot', bound.root.id);
377
- continue;
378
- }
379
- consume(bound, next.value);
380
- }
381
- }
382
- catch (error) {
383
- if (!bound.abort.signal.aborted)
384
- report(error, bound.root.id);
385
- }
386
- };
387
- const followControl = async (bound) => {
388
- try {
389
- for (;;) {
390
- const next = await bound.controlIterator.next();
391
- if (next.done === true)
392
- break;
393
- const frame = next.value;
394
- if (frame.type === 'baseline') {
395
- report('DSH session control emitted a second baseline', bound.root.id);
396
- }
397
- else if (frame.type === 'jobs' && String(frame.sessionId) === bound.root.id) {
398
- const jobs = jobViews(frame.jobs);
399
- bound.root = { ...bound.root, jobs };
400
- dispatch({ type: 'root-presentation', jobs, rootId: bound.root.id });
401
- }
402
- else if (frame.type === 'projection') {
403
- if (String(frame.sessionId) === bound.root.id && frame.key === 'contextPressure') {
404
- const percent = contextPercent(frame.value);
405
- const { contextPercent: _previous, ...root } = bound.root;
406
- bound.root = { ...root, ...(percent === undefined ? {} : { contextPercent: percent }) };
407
- dispatch({ type: 'root-presentation', contextPercent: percent ?? null, rootId: bound.root.id });
408
- }
409
- if (frame.key === 'subagent' || frame.key === 'subagentTiming'
410
- || frame.key === 'title' || frame.key === 'tokenUsage')
411
- scheduleSubagents(bound);
412
- }
413
- }
414
- }
415
- catch (error) {
416
- if (!bound.abort.signal.aborted)
417
- report(error, bound.root.id);
418
- }
419
- };
420
- const openPermission = (bound) => {
421
- dispatch({ type: 'open-overlay', overlay: permissionOverlay(ctx, bound.agent) });
422
- };
423
- const openModel = async (bound) => {
424
- const catalog = await ctx.sessionController.modelCatalog();
425
- const overlay = modelOverlay(catalog, currentModel(ctx, bound.agent));
426
- if (overlay.kind === 'list' && overlay.options.length === 0) {
427
- throw new Error(catalog.failures.map(failure => `${failure.name}: ${failure.message}`).join('; ') || 'no models available');
428
- }
429
- dispatch({ type: 'open-overlay', overlay });
430
- };
431
- const replace = async (sessionId, name) => {
432
- const previous = current();
433
- if (previous.agent.status === 'running')
434
- throw new Error('wait for or interrupt the running root before switching sessions');
435
- if (previous.root.id === String(sessionId) && name === undefined)
436
- return;
437
- const candidate = await prepare(sessionId, name);
438
- await install(candidate);
439
- binding = candidate;
440
- tuiRoot.bind(candidate.agent);
441
- dispatch({
442
- type: 'root-bound', cells: transcriptCells(candidate),
443
- hasMore: candidate.hasMore, prompts: historyInputs(candidate.events), root: candidate.root,
444
- });
445
- if (started) {
446
- candidate.pump = follow(candidate);
447
- candidate.controlPump = followControl(candidate);
448
- }
449
- await stop(previous);
450
- };
451
- const readyForRootOperation = async (operation) => {
452
- const bound = current();
453
- if (bound.agent.status === 'idle')
454
- return bound;
455
- if (operation.interrupt !== true) {
456
- dispatch({ type: 'open-overlay', overlay: {
457
- acceptLabel: 'Interrupt',
458
- cursor: 1,
459
- detail: ['The current turn will stop before the session changes.'],
460
- kind: 'confirm',
461
- title: 'Interrupt current turn?',
462
- value: { ...operation, interrupt: true },
463
- } });
464
- return undefined;
465
- }
466
- bound.agent.cancel({ kind: 'user' }, { keepInbox: true });
467
- await bound.agent.whenIdle();
468
- if (current() !== bound)
469
- throw new Error('the current root changed while interruption completed');
470
- return bound;
471
- };
472
- const restoreFiles = async (bound, target) => {
473
- const execution = await ctx.commands.execute(bound.agent, `/roller-restore ${String(target)}`, [], lifetime.signal);
474
- if (execution === undefined)
475
- throw new Error('roller-restore disappeared before execution');
476
- if (execution.result.kind === 'error')
477
- throw new Error(execution.result.text ?? 'roller restore failed');
478
- };
479
- const restoreFirstConversation = async (bound, prompt) => {
480
- const agentPreset = ctx.sessionProjections.stateOf(bound.agent.session, 'agentPreset')
481
- ?? ctx.agentPresets.defaultId;
482
- const modelProjection = ctx.sessionProjections.snapshot(bound.agent.session, ['modelSelection'])
483
- .values.modelSelection;
484
- const selection = rewindModelSelection(modelProjection);
485
- const created = await ctx.sessionController.create({ cwd: bound.root.cwd, agentPreset });
486
- let selectionFailure;
487
- try {
488
- if (selection !== undefined)
489
- await ctx.sessionController.selectModel({ sessionId: created.sessionId, ...selection });
490
- }
491
- catch (error) {
492
- selectionFailure = error;
493
- }
494
- await replace(created.sessionId);
495
- dispatch({ type: 'composer-set', text: prompt });
496
- if (selectionFailure !== undefined)
497
- throw selectionFailure;
498
- };
499
- const runRootOperation = async (operation) => {
500
- const bound = await readyForRootOperation(operation);
501
- if (bound === undefined)
502
- return;
503
- switch (operation.kind) {
504
- case 'new': {
505
- const created = await ctx.sessionController.create({
506
- cwd: bound.root.cwd,
507
- ...(operation.agentPreset === undefined ? {} : { agentPreset: operation.agentPreset }),
508
- });
509
- await replace(created.sessionId, operation.title);
510
- return;
511
- }
512
- case 'open-resume':
513
- dispatch({
514
- type: 'open-overlay',
515
- overlay: sessionOverlay((await ctx.sessionController.list({}, lifetime.signal)).items, bound.root.id, operation.all === true ? undefined : bound.root.cwd),
516
- });
517
- return;
518
- case 'resume':
519
- if (isSessionId(operation.sessionId))
520
- await replace(SessionId(operation.sessionId));
521
- else {
522
- const matches = sessionMatches((await ctx.sessionController.list({}, lifetime.signal)).items, operation.sessionId, operation.all === true ? undefined : bound.root.cwd);
523
- const [match] = matches;
524
- if (match === undefined)
525
- throw sessionNotFound(operation.sessionId);
526
- if (matches.length === 1)
527
- await replace(match.sessionId);
528
- else
529
- dispatch({ type: 'open-overlay', overlay: sessionOverlay(matches, bound.root.id) });
530
- }
531
- return;
532
- case 'fork': {
533
- const child = await ctx.sessionController.fork({ sessionId: bound.agent.id });
534
- await replace(child.sessionId);
535
- return;
536
- }
537
- case 'open-rewind': {
538
- const roller = ctx.commands.list(bound.agent).some(command => command.name === 'roller-restore');
539
- const overlay = rewindOverlay(bound.agent.session.snapshotEvents(), roller);
540
- if (overlay.kind !== 'list' || overlay.options.length === 0) {
541
- throw new Error('the current session has no input to rewind');
542
- }
543
- dispatch({ type: 'open-overlay', overlay });
544
- return;
545
- }
546
- case 'rewind': {
547
- if (operation.mode === 'code') {
548
- await restoreFiles(bound, operation.atSeq ?? 'start');
549
- return;
550
- }
551
- if (operation.atSeq === undefined) {
552
- if (operation.mode === 'both')
553
- await restoreFiles(bound, 'start');
554
- await restoreFirstConversation(bound, operation.prompt);
555
- return;
556
- }
557
- const child = await ctx.sessionController.fork({
558
- atSeq: operation.atSeq, sessionId: bound.agent.id,
559
- });
560
- await replace(child.sessionId);
561
- if (operation.mode === 'both')
562
- await restoreFiles(current(), operation.atSeq);
563
- dispatch({ type: 'composer-set', text: operation.prompt });
564
- return;
565
- }
566
- }
567
- };
568
- const commandDefinitions = (bound) => {
569
- const nativePermission = ctx.commands.find(bound.agent, 'permission');
570
- if (nativePermission === undefined)
571
- throw new Error('dashi: DSH supplied no /permission command');
572
- const ensureCurrent = (invocation) => current().agent === invocation.agent ? undefined : { kind: 'error', text: 'this root is no longer current' };
573
- const tasks = (invocation) => {
574
- const stale = ensureCurrent(invocation);
575
- if (stale !== undefined)
576
- return stale;
577
- const raw = invocation.rawInput.trim();
578
- if (raw === '') {
579
- dispatch({ type: 'open-details' });
580
- return { kind: 'success' };
581
- }
582
- const match = /^kill\s+(\S+)$/u.exec(raw);
583
- if (match?.[1] === undefined)
584
- return { kind: 'error', text: 'usage: /tasks [kill ID]' };
585
- const result = ctx.jobs.kill(JobId(match[1]), bound.agent, 'stopped by /tasks');
586
- return { kind: 'success', text: result === 'requested' ? `stopping ${match[1]}` : `${match[1]} already finished` };
587
- };
588
- const loop = async (invocation) => {
589
- const stale = ensureCurrent(invocation);
590
- if (stale !== undefined)
591
- return stale;
592
- const raw = invocation.rawInput.trim();
593
- const stop = /^stop\s+(\S+)$/u.exec(raw);
594
- const create = /^(\d+)([mh])\s+(.+)$/su.exec(raw);
595
- if (raw !== '' && stop === null && create === null) {
596
- return { kind: 'error', text: 'usage: /loop [INTERVAL TEXT | stop ID]' };
597
- }
598
- const name = raw === '' ? 'schedule_list' : stop === null ? 'schedule_create' : 'schedule_delete';
599
- const args = raw === '' ? {} : stop === null ? {
600
- every_seconds: Number(create[1]) * (create[2] === 'h' ? 3_600 : 60), prompt: create[3],
601
- } : { id: stop[1] };
602
- const result = await ctx.agents.withInitiator(bound.agent, () => ctx.tools.execute({
603
- agent: bound.agent, arguments: args, callId: ToolCallId(`${String(invocation.commandId)}:loop`),
604
- name, signal: invocation.signal,
605
- }));
606
- if (result.isError)
607
- return { kind: 'error', text: result.error.message };
608
- const value = result.value;
609
- if (!Array.isArray(value) && 'message' in value)
610
- return { kind: 'error', text: value.message };
611
- if (Array.isArray(value))
612
- return { kind: 'success', text: value.length === 0 ? 'no active schedules' : value.map(loopLine).join('\n') };
613
- if ('deleted' in value)
614
- return value.deleted ? { kind: 'success', text: `stopped ${String(value.id)}` }
615
- : { kind: 'error', text: `schedule ${String(value.id)} not found` };
616
- return { kind: 'success', text: `scheduled ${loopLine(value)}` };
617
- };
618
- const askAside = async (invocation, prompt = invocation.rawInput.trim()) => {
619
- const stale = ensureCurrent(invocation);
620
- if (stale !== undefined)
621
- return stale;
622
- if (prompt === '')
623
- return { kind: 'error', text: 'usage: /btw TEXT' };
624
- if (bound.agent.status !== 'idle')
625
- return { kind: 'error', text: 'finish or interrupt the turn first' };
626
- const boundary = latestCompletedTurn(bound.agent.session.snapshotEvents());
627
- if (boundary.atSeq === undefined)
628
- throw new Error('the current session has no completed turn to fork');
629
- const forked = await ctx.sessionController.fork({ atSeq: boundary.atSeq, sessionId: bound.agent.id });
630
- const resolved = await ctx.sessionController.resolveAgent(forked.sessionId);
631
- if ('error' in resolved)
632
- throw resolved.error;
633
- const child = resolved.agent;
634
- const turn = inheritedTurn(child.session.snapshotEvents(), child.session.inheritedEventCount);
635
- if (turn === undefined)
636
- throw new Error('the fork contains no completed turn');
637
- const iterator = ctx.sessionController.follow({
638
- address: { kind: 'session', sessionId: child.id }, maxMessages: 50,
639
- }, invocation.signal)[Symbol.asyncIterator]();
640
- await firstSnapshot(iterator);
641
- await ctx.sessionController.rename({
642
- sessionId: child.id, title: `btw · ${prompt.split(/\s+/u).slice(0, 6).join(' ')}`,
643
- });
644
- const from = child.session.seq;
645
- child.steer(createUserMessage({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } }));
646
- try {
647
- for (;;) {
648
- const next = await iterator.next();
649
- if (next.done === true)
650
- throw new Error('DSH session follow ended before the side turn completed');
651
- if (next.value.type === 'event' && eventsFromRecords([next.value])[0]?.type === 'turn/end')
652
- break;
653
- }
654
- }
655
- finally {
656
- await iterator.return?.();
657
- }
658
- await ctx.sessions.flush(child.session);
659
- const cells = foldCells(child.session.snapshotEvents(from), createToolPresenter(tool => ctx.tools.get(tool, child)))
660
- .filter(cell => cell.kind !== 'user');
661
- dispatch({ type: 'open-overlay', overlay: {
662
- cells, kind: 'info', lines: [], title: `Btw · turn ${String(turn)}${boundary.open ? ' · trailing turn open' : ''}`,
663
- } });
664
- return { kind: 'success' };
665
- };
666
- const definitions = [
667
- {
668
- name: 'help', description: 'Show keyboard and command help',
669
- handler: (invocation) => {
670
- const stale = ensureCurrent(invocation);
671
- if (stale !== undefined)
672
- return stale;
673
- const invalid = usage(invocation.rawInput, '/help');
674
- if (invalid !== undefined)
675
- return invalid;
676
- dispatch({ type: 'help' });
677
- return { kind: 'success' };
678
- },
679
- },
680
- {
681
- name: 'status', description: 'Show exact session, model, permission, and usage facts',
682
- handler: (invocation) => {
683
- const stale = ensureCurrent(invocation);
684
- if (stale !== undefined)
685
- return stale;
686
- const invalid = usage(invocation.rawInput, '/status');
687
- if (invalid !== undefined)
688
- return invalid;
689
- dispatch({ type: 'open-overlay', overlay: statusOverlay(ctx, bound.agent, bound.root) });
690
- return { kind: 'success' };
691
- },
692
- },
693
- {
694
- name: 'context', description: 'Show DSH context composition estimates',
695
- handler: (invocation) => {
696
- const stale = ensureCurrent(invocation);
697
- if (stale !== undefined)
698
- return stale;
699
- const invalid = usage(invocation.rawInput, '/context');
700
- if (invalid !== undefined)
701
- return invalid;
702
- dispatch({ type: 'open-overlay', overlay: contextOverlay(ctx, bound.agent) });
703
- return { kind: 'success' };
704
- },
705
- },
706
- {
707
- name: 'usage', description: 'Show session token and timing totals',
708
- handler: (invocation) => {
709
- const invalid = ensureCurrent(invocation) ?? usage(invocation.rawInput, '/usage');
710
- if (invalid !== undefined)
711
- return invalid;
712
- dispatch({ type: 'open-overlay', overlay: usageOverlay(ctx, bound.agent) });
713
- return { kind: 'success' };
714
- },
715
- },
716
- {
717
- name: 'memory', description: 'Open an instruction file loaded by DSH',
718
- handler: (invocation) => {
719
- const invalid = ensureCurrent(invocation) ?? usage(invocation.rawInput, '/memory');
720
- if (invalid !== undefined)
721
- return invalid;
722
- dispatch({ type: 'open-overlay', overlay: memoryOverlay(bound.agent) });
723
- return { kind: 'success' };
724
- },
725
- },
726
- {
727
- name: 'skills', description: 'List skills resolved for this session', input: { hint: '[TEXT]' },
728
- handler: async (invocation) => {
729
- const stale = ensureCurrent(invocation);
730
- if (stale !== undefined)
731
- return stale;
732
- const overlay = await skillOverlay(ctx, bound.agent, invocation.rawInput, invocation.signal);
733
- const replaced = ensureCurrent(invocation);
734
- if (replaced !== undefined)
735
- return replaced;
736
- dispatch({ type: 'open-overlay', overlay });
737
- return { kind: 'success' };
738
- },
739
- },
740
- {
741
- name: 'config', description: 'Show or update native DSH settings', input: { hint: '[NAMESPACE KEY=VALUE]' },
742
- handler: async (invocation) => {
743
- const stale = ensureCurrent(invocation);
744
- if (stale !== undefined)
745
- return stale;
746
- const raw = invocation.rawInput.trim();
747
- if (raw === '')
748
- return { kind: 'success', text: ctx.settings.describe({ redactSecrets: true }).map(item => `${String(item.ns)}\n value: ${configLayer(item.value)}\n base: ${configLayer(item.base)}\n user: ${configLayer(item.user)}`).join('\n') };
749
- const match = /^(\S+)\s+([^\s=]+)=(.*)$/su.exec(raw);
750
- if (match?.[1] === undefined || match[2] === undefined || match[3] === undefined) {
751
- return { kind: 'error', text: 'usage: /config [NAMESPACE KEY=VALUE]' };
752
- }
753
- const value = configValue(match[3]);
754
- if (match[2].includes('.'))
755
- await ctx.settings.mutate(match[1], [{ op: 'set', path: match[2].split('.'), value }]);
756
- else
757
- await ctx.settings.update(match[1], { [match[2]]: value });
758
- return { kind: 'success', text: `updated ${match[1]}.${match[2]}` };
759
- },
760
- },
761
- { name: 'login', description: 'List or start provider sign-in', input: { hint: '[KEY [METHOD]]' },
762
- handler: async (invocation) => {
763
- const stale = ensureCurrent(invocation);
764
- if (stale !== undefined)
765
- return stale;
766
- const values = invocation.rawInput.trim().split(/\s+/u).filter(Boolean);
767
- if (values.length === 0)
768
- return { kind: 'success', text: ctx.authorization.list().map(entry => `${String(entry.key)} · ${entry.label} · ${entry.methods.map(method => `${method.id}: ${method.label}`).join(', ')}${entry.inFlight ? ' · in progress' : ''}`).join('\n') || 'no authorization flows' };
769
- if (values.length > 2)
770
- return { kind: 'error', text: 'usage: /login [KEY [METHOD]]' };
771
- const entry = ctx.authorization.describe(parseCredentialKey(values[0]));
772
- if (entry === undefined)
773
- return { kind: 'error', text: `no authorization flow for ${values[0]}` };
774
- const notices = [];
775
- const outcome = await ctx.authorization.begin({ key: entry.key, ...(values[1] === undefined ? {} : { method: values[1] }), signal: invocation.signal,
776
- interaction: { notify: notice => {
777
- notices.push(notice.message, ...(notice.url === undefined ? [] : [notice.url]), ...(notice.code === undefined ? [] : [`Code: ${notice.code}`]));
778
- dispatch({ type: 'open-overlay', overlay: { kind: 'info', title: 'Login', lines: notices } });
779
- }, prompt: prompt => authorizationPrompt(bound, prompt, notices.join('\n')) },
780
- }).finally(() => { dispatch({ type: 'overlay-close' }); });
781
- return { kind: 'success', text: outcome.status === 'authorized' ? `signed in to ${entry.label}` : 'login cancelled' };
782
- },
783
- },
784
- { name: 'logout', description: 'List or forget provider credentials', input: { hint: '[KEY]' },
785
- handler: async (invocation) => {
786
- const stale = ensureCurrent(invocation);
787
- if (stale !== undefined)
788
- return stale;
789
- const raw = invocation.rawInput.trim();
790
- if (raw === '')
791
- return { kind: 'success', text: (await ctx.credentials.listRecords()).map(record => `${String(record.key)} · ${record.kind}`).join('\n') || 'no stored credentials' };
792
- if (/\s/u.test(raw))
793
- return { kind: 'error', text: 'usage: /logout [KEY]' };
794
- await ctx.credentials.deleteRecord(parseCredentialKey(raw));
795
- return { kind: 'success', text: `signed out ${raw}` };
796
- },
797
- },
798
- {
799
- name: 'diff', description: 'Show working tree or last-turn file changes', input: { hint: '[turn]' }, handler: async (invocation) => {
800
- const invalid = ensureCurrent(invocation);
801
- if (invalid !== undefined)
802
- return invalid;
803
- const mode = invocation.rawInput.trim();
804
- if (mode !== '' && mode !== 'turn')
805
- return { kind: 'error', text: 'usage: /diff [turn]' };
806
- dispatch({ type: 'open-overlay', overlay: await diffOverlay(ctx, bound.agent, bound.presenter, mode, lifetime.signal) });
807
- return { kind: 'success' };
808
- },
809
- },
810
- {
811
- name: 'plugins', description: 'List the running profile plugins',
812
- handler: async (invocation) => {
813
- const stale = ensureCurrent(invocation);
814
- if (stale !== undefined)
815
- return stale;
816
- if (invocation.rawInput.trim() !== '')
817
- return { kind: 'error', text: 'usage: /plugins' };
818
- const servers = new Map();
819
- for (const entry of ctx.loader.entries())
820
- if (entry.options.name === '@deepseek-ai/dsh-mcp-client'
821
- && typeof entry.options.config?.serverName === 'string')
822
- servers.set(entry.id, entry.options.config.serverName);
823
- const snapshot = await ctx.get('pluginInventory').list();
824
- return { kind: 'success', text: snapshot.entries.map(entry => `${String(entry.entryId)} · ${entry.moduleName}${servers.has(String(entry.entryId)) ? ` · ${servers.get(String(entry.entryId))}` : ''} · ${entry.enabled ? 'enabled' : 'disabled'} · ${entry.fiberPhase ?? 'inactive'}`).join('\n') };
825
- },
826
- },
827
- {
828
- name: 'plugin', description: 'Run DSH profile plugin management', input: { hint: 'ARGS' },
829
- handler: async (invocation) => {
830
- const stale = ensureCurrent(invocation);
831
- if (stale !== undefined)
832
- return stale;
833
- if (invocation.rawInput.trim() === '')
834
- return { kind: 'error', text: 'usage: /plugin ARGS' };
835
- const command = `dsh plugin --profile ${quoteShellWord(decodeURIComponent(new URL(ctx.baseUrl).pathname).split('/').at(-2))}${invocation.rawInput}`;
836
- const [message, result] = await bound.agent.runMaintenance(signal => runHumanShell(ctx, bound.agent, command, signal, 'bundles load on the next launch; plain plugins need a patch row', false));
837
- bound.agent.inject(message);
838
- await ctx.sessions.flush(bound.agent.session);
839
- publish(bound);
840
- return result.exitCode === 0 ? { kind: 'success' } : { kind: 'error', text: `dsh plugin exited ${String(result.exitCode ?? result.signal ?? 'without status')}` };
841
- },
842
- },
843
- {
844
- name: 'new', description: 'Start a new session', input: { hint: '[NAME | --name TITLE]' },
845
- handler: async (invocation) => {
846
- const stale = ensureCurrent(invocation);
847
- if (stale !== undefined)
848
- return stale;
849
- const raw = invocation.rawInput.trim();
850
- const match = /^--name\s+(.+)$/su.exec(raw);
851
- if (raw.startsWith('--') && match === null)
852
- return { kind: 'error', text: 'usage: /new [NAME | --name TITLE]' };
853
- const title = match?.[1]?.trim();
854
- if (raw !== '' && match === null) {
855
- await ctx.sessionController.rename({ sessionId: bound.agent.id, title: raw });
856
- await ctx.sessions.flush(bound.agent.session);
857
- }
858
- await runRootOperation({ kind: 'new', ...(title === undefined ? {} : { title }) });
859
- return { kind: 'success' };
860
- },
861
- },
862
- {
863
- name: 'clear', description: 'Start a new session', input: { hint: '[NAME]' },
864
- handler: async (invocation) => {
865
- const stale = ensureCurrent(invocation);
866
- if (stale !== undefined)
867
- return stale;
868
- const title = invocation.rawInput.trim();
869
- if (title !== '') {
870
- await ctx.sessionController.rename({ sessionId: bound.agent.id, title });
871
- await ctx.sessions.flush(bound.agent.session);
872
- }
873
- await runRootOperation({ kind: 'new' });
874
- return { kind: 'success' };
875
- },
876
- },
877
- {
878
- name: 'resume', description: 'Resume a session by name or ID, or open the picker', input: { hint: '[--all] [NAME|UUID]' },
879
- handler: async (invocation) => {
880
- const stale = ensureCurrent(invocation);
881
- if (stale !== undefined)
882
- return stale;
883
- const raw = invocation.rawInput.trim();
884
- const all = raw === '--all' || raw.startsWith('--all ');
885
- const value = all ? raw.slice('--all'.length).trim() : raw;
886
- if (raw.startsWith('--') && !all)
887
- return { kind: 'error', text: 'usage: /resume [--all] [NAME|UUID]' };
888
- await runRootOperation(value === '' ? { kind: 'open-resume', ...(all ? { all: true } : {}) }
889
- : { kind: 'resume', sessionId: value, ...(all ? { all: true } : {}) });
890
- return { kind: 'success' };
891
- },
892
- },
893
- {
894
- name: 'fork', description: 'Fork at the latest completed turn',
895
- handler: async (invocation) => {
896
- const stale = ensureCurrent(invocation);
897
- if (stale !== undefined)
898
- return stale;
899
- const invalid = usage(invocation.rawInput, '/fork');
900
- if (invalid !== undefined)
901
- return invalid;
902
- await runRootOperation({ kind: 'fork' });
903
- return { kind: 'success' };
904
- },
905
- },
906
- {
907
- name: 'rewind', description: 'Restore conversation or code at a prompt',
908
- handler: async (invocation) => {
909
- const stale = ensureCurrent(invocation);
910
- if (stale !== undefined)
911
- return stale;
912
- const invalid = usage(invocation.rawInput, '/rewind');
913
- if (invalid !== undefined)
914
- return invalid;
915
- await runRootOperation({ kind: 'open-rewind' });
916
- return { kind: 'success' };
917
- },
918
- },
919
- {
920
- name: 'history', description: 'Browse loaded transcript cells',
921
- handler: (invocation) => {
922
- const stale = ensureCurrent(invocation);
923
- if (stale !== undefined)
924
- return stale;
925
- const invalid = usage(invocation.rawInput, '/history');
926
- if (invalid !== undefined)
927
- return invalid;
928
- dispatch({ type: 'open-history' });
929
- return { kind: 'success' };
930
- },
931
- },
932
- {
933
- name: 'tasks', description: 'Show jobs and subagents, or kill a job', input: { hint: '[kill ID]' },
934
- handler: tasks,
935
- },
936
- {
937
- name: 'bashes', description: 'Alias of /tasks', input: { hint: '[kill ID]' },
938
- handler: tasks,
939
- },
940
- {
941
- name: 'subtask', description: 'Start a continuable child', input: { hint: 'TEXT' },
942
- handler: async (invocation) => {
943
- const stale = ensureCurrent(invocation);
944
- if (stale !== undefined)
945
- return stale;
946
- const text = invocation.rawInput.trim();
947
- if (text === '')
948
- return { kind: 'error', text: 'usage: /subtask TEXT' };
949
- const started = await ctx.subagents.startContinuable({
950
- label: text, provider: 'spawn',
951
- request: { parent: bound.agent, prompt: [{ type: 'text', text }] },
952
- signal: invocation.signal,
953
- });
954
- return { kind: 'success', text: `subtask ${String(started.childId)} started` };
955
- },
956
- },
957
- {
958
- name: 'loop', description: 'List, schedule, or stop a fixed-rate reminder',
959
- input: { hint: '[INTERVAL TEXT | stop ID]' }, handler: loop,
960
- },
961
- {
962
- name: 'btw', description: 'Ask a side question in a fork', input: { hint: 'TEXT' }, recordInput: false,
963
- handler: invocation => askAside(invocation),
964
- },
965
- {
966
- name: 'recap', description: 'Summarize the conversation in a fork', recordInput: false,
967
- handler: invocation => usage(invocation.rawInput, '/recap') ?? askAside(invocation, RECAP_PROMPT),
968
- },
969
- {
970
- name: 'agents', description: 'Choose or author a native DSH agent preset',
971
- input: { hint: '[new NAME | copy SRC DEST | delete NAME]' },
972
- handler: async (invocation) => {
973
- const stale = ensureCurrent(invocation);
974
- if (stale !== undefined)
975
- return stale;
976
- const raw = invocation.rawInput.trim();
977
- const blank = !bound.agent.session.snapshotEvents().some(event => event.type === 'turn/start');
978
- const selected = ctx.sessionProjections.stateOf(bound.agent.session, 'agentPreset')
979
- ?? ctx.agentPresets.defaultId;
980
- const create = /^new\s+(\S+)$/u.exec(raw);
981
- const copy = /^copy\s+(\S+)\s+(\S+)$/u.exec(raw);
982
- const remove = /^delete\s+(\S+)$/u.exec(raw);
983
- if (create?.[1] !== undefined) {
984
- await ctx.agentPresets.copy(selected, create[1]);
985
- dispatch({ type: 'open-file', path: (await ctx.agentPresets.resolve(create[1])).path });
986
- return { kind: 'success', text: `created ${create[1]} from ${selected}` };
987
- }
988
- if (copy?.[1] !== undefined && copy[2] !== undefined) {
989
- await ctx.agentPresets.copy(copy[1], copy[2]);
990
- return { kind: 'success', text: `copied ${copy[1]} to ${copy[2]}` };
991
- }
992
- if (remove?.[1] !== undefined) {
993
- await ctx.agentPresets.remove(remove[1]);
994
- return { kind: 'success', text: `deleted ${remove[1]}` };
995
- }
996
- if (raw !== '')
997
- return { kind: 'error', text: 'usage: /agents [new NAME | copy SRC DEST | delete NAME]' };
998
- dispatch({
999
- type: 'open-overlay',
1000
- overlay: agentPresetOverlay(await ctx.agentPresets.list(), selected, blank),
1001
- });
1002
- return { kind: 'success' };
1003
- },
1004
- },
1005
- {
1006
- name: 'export', description: 'Export the current transcript as Markdown', input: { hint: '[path]' },
1007
- handler: async (invocation) => {
1008
- const stale = ensureCurrent(invocation);
1009
- if (stale !== undefined)
1010
- return stale;
1011
- const path = invocation.rawInput.trim() || `dashi-${bound.root.id}.md`;
1012
- const fs = ctx.fs;
1013
- const [cwd, target, inspection] = await Promise.all([
1014
- fs.resolve('.', { cwd: bound.root.cwd, signal: lifetime.signal }),
1015
- fs.resolve(path, { cwd: bound.root.cwd, signal: lifetime.signal }),
1016
- ctx.sessionController.inspect(bound.agent.id, lifetime.signal),
1017
- ]);
1018
- if (!fs.contains(cwd, target)) {
1019
- return { kind: 'error', text: 'export path must be inside the session working directory' };
1020
- }
1021
- const content = markdownTranscript(bound.root.id, bound.root.cwd, bound.root.title, foldCells(inspection.events, bound.presenter));
1022
- await fs.writeText(target, content, undefined, lifetime.signal, ctx.sandboxPolicy.resolve({ session: bound.agent.session }));
1023
- return { kind: 'success', text: `exported ${fs.processPath(target)}` };
1024
- },
1025
- },
1026
- {
1027
- name: 'copy', description: 'Copy an assistant response or code block', input: { hint: '[N|code]' },
1028
- handler: (invocation) => {
1029
- const stale = ensureCurrent(invocation);
1030
- if (stale !== undefined)
1031
- return stale;
1032
- const raw = invocation.rawInput.trim();
1033
- const selection = raw === '' ? 1 : raw === 'code' ? 'code' : Number(raw);
1034
- if (raw !== '' && selection !== 'code' && (!/^[1-9]\d*$/u.test(raw) || !Number.isSafeInteger(selection))) {
1035
- return { kind: 'error', text: 'usage: /copy [N|code]' };
1036
- }
1037
- dispatch({ selection, type: 'copy-assistant' });
1038
- return { kind: 'success' };
1039
- },
1040
- },
1041
- {
1042
- name: 'rename', description: 'Rename the current session', input: { hint: 'TITLE' }, recordInput: false,
1043
- handler: async (invocation) => {
1044
- const stale = ensureCurrent(invocation);
1045
- if (stale !== undefined)
1046
- return stale;
1047
- const title = invocation.rawInput.trim();
1048
- if (title === '')
1049
- return { kind: 'error', text: 'usage: /rename TITLE' };
1050
- const renamed = await ctx.sessionController.rename({ sessionId: bound.agent.id, title });
1051
- await ctx.sessions.flush(bound.agent.session);
1052
- bound.root = { ...bound.root, title: renamed.title };
1053
- dispatch({ type: 'root-title', rootId: bound.root.id, title: renamed.title });
1054
- return { kind: 'success', sourceEventSeq: SessionSeq(renamed.seq) };
1055
- },
1056
- },
1057
- {
1058
- name: 'model', description: 'Select the model and reasoning effort',
1059
- handler: async (invocation) => {
1060
- const stale = ensureCurrent(invocation);
1061
- if (stale !== undefined)
1062
- return stale;
1063
- const invalid = usage(invocation.rawInput, '/model');
1064
- if (invalid !== undefined)
1065
- return invalid;
1066
- await openModel(bound);
1067
- return { kind: 'success' };
1068
- },
1069
- },
1070
- {
1071
- name: 'init', description: 'Create a starter AGENTS.md',
1072
- handler: async (invocation) => {
1073
- const stale = ensureCurrent(invocation);
1074
- if (stale !== undefined)
1075
- return stale;
1076
- const invalid = usage(invocation.rawInput, '/init');
1077
- if (invalid !== undefined)
1078
- return invalid;
1079
- const target = await ctx.fs.resolve('AGENTS.md', { cwd: bound.root.cwd, signal: lifetime.signal });
1080
- await ctx.fs.writeText(target, starterAgents(bound.root.cwd), { kind: 'createIfAbsent' }, lifetime.signal, ctx.sandboxPolicy.resolve({ session: bound.agent.session }));
1081
- return { kind: 'success', text: `created ${ctx.fs.processPath(target)}` };
1082
- },
1083
- },
1084
- {
1085
- name: 'effort', description: 'Select the reasoning effort', input: { hint: 'LEVEL' },
1086
- handler: async (invocation) => {
1087
- const invalid = ensureCurrent(invocation);
1088
- const effort = invocation.rawInput.trim();
1089
- if (invalid !== undefined || effort === '')
1090
- return invalid ?? { kind: 'error', text: 'usage: /effort LEVEL' };
1091
- const selected = currentModel(ctx, bound.agent) ?? (await ctx.sessionController.modelCatalog()).default;
1092
- await activate({ kind: 'model', effort, model: selected.model, provider: selected.provider });
1093
- return { kind: 'success' };
1094
- },
1095
- },
1096
- {
1097
- ...nativePermission,
1098
- handler: async (invocation) => {
1099
- const stale = ensureCurrent(invocation);
1100
- if (stale !== undefined)
1101
- return stale;
1102
- if (invocation.rawInput.trim() === '') {
1103
- openPermission(bound);
1104
- return { kind: 'success' };
1105
- }
1106
- return nativePermission.handler(invocation);
1107
- },
1108
- },
1109
- {
1110
- name: 'queue', description: 'Queue one explicit next turn', input: { hint: 'TEXT' },
1111
- handler: (invocation) => {
1112
- const stale = ensureCurrent(invocation);
1113
- if (stale !== undefined)
1114
- return stale;
1115
- const text = invocation.rawInput.trim();
1116
- if (text === '')
1117
- return { kind: 'error', text: 'usage: /queue TEXT' };
1118
- bound.agent.followup(createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } }));
1119
- return { kind: 'success', text: 'queued next turn' };
1120
- },
1121
- },
1122
- {
1123
- name: 'exit', description: 'Exit dashi',
1124
- handler: (invocation) => {
1125
- const stale = ensureCurrent(invocation);
1126
- if (stale !== undefined)
1127
- return stale;
1128
- const invalid = usage(invocation.rawInput, '/exit');
1129
- if (invalid !== undefined)
1130
- return invalid;
1131
- dispatch({ type: 'exit' });
1132
- return { kind: 'success' };
1133
- },
1134
- },
1135
- ];
1136
- return [...definitions, ...[['quit', 'exit'], ['reset', 'clear'], ['continue', 'resume'], ['branch', 'fork']]
1137
- .map(([name, target]) => ({ ...definitions.find(definition => definition.name === target), name }))];
1138
- };
1139
- async function install(bound) {
1140
- const id = bound.root.id;
1141
- const definitions = commandDefinitions(bound);
1142
- const commandScope = bound.agent.ctx.inject(['commands'], (commandCtx) => {
1143
- for (const definition of definitions)
1144
- commandCtx.commands.register(definition);
1145
- });
1146
- await commandScope;
1147
- bound.commandScope = commandScope;
1148
- bound.disposers.push(ctx.on('agent/status', ({ agent, status }) => {
1149
- if (agent !== bound.agent)
1150
- return;
1151
- bound.root = { ...bound.root, status };
1152
- dispatch({ type: 'root-status', rootId: id, status });
1153
- }, { global: true }), ctx.on('agent/error', ({ agent, error }) => {
1154
- if (agent === bound.agent)
1155
- report(error, id);
1156
- }, { global: true }), ctx.on('approval/request', (request, next) => {
1157
- if (request.agent !== bound.agent)
1158
- return next();
1159
- const decisionId = `approval:${String(++decisionSequence)}`;
1160
- return decide({
1161
- cursor: 0, id: decisionId, kind: 'approval',
1162
- owner: { id, ...(bound.root.title === undefined ? {} : { label: bound.root.title }) },
1163
- prompt: request.reason ?? `${request.toolName} requests wider access`, toolName: request.toolName,
1164
- }, request.signal);
1165
- }, { global: true }), ctx.on('user-questions/request', (request, next) => {
1166
- if (request.agent !== bound.agent)
1167
- return next();
1168
- const decisionId = `question:${String(++decisionSequence)}`;
1169
- return decide({
1170
- answers: [], cursor: 0, custom: '', id: decisionId, index: 0, kind: 'question',
1171
- owner: { id, ...(bound.root.title === undefined ? {} : { label: bound.root.title }) },
1172
- questions: request.questions.map(question => ({
1173
- id: question.id, multiSelect: question.multiSelect ?? false,
1174
- options: (question.options ?? []).map(option => ({ ...option })), question: question.question,
1175
- ...(question.detail === undefined ? {} : { detail: question.detail }),
1176
- ...(question.header === undefined ? {} : { header: question.header }),
1177
- ...(question.intent === undefined ? {} : { intent: question.intent }),
1178
- })),
1179
- selected: [],
1180
- }, request.signal).then(answer => ({ answers: answer.answers.map(item => ({
1181
- id: item.id, selected: [...item.selected],
1182
- ...(item.custom === undefined ? {} : { custom: item.custom }),
1183
- })) }));
1184
- }, { global: true }), ctx.on('agent/disposed', ({ agent }) => {
1185
- if (agent === bound.agent)
1186
- bound.abort.abort();
1187
- }, { global: true }));
1188
- }
1189
- async function stop(bound) {
1190
- bound.abort.abort();
1191
- if (bound.timer !== undefined)
1192
- clearTimeout(bound.timer);
1193
- bound.timer = undefined;
1194
- if (bound.presentationTimer !== undefined)
1195
- clearTimeout(bound.presentationTimer);
1196
- bound.presentationTimer = undefined;
1197
- for (const dispose of bound.disposers.splice(0).reverse())
1198
- dispose();
1199
- await bound.commandScope?.dispose();
1200
- await bound.pump;
1201
- await bound.controlPump;
1202
- }
1203
- await install(binding);
1204
- if (options.model !== undefined || options.effort !== undefined) {
1205
- const catalog = await ctx.sessionController.modelCatalog();
1206
- const currentSelection = currentModel(ctx, binding.agent) ?? catalog.default;
1207
- const model = options.model ?? currentSelection.model;
1208
- const candidates = catalog.groups.filter(group => group.models.some(item => item.id === model));
1209
- if (options.provider === undefined && candidates.length > 1) {
1210
- throw new Error(`model "${model}" is available from multiple providers: ${candidates.map(group => group.id).join(', ')}; pass --provider`);
1211
- }
1212
- const result = await ctx.sessionController.selectModel({
1213
- sessionId: binding.agent.id,
1214
- model,
1215
- provider: options.provider ?? candidates[0]?.id ?? currentSelection.provider,
1216
- ...(options.effort === undefined ? {} : { reasoningEffort: options.effort }),
1217
- });
1218
- const { effort: _previousEffort, ...root } = binding.root;
1219
- binding.root = {
1220
- ...root, model: result.selected.model, provider: result.selected.provider,
1221
- ...(result.selected.reasoningEffort === undefined ? {} : { effort: result.selected.reasoningEffort }),
1222
- };
1223
- }
1224
- if (options.permission !== undefined) {
1225
- const execution = await ctx.commands.execute(binding.agent, `/permission ${options.permission}`, [], lifetime.signal);
1226
- if (execution === undefined)
1227
- throw new Error('DSH supplied no /permission command');
1228
- if (execution.result.kind === 'error')
1229
- throw new Error(execution.result.text ?? 'permission change failed');
1230
- binding.root = { ...binding.root, permission: options.permission };
1231
- }
1232
- tuiRoot.bind(binding.agent);
1233
- const activate = async (value) => {
1234
- try {
1235
- const bound = current();
1236
- switch (value.kind) {
1237
- case 'fork':
1238
- case 'new':
1239
- case 'open-resume':
1240
- case 'open-rewind':
1241
- case 'resume':
1242
- case 'rewind':
1243
- await runRootOperation(value);
1244
- break;
1245
- case 'rewind-boundary':
1246
- dispatch({ type: 'open-overlay', overlay: rewindActionOverlay(value) });
1247
- break;
1248
- case 'search-result': {
1249
- if (bound.root.id !== value.rootId)
1250
- return;
1251
- const window = await ctx.sessionQuery.readEvent({
1252
- seq: SessionSeq(value.seq), sessionId: SessionId(value.sessionId),
1253
- }, lifetime.signal);
1254
- const prompt = humanPrompt(window.target);
1255
- if (prompt === undefined)
1256
- throw new Error('the selected result is not a direct human prompt');
1257
- if (current() !== bound)
1258
- return;
1259
- dispatch({ type: 'composer-set', text: prompt });
1260
- break;
1261
- }
1262
- case 'agent-preset': {
1263
- const blank = !bound.agent.session.snapshotEvents().some(event => event.type === 'turn/start');
1264
- if (blank) {
1265
- await ctx.agentPresets.select(bound.agent, value.preset);
1266
- await ctx.sessions.flush(bound.agent.session);
1267
- }
1268
- else {
1269
- await runRootOperation({ agentPreset: value.preset, kind: 'new' });
1270
- }
1271
- break;
1272
- }
1273
- case 'job-output': {
1274
- const result = ctx.jobs.read(JobId(value.jobId), bound.agent);
1275
- dispatch({ type: 'open-overlay', overlay: {
1276
- cells: [{ key: `job:${value.jobId}`, kind: 'outcome', text: result.text }],
1277
- kind: 'info', lines: [], title: `Job ${value.jobId} · ${result.snapshot.status}`,
1278
- } });
1279
- break;
1280
- }
1281
- case 'model': {
1282
- const result = await ctx.sessionController.selectModel({
1283
- sessionId: bound.agent.id, model: value.model, provider: value.provider,
1284
- ...(value.effort === undefined ? {} : { reasoningEffort: value.effort }),
1285
- });
1286
- const { effort: _oldEffort, ...root } = bound.root;
1287
- bound.root = {
1288
- ...root, model: result.selected.model, provider: result.selected.provider,
1289
- ...(result.selected.reasoningEffort === undefined ? {} : { effort: result.selected.reasoningEffort }),
1290
- };
1291
- dispatch({
1292
- type: 'root-model', rootId: bound.root.id,
1293
- model: result.selected.model, provider: result.selected.provider,
1294
- ...(result.selected.reasoningEffort === undefined ? {} : { effort: result.selected.reasoningEffort }),
1295
- });
1296
- break;
1297
- }
1298
- case 'permission': {
1299
- const accepted = await executeAccepted(bound, `/permission ${value.preset}`, [], execution => {
1300
- if (execution.result.kind === 'error')
1301
- throw new Error(execution.result.text ?? 'permission change failed');
1302
- bound.root = { ...bound.root, permission: value.preset };
1303
- dispatch({ type: 'root-permission', preset: value.preset, rootId: bound.root.id });
1304
- });
1305
- if (!accepted)
1306
- throw new Error('DSH supplied no /permission command');
1307
- }
1308
- }
1309
- }
1310
- catch (error) {
1311
- report(error);
1312
- }
1313
- };
1314
- return {
1315
- initialAttachments,
1316
- get initialCells() { return transcriptCells(current()); },
1317
- get initialHasMore() { return current().hasMore; },
1318
- get initialPrompts() { return historyInputs(current().events); },
1319
- get root() { return current().root; },
1320
- get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.repo); },
1321
- get summary() { return `Resume with: dsh --profile dashi --resume ${current().root.id}`; },
1322
- activate,
1323
- async attach(path) {
1324
- try {
1325
- return (await readDraftImages(current().root.cwd, [path]))[0];
1326
- }
1327
- catch (error) {
1328
- report(error);
1329
- }
1330
- },
1331
- async complete(query, caret) {
1332
- try {
1333
- const bound = current();
1334
- return await completionOptions(ctx, bound.agent, query, caret, lifetime.signal);
1335
- }
1336
- catch (error) {
1337
- report(error);
1338
- return [];
1339
- }
1340
- },
1341
- async cyclePermission() {
1342
- try {
1343
- const bound = current();
1344
- const names = ctx.permissionPresets.names;
1345
- if (names.length === 0)
1346
- throw new Error('no permission presets available');
1347
- const index = names.indexOf(ctx.permissionPresets.current(bound.agent.session));
1348
- const preset = names[(index + 1 + names.length) % names.length];
1349
- if (preset === undefined)
1350
- return;
1351
- const spec = ctx.permissionPresets.resolve(preset);
1352
- if (spec.approval === 'never') {
1353
- dispatch({ type: 'open-overlay', overlay: {
1354
- acceptLabel: 'Enable',
1355
- cursor: 1,
1356
- detail: [`Sandbox: ${spec.sandbox}`, `Approval: ${spec.approval}`],
1357
- kind: 'confirm',
1358
- title: `Enable ${ctx.permissionPresets.optionOf(preset).name}?`,
1359
- value: { kind: 'permission', preset },
1360
- } });
1361
- }
1362
- else {
1363
- await activate({ kind: 'permission', preset });
1364
- }
1365
- }
1366
- catch (error) {
1367
- report(error);
1368
- }
1369
- },
1370
- interrupt() {
1371
- const agent = current().agent;
1372
- if (accepting && agent.status === 'running')
1373
- agent.cancel({ kind: 'user' }, { keepInbox: true });
1374
- },
1375
- async loadHistory(rootId) {
1376
- const bound = current();
1377
- if (bound.root.id !== rootId || !bound.hasMore)
1378
- return;
1379
- try {
1380
- const first = bound.events[0];
1381
- if (first === undefined)
1382
- throw new Error('DSH reported earlier history without an opening event');
1383
- const page = await ctx.sessionController.page({
1384
- address: { kind: 'session', sessionId: bound.agent.id },
1385
- beforeSeq: first.seq,
1386
- maxMessages: 50,
1387
- throughSeq: bound.cursor,
1388
- }, lifetime.signal);
1389
- if (current() !== bound)
1390
- return;
1391
- const older = eventsFromRecords(page.records);
1392
- if (older.length === 0 && page.hasMore)
1393
- throw new Error('DSH returned an empty non-final history page');
1394
- bound.events.unshift(...older);
1395
- bound.hasMore = page.hasMore;
1396
- dispatch({
1397
- type: 'history-page', cells: transcriptCells(bound),
1398
- hasMore: bound.hasMore, rootId,
1399
- });
1400
- }
1401
- catch (error) {
1402
- if (!lifetime.signal.aborted)
1403
- report(error, rootId);
1404
- }
1405
- },
1406
- async search(query, rootId) {
1407
- try {
1408
- const bound = current();
1409
- if (bound.root.id !== rootId)
1410
- return;
1411
- const page = await ctx.sessionQuery.searchSessions({
1412
- query,
1413
- sessionFilters: [{ kind: 'cwd', values: [bound.root.cwd] }],
1414
- eventFilters: [
1415
- { kind: 'type', values: ['user/message'] },
1416
- { kind: 'surface', values: ['current'] },
1417
- ],
1418
- limit: 20,
1419
- }, { signal: lifetime.signal });
1420
- if (current() !== bound)
1421
- return;
1422
- dispatch({ type: 'open-overlay', overlay: searchOverlay(page.items, rootId) });
1423
- }
1424
- catch (error) {
1425
- if (!lifetime.signal.aborted)
1426
- report(error, rootId);
1427
- }
1428
- },
1429
- shutdown() {
1430
- if (shutdown !== undefined)
1431
- return shutdown;
1432
- accepting = false;
1433
- lifetime.abort();
1434
- const bound = current();
1435
- shutdown = (async () => {
1436
- if (bound.agent.status === 'running')
1437
- bound.agent.cancel({ kind: 'user' }, { keepInbox: true });
1438
- await bound.agent.whenIdle();
1439
- if (ctx.sessions.get(bound.agent.id) === bound.agent.session)
1440
- await ctx.sessions.flush(bound.agent.session);
1441
- tuiRoot.clear(bound.agent);
1442
- await stop(bound);
1443
- })();
1444
- return shutdown;
1445
- },
1446
- start() {
1447
- if (started)
1448
- return;
1449
- started = true;
1450
- binding.pump = follow(binding);
1451
- binding.controlPump = followControl(binding);
1452
- },
1453
- async submit(text, mode, attachments) {
1454
- if (!accepting)
1455
- return;
1456
- const bound = current();
1457
- try {
1458
- if (text.startsWith('!')) {
1459
- const command = text.slice(1).trim();
1460
- if (command === '')
1461
- throw new Error('usage: !command');
1462
- if (attachments.length > 0)
1463
- throw new Error('!command does not accept image attachments');
1464
- await bound.agent.runMaintenance(async (signal) => {
1465
- const [message] = await runHumanShell(ctx, bound.agent, command, signal);
1466
- bound.agent.inject(message);
1467
- await ctx.sessions.flush(bound.agent.session);
1468
- });
1469
- dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: `!${command}` });
1470
- publish(bound);
1471
- return;
1472
- }
1473
- if (await executeAccepted(bound, text, encodeDraftImages(attachments)))
1474
- return;
1475
- const refs = await admitDraftImages(ctx.attachments, attachments);
1476
- const content = [
1477
- ...(text === '' ? [] : [{ type: 'text', text }]),
1478
- ...refs.map(attachment => ({ type: 'image', attachment })),
1479
- ];
1480
- const message = createUserMessage({ content, source: { kind: 'user' } });
1481
- if (mode === 'steer')
1482
- bound.agent.steer(message);
1483
- else
1484
- bound.agent.followup(message);
1485
- }
1486
- catch (error) {
1487
- if (!lifetime.signal.aborted)
1488
- report(error, bound.root.id);
1489
- }
1490
- },
1491
- };
1492
- }