@antst/dashi 0.1.0-alpha.7 → 0.1.0-alpha.9
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/lib/application.js +14 -1
- package/lib/catalogs.d.ts +2 -0
- package/lib/catalogs.js +17 -0
- package/lib/diff-view.d.ts +5 -0
- package/lib/diff-view.js +24 -0
- package/lib/external-editor.d.ts +1 -0
- package/lib/external-editor.js +1 -1
- package/lib/help.d.ts +2 -0
- package/lib/help.js +6 -0
- package/lib/index.js +25 -8
- package/lib/input.js +2 -0
- package/lib/memory.d.ts +3 -0
- package/lib/memory.js +27 -0
- package/lib/renderer.d.ts +1 -0
- package/lib/renderer.js +22 -6
- package/lib/session-runtime.d.ts +3 -0
- package/lib/session-runtime.js +199 -11
- package/lib/shell-command.d.ts +2 -1
- package/lib/shell-command.js +11 -8
- package/lib/state.d.ts +14 -2
- package/lib/state.js +33 -9
- package/lib/transcript.js +2 -1
- package/package.json +8 -1
package/lib/application.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRenderer } from './renderer.js';
|
|
2
|
+
import { runEditor } from './external-editor.js';
|
|
2
3
|
import { initialViewState, reduce, } from './state.js';
|
|
3
4
|
import { createTerminalGuard } from './terminal-guard.js';
|
|
4
5
|
function suspendProcess() {
|
|
@@ -44,7 +45,14 @@ export function createTerminalShell(options) {
|
|
|
44
45
|
async function run(effect) {
|
|
45
46
|
switch (effect.type) {
|
|
46
47
|
case 'activate-overlay':
|
|
47
|
-
|
|
48
|
+
if (effect.value.kind === 'open-file') {
|
|
49
|
+
const path = effect.value.path;
|
|
50
|
+
await guard.withCookedTerminal(() => runEditor(process.env.EDITOR?.trim() || 'vi', path, options.cwd)).catch(error => {
|
|
51
|
+
dispatch({ type: 'runtime-error', message: error instanceof Error ? error.message : String(error) });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
await options.activateOverlay?.(effect.value);
|
|
48
56
|
return;
|
|
49
57
|
case 'attach': {
|
|
50
58
|
try {
|
|
@@ -168,6 +176,7 @@ export function createTerminalShell(options) {
|
|
|
168
176
|
return;
|
|
169
177
|
failed = true;
|
|
170
178
|
accepting = false;
|
|
179
|
+
renderer.discardSecretComposer();
|
|
171
180
|
const detail = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
172
181
|
writeError(`dashi: terminal failure\n${detail}\n`);
|
|
173
182
|
try {
|
|
@@ -193,6 +202,9 @@ export function createTerminalShell(options) {
|
|
|
193
202
|
function dispatch(action) {
|
|
194
203
|
if (!accepting)
|
|
195
204
|
return;
|
|
205
|
+
const question = state.decisions[0]?.kind === 'question' ? state.decisions[0].questions[state.decisions[0].index] : undefined;
|
|
206
|
+
if (question?.secret === true && ['decision-submit', 'decision-cancel', 'decision-withdrawn', 'escape', 'terminal-lost'].includes(action.type))
|
|
207
|
+
renderer.discardSecretComposer();
|
|
196
208
|
const [next, effects] = reduce(state, action);
|
|
197
209
|
state = next;
|
|
198
210
|
if (effects.length === 0)
|
|
@@ -216,6 +228,7 @@ export function createTerminalShell(options) {
|
|
|
216
228
|
},
|
|
217
229
|
dispatch,
|
|
218
230
|
async dispose() {
|
|
231
|
+
renderer.discardSecretComposer();
|
|
219
232
|
const [next, effects] = reduce(state, { type: 'terminal-lost' });
|
|
220
233
|
state = next;
|
|
221
234
|
for (const effect of effects)
|
package/lib/catalogs.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { SessionSearchHit } from '@deepseek-ai/dsh-session-query';
|
|
|
6
6
|
import type { Overlay, OverlayOption, RootView } from './state.js';
|
|
7
7
|
/** Build command and skill completion rows for one exact root. */
|
|
8
8
|
export declare function completionOptions(ctx: Context, agent: Agent, input: string, caret?: number, signal?: AbortSignal): Promise<readonly OverlayOption[]>;
|
|
9
|
+
/** Build a filtered view of the skills resolved for one live session. */
|
|
10
|
+
export declare function skillOverlay(ctx: Context, agent: Agent, filter: string, signal?: AbortSignal): Promise<Overlay>;
|
|
9
11
|
/** Build the cold session picker without adopting any candidate. */
|
|
10
12
|
export declare function sessionOverlay(items: readonly SessionSummary[], currentId: string, cwd?: string): Overlay;
|
|
11
13
|
/** Flatten DSH's provider/model/effort catalog into one terminal picker. */
|
package/lib/catalogs.js
CHANGED
|
@@ -69,6 +69,23 @@ export async function completionOptions(ctx, agent, input, caret = input.length,
|
|
|
69
69
|
}));
|
|
70
70
|
return [...commands, ...skills];
|
|
71
71
|
}
|
|
72
|
+
/** Build a filtered view of the skills resolved for one live session. */
|
|
73
|
+
export async function skillOverlay(ctx, agent, filter, signal) {
|
|
74
|
+
const lookup = { cwd: agent.session.header.cwd, scope: agent, signal };
|
|
75
|
+
const needle = filter.trim().toLocaleLowerCase();
|
|
76
|
+
const skills = (await ctx.skills.list(lookup)).filter(skill => needle === ''
|
|
77
|
+
|| skill.name.toLocaleLowerCase().includes(needle)
|
|
78
|
+
|| skill.description.toLocaleLowerCase().includes(needle));
|
|
79
|
+
const definitions = await Promise.all(skills.map(skill => ctx.skills.get(skill.name, lookup)));
|
|
80
|
+
return {
|
|
81
|
+
cursor: 0, kind: 'list', purpose: 'skills', title: needle === '' ? 'Skills' : `Skills · ${filter.trim()}`,
|
|
82
|
+
options: skills.map((skill, index) => ({
|
|
83
|
+
detail: `${skill.description} · user ${skill.invocation.userInvocable ? 'yes' : 'no'} · model ${skill.invocation.modelInvocable ? 'yes' : 'no'} · source ${skill.source} · provider ${skill.provider} · path ${definitions[index]?.path ?? 'unavailable'}`,
|
|
84
|
+
label: `/${skill.name}`,
|
|
85
|
+
value: { kind: 'insert', text: `/${skill.name} ` },
|
|
86
|
+
})),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
72
89
|
/** Build the cold session picker without adopting any candidate. */
|
|
73
90
|
export function sessionOverlay(items, currentId, cwd) {
|
|
74
91
|
return {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
3
|
+
import type { Overlay } from './state.js';
|
|
4
|
+
import { type ToolPresenter } from './tool-presentation.js';
|
|
5
|
+
export declare function diffOverlay(ctx: Context, agent: Agent, presenter: ToolPresenter, mode: '' | 'turn', signal: AbortSignal): Promise<Overlay>;
|
package/lib/diff-view.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HUMAN_SHELL_STREAM_BYTES } from './shell-command.js';
|
|
2
|
+
import { boundedBody } from './tool-presentation.js';
|
|
3
|
+
import { foldCells } from './transcript.js';
|
|
4
|
+
export async function diffOverlay(ctx, agent, presenter, mode, signal) {
|
|
5
|
+
if (mode === 'turn') {
|
|
6
|
+
const events = agent.session.snapshotEvents();
|
|
7
|
+
const start = events.map(event => event.type).lastIndexOf('turn/start');
|
|
8
|
+
const cells = (start < 0 ? [] : foldCells(events.slice(start), presenter)).filter(cell => cell.pending !== true && cell.tool?.card === 'diff' && (cell.tool.diffs?.length ?? 0) > 0);
|
|
9
|
+
return { cells, kind: 'info', lines: cells.length === 0 ? ['No write/edit changes in the last turn.'] : [], title: 'Last turn diff' };
|
|
10
|
+
}
|
|
11
|
+
const command = 'git diff --no-ext-diff --no-color HEAD --';
|
|
12
|
+
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
13
|
+
command, sandboxPolicy: ctx.sandboxPolicy.resolve({ session: agent.session }), signal,
|
|
14
|
+
stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES, timeoutMs: 30_000, workdir: agent.session.header.cwd,
|
|
15
|
+
}));
|
|
16
|
+
const output = [result.stdout.text, result.stderr.text].filter(Boolean).join('\n');
|
|
17
|
+
const cell = {
|
|
18
|
+
key: 'diff:working', kind: result.exitCode === 0 ? 'tool' : 'error', text: 'git diff',
|
|
19
|
+
tool: { card: 'terminal', title: 'git diff', status: result.exitCode === 0 ? output === '' ? 'clean' : 'changed'
|
|
20
|
+
: result.exitCode === null ? result.signal ?? 'failed' : `exit ${String(result.exitCode)}`,
|
|
21
|
+
...(output === '' ? {} : { body: boundedBody(output) }) },
|
|
22
|
+
};
|
|
23
|
+
return { cells: [cell], kind: 'info', lines: [], title: 'Working tree diff' };
|
|
24
|
+
}
|
package/lib/external-editor.d.ts
CHANGED
package/lib/external-editor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { mkdtemp, readFile, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
function runEditor(command, file, cwd) {
|
|
5
|
+
export function runEditor(command, file, cwd) {
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
7
|
const child = spawn('/bin/sh', ['-c', `exec ${command} "$1"`, 'dashi-editor', file], {
|
|
8
8
|
cwd, stdio: 'inherit',
|
package/lib/help.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const VERSION_LINE: string;
|
|
2
|
+
export declare const FLAG_HELP: readonly ["Launch flags: --inline | --fullscreen · --accessible · -r, --resume [NAME|UUID] · -c, --continue · --fork-session · --all", " -n, --name TITLE · --agent PRESET · --session-id UUID · --model ID [--provider ID] · --effort LEVEL", " --permission PRESET · --yolo | --dangerously-skip-permissions · --image PATH · -h, --help"];
|
package/lib/help.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import dashi from '../package.json' with { type: 'json' };
|
|
2
|
+
import dsh from '@deepseek-ai/dsh-cmdline/package.json' with { type: 'json' };
|
|
3
|
+
export const VERSION_LINE = `dashi ${dashi.version} on DSH ${dsh.version}`;
|
|
4
|
+
export const FLAG_HELP = [
|
|
5
|
+
'Launch flags: --inline | --fullscreen · --accessible · -r, --resume [NAME|UUID] · -c, --continue · --fork-session · --all', ' -n, --name TITLE · --agent PRESET · --session-id UUID · --model ID [--provider ID] · --effort LEVEL', ' --permission PRESET · --yolo | --dangerously-skip-permissions · --image PATH · -h, --help'
|
|
6
|
+
];
|
package/lib/index.js
CHANGED
|
@@ -6,17 +6,20 @@ import { createTerminalShell } from './application.js';
|
|
|
6
6
|
import { sessionOverlay } from './catalogs.js';
|
|
7
7
|
import { readClipboardImage } from './clipboard-image.js';
|
|
8
8
|
import { editDraftExternally } from './external-editor.js';
|
|
9
|
+
import { FLAG_HELP, VERSION_LINE } from './help.js';
|
|
9
10
|
import { formatSessionList, isSessionId, parseSessionListArgs, sessionMatches, sessionNotFound, } from './session-list.js';
|
|
10
11
|
import { createSessionRuntime } from './session-runtime.js';
|
|
11
12
|
import { TuiRoot } from './tui-root.js';
|
|
12
13
|
export const name = 'dashi';
|
|
13
14
|
export const inject = [
|
|
14
|
-
'agentPresets', 'agents', 'attachments', 'cmdlineArgs', 'commands', 'fs', '
|
|
15
|
-
'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'shell', 'skills', 'tools',
|
|
15
|
+
'agentPresets', 'agents', 'attachments', 'authorization', 'cmdlineArgs', 'commands', 'credentials', 'fs', 'loader', 'permissionPresets',
|
|
16
|
+
'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'tools',
|
|
16
17
|
];
|
|
17
18
|
const validatedVersions = JSON.parse(readFileSync(new URL('../validated-dsh-versions.json', import.meta.url), 'utf8'));
|
|
18
19
|
function parseArgs(args) {
|
|
19
20
|
let accessible = false;
|
|
21
|
+
let agentPreset, sessionId;
|
|
22
|
+
let forkSession = false;
|
|
20
23
|
let inline = true;
|
|
21
24
|
let name;
|
|
22
25
|
let effort;
|
|
@@ -37,13 +40,15 @@ function parseArgs(args) {
|
|
|
37
40
|
inline = true;
|
|
38
41
|
else if (argument === '--fullscreen')
|
|
39
42
|
inline = false;
|
|
40
|
-
else if (
|
|
41
|
-
|
|
43
|
+
else if (['--name', '-n', '--agent', '--image', '--effort', '--model', '--permission', '--provider',
|
|
44
|
+
'--session-id'].includes(argument ?? '')) {
|
|
42
45
|
const value = args[++index];
|
|
43
46
|
if (value === undefined || value === '')
|
|
44
47
|
return undefined;
|
|
45
|
-
if (argument === '--name')
|
|
48
|
+
if (argument === '--name' || argument === '-n')
|
|
46
49
|
name = value;
|
|
50
|
+
else if (argument === '--agent')
|
|
51
|
+
agentPreset = value;
|
|
47
52
|
else if (argument === '--image')
|
|
48
53
|
images.push(value);
|
|
49
54
|
else if (argument === '--effort')
|
|
@@ -52,6 +57,8 @@ function parseArgs(args) {
|
|
|
52
57
|
model = value;
|
|
53
58
|
else if (argument === '--permission')
|
|
54
59
|
permission = value;
|
|
60
|
+
else if (argument === '--session-id')
|
|
61
|
+
sessionId = value;
|
|
55
62
|
else
|
|
56
63
|
provider = value;
|
|
57
64
|
}
|
|
@@ -68,6 +75,8 @@ function parseArgs(args) {
|
|
|
68
75
|
resumeAll = true;
|
|
69
76
|
else if (argument === '--continue' || argument === '-c')
|
|
70
77
|
useContinue = true;
|
|
78
|
+
else if (argument === '--fork-session')
|
|
79
|
+
forkSession = true;
|
|
71
80
|
else if (argument === '--yolo' || argument === '--dangerously-skip-permissions') {
|
|
72
81
|
permission = 'danger-full-access';
|
|
73
82
|
}
|
|
@@ -79,12 +88,15 @@ function parseArgs(args) {
|
|
|
79
88
|
if (resume !== undefined && (resumePicker || useContinue) || resumePicker && useContinue
|
|
80
89
|
|| name !== undefined && (resume !== undefined || resumePicker || useContinue)
|
|
81
90
|
|| resumeAll && resume === undefined && !resumePicker || resumeAll && useContinue
|
|
82
|
-
|| provider !== undefined && model === undefined
|
|
91
|
+
|| provider !== undefined && model === undefined
|
|
92
|
+
|| sessionId !== undefined && (!isSessionId(sessionId) || resume !== undefined || resumePicker || useContinue)
|
|
93
|
+
|| agentPreset !== undefined && (resume !== undefined || resumePicker || useContinue)
|
|
94
|
+
|| forkSession && resume === undefined && !resumePicker && !useContinue)
|
|
83
95
|
return undefined;
|
|
84
96
|
return {
|
|
85
97
|
accessible,
|
|
86
98
|
continue: useContinue,
|
|
87
|
-
inline,
|
|
99
|
+
...(agentPreset === undefined ? {} : { agentPreset }), forkSession, inline,
|
|
88
100
|
resumeAll,
|
|
89
101
|
resumePicker,
|
|
90
102
|
...(images.length === 0 ? {} : { images }),
|
|
@@ -94,7 +106,7 @@ function parseArgs(args) {
|
|
|
94
106
|
...(permission === undefined ? {} : { permission }),
|
|
95
107
|
...(prompt.length === 0 ? {} : { prompt: prompt.join(' ') }),
|
|
96
108
|
...(provider === undefined ? {} : { provider }),
|
|
97
|
-
...(resume === undefined ? {} : { resume }),
|
|
109
|
+
...(resume === undefined ? {} : { resume }), ...(sessionId === undefined ? {} : { sessionId }),
|
|
98
110
|
};
|
|
99
111
|
}
|
|
100
112
|
function packageVersion(path) {
|
|
@@ -154,6 +166,11 @@ export function apply(ctx) {
|
|
|
154
166
|
throw new Error('dashi: the launcher must provide ctx.appExit, ctx.appReady, and ctx.cmdlineArgs');
|
|
155
167
|
}
|
|
156
168
|
const args = cmdline.get();
|
|
169
|
+
if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
|
|
170
|
+
process.stdout.write(`${VERSION_LINE}\n\nUsage: dashi [options] [prompt]\n\n${FLAG_HELP.join('\n')}\n`);
|
|
171
|
+
exit(0);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
157
174
|
if (args[0] === 'sessions') {
|
|
158
175
|
const options = parseSessionListArgs(args);
|
|
159
176
|
if (options === undefined) {
|
package/lib/input.js
CHANGED
|
@@ -102,6 +102,8 @@ export function installInput(tui, bindings) {
|
|
|
102
102
|
bindings.dispatch({ type: 'overlay-move', offset: -1 });
|
|
103
103
|
else if (matchesKey(data, 'down'))
|
|
104
104
|
bindings.dispatch({ type: 'overlay-move', offset: 1 });
|
|
105
|
+
else if (matchesKey(data, 'ctrl+o'))
|
|
106
|
+
bindings.dispatch({ type: 'toggle-tool-mode' });
|
|
105
107
|
else if (matchesKey(data, 'enter'))
|
|
106
108
|
bindings.dispatch({ type: 'overlay-submit', execute: true });
|
|
107
109
|
else if (matchesKey(data, 'tab'))
|
package/lib/memory.d.ts
ADDED
package/lib/memory.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
|
|
3
|
+
export function memoryOverlay(agent) {
|
|
4
|
+
const cwd = agent.session.header.cwd ?? process.cwd();
|
|
5
|
+
let root = cwd;
|
|
6
|
+
const active = new Map();
|
|
7
|
+
for (const seq of agent.session.surface.nodes) {
|
|
8
|
+
const event = agent.session.eventAt(seq);
|
|
9
|
+
if (event?.type !== 'user/message' || event.data.source.kind !== 'agent-instructions')
|
|
10
|
+
continue;
|
|
11
|
+
const source = event.data.source, identity = source.baselineIdentity === undefined
|
|
12
|
+
? undefined : JSON.parse(source.baselineIdentity);
|
|
13
|
+
if (typeof identity?.projectRoot === 'string')
|
|
14
|
+
root = resolve(cwd, identity.projectRoot);
|
|
15
|
+
for (const change of source.changes) {
|
|
16
|
+
if (change.action === 'remove')
|
|
17
|
+
active.delete(change.scope);
|
|
18
|
+
else
|
|
19
|
+
active.set(change.scope, { path: change.path, scope: change.scope.split('\0')[0] ?? change.scope });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { cursor: 0, kind: 'list', options: [...active.values()].map(item => {
|
|
23
|
+
const homePath = /^(?:~\/\.dsh|\$DSH_HOME)\/(.*)$/u.exec(item.path)?.[1];
|
|
24
|
+
return { detail: `scope ${item.scope}`, label: item.path,
|
|
25
|
+
value: { kind: 'open-file', path: resolve(homePath === undefined ? root : resolveDshHome(), homePath ?? item.path) } };
|
|
26
|
+
}), purpose: 'memory', title: 'Memory' };
|
|
27
|
+
}
|
package/lib/renderer.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type CopyResult = 'ok' | 'too-large' | 'unsupported';
|
|
|
6
6
|
export interface Renderer extends TerminalSession {
|
|
7
7
|
bell(): void;
|
|
8
8
|
copy(text: string): CopyResult;
|
|
9
|
+
discardSecretComposer(): void;
|
|
9
10
|
materializedCells(): number;
|
|
10
11
|
setComposer(text: string, cursor?: ComposerCursor): void;
|
|
11
12
|
}
|
package/lib/renderer.js
CHANGED
|
@@ -14,6 +14,7 @@ const OVERSCAN_CELLS = 8;
|
|
|
14
14
|
const THEME_GENERATION = 1;
|
|
15
15
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
16
16
|
const SPINNER_INTERVAL_MS = 80;
|
|
17
|
+
const MASK_TOKEN = new RegExp(String.raw `\u001B(?:\[[0-?]*[ -/]*[@-~]|[_P^X][^\u0007]*(?:\u0007|\u001B\\))|[^\s]`, 'gu');
|
|
17
18
|
export function supportsOsc52(terminalType) {
|
|
18
19
|
return terminalType?.toLocaleLowerCase() !== 'dumb';
|
|
19
20
|
}
|
|
@@ -160,14 +161,22 @@ export function createRenderer(options) {
|
|
|
160
161
|
const tui = options.inline
|
|
161
162
|
? new TuiMainScreen(terminal, true)
|
|
162
163
|
: new TuiAltScreen(terminal, true, undefined, { mouse: true });
|
|
163
|
-
|
|
164
|
+
let editor = new Editor(tui, theme(), { paddingX: 1 });
|
|
164
165
|
editor.disableSubmit = true;
|
|
165
166
|
editor.onChange = text => {
|
|
167
|
+
const decision = options.readState().decisions[0];
|
|
166
168
|
const caret = caretOffset(text, editor.getCursor());
|
|
167
169
|
options.dispatch({
|
|
168
|
-
type: 'composer-changed', text, caret, completion: hasCompletionTrigger(text, caret),
|
|
170
|
+
type: 'composer-changed', text: decision?.kind === 'question' && decision.questions[decision.index]?.secret === true ? editor.getExpandedText() : text, caret, completion: hasCompletionTrigger(text, caret),
|
|
169
171
|
});
|
|
170
172
|
};
|
|
173
|
+
const replaceEditor = () => {
|
|
174
|
+
const discarded = editor;
|
|
175
|
+
editor = new Editor(tui, theme(), { paddingX: 1 });
|
|
176
|
+
editor.disableSubmit = true;
|
|
177
|
+
editor.onChange = discarded.onChange;
|
|
178
|
+
tui.setFocus(editor);
|
|
179
|
+
};
|
|
171
180
|
const lineCache = new Map();
|
|
172
181
|
let animationTimer;
|
|
173
182
|
const cellLines = (cell, width, toolMode, now) => {
|
|
@@ -203,14 +212,18 @@ export function createRenderer(options) {
|
|
|
203
212
|
return [`${RED}Invalid empty question batch${RESET}`];
|
|
204
213
|
const planReview = question.intent?.kind === 'plan-review';
|
|
205
214
|
const heading = planReview ? 'Plan review' : question.header ?? `Question ${String(decision.index + 1)}/${String(decision.questions.length)}`;
|
|
215
|
+
const customAllowed = question.allowCustom !== false;
|
|
206
216
|
const selected = question.options[decision.cursor]?.label ?? 'Other (type an answer)';
|
|
207
|
-
const announced = announcement(plain(heading), question.options.length +
|
|
217
|
+
const announced = announcement(plain(heading), question.options.length + Number(customAllowed), selected, decision.cursor);
|
|
208
218
|
const prompt = wrapTextWithAnsi(`${BOLD_CYAN}${announced}${RESET}\n${plain(question.question, true)}`, width);
|
|
209
219
|
const choiceRows = question.options.map((option, index) => optionLine(option.description === undefined ? option.label : `${option.label} — ${option.description}`, index + 1, decision.cursor === index, decision.selected.includes(option.label)));
|
|
210
|
-
|
|
220
|
+
if (customAllowed)
|
|
221
|
+
choiceRows.push(optionLine('Other (type an answer)', question.options.length + 1, decision.cursor === question.options.length, false));
|
|
211
222
|
const detailLimit = planReview ? Math.max(3, rows - prompt.length - choiceRows.length - 5) : 3;
|
|
212
223
|
const detail = question.detail === undefined ? [] : wrapTextWithAnsi(`${DIM}${plain(question.detail, true)}${RESET}`, width).slice(0, detailLimit);
|
|
213
|
-
const
|
|
224
|
+
const editorLines = decision.cursor === question.options.length ? editor.render(width) : [];
|
|
225
|
+
const custom = question.secret === true
|
|
226
|
+
? editorLines.map((value, index) => index === 0 || index === editorLines.length - 1 ? value : value.replace(MASK_TOKEN, token => token.startsWith('\u001B') ? token : '•')) : editorLines;
|
|
214
227
|
const rendered = [...prompt, ...detail, ...choiceRows, ...custom];
|
|
215
228
|
return rows < 12 ? rendered.slice(0, Math.max(2, rows - 4)) : rendered;
|
|
216
229
|
};
|
|
@@ -268,9 +281,11 @@ export function createRenderer(options) {
|
|
|
268
281
|
if (overlay.kind === 'details')
|
|
269
282
|
return detailsPanel(overlay, state, width, rows);
|
|
270
283
|
if (overlay.kind === 'info') {
|
|
284
|
+
const cards = overlay.cells?.flatMap(cell => wrapTextWithAnsi(cellText(cell, state.toolMode, accessible), width)) ?? [];
|
|
271
285
|
return [
|
|
272
|
-
`${BOLD_CYAN}${announcement(plain(overlay.title), overlay.lines.length)}${RESET}`,
|
|
286
|
+
`${BOLD_CYAN}${announcement(plain(overlay.title), overlay.lines.length + (overlay.cells?.length ?? 0))}${RESET}`,
|
|
273
287
|
...overlay.lines.flatMap(value => wrapTextWithAnsi(plain(value, true), width)),
|
|
288
|
+
...cards,
|
|
274
289
|
`${DIM}Enter or Esc to close${RESET}`,
|
|
275
290
|
].slice(0, Math.max(2, rows - 3));
|
|
276
291
|
}
|
|
@@ -404,6 +419,7 @@ export function createRenderer(options) {
|
|
|
404
419
|
terminal.write(`\u001B]52;c;${Buffer.from(safe).toString('base64')}\u0007`);
|
|
405
420
|
return 'ok';
|
|
406
421
|
},
|
|
422
|
+
discardSecretComposer: replaceEditor,
|
|
407
423
|
drainInput: () => terminal.drainInput(),
|
|
408
424
|
materializedCells: () => materializedCount,
|
|
409
425
|
render: force => { tui.renderNow(force); },
|
package/lib/session-runtime.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import type { ActivatableOverlayValue, DecisionAnswer, DraftAttachment, OverlayOption, PendingDecision, RootView, SendMode, TerminalCell, UiAction } from './state.js';
|
|
3
3
|
import type { TuiRoot } from './tui-root.js';
|
|
4
4
|
export interface RootLaunchOptions {
|
|
5
|
+
readonly agentPreset?: string;
|
|
5
6
|
readonly continue: boolean;
|
|
6
7
|
readonly cwd: string;
|
|
7
8
|
readonly effort?: string;
|
|
9
|
+
readonly forkSession: boolean;
|
|
8
10
|
readonly name?: string;
|
|
9
11
|
readonly images?: readonly string[];
|
|
10
12
|
readonly model?: string;
|
|
@@ -12,6 +14,7 @@ export interface RootLaunchOptions {
|
|
|
12
14
|
readonly prompt?: string;
|
|
13
15
|
readonly provider?: string;
|
|
14
16
|
readonly resume?: string;
|
|
17
|
+
readonly sessionId?: string;
|
|
15
18
|
}
|
|
16
19
|
export interface SessionRuntime {
|
|
17
20
|
readonly initialAttachments: readonly DraftAttachment[];
|
package/lib/session-runtime.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { AuthorizationDeclinedError } from '@deepseek-ai/dsh-authorization';
|
|
3
|
+
import { parseCredentialKey } from '@deepseek-ai/dsh-credentials';
|
|
1
4
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
2
5
|
import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
|
|
3
|
-
import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, statusOverlay, } from './catalogs.js';
|
|
6
|
+
import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, skillOverlay, statusOverlay, } from './catalogs.js';
|
|
7
|
+
import { diffOverlay } from './diff-view.js';
|
|
4
8
|
import { eventsFromRecords } from './history-records.js';
|
|
5
9
|
import { admitDraftImages, encodeDraftImages, readDraftImages } from './image-input.js';
|
|
6
10
|
import { contextPercent, jobViews, subagentViews } from './presentation.js';
|
|
7
11
|
import { humanPrompt, humanPrompts, inheritedTurn, rewindActionOverlay, rewindOverlay, } from './rewind.js';
|
|
8
12
|
import { createToolPresenter } from './tool-presentation.js';
|
|
9
|
-
import { runHumanShell } from './shell-command.js';
|
|
13
|
+
import { quoteShellWord, runHumanShell } from './shell-command.js';
|
|
10
14
|
import { markdownTranscript } from './session-export.js';
|
|
11
15
|
import { isSessionId, sessionMatches, sessionNotFound } from './session-list.js';
|
|
16
|
+
import { memoryOverlay } from './memory.js';
|
|
12
17
|
import { foldCells, pendingShellCells } from './transcript.js';
|
|
18
|
+
function starterAgents(cwd) {
|
|
19
|
+
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
|
+
}
|
|
13
21
|
function detail(error) {
|
|
14
22
|
return error instanceof Error ? error.message : String(error);
|
|
15
23
|
}
|
|
@@ -71,6 +79,17 @@ async function firstControl(iterator) {
|
|
|
71
79
|
function usage(raw, expected) {
|
|
72
80
|
return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
|
|
73
81
|
}
|
|
82
|
+
function configValue(raw) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(raw);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return raw;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function configLayer(value) {
|
|
91
|
+
return value === undefined ? '—' : JSON.stringify(value);
|
|
92
|
+
}
|
|
74
93
|
/** Create or resume the exact current root and own its replaceable terminal binding. */
|
|
75
94
|
export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, decisions) {
|
|
76
95
|
const lifetime = new AbortController();
|
|
@@ -78,11 +97,13 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
78
97
|
let started = false;
|
|
79
98
|
let shutdown;
|
|
80
99
|
let decisionSequence = 0;
|
|
81
|
-
|
|
100
|
+
let initialId = options.resume === undefined
|
|
82
101
|
? options.continue
|
|
83
102
|
? continuation((await ctx.sessionController.list({}, lifetime.signal)).items, options.cwd)
|
|
84
|
-
: (await ctx.sessionController.create({ cwd: options.cwd })).sessionId
|
|
103
|
+
: (await ctx.sessionController.create({ cwd: options.cwd, ...(options.agentPreset === undefined ? {} : { agentPreset: options.agentPreset }), ...(options.sessionId === undefined ? {} : { sessionId: SessionId(options.sessionId) }) })).sessionId
|
|
85
104
|
: SessionId(options.resume);
|
|
105
|
+
if (options.forkSession)
|
|
106
|
+
initialId = (await ctx.sessionController.fork({ sessionId: initialId })).sessionId;
|
|
86
107
|
const prepare = async (sessionId, name) => {
|
|
87
108
|
const abort = new AbortController();
|
|
88
109
|
try {
|
|
@@ -178,6 +199,15 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
178
199
|
signal?.removeEventListener('abort', abortDecision);
|
|
179
200
|
});
|
|
180
201
|
};
|
|
202
|
+
const authorizationPrompt = (bound, prompt, detail) => decide({
|
|
203
|
+
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: [],
|
|
204
|
+
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' }],
|
|
205
|
+
}, prompt.signal).then(result => prompt.kind === 'select' ? prompt.options[prompt.options.findIndex(option => option.label === result.answers[0]?.selected[0])]?.id ?? ''
|
|
206
|
+
: result.answers[0]?.custom ?? '').catch(error => {
|
|
207
|
+
if (prompt.signal?.aborted !== true && error.code === 'ASK_ABORTED')
|
|
208
|
+
throw new AuthorizationDeclinedError();
|
|
209
|
+
throw error;
|
|
210
|
+
});
|
|
181
211
|
const transcriptCells = (bound) => [
|
|
182
212
|
...foldCells(bound.events, bound.presenter, { truncatedStart: bound.hasMore }),
|
|
183
213
|
...pendingShellCells(bound.agent.inbox.nextStep),
|
|
@@ -451,7 +481,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
451
481
|
if (nativePermission === undefined)
|
|
452
482
|
throw new Error('dashi: DSH supplied no /permission command');
|
|
453
483
|
const ensureCurrent = (invocation) => current().agent === invocation.agent ? undefined : { kind: 'error', text: 'this root is no longer current' };
|
|
454
|
-
|
|
484
|
+
const definitions = [
|
|
455
485
|
{
|
|
456
486
|
name: 'help', description: 'Show keyboard and command help',
|
|
457
487
|
handler: (invocation) => {
|
|
@@ -491,6 +521,133 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
491
521
|
return { kind: 'success' };
|
|
492
522
|
},
|
|
493
523
|
},
|
|
524
|
+
{
|
|
525
|
+
name: 'memory', description: 'Open an instruction file loaded by DSH',
|
|
526
|
+
handler: (invocation) => {
|
|
527
|
+
const invalid = ensureCurrent(invocation) ?? usage(invocation.rawInput, '/memory');
|
|
528
|
+
if (invalid !== undefined)
|
|
529
|
+
return invalid;
|
|
530
|
+
dispatch({ type: 'open-overlay', overlay: memoryOverlay(bound.agent) });
|
|
531
|
+
return { kind: 'success' };
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: 'skills', description: 'List skills resolved for this session', input: { hint: '[TEXT]' },
|
|
536
|
+
handler: async (invocation) => {
|
|
537
|
+
const stale = ensureCurrent(invocation);
|
|
538
|
+
if (stale !== undefined)
|
|
539
|
+
return stale;
|
|
540
|
+
const overlay = await skillOverlay(ctx, bound.agent, invocation.rawInput, invocation.signal);
|
|
541
|
+
const replaced = ensureCurrent(invocation);
|
|
542
|
+
if (replaced !== undefined)
|
|
543
|
+
return replaced;
|
|
544
|
+
dispatch({ type: 'open-overlay', overlay });
|
|
545
|
+
return { kind: 'success' };
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: 'config', description: 'Show or update native DSH settings', input: { hint: '[NAMESPACE KEY=VALUE]' },
|
|
550
|
+
handler: async (invocation) => {
|
|
551
|
+
const stale = ensureCurrent(invocation);
|
|
552
|
+
if (stale !== undefined)
|
|
553
|
+
return stale;
|
|
554
|
+
const raw = invocation.rawInput.trim();
|
|
555
|
+
if (raw === '')
|
|
556
|
+
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') };
|
|
557
|
+
const match = /^(\S+)\s+([^\s=]+)=(.*)$/su.exec(raw);
|
|
558
|
+
if (match?.[1] === undefined || match[2] === undefined || match[3] === undefined) {
|
|
559
|
+
return { kind: 'error', text: 'usage: /config [NAMESPACE KEY=VALUE]' };
|
|
560
|
+
}
|
|
561
|
+
const value = configValue(match[3]);
|
|
562
|
+
if (match[2].includes('.'))
|
|
563
|
+
await ctx.settings.mutate(match[1], [{ op: 'set', path: match[2].split('.'), value }]);
|
|
564
|
+
else
|
|
565
|
+
await ctx.settings.update(match[1], { [match[2]]: value });
|
|
566
|
+
return { kind: 'success', text: `updated ${match[1]}.${match[2]}` };
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
{ name: 'login', description: 'List or start provider sign-in', input: { hint: '[KEY [METHOD]]' },
|
|
570
|
+
handler: async (invocation) => {
|
|
571
|
+
const stale = ensureCurrent(invocation);
|
|
572
|
+
if (stale !== undefined)
|
|
573
|
+
return stale;
|
|
574
|
+
const values = invocation.rawInput.trim().split(/\s+/u).filter(Boolean);
|
|
575
|
+
if (values.length === 0)
|
|
576
|
+
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' };
|
|
577
|
+
if (values.length > 2)
|
|
578
|
+
return { kind: 'error', text: 'usage: /login [KEY [METHOD]]' };
|
|
579
|
+
const entry = ctx.authorization.describe(parseCredentialKey(values[0]));
|
|
580
|
+
if (entry === undefined)
|
|
581
|
+
return { kind: 'error', text: `no authorization flow for ${values[0]}` };
|
|
582
|
+
const notices = [];
|
|
583
|
+
const outcome = await ctx.authorization.begin({ key: entry.key, ...(values[1] === undefined ? {} : { method: values[1] }), signal: invocation.signal,
|
|
584
|
+
interaction: { notify: notice => {
|
|
585
|
+
notices.push(notice.message, ...(notice.url === undefined ? [] : [notice.url]), ...(notice.code === undefined ? [] : [`Code: ${notice.code}`]));
|
|
586
|
+
dispatch({ type: 'open-overlay', overlay: { kind: 'info', title: 'Login', lines: notices } });
|
|
587
|
+
}, prompt: prompt => authorizationPrompt(bound, prompt, notices.join('\n')) },
|
|
588
|
+
}).finally(() => { dispatch({ type: 'overlay-close' }); });
|
|
589
|
+
return { kind: 'success', text: outcome.status === 'authorized' ? `signed in to ${entry.label}` : 'login cancelled' };
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
{ name: 'logout', description: 'List or forget provider credentials', input: { hint: '[KEY]' },
|
|
593
|
+
handler: async (invocation) => {
|
|
594
|
+
const stale = ensureCurrent(invocation);
|
|
595
|
+
if (stale !== undefined)
|
|
596
|
+
return stale;
|
|
597
|
+
const raw = invocation.rawInput.trim();
|
|
598
|
+
if (raw === '')
|
|
599
|
+
return { kind: 'success', text: (await ctx.credentials.listRecords()).map(record => `${String(record.key)} · ${record.kind}`).join('\n') || 'no stored credentials' };
|
|
600
|
+
if (/\s/u.test(raw))
|
|
601
|
+
return { kind: 'error', text: 'usage: /logout [KEY]' };
|
|
602
|
+
await ctx.credentials.deleteRecord(parseCredentialKey(raw));
|
|
603
|
+
return { kind: 'success', text: `signed out ${raw}` };
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: 'diff', description: 'Show working tree or last-turn file changes', input: { hint: '[turn]' }, handler: async (invocation) => {
|
|
608
|
+
const invalid = ensureCurrent(invocation);
|
|
609
|
+
if (invalid !== undefined)
|
|
610
|
+
return invalid;
|
|
611
|
+
const mode = invocation.rawInput.trim();
|
|
612
|
+
if (mode !== '' && mode !== 'turn')
|
|
613
|
+
return { kind: 'error', text: 'usage: /diff [turn]' };
|
|
614
|
+
dispatch({ type: 'open-overlay', overlay: await diffOverlay(ctx, bound.agent, bound.presenter, mode, lifetime.signal) });
|
|
615
|
+
return { kind: 'success' };
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: 'plugins', description: 'List the running profile plugins',
|
|
620
|
+
handler: async (invocation) => {
|
|
621
|
+
const stale = ensureCurrent(invocation);
|
|
622
|
+
if (stale !== undefined)
|
|
623
|
+
return stale;
|
|
624
|
+
if (invocation.rawInput.trim() !== '')
|
|
625
|
+
return { kind: 'error', text: 'usage: /plugins' };
|
|
626
|
+
const servers = new Map();
|
|
627
|
+
for (const entry of ctx.loader.entries())
|
|
628
|
+
if (entry.options.name === '@deepseek-ai/dsh-mcp-client'
|
|
629
|
+
&& typeof entry.options.config?.serverName === 'string')
|
|
630
|
+
servers.set(entry.id, entry.options.config.serverName);
|
|
631
|
+
const snapshot = await ctx.get('pluginInventory').list();
|
|
632
|
+
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') };
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: 'plugin', description: 'Run DSH profile plugin management', input: { hint: 'ARGS' },
|
|
637
|
+
handler: async (invocation) => {
|
|
638
|
+
const stale = ensureCurrent(invocation);
|
|
639
|
+
if (stale !== undefined)
|
|
640
|
+
return stale;
|
|
641
|
+
if (invocation.rawInput.trim() === '')
|
|
642
|
+
return { kind: 'error', text: 'usage: /plugin ARGS' };
|
|
643
|
+
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, 'changes load on the next launch; exit and run dashi again'));
|
|
645
|
+
bound.agent.inject(message);
|
|
646
|
+
await ctx.sessions.flush(bound.agent.session);
|
|
647
|
+
publish(bound);
|
|
648
|
+
return result.exitCode === 0 ? { kind: 'success' } : { kind: 'error', text: `dsh plugin exited ${String(result.exitCode ?? result.signal ?? 'without status')}` };
|
|
649
|
+
},
|
|
650
|
+
},
|
|
494
651
|
{
|
|
495
652
|
name: 'new', description: 'Start a new session', input: { hint: '[--name TITLE]' },
|
|
496
653
|
handler: async (invocation) => {
|
|
@@ -628,15 +785,17 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
628
785
|
},
|
|
629
786
|
},
|
|
630
787
|
{
|
|
631
|
-
name: 'copy', description: 'Copy
|
|
788
|
+
name: 'copy', description: 'Copy an assistant response or code block', input: { hint: '[N|code]' },
|
|
632
789
|
handler: (invocation) => {
|
|
633
790
|
const stale = ensureCurrent(invocation);
|
|
634
791
|
if (stale !== undefined)
|
|
635
792
|
return stale;
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
793
|
+
const raw = invocation.rawInput.trim();
|
|
794
|
+
const selection = raw === '' ? 1 : raw === 'code' ? 'code' : Number(raw);
|
|
795
|
+
if (raw !== '' && selection !== 'code' && (!/^[1-9]\d*$/u.test(raw) || !Number.isSafeInteger(selection))) {
|
|
796
|
+
return { kind: 'error', text: 'usage: /copy [N|code]' };
|
|
797
|
+
}
|
|
798
|
+
dispatch({ selection, type: 'copy-assistant' });
|
|
640
799
|
return { kind: 'success' };
|
|
641
800
|
},
|
|
642
801
|
},
|
|
@@ -669,6 +828,32 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
669
828
|
return { kind: 'success' };
|
|
670
829
|
},
|
|
671
830
|
},
|
|
831
|
+
{
|
|
832
|
+
name: 'init', description: 'Create a starter AGENTS.md',
|
|
833
|
+
handler: async (invocation) => {
|
|
834
|
+
const stale = ensureCurrent(invocation);
|
|
835
|
+
if (stale !== undefined)
|
|
836
|
+
return stale;
|
|
837
|
+
const invalid = usage(invocation.rawInput, '/init');
|
|
838
|
+
if (invalid !== undefined)
|
|
839
|
+
return invalid;
|
|
840
|
+
const target = await ctx.fs.resolve('AGENTS.md', { cwd: bound.root.cwd, signal: lifetime.signal });
|
|
841
|
+
await ctx.fs.writeText(target, starterAgents(bound.root.cwd), { kind: 'createIfAbsent' }, lifetime.signal, ctx.sandboxPolicy.resolve({ session: bound.agent.session }));
|
|
842
|
+
return { kind: 'success', text: `created ${ctx.fs.processPath(target)}` };
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: 'effort', description: 'Select the reasoning effort', input: { hint: 'LEVEL' },
|
|
847
|
+
handler: async (invocation) => {
|
|
848
|
+
const invalid = ensureCurrent(invocation);
|
|
849
|
+
const effort = invocation.rawInput.trim();
|
|
850
|
+
if (invalid !== undefined || effort === '')
|
|
851
|
+
return invalid ?? { kind: 'error', text: 'usage: /effort LEVEL' };
|
|
852
|
+
const selected = currentModel(ctx, bound.agent) ?? (await ctx.sessionController.modelCatalog()).default;
|
|
853
|
+
await activate({ kind: 'model', effort, model: selected.model, provider: selected.provider });
|
|
854
|
+
return { kind: 'success' };
|
|
855
|
+
},
|
|
856
|
+
},
|
|
672
857
|
{
|
|
673
858
|
...nativePermission,
|
|
674
859
|
handler: async (invocation) => {
|
|
@@ -709,6 +894,8 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
709
894
|
},
|
|
710
895
|
},
|
|
711
896
|
];
|
|
897
|
+
return [...definitions, ...[['quit', 'exit'], ['reset', 'clear'], ['continue', 'resume'], ['branch', 'fork']]
|
|
898
|
+
.map(([name, target]) => ({ ...definitions.find(definition => definition.name === target), name }))];
|
|
712
899
|
};
|
|
713
900
|
async function install(bound) {
|
|
714
901
|
const id = bound.root.id;
|
|
@@ -1027,7 +1214,8 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
1027
1214
|
if (attachments.length > 0)
|
|
1028
1215
|
throw new Error('!command does not accept image attachments');
|
|
1029
1216
|
await bound.agent.runMaintenance(async (signal) => {
|
|
1030
|
-
|
|
1217
|
+
const [message] = await runHumanShell(ctx, bound.agent, command, signal);
|
|
1218
|
+
bound.agent.inject(message);
|
|
1031
1219
|
await ctx.sessions.flush(bound.agent.session);
|
|
1032
1220
|
});
|
|
1033
1221
|
publish(bound);
|
package/lib/shell-command.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ import type { ShellRunResult } from '@deepseek-ai/dsh-shell';
|
|
|
5
5
|
export declare const HUMAN_SHELL_TIMEOUT_MS = 30000;
|
|
6
6
|
export declare const HUMAN_SHELL_STREAM_BYTES: number;
|
|
7
7
|
export declare function shellResultText(command: string, result: ShellRunResult): string;
|
|
8
|
+
export declare function quoteShellWord(value: string): string;
|
|
8
9
|
/** Run an explicit human command through DSH and identify its next-step context. */
|
|
9
|
-
export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal): Promise<UserMessage>;
|
|
10
|
+
export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal, success?: string): Promise<readonly [UserMessage, ShellRunResult]>;
|
package/lib/shell-command.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
|
|
2
3
|
export const HUMAN_SHELL_TIMEOUT_MS = 30_000;
|
|
3
4
|
export const HUMAN_SHELL_STREAM_BYTES = 32 * 1024;
|
|
4
5
|
function tail(output) {
|
|
@@ -42,21 +43,23 @@ export function shellResultText(command, result) {
|
|
|
42
43
|
}
|
|
43
44
|
return parts.join('\n');
|
|
44
45
|
}
|
|
46
|
+
export function quoteShellWord(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
|
|
45
47
|
/** Run an explicit human command through DSH and identify its next-step context. */
|
|
46
|
-
export async function runHumanShell(ctx, agent, command, signal) {
|
|
48
|
+
export async function runHumanShell(ctx, agent, command, signal, success) {
|
|
47
49
|
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
48
50
|
command,
|
|
51
|
+
env: { DSH_HOME: resolveDshHome() },
|
|
49
52
|
sandboxPolicy: ctx.sandboxPolicy.resolve({ session: agent.session }),
|
|
50
53
|
signal,
|
|
51
54
|
stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES,
|
|
52
55
|
timeoutMs: HUMAN_SHELL_TIMEOUT_MS,
|
|
53
56
|
workdir: agent.session.header.cwd,
|
|
54
57
|
}));
|
|
55
|
-
return createUserMessage({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
return [createUserMessage({
|
|
59
|
+
content: [{ type: 'text', text: shellResultText(command, result) + (result.exitCode === 0 && success ? `\n${success}` : '') }],
|
|
60
|
+
source: {
|
|
61
|
+
form: 'notice', kind: 'plugin', plugin: 'dashi',
|
|
62
|
+
summary: boundContextSummary(`Shell: ${command.replace(/\s+/gu, ' ').trim()}`),
|
|
63
|
+
},
|
|
64
|
+
}), result];
|
|
62
65
|
}
|
package/lib/state.d.ts
CHANGED
|
@@ -87,6 +87,7 @@ export interface QuestionOption {
|
|
|
87
87
|
readonly label: string;
|
|
88
88
|
}
|
|
89
89
|
export interface QuestionItem {
|
|
90
|
+
readonly allowCustom?: boolean;
|
|
90
91
|
readonly detail?: string;
|
|
91
92
|
readonly header?: string;
|
|
92
93
|
readonly id: string;
|
|
@@ -97,6 +98,7 @@ export interface QuestionItem {
|
|
|
97
98
|
readonly multiSelect: boolean;
|
|
98
99
|
readonly options: readonly QuestionOption[];
|
|
99
100
|
readonly question: string;
|
|
101
|
+
readonly secret?: boolean;
|
|
100
102
|
}
|
|
101
103
|
export interface QuestionAnswer {
|
|
102
104
|
readonly custom?: string;
|
|
@@ -129,6 +131,9 @@ export type OverlayValue = {
|
|
|
129
131
|
readonly path: string;
|
|
130
132
|
readonly source: string;
|
|
131
133
|
readonly text: string;
|
|
134
|
+
} | {
|
|
135
|
+
readonly kind: 'copy';
|
|
136
|
+
readonly text: string;
|
|
132
137
|
} | {
|
|
133
138
|
readonly kind: 'insert';
|
|
134
139
|
readonly text: string;
|
|
@@ -137,6 +142,9 @@ export type OverlayValue = {
|
|
|
137
142
|
readonly effort?: string;
|
|
138
143
|
readonly model: string;
|
|
139
144
|
readonly provider: string;
|
|
145
|
+
} | {
|
|
146
|
+
readonly kind: 'open-file';
|
|
147
|
+
readonly path: string;
|
|
140
148
|
} | {
|
|
141
149
|
readonly interrupt?: true;
|
|
142
150
|
readonly kind: 'fork';
|
|
@@ -179,6 +187,8 @@ export type OverlayValue = {
|
|
|
179
187
|
};
|
|
180
188
|
export type ActivatableOverlayValue = Exclude<OverlayValue, {
|
|
181
189
|
kind: 'attach';
|
|
190
|
+
} | {
|
|
191
|
+
kind: 'copy';
|
|
182
192
|
} | {
|
|
183
193
|
kind: 'insert';
|
|
184
194
|
}>;
|
|
@@ -197,9 +207,10 @@ export type Overlay = {
|
|
|
197
207
|
readonly kind: 'list';
|
|
198
208
|
readonly notice?: string;
|
|
199
209
|
readonly options: readonly OverlayOption[];
|
|
200
|
-
readonly purpose: 'agents' | 'completion' | 'model' | 'permission' | 'resume' | 'rewind' | 'rewind-action' | 'search';
|
|
210
|
+
readonly purpose: 'agents' | 'completion' | 'copy' | 'memory' | 'model' | 'permission' | 'resume' | 'rewind' | 'rewind-action' | 'search' | 'skills';
|
|
201
211
|
readonly title: string;
|
|
202
212
|
} | {
|
|
213
|
+
readonly cells?: readonly TerminalCell[];
|
|
203
214
|
readonly kind: 'info';
|
|
204
215
|
readonly lines: readonly string[];
|
|
205
216
|
readonly title: string;
|
|
@@ -424,7 +435,8 @@ export type UiAction = {
|
|
|
424
435
|
readonly hasMore?: boolean;
|
|
425
436
|
readonly rootId?: string;
|
|
426
437
|
} | {
|
|
427
|
-
readonly
|
|
438
|
+
readonly selection: number | 'code';
|
|
439
|
+
readonly type: 'copy-assistant';
|
|
428
440
|
};
|
|
429
441
|
export type UiEffect = {
|
|
430
442
|
readonly type: 'activate-overlay';
|
package/lib/state.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FLAG_HELP, VERSION_LINE } from './help.js';
|
|
1
2
|
export function initialViewState(cwd, inline, root, cells = [], prompts = [], historyHasMore = false, attachments = []) {
|
|
2
3
|
return {
|
|
3
4
|
attachments, cells, composer: '', cwd, decisions: [], exitArmed: false, historyHasMore, inline,
|
|
@@ -24,13 +25,14 @@ function moveCursor(decision, cursor) {
|
|
|
24
25
|
const question = decision.questions[decision.index];
|
|
25
26
|
if (question === undefined)
|
|
26
27
|
return decision;
|
|
27
|
-
return { ...decision, cursor: Math.max(0, Math.min(question.options.length, cursor)) };
|
|
28
|
+
return { ...decision, cursor: Math.max(0, Math.min(question.options.length - (question.allowCustom === false ? 1 : 0), cursor)) };
|
|
28
29
|
}
|
|
29
30
|
function help() {
|
|
30
31
|
return {
|
|
31
32
|
kind: 'info',
|
|
32
33
|
title: 'Help',
|
|
33
34
|
lines: [
|
|
35
|
+
VERSION_LINE,
|
|
34
36
|
'Enter send · Ctrl+J newline · Ctrl+T steer/next turn',
|
|
35
37
|
'Esc interrupt · Ctrl+C clear/interrupt · Ctrl+C/D again within 2s to exit',
|
|
36
38
|
'/ and @ complete live · Tab insert · Shift+Tab permission · Ctrl+O cards',
|
|
@@ -39,8 +41,9 @@ function help() {
|
|
|
39
41
|
'Ctrl+B activity details for jobs and subagents',
|
|
40
42
|
'@ paths require DSH file-reference · Ctrl+V pastes an image · Backspace removes it',
|
|
41
43
|
'Ctrl+S stash · Ctrl+G editor · F1 help · Ctrl+L redraw · Ctrl+Z suspend',
|
|
42
|
-
'/new /clear /resume [--all] [NAME|UUID] /fork /rewind /rename /model /permission /agents /queue
|
|
43
|
-
'/
|
|
44
|
+
'/new /clear /reset /resume /continue [--all] [NAME|UUID] /fork /branch /rewind /rename /model /effort /permission /agents /queue /status /context',
|
|
45
|
+
'/init /memory /skills /config /login /logout /diff /plugins /plugin /tasks /history /export /copy /exit /quit',
|
|
46
|
+
...FLAG_HELP,
|
|
44
47
|
],
|
|
45
48
|
};
|
|
46
49
|
}
|
|
@@ -83,13 +86,24 @@ function refreshSearch(search, cells) {
|
|
|
83
86
|
const found = matches(cells, search.query);
|
|
84
87
|
return { ...search, cursor: Math.min(search.cursor, Math.max(0, found.length - 1)), matches: found };
|
|
85
88
|
}
|
|
86
|
-
function
|
|
89
|
+
function completedAssistant(cells, nth) {
|
|
90
|
+
let seen = 0;
|
|
87
91
|
for (let index = cells.length - 1; index >= 0; index--) {
|
|
88
92
|
const cell = cells[index];
|
|
89
|
-
if (cell?.kind === 'assistant' && cell.pending !== true)
|
|
93
|
+
if (cell?.kind === 'assistant' && cell.pending !== true && ++seen === nth)
|
|
90
94
|
return cell;
|
|
91
95
|
}
|
|
92
96
|
}
|
|
97
|
+
function codeBlockOptions(text) {
|
|
98
|
+
const fence = /(?:^|\r?\n)(`{3,}|~{3,})([^\r\n]*)\r?\n([\s\S]*?)\r?\n\1[ \t]*(?=\r?\n|$)/gu;
|
|
99
|
+
return [...text.matchAll(fence)].map(match => {
|
|
100
|
+
const code = match[3] ?? '';
|
|
101
|
+
return {
|
|
102
|
+
label: `${match[2]?.trim() || 'text'} · ${code.split(/\r?\n/u)[0] ?? ''}`,
|
|
103
|
+
value: { kind: 'copy', text: code },
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
93
107
|
export function reduce(state, action) {
|
|
94
108
|
const head = state.decisions[0];
|
|
95
109
|
switch (action.type) {
|
|
@@ -215,7 +229,7 @@ export function reduce(state, action) {
|
|
|
215
229
|
const question = head.questions[head.index];
|
|
216
230
|
if (question === undefined)
|
|
217
231
|
return [state, []];
|
|
218
|
-
const custom = head.cursor === question.options.length ? head.custom.trim() : undefined;
|
|
232
|
+
const custom = question.allowCustom !== false && head.cursor === question.options.length ? question.secret === true ? head.custom : head.custom.trim() : undefined;
|
|
219
233
|
if (custom !== undefined && custom === '')
|
|
220
234
|
return [state, []];
|
|
221
235
|
const option = question.options[head.cursor];
|
|
@@ -407,6 +421,10 @@ export function reduce(state, action) {
|
|
|
407
421
|
type: 'attach', path: option.value.path, source: option.value.source, text: option.value.text,
|
|
408
422
|
}, ...redraw()]];
|
|
409
423
|
}
|
|
424
|
+
if (option.value.kind === 'copy') {
|
|
425
|
+
const { overlay: _overlay, ...withoutOverlay } = state;
|
|
426
|
+
return [withoutOverlay, [{ type: 'copy', text: option.value.text }, ...redraw()]];
|
|
427
|
+
}
|
|
410
428
|
if (option.value.kind === 'permission' && option.danger === true) {
|
|
411
429
|
return [{ ...state, overlay: {
|
|
412
430
|
acceptLabel: 'Enable',
|
|
@@ -633,9 +651,15 @@ export function reduce(state, action) {
|
|
|
633
651
|
}),
|
|
634
652
|
}, redraw()];
|
|
635
653
|
}
|
|
636
|
-
case 'copy-
|
|
637
|
-
const cell =
|
|
638
|
-
|
|
654
|
+
case 'copy-assistant': {
|
|
655
|
+
const cell = completedAssistant(state.cells, action.selection === 'code' ? 1 : action.selection);
|
|
656
|
+
if (cell === undefined)
|
|
657
|
+
return [state, []];
|
|
658
|
+
if (action.selection !== 'code')
|
|
659
|
+
return [state, [{ type: 'copy', text: cell.text }]];
|
|
660
|
+
return [{ ...state, overlay: {
|
|
661
|
+
cursor: 0, kind: 'list', options: codeBlockOptions(cell.text), purpose: 'copy', title: 'Copy code block',
|
|
662
|
+
} }, redraw()];
|
|
639
663
|
}
|
|
640
664
|
}
|
|
641
665
|
}
|
package/lib/transcript.js
CHANGED
|
@@ -29,7 +29,8 @@ function shellCell(message, pending) {
|
|
|
29
29
|
function collapsedContextCell(message, key) {
|
|
30
30
|
const source = message.source;
|
|
31
31
|
const form = source.form;
|
|
32
|
-
if (source.kind !== '
|
|
32
|
+
if (source.kind !== 'agent-instructions'
|
|
33
|
+
&& (source.kind !== 'plugin' || !['instructions', 'catalog', 'snapshot', 'notice', 'recall'].includes(String(form)))) {
|
|
33
34
|
return undefined;
|
|
34
35
|
}
|
|
35
36
|
const text = messageText(message.content);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antst/dashi",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.9",
|
|
4
4
|
"description": "A terminal UI for DeepSeek Harness",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -37,15 +37,21 @@
|
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "4.0.2",
|
|
40
|
+
"@deepseek-ai/cordis-plugin-loader": "1.0.3",
|
|
40
41
|
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
|
|
42
|
+
"@deepseek-ai/dsh-agent-instructions": "0.1.2-rc.1",
|
|
41
43
|
"@deepseek-ai/dsh-agent-presets": "0.1.2-rc.1",
|
|
42
44
|
"@deepseek-ai/dsh-api-session-controller": "0.1.2-rc.1",
|
|
43
45
|
"@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-authorization": "0.1.2-rc.1",
|
|
44
47
|
"@deepseek-ai/dsh-cmdline": "0.1.2-rc.1",
|
|
45
48
|
"@deepseek-ai/dsh-commands": "0.1.2-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
|
|
46
50
|
"@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
|
|
47
51
|
"@deepseek-ai/dsh-file-reference": "0.1.2-rc.1",
|
|
48
52
|
"@deepseek-ai/dsh-fs": "0.1.2-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-home-paths": "0.1.2-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-host-plugin-inventory": "0.1.2-rc.1",
|
|
49
55
|
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
|
|
50
56
|
"@deepseek-ai/dsh-permission-presets": "0.1.2-rc.1",
|
|
51
57
|
"@deepseek-ai/dsh-plan-mode": "0.1.2-rc.1",
|
|
@@ -53,6 +59,7 @@
|
|
|
53
59
|
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
|
|
54
60
|
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
|
|
55
61
|
"@deepseek-ai/dsh-session-query": "0.1.2-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-settings": "0.1.2-rc.1",
|
|
56
63
|
"@deepseek-ai/dsh-shell": "0.1.2-rc.1",
|
|
57
64
|
"@deepseek-ai/dsh-skill": "0.1.2-rc.1",
|
|
58
65
|
"@deepseek-ai/dsh-subagent": "0.1.2-rc.1",
|