@antst/dashi 0.1.0-alpha.9 → 0.1.1
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.
- package/README.md +6 -6
- package/lib/application.d.ts +2 -0
- package/lib/application.js +3 -3
- package/lib/catalogs.d.ts +3 -0
- package/lib/catalogs.js +74 -1
- package/lib/completion-trigger.d.ts +4 -0
- package/lib/completion-trigger.js +13 -1
- package/lib/diff-view.js +9 -3
- package/lib/help.d.ts +1 -1
- package/lib/help.js +1 -1
- package/lib/history-records.d.ts +2 -2
- package/lib/history-records.js +2 -12
- package/lib/index.js +46 -9
- package/lib/input.d.ts +8 -1
- package/lib/input.js +45 -0
- package/lib/renderer.d.ts +1 -0
- package/lib/renderer.js +71 -15
- package/lib/rewind.d.ts +15 -2
- package/lib/rewind.js +46 -16
- package/lib/session-runtime.d.ts +7 -0
- package/lib/session-runtime.js +284 -35
- package/lib/shell-command.d.ts +1 -1
- package/lib/shell-command.js +3 -2
- package/lib/state.d.ts +13 -1
- package/lib/state.js +36 -16
- package/lib/transcript.d.ts +3 -1
- package/lib/transcript.js +57 -44
- package/package.json +38 -35
- package/validated-dsh-versions.json +0 -3
package/lib/session-runtime.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { AuthorizationDeclinedError } from '@deepseek-ai/dsh-authorization';
|
|
3
3
|
import { parseCredentialKey } from '@deepseek-ai/dsh-credentials';
|
|
4
|
-
import {
|
|
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';
|
|
5
7
|
import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
|
|
6
|
-
import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, skillOverlay, statusOverlay, } from './catalogs.js';
|
|
8
|
+
import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionStatusLine, sessionOverlay, skillOverlay, statusOverlay, usageOverlay, } from './catalogs.js';
|
|
7
9
|
import { diffOverlay } from './diff-view.js';
|
|
8
10
|
import { eventsFromRecords } from './history-records.js';
|
|
9
11
|
import { admitDraftImages, encodeDraftImages, readDraftImages } from './image-input.js';
|
|
10
12
|
import { contextPercent, jobViews, subagentViews } from './presentation.js';
|
|
11
|
-
import {
|
|
13
|
+
import { historyInput, historyInputs, humanPrompt, inheritedTurn, latestCompletedTurn, rewindActionOverlay, rewindOverlay, rewindModelSelection, } from './rewind.js';
|
|
12
14
|
import { createToolPresenter } from './tool-presentation.js';
|
|
13
15
|
import { quoteShellWord, runHumanShell } from './shell-command.js';
|
|
14
16
|
import { markdownTranscript } from './session-export.js';
|
|
15
17
|
import { isSessionId, sessionMatches, sessionNotFound } from './session-list.js';
|
|
16
18
|
import { memoryOverlay } from './memory.js';
|
|
17
|
-
import { foldCells, pendingShellCells } from './transcript.js';
|
|
19
|
+
import { foldAssistantStream, foldCells, pendingShellCells } from './transcript.js';
|
|
20
|
+
export class LaunchArgumentError extends Error {
|
|
21
|
+
}
|
|
18
22
|
function starterAgents(cwd) {
|
|
19
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`;
|
|
20
24
|
}
|
|
25
|
+
const RECAP_PROMPT = 'Summarize this conversation so far in ten lines: goal, decisions, open items.';
|
|
21
26
|
function detail(error) {
|
|
22
27
|
return error instanceof Error ? error.message : String(error);
|
|
23
28
|
}
|
|
@@ -56,6 +61,20 @@ function turnStartTime(events, turn) {
|
|
|
56
61
|
return event.time;
|
|
57
62
|
}
|
|
58
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
|
+
}
|
|
59
78
|
function titleFromEvent(event) {
|
|
60
79
|
if (String(event.type) !== 'session/title')
|
|
61
80
|
return undefined;
|
|
@@ -76,9 +95,33 @@ async function firstControl(iterator) {
|
|
|
76
95
|
}
|
|
77
96
|
return first.value;
|
|
78
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
|
+
}
|
|
79
112
|
function usage(raw, expected) {
|
|
80
113
|
return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
|
|
81
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
|
+
}
|
|
82
125
|
function configValue(raw) {
|
|
83
126
|
try {
|
|
84
127
|
return JSON.parse(raw);
|
|
@@ -111,16 +154,37 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
111
154
|
if ('error' in resolved)
|
|
112
155
|
throw resolved.error;
|
|
113
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
|
+
}));
|
|
114
177
|
let title;
|
|
115
178
|
if (name !== undefined)
|
|
116
179
|
title = (await ctx.sessionController.rename({ sessionId, title: name })).title;
|
|
117
180
|
await ctx.sessions.flush(agent.session);
|
|
118
181
|
const iterator = ctx.sessionController.follow({
|
|
119
|
-
address: { kind: 'session', sessionId }, maxMessages: 50,
|
|
182
|
+
address: { kind: 'session', sessionId }, assistantStream: true, maxMessages: 50,
|
|
120
183
|
}, abort.signal)[Symbol.asyncIterator]();
|
|
121
184
|
const controlIterator = ctx.sessionController.control(abort.signal)[Symbol.asyncIterator]();
|
|
122
|
-
const [opening, controlOpening, subagents] = await Promise.all([
|
|
185
|
+
const [opening, controlOpening, subagents, repo] = await Promise.all([
|
|
123
186
|
firstSnapshot(iterator), firstControl(controlIterator), subagentViews(ctx, sessionId, abort.signal),
|
|
187
|
+
gitRepo(ctx, agent, abort.signal),
|
|
124
188
|
]);
|
|
125
189
|
title ??= projectionString(opening.projections, 'title');
|
|
126
190
|
const selected = projectionModel(opening.projections);
|
|
@@ -132,14 +196,16 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
132
196
|
return {
|
|
133
197
|
abort,
|
|
134
198
|
agent,
|
|
199
|
+
repo,
|
|
135
200
|
controlIterator,
|
|
136
201
|
cursor: opening.cursor,
|
|
137
|
-
disposers
|
|
202
|
+
disposers,
|
|
138
203
|
events: eventsFromRecords(opening.records),
|
|
139
204
|
hasMore: opening.hasMore,
|
|
140
205
|
iterator,
|
|
141
206
|
presenter: createToolPresenter(tool => ctx.tools.get(tool, agent)),
|
|
142
207
|
presentationTimer: undefined,
|
|
208
|
+
streamCells: openingStream(opening.assistantStream),
|
|
143
209
|
root: {
|
|
144
210
|
...(initialContextPercent === undefined ? {} : { contextPercent: initialContextPercent }),
|
|
145
211
|
cwd: opening.header.cwd ?? options.cwd,
|
|
@@ -172,7 +238,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
172
238
|
};
|
|
173
239
|
const executeAccepted = async (bound, line, images, onSettled) => {
|
|
174
240
|
const from = bound.agent.session.seq;
|
|
175
|
-
const execution = ctx.commands.execute(bound.agent, line, images, lifetime.signal);
|
|
241
|
+
const execution = ctx.commands.execute(bound.agent, line, images.map(image => ({ type: 'image', ...image })), lifetime.signal);
|
|
176
242
|
const accepted = bound.agent.session.snapshotEvents(from).some(event => event.type === 'command/run');
|
|
177
243
|
if (!accepted) {
|
|
178
244
|
const result = await execution;
|
|
@@ -210,6 +276,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
210
276
|
});
|
|
211
277
|
const transcriptCells = (bound) => [
|
|
212
278
|
...foldCells(bound.events, bound.presenter, { truncatedStart: bound.hasMore }),
|
|
279
|
+
...bound.streamCells,
|
|
213
280
|
...pendingShellCells(bound.agent.inbox.nextStep),
|
|
214
281
|
];
|
|
215
282
|
const publish = (bound) => {
|
|
@@ -241,12 +308,35 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
241
308
|
});
|
|
242
309
|
}, 34);
|
|
243
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
|
+
};
|
|
244
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
|
+
}
|
|
245
332
|
const [event] = eventsFromRecords([frame]);
|
|
246
333
|
if (event === undefined)
|
|
247
334
|
return;
|
|
248
335
|
bound.events.push(event);
|
|
336
|
+
if (event.type === 'assistant/message' || event.type === 'assistant/attempt')
|
|
337
|
+
bound.streamCells = [];
|
|
249
338
|
if (event.type === 'turn/end') {
|
|
339
|
+
void refreshRepo(bound);
|
|
250
340
|
const startedAt = turnStartTime(bound.events, event.data.turn);
|
|
251
341
|
if (startedAt !== undefined) {
|
|
252
342
|
dispatch({
|
|
@@ -271,7 +361,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
271
361
|
...(model.reasoningEffort === undefined ? {} : { effort: model.reasoningEffort }),
|
|
272
362
|
});
|
|
273
363
|
}
|
|
274
|
-
const prompt =
|
|
364
|
+
const prompt = event.type === 'agent/inbox/spliced' ? undefined : historyInput(event);
|
|
275
365
|
if (prompt !== undefined)
|
|
276
366
|
dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: prompt });
|
|
277
367
|
schedule(bound);
|
|
@@ -350,7 +440,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
350
440
|
tuiRoot.bind(candidate.agent);
|
|
351
441
|
dispatch({
|
|
352
442
|
type: 'root-bound', cells: transcriptCells(candidate),
|
|
353
|
-
hasMore: candidate.hasMore, prompts:
|
|
443
|
+
hasMore: candidate.hasMore, prompts: historyInputs(candidate.events), root: candidate.root,
|
|
354
444
|
});
|
|
355
445
|
if (started) {
|
|
356
446
|
candidate.pump = follow(candidate);
|
|
@@ -391,13 +481,12 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
391
481
|
?? ctx.agentPresets.defaultId;
|
|
392
482
|
const modelProjection = ctx.sessionProjections.snapshot(bound.agent.session, ['modelSelection'])
|
|
393
483
|
.values.modelSelection;
|
|
394
|
-
const selection = modelProjection
|
|
484
|
+
const selection = rewindModelSelection(modelProjection);
|
|
395
485
|
const created = await ctx.sessionController.create({ cwd: bound.root.cwd, agentPreset });
|
|
396
486
|
let selectionFailure;
|
|
397
487
|
try {
|
|
398
|
-
if (selection
|
|
399
|
-
|
|
400
|
-
await ctx.sessionController.selectModel({ sessionId: created.sessionId, ...selection });
|
|
488
|
+
if (selection !== undefined)
|
|
489
|
+
await ctx.sessionController.selectModel({ sessionId: created.sessionId, ...selection });
|
|
401
490
|
}
|
|
402
491
|
catch (error) {
|
|
403
492
|
selectionFailure = error;
|
|
@@ -449,7 +538,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
449
538
|
const roller = ctx.commands.list(bound.agent).some(command => command.name === 'roller-restore');
|
|
450
539
|
const overlay = rewindOverlay(bound.agent.session.snapshotEvents(), roller);
|
|
451
540
|
if (overlay.kind !== 'list' || overlay.options.length === 0) {
|
|
452
|
-
throw new Error('the current session has no
|
|
541
|
+
throw new Error('the current session has no input to rewind');
|
|
453
542
|
}
|
|
454
543
|
dispatch({ type: 'open-overlay', overlay });
|
|
455
544
|
return;
|
|
@@ -481,6 +570,99 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
481
570
|
if (nativePermission === undefined)
|
|
482
571
|
throw new Error('dashi: DSH supplied no /permission command');
|
|
483
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
|
+
};
|
|
484
666
|
const definitions = [
|
|
485
667
|
{
|
|
486
668
|
name: 'help', description: 'Show keyboard and command help',
|
|
@@ -521,6 +703,16 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
521
703
|
return { kind: 'success' };
|
|
522
704
|
},
|
|
523
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
|
+
},
|
|
524
716
|
{
|
|
525
717
|
name: 'memory', description: 'Open an instruction file loaded by DSH',
|
|
526
718
|
handler: (invocation) => {
|
|
@@ -641,7 +833,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
641
833
|
if (invocation.rawInput.trim() === '')
|
|
642
834
|
return { kind: 'error', text: 'usage: /plugin ARGS' };
|
|
643
835
|
const command = `dsh plugin --profile ${quoteShellWord(decodeURIComponent(new URL(ctx.baseUrl).pathname).split('/').at(-2))}${invocation.rawInput}`;
|
|
644
|
-
const [message, result] = await bound.agent.runMaintenance(signal => runHumanShell(ctx, bound.agent, command, signal, '
|
|
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));
|
|
645
837
|
bound.agent.inject(message);
|
|
646
838
|
await ctx.sessions.flush(bound.agent.session);
|
|
647
839
|
publish(bound);
|
|
@@ -649,29 +841,35 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
649
841
|
},
|
|
650
842
|
},
|
|
651
843
|
{
|
|
652
|
-
name: 'new', description: 'Start a new session', input: { hint: '[--name TITLE]' },
|
|
844
|
+
name: 'new', description: 'Start a new session', input: { hint: '[NAME | --name TITLE]' },
|
|
653
845
|
handler: async (invocation) => {
|
|
654
846
|
const stale = ensureCurrent(invocation);
|
|
655
847
|
if (stale !== undefined)
|
|
656
848
|
return stale;
|
|
657
849
|
const raw = invocation.rawInput.trim();
|
|
658
850
|
const match = /^--name\s+(.+)$/su.exec(raw);
|
|
659
|
-
if (raw
|
|
660
|
-
return { kind: 'error', text: 'usage: /new [--name TITLE]' };
|
|
851
|
+
if (raw.startsWith('--') && match === null)
|
|
852
|
+
return { kind: 'error', text: 'usage: /new [NAME | --name TITLE]' };
|
|
661
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
|
+
}
|
|
662
858
|
await runRootOperation({ kind: 'new', ...(title === undefined ? {} : { title }) });
|
|
663
859
|
return { kind: 'success' };
|
|
664
860
|
},
|
|
665
861
|
},
|
|
666
862
|
{
|
|
667
|
-
name: 'clear', description: 'Start a new session',
|
|
863
|
+
name: 'clear', description: 'Start a new session', input: { hint: '[NAME]' },
|
|
668
864
|
handler: async (invocation) => {
|
|
669
865
|
const stale = ensureCurrent(invocation);
|
|
670
866
|
if (stale !== undefined)
|
|
671
867
|
return stale;
|
|
672
|
-
const
|
|
673
|
-
if (
|
|
674
|
-
|
|
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
|
+
}
|
|
675
873
|
await runRootOperation({ kind: 'new' });
|
|
676
874
|
return { kind: 'success' };
|
|
677
875
|
},
|
|
@@ -732,30 +930,71 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
732
930
|
},
|
|
733
931
|
},
|
|
734
932
|
{
|
|
735
|
-
name: 'tasks', description: 'Show jobs and subagents',
|
|
736
|
-
handler:
|
|
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) => {
|
|
737
943
|
const stale = ensureCurrent(invocation);
|
|
738
944
|
if (stale !== undefined)
|
|
739
945
|
return stale;
|
|
740
|
-
const
|
|
741
|
-
if (
|
|
742
|
-
return
|
|
743
|
-
|
|
744
|
-
|
|
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` };
|
|
745
955
|
},
|
|
746
956
|
},
|
|
747
957
|
{
|
|
748
|
-
name: '
|
|
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]' },
|
|
749
972
|
handler: async (invocation) => {
|
|
750
973
|
const stale = ensureCurrent(invocation);
|
|
751
974
|
if (stale !== undefined)
|
|
752
975
|
return stale;
|
|
753
|
-
const
|
|
754
|
-
if (invalid !== undefined)
|
|
755
|
-
return invalid;
|
|
976
|
+
const raw = invocation.rawInput.trim();
|
|
756
977
|
const blank = !bound.agent.session.snapshotEvents().some(event => event.type === 'turn/start');
|
|
757
978
|
const selected = ctx.sessionProjections.stateOf(bound.agent.session, 'agentPreset')
|
|
758
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]' };
|
|
759
998
|
dispatch({
|
|
760
999
|
type: 'open-overlay',
|
|
761
1000
|
overlay: agentPresetOverlay(await ctx.agentPresets.list(), selected, blank),
|
|
@@ -1031,6 +1270,14 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
1031
1270
|
}
|
|
1032
1271
|
break;
|
|
1033
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
|
+
}
|
|
1034
1281
|
case 'model': {
|
|
1035
1282
|
const result = await ctx.sessionController.selectModel({
|
|
1036
1283
|
sessionId: bound.agent.id, model: value.model, provider: value.provider,
|
|
@@ -1068,8 +1315,9 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
1068
1315
|
initialAttachments,
|
|
1069
1316
|
get initialCells() { return transcriptCells(current()); },
|
|
1070
1317
|
get initialHasMore() { return current().hasMore; },
|
|
1071
|
-
get initialPrompts() { return
|
|
1318
|
+
get initialPrompts() { return historyInputs(current().events); },
|
|
1072
1319
|
get root() { return current().root; },
|
|
1320
|
+
get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.repo); },
|
|
1073
1321
|
get summary() { return `Resume with: dsh --profile dashi --resume ${current().root.id}`; },
|
|
1074
1322
|
activate,
|
|
1075
1323
|
async attach(path) {
|
|
@@ -1218,6 +1466,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
1218
1466
|
bound.agent.inject(message);
|
|
1219
1467
|
await ctx.sessions.flush(bound.agent.session);
|
|
1220
1468
|
});
|
|
1469
|
+
dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: `!${command}` });
|
|
1221
1470
|
publish(bound);
|
|
1222
1471
|
return;
|
|
1223
1472
|
}
|
package/lib/shell-command.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export declare const HUMAN_SHELL_STREAM_BYTES: number;
|
|
|
7
7
|
export declare function shellResultText(command: string, result: ShellRunResult): string;
|
|
8
8
|
export declare function quoteShellWord(value: string): string;
|
|
9
9
|
/** Run an explicit human command through DSH and identify its next-step context. */
|
|
10
|
-
export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal, success?: string): Promise<readonly [UserMessage, ShellRunResult]>;
|
|
10
|
+
export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal, success?: string, useSessionSandbox?: boolean): Promise<readonly [UserMessage, ShellRunResult]>;
|
package/lib/shell-command.js
CHANGED
|
@@ -45,11 +45,12 @@ export function shellResultText(command, result) {
|
|
|
45
45
|
}
|
|
46
46
|
export function quoteShellWord(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
|
|
47
47
|
/** Run an explicit human command through DSH and identify its next-step context. */
|
|
48
|
-
export async function runHumanShell(ctx, agent, command, signal, success) {
|
|
48
|
+
export async function runHumanShell(ctx, agent, command, signal, success, useSessionSandbox = true) {
|
|
49
49
|
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
50
50
|
command,
|
|
51
51
|
env: { DSH_HOME: resolveDshHome() },
|
|
52
|
-
sandboxPolicy: ctx.sandboxPolicy.resolve(
|
|
52
|
+
sandboxPolicy: ctx.sandboxPolicy.resolve(useSessionSandbox
|
|
53
|
+
? { session: agent.session } : { mode: 'danger-full-access' }),
|
|
53
54
|
signal,
|
|
54
55
|
stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES,
|
|
55
56
|
timeoutMs: HUMAN_SHELL_TIMEOUT_MS,
|
package/lib/state.d.ts
CHANGED
|
@@ -137,6 +137,9 @@ export type OverlayValue = {
|
|
|
137
137
|
} | {
|
|
138
138
|
readonly kind: 'insert';
|
|
139
139
|
readonly text: string;
|
|
140
|
+
} | {
|
|
141
|
+
readonly jobId: string;
|
|
142
|
+
readonly kind: 'job-output';
|
|
140
143
|
} | {
|
|
141
144
|
readonly kind: 'model';
|
|
142
145
|
readonly effort?: string;
|
|
@@ -213,6 +216,7 @@ export type Overlay = {
|
|
|
213
216
|
readonly cells?: readonly TerminalCell[];
|
|
214
217
|
readonly kind: 'info';
|
|
215
218
|
readonly lines: readonly string[];
|
|
219
|
+
readonly scrollOffset?: number;
|
|
216
220
|
readonly title: string;
|
|
217
221
|
} | {
|
|
218
222
|
readonly cursor: number;
|
|
@@ -268,6 +272,9 @@ export type UiAction = {
|
|
|
268
272
|
readonly source?: string;
|
|
269
273
|
readonly text?: string;
|
|
270
274
|
readonly type: 'attachment-added';
|
|
275
|
+
} | {
|
|
276
|
+
readonly path: string;
|
|
277
|
+
readonly type: 'attachment-path';
|
|
271
278
|
} | {
|
|
272
279
|
readonly index: number;
|
|
273
280
|
readonly type: 'attachment-remove';
|
|
@@ -330,6 +337,9 @@ export type UiAction = {
|
|
|
330
337
|
readonly type: 'open-history';
|
|
331
338
|
} | {
|
|
332
339
|
readonly type: 'open-details';
|
|
340
|
+
} | {
|
|
341
|
+
readonly path: string;
|
|
342
|
+
readonly type: 'open-file';
|
|
333
343
|
} | {
|
|
334
344
|
readonly type: 'completion-ready';
|
|
335
345
|
readonly options: readonly OverlayOption[];
|
|
@@ -340,6 +350,7 @@ export type UiAction = {
|
|
|
340
350
|
} | {
|
|
341
351
|
readonly type: 'overlay-close';
|
|
342
352
|
} | {
|
|
353
|
+
readonly limit?: number;
|
|
343
354
|
readonly type: 'overlay-move';
|
|
344
355
|
readonly offset: number;
|
|
345
356
|
} | {
|
|
@@ -500,6 +511,7 @@ export type UiEffect = {
|
|
|
500
511
|
} | {
|
|
501
512
|
readonly type: 'suspend';
|
|
502
513
|
};
|
|
503
|
-
export declare function initialViewState(cwd: string, inline: boolean, root?: RootView, cells?: readonly TerminalCell[], prompts?: readonly string[], historyHasMore?: boolean, attachments?: readonly DraftAttachment[]): ViewState;
|
|
514
|
+
export declare function initialViewState(cwd: string, inline: boolean, root?: RootView, cells?: readonly TerminalCell[], prompts?: readonly string[], historyHasMore?: boolean, attachments?: readonly DraftAttachment[], toolMode?: ToolMode): ViewState;
|
|
504
515
|
export declare const TURN_BELL_THRESHOLD_MS = 10000;
|
|
516
|
+
export declare const PAGE_CELLS = 8;
|
|
505
517
|
export declare function reduce(state: ViewState, action: UiAction): readonly [ViewState, readonly UiEffect[]];
|