@antst/dashi 0.1.0-alpha.8 → 0.1.0
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 +8 -3
- package/lib/catalogs.d.ts +5 -0
- package/lib/catalogs.js +91 -1
- package/lib/completion-trigger.d.ts +4 -0
- package/lib/completion-trigger.js +13 -1
- package/lib/diff-view.js +11 -4
- 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 +62 -14
- package/lib/input.d.ts +8 -1
- package/lib/input.js +45 -0
- package/lib/renderer.d.ts +2 -0
- package/lib/renderer.js +90 -20
- package/lib/rewind.d.ts +15 -2
- package/lib/rewind.js +46 -16
- package/lib/session-runtime.d.ts +10 -0
- package/lib/session-runtime.js +390 -43
- package/lib/shell-command.d.ts +1 -1
- package/lib/shell-command.js +3 -2
- package/lib/state.d.ts +23 -3
- package/lib/state.js +64 -23
- package/lib/transcript.d.ts +3 -1
- package/lib/transcript.js +57 -44
- package/package.json +38 -33
- package/validated-dsh-versions.json +0 -3
package/lib/session-runtime.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
2
7
|
import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
|
|
3
|
-
import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, statusOverlay, } from './catalogs.js';
|
|
8
|
+
import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionStatusLine, sessionOverlay, skillOverlay, statusOverlay, usageOverlay, } from './catalogs.js';
|
|
4
9
|
import { diffOverlay } from './diff-view.js';
|
|
5
10
|
import { eventsFromRecords } from './history-records.js';
|
|
6
11
|
import { admitDraftImages, encodeDraftImages, readDraftImages } from './image-input.js';
|
|
7
12
|
import { contextPercent, jobViews, subagentViews } from './presentation.js';
|
|
8
|
-
import {
|
|
13
|
+
import { historyInput, historyInputs, humanPrompt, inheritedTurn, latestCompletedTurn, rewindActionOverlay, rewindOverlay, rewindModelSelection, } from './rewind.js';
|
|
9
14
|
import { createToolPresenter } from './tool-presentation.js';
|
|
10
15
|
import { quoteShellWord, runHumanShell } from './shell-command.js';
|
|
11
16
|
import { markdownTranscript } from './session-export.js';
|
|
12
17
|
import { isSessionId, sessionMatches, sessionNotFound } from './session-list.js';
|
|
13
18
|
import { memoryOverlay } from './memory.js';
|
|
14
|
-
import { foldCells, pendingShellCells } from './transcript.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.';
|
|
15
26
|
function detail(error) {
|
|
16
27
|
return error instanceof Error ? error.message : String(error);
|
|
17
28
|
}
|
|
@@ -50,6 +61,20 @@ function turnStartTime(events, turn) {
|
|
|
50
61
|
return event.time;
|
|
51
62
|
}
|
|
52
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
|
+
}
|
|
53
78
|
function titleFromEvent(event) {
|
|
54
79
|
if (String(event.type) !== 'session/title')
|
|
55
80
|
return undefined;
|
|
@@ -70,9 +95,33 @@ async function firstControl(iterator) {
|
|
|
70
95
|
}
|
|
71
96
|
return first.value;
|
|
72
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
|
+
}
|
|
73
112
|
function usage(raw, expected) {
|
|
74
113
|
return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
|
|
75
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
|
+
}
|
|
76
125
|
function configValue(raw) {
|
|
77
126
|
try {
|
|
78
127
|
return JSON.parse(raw);
|
|
@@ -91,11 +140,13 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
91
140
|
let started = false;
|
|
92
141
|
let shutdown;
|
|
93
142
|
let decisionSequence = 0;
|
|
94
|
-
|
|
143
|
+
let initialId = options.resume === undefined
|
|
95
144
|
? options.continue
|
|
96
145
|
? continuation((await ctx.sessionController.list({}, lifetime.signal)).items, options.cwd)
|
|
97
|
-
: (await ctx.sessionController.create({ cwd: options.cwd })).sessionId
|
|
146
|
+
: (await ctx.sessionController.create({ cwd: options.cwd, ...(options.agentPreset === undefined ? {} : { agentPreset: options.agentPreset }), ...(options.sessionId === undefined ? {} : { sessionId: SessionId(options.sessionId) }) })).sessionId
|
|
98
147
|
: SessionId(options.resume);
|
|
148
|
+
if (options.forkSession)
|
|
149
|
+
initialId = (await ctx.sessionController.fork({ sessionId: initialId })).sessionId;
|
|
99
150
|
const prepare = async (sessionId, name) => {
|
|
100
151
|
const abort = new AbortController();
|
|
101
152
|
try {
|
|
@@ -103,16 +154,37 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
103
154
|
if ('error' in resolved)
|
|
104
155
|
throw resolved.error;
|
|
105
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
|
+
}));
|
|
106
177
|
let title;
|
|
107
178
|
if (name !== undefined)
|
|
108
179
|
title = (await ctx.sessionController.rename({ sessionId, title: name })).title;
|
|
109
180
|
await ctx.sessions.flush(agent.session);
|
|
110
181
|
const iterator = ctx.sessionController.follow({
|
|
111
|
-
address: { kind: 'session', sessionId }, maxMessages: 50,
|
|
182
|
+
address: { kind: 'session', sessionId }, assistantStream: true, maxMessages: 50,
|
|
112
183
|
}, abort.signal)[Symbol.asyncIterator]();
|
|
113
184
|
const controlIterator = ctx.sessionController.control(abort.signal)[Symbol.asyncIterator]();
|
|
114
|
-
const [opening, controlOpening, subagents] = await Promise.all([
|
|
185
|
+
const [opening, controlOpening, subagents, repo] = await Promise.all([
|
|
115
186
|
firstSnapshot(iterator), firstControl(controlIterator), subagentViews(ctx, sessionId, abort.signal),
|
|
187
|
+
gitRepo(ctx, agent, abort.signal),
|
|
116
188
|
]);
|
|
117
189
|
title ??= projectionString(opening.projections, 'title');
|
|
118
190
|
const selected = projectionModel(opening.projections);
|
|
@@ -124,14 +196,16 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
124
196
|
return {
|
|
125
197
|
abort,
|
|
126
198
|
agent,
|
|
199
|
+
repo,
|
|
127
200
|
controlIterator,
|
|
128
201
|
cursor: opening.cursor,
|
|
129
|
-
disposers
|
|
202
|
+
disposers,
|
|
130
203
|
events: eventsFromRecords(opening.records),
|
|
131
204
|
hasMore: opening.hasMore,
|
|
132
205
|
iterator,
|
|
133
206
|
presenter: createToolPresenter(tool => ctx.tools.get(tool, agent)),
|
|
134
207
|
presentationTimer: undefined,
|
|
208
|
+
streamCells: openingStream(opening.assistantStream),
|
|
135
209
|
root: {
|
|
136
210
|
...(initialContextPercent === undefined ? {} : { contextPercent: initialContextPercent }),
|
|
137
211
|
cwd: opening.header.cwd ?? options.cwd,
|
|
@@ -164,7 +238,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
164
238
|
};
|
|
165
239
|
const executeAccepted = async (bound, line, images, onSettled) => {
|
|
166
240
|
const from = bound.agent.session.seq;
|
|
167
|
-
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);
|
|
168
242
|
const accepted = bound.agent.session.snapshotEvents(from).some(event => event.type === 'command/run');
|
|
169
243
|
if (!accepted) {
|
|
170
244
|
const result = await execution;
|
|
@@ -191,8 +265,18 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
191
265
|
signal?.removeEventListener('abort', abortDecision);
|
|
192
266
|
});
|
|
193
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
|
+
});
|
|
194
277
|
const transcriptCells = (bound) => [
|
|
195
278
|
...foldCells(bound.events, bound.presenter, { truncatedStart: bound.hasMore }),
|
|
279
|
+
...bound.streamCells,
|
|
196
280
|
...pendingShellCells(bound.agent.inbox.nextStep),
|
|
197
281
|
];
|
|
198
282
|
const publish = (bound) => {
|
|
@@ -224,12 +308,35 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
224
308
|
});
|
|
225
309
|
}, 34);
|
|
226
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
|
+
};
|
|
227
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
|
+
}
|
|
228
332
|
const [event] = eventsFromRecords([frame]);
|
|
229
333
|
if (event === undefined)
|
|
230
334
|
return;
|
|
231
335
|
bound.events.push(event);
|
|
336
|
+
if (event.type === 'assistant/message' || event.type === 'assistant/attempt')
|
|
337
|
+
bound.streamCells = [];
|
|
232
338
|
if (event.type === 'turn/end') {
|
|
339
|
+
void refreshRepo(bound);
|
|
233
340
|
const startedAt = turnStartTime(bound.events, event.data.turn);
|
|
234
341
|
if (startedAt !== undefined) {
|
|
235
342
|
dispatch({
|
|
@@ -254,7 +361,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
254
361
|
...(model.reasoningEffort === undefined ? {} : { effort: model.reasoningEffort }),
|
|
255
362
|
});
|
|
256
363
|
}
|
|
257
|
-
const prompt =
|
|
364
|
+
const prompt = event.type === 'agent/inbox/spliced' ? undefined : historyInput(event);
|
|
258
365
|
if (prompt !== undefined)
|
|
259
366
|
dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: prompt });
|
|
260
367
|
schedule(bound);
|
|
@@ -333,7 +440,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
333
440
|
tuiRoot.bind(candidate.agent);
|
|
334
441
|
dispatch({
|
|
335
442
|
type: 'root-bound', cells: transcriptCells(candidate),
|
|
336
|
-
hasMore: candidate.hasMore, prompts:
|
|
443
|
+
hasMore: candidate.hasMore, prompts: historyInputs(candidate.events), root: candidate.root,
|
|
337
444
|
});
|
|
338
445
|
if (started) {
|
|
339
446
|
candidate.pump = follow(candidate);
|
|
@@ -374,13 +481,12 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
374
481
|
?? ctx.agentPresets.defaultId;
|
|
375
482
|
const modelProjection = ctx.sessionProjections.snapshot(bound.agent.session, ['modelSelection'])
|
|
376
483
|
.values.modelSelection;
|
|
377
|
-
const selection = modelProjection
|
|
484
|
+
const selection = rewindModelSelection(modelProjection);
|
|
378
485
|
const created = await ctx.sessionController.create({ cwd: bound.root.cwd, agentPreset });
|
|
379
486
|
let selectionFailure;
|
|
380
487
|
try {
|
|
381
|
-
if (selection
|
|
382
|
-
|
|
383
|
-
await ctx.sessionController.selectModel({ sessionId: created.sessionId, ...selection });
|
|
488
|
+
if (selection !== undefined)
|
|
489
|
+
await ctx.sessionController.selectModel({ sessionId: created.sessionId, ...selection });
|
|
384
490
|
}
|
|
385
491
|
catch (error) {
|
|
386
492
|
selectionFailure = error;
|
|
@@ -432,7 +538,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
432
538
|
const roller = ctx.commands.list(bound.agent).some(command => command.name === 'roller-restore');
|
|
433
539
|
const overlay = rewindOverlay(bound.agent.session.snapshotEvents(), roller);
|
|
434
540
|
if (overlay.kind !== 'list' || overlay.options.length === 0) {
|
|
435
|
-
throw new Error('the current session has no
|
|
541
|
+
throw new Error('the current session has no input to rewind');
|
|
436
542
|
}
|
|
437
543
|
dispatch({ type: 'open-overlay', overlay });
|
|
438
544
|
return;
|
|
@@ -464,7 +570,100 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
464
570
|
if (nativePermission === undefined)
|
|
465
571
|
throw new Error('dashi: DSH supplied no /permission command');
|
|
466
572
|
const ensureCurrent = (invocation) => current().agent === invocation.agent ? undefined : { kind: 'error', text: 'this root is no longer current' };
|
|
467
|
-
|
|
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 = [
|
|
468
667
|
{
|
|
469
668
|
name: 'help', description: 'Show keyboard and command help',
|
|
470
669
|
handler: (invocation) => {
|
|
@@ -504,6 +703,16 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
504
703
|
return { kind: 'success' };
|
|
505
704
|
},
|
|
506
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
|
+
},
|
|
507
716
|
{
|
|
508
717
|
name: 'memory', description: 'Open an instruction file loaded by DSH',
|
|
509
718
|
handler: (invocation) => {
|
|
@@ -514,6 +723,20 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
514
723
|
return { kind: 'success' };
|
|
515
724
|
},
|
|
516
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
|
+
},
|
|
517
740
|
{
|
|
518
741
|
name: 'config', description: 'Show or update native DSH settings', input: { hint: '[NAMESPACE KEY=VALUE]' },
|
|
519
742
|
handler: async (invocation) => {
|
|
@@ -535,6 +758,43 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
535
758
|
return { kind: 'success', text: `updated ${match[1]}.${match[2]}` };
|
|
536
759
|
},
|
|
537
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
|
+
},
|
|
538
798
|
{
|
|
539
799
|
name: 'diff', description: 'Show working tree or last-turn file changes', input: { hint: '[turn]' }, handler: async (invocation) => {
|
|
540
800
|
const invalid = ensureCurrent(invocation);
|
|
@@ -573,7 +833,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
573
833
|
if (invocation.rawInput.trim() === '')
|
|
574
834
|
return { kind: 'error', text: 'usage: /plugin ARGS' };
|
|
575
835
|
const command = `dsh plugin --profile ${quoteShellWord(decodeURIComponent(new URL(ctx.baseUrl).pathname).split('/').at(-2))}${invocation.rawInput}`;
|
|
576
|
-
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));
|
|
577
837
|
bound.agent.inject(message);
|
|
578
838
|
await ctx.sessions.flush(bound.agent.session);
|
|
579
839
|
publish(bound);
|
|
@@ -581,29 +841,35 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
581
841
|
},
|
|
582
842
|
},
|
|
583
843
|
{
|
|
584
|
-
name: 'new', description: 'Start a new session', input: { hint: '[--name TITLE]' },
|
|
844
|
+
name: 'new', description: 'Start a new session', input: { hint: '[NAME | --name TITLE]' },
|
|
585
845
|
handler: async (invocation) => {
|
|
586
846
|
const stale = ensureCurrent(invocation);
|
|
587
847
|
if (stale !== undefined)
|
|
588
848
|
return stale;
|
|
589
849
|
const raw = invocation.rawInput.trim();
|
|
590
850
|
const match = /^--name\s+(.+)$/su.exec(raw);
|
|
591
|
-
if (raw
|
|
592
|
-
return { kind: 'error', text: 'usage: /new [--name TITLE]' };
|
|
851
|
+
if (raw.startsWith('--') && match === null)
|
|
852
|
+
return { kind: 'error', text: 'usage: /new [NAME | --name TITLE]' };
|
|
593
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
|
+
}
|
|
594
858
|
await runRootOperation({ kind: 'new', ...(title === undefined ? {} : { title }) });
|
|
595
859
|
return { kind: 'success' };
|
|
596
860
|
},
|
|
597
861
|
},
|
|
598
862
|
{
|
|
599
|
-
name: 'clear', description: 'Start a new session',
|
|
863
|
+
name: 'clear', description: 'Start a new session', input: { hint: '[NAME]' },
|
|
600
864
|
handler: async (invocation) => {
|
|
601
865
|
const stale = ensureCurrent(invocation);
|
|
602
866
|
if (stale !== undefined)
|
|
603
867
|
return stale;
|
|
604
|
-
const
|
|
605
|
-
if (
|
|
606
|
-
|
|
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
|
+
}
|
|
607
873
|
await runRootOperation({ kind: 'new' });
|
|
608
874
|
return { kind: 'success' };
|
|
609
875
|
},
|
|
@@ -664,30 +930,71 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
664
930
|
},
|
|
665
931
|
},
|
|
666
932
|
{
|
|
667
|
-
name: 'tasks', description: 'Show jobs and subagents',
|
|
668
|
-
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) => {
|
|
669
943
|
const stale = ensureCurrent(invocation);
|
|
670
944
|
if (stale !== undefined)
|
|
671
945
|
return stale;
|
|
672
|
-
const
|
|
673
|
-
if (
|
|
674
|
-
return
|
|
675
|
-
|
|
676
|
-
|
|
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` };
|
|
677
955
|
},
|
|
678
956
|
},
|
|
679
957
|
{
|
|
680
|
-
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]' },
|
|
681
972
|
handler: async (invocation) => {
|
|
682
973
|
const stale = ensureCurrent(invocation);
|
|
683
974
|
if (stale !== undefined)
|
|
684
975
|
return stale;
|
|
685
|
-
const
|
|
686
|
-
if (invalid !== undefined)
|
|
687
|
-
return invalid;
|
|
976
|
+
const raw = invocation.rawInput.trim();
|
|
688
977
|
const blank = !bound.agent.session.snapshotEvents().some(event => event.type === 'turn/start');
|
|
689
978
|
const selected = ctx.sessionProjections.stateOf(bound.agent.session, 'agentPreset')
|
|
690
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]' };
|
|
691
998
|
dispatch({
|
|
692
999
|
type: 'open-overlay',
|
|
693
1000
|
overlay: agentPresetOverlay(await ctx.agentPresets.list(), selected, blank),
|
|
@@ -717,15 +1024,17 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
717
1024
|
},
|
|
718
1025
|
},
|
|
719
1026
|
{
|
|
720
|
-
name: 'copy', description: 'Copy
|
|
1027
|
+
name: 'copy', description: 'Copy an assistant response or code block', input: { hint: '[N|code]' },
|
|
721
1028
|
handler: (invocation) => {
|
|
722
1029
|
const stale = ensureCurrent(invocation);
|
|
723
1030
|
if (stale !== undefined)
|
|
724
1031
|
return stale;
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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' });
|
|
729
1038
|
return { kind: 'success' };
|
|
730
1039
|
},
|
|
731
1040
|
},
|
|
@@ -758,6 +1067,32 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
758
1067
|
return { kind: 'success' };
|
|
759
1068
|
},
|
|
760
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
|
+
},
|
|
761
1096
|
{
|
|
762
1097
|
...nativePermission,
|
|
763
1098
|
handler: async (invocation) => {
|
|
@@ -798,6 +1133,8 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
798
1133
|
},
|
|
799
1134
|
},
|
|
800
1135
|
];
|
|
1136
|
+
return [...definitions, ...[['quit', 'exit'], ['reset', 'clear'], ['continue', 'resume'], ['branch', 'fork']]
|
|
1137
|
+
.map(([name, target]) => ({ ...definitions.find(definition => definition.name === target), name }))];
|
|
801
1138
|
};
|
|
802
1139
|
async function install(bound) {
|
|
803
1140
|
const id = bound.root.id;
|
|
@@ -933,6 +1270,14 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
933
1270
|
}
|
|
934
1271
|
break;
|
|
935
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
|
+
}
|
|
936
1281
|
case 'model': {
|
|
937
1282
|
const result = await ctx.sessionController.selectModel({
|
|
938
1283
|
sessionId: bound.agent.id, model: value.model, provider: value.provider,
|
|
@@ -970,8 +1315,9 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
970
1315
|
initialAttachments,
|
|
971
1316
|
get initialCells() { return transcriptCells(current()); },
|
|
972
1317
|
get initialHasMore() { return current().hasMore; },
|
|
973
|
-
get initialPrompts() { return
|
|
1318
|
+
get initialPrompts() { return historyInputs(current().events); },
|
|
974
1319
|
get root() { return current().root; },
|
|
1320
|
+
get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.repo); },
|
|
975
1321
|
get summary() { return `Resume with: dsh --profile dashi --resume ${current().root.id}`; },
|
|
976
1322
|
activate,
|
|
977
1323
|
async attach(path) {
|
|
@@ -1120,6 +1466,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
1120
1466
|
bound.agent.inject(message);
|
|
1121
1467
|
await ctx.sessions.flush(bound.agent.session);
|
|
1122
1468
|
});
|
|
1469
|
+
dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: `!${command}` });
|
|
1123
1470
|
publish(bound);
|
|
1124
1471
|
return;
|
|
1125
1472
|
}
|