@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/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
dashi is a terminal UI for DeepSeek Harness (DSH). Install it as a bundle
|
|
4
4
|
via `@antst/dashi-app`, not as a standalone dependency:
|
|
5
5
|
|
|
6
|
-
Install
|
|
6
|
+
Install DSH 0.1.5-rc.2 or newer and start dashi:
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
pnpm install @deepseek-ai/dsh@0.1.
|
|
9
|
+
pnpm install @deepseek-ai/dsh@0.1.5-rc.2
|
|
10
10
|
dsh plugin --profile dashi add @antst/dashi-app
|
|
11
11
|
dsh --profile dashi
|
|
12
12
|
```
|
|
@@ -15,11 +15,11 @@ Npm is unsupported for prerelease DSH because it cannot constrain a scoped
|
|
|
15
15
|
package family to one prerelease.
|
|
16
16
|
When upgrading DSH, remove `node_modules` and the lockfile before installing so
|
|
17
17
|
pnpm cannot retain stale peer-only DSH packages.
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
A DSH profile must use one uniform DSH version. The minimum supported version
|
|
19
|
+
is `0.1.5-rc.2`; CI tests `0.1.5-rc.2`, `0.1.6-alpha.1`, and
|
|
20
|
+
`0.1.6-alpha.2` with exact, reproducible graphs.
|
|
20
21
|
|
|
21
|
-
Status: pre-release alpha. Interfaces and behavior may change between
|
|
22
|
-
alpha versions; the validated DSH version is `0.1.2-rc.1`.
|
|
22
|
+
Status: pre-release alpha. Interfaces and behavior may change between alpha versions.
|
|
23
23
|
|
|
24
24
|
Full documentation, source, and issue tracker:
|
|
25
25
|
https://forgejo.antst.net/ai/dashi
|
package/lib/application.d.ts
CHANGED
|
@@ -20,12 +20,14 @@ export interface TerminalShellOptions {
|
|
|
20
20
|
readonly initialHasMore?: boolean;
|
|
21
21
|
readonly initialPrompts?: ViewState['prompts'];
|
|
22
22
|
readonly initialRoot?: ViewState['root'];
|
|
23
|
+
readonly initialToolMode?: ViewState['toolMode'];
|
|
23
24
|
readonly interrupt?: () => void;
|
|
24
25
|
readonly inline: boolean;
|
|
25
26
|
readonly loadHistory?: (rootId: string) => Promise<void>;
|
|
26
27
|
readonly onReleased?: () => void;
|
|
27
28
|
readonly pasteImage?: () => Promise<DraftAttachment>;
|
|
28
29
|
readonly search?: (query: string, rootId: string) => Promise<void>;
|
|
30
|
+
readonly statusLine?: () => string | undefined;
|
|
29
31
|
readonly submit?: (text: string, mode: 'next-turn' | 'steer', attachments: readonly DraftAttachment[]) => Promise<void> | void;
|
|
30
32
|
readonly suspend?: () => Promise<void>;
|
|
31
33
|
readonly writeError?: (message: string) => void;
|
package/lib/application.js
CHANGED
|
@@ -16,7 +16,7 @@ function suspendProcess() {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
export function createTerminalShell(options) {
|
|
19
|
-
let state = initialViewState(options.cwd, options.inline, options.initialRoot, options.initialCells, options.initialPrompts, options.initialHasMore, options.initialAttachments);
|
|
19
|
+
let state = initialViewState(options.cwd, options.inline, options.initialRoot, options.initialCells, options.initialPrompts, options.initialHasMore, options.initialAttachments, options.initialToolMode);
|
|
20
20
|
let accepting = true;
|
|
21
21
|
let failed = false;
|
|
22
22
|
let queue = Promise.resolve();
|
|
@@ -26,7 +26,7 @@ export function createTerminalShell(options) {
|
|
|
26
26
|
readState: () => state,
|
|
27
27
|
};
|
|
28
28
|
const renderer = options.createView?.(bindings) ?? createRenderer({
|
|
29
|
-
...bindings, ...(options.accessible === undefined ? {} : { accessible: options.accessible }), inline: options.inline,
|
|
29
|
+
...bindings, ...(options.accessible === undefined ? {} : { accessible: options.accessible }), ...(options.statusLine === undefined ? {} : { statusLine: options.statusLine }), inline: options.inline,
|
|
30
30
|
});
|
|
31
31
|
const guard = createTerminalGuard(renderer);
|
|
32
32
|
const writeError = options.writeError ?? (message => { process.stderr.write(message); });
|
|
@@ -107,7 +107,7 @@ export function createTerminalShell(options) {
|
|
|
107
107
|
case 'exit-timer':
|
|
108
108
|
if (exitTimer !== undefined)
|
|
109
109
|
clearTimeout(exitTimer);
|
|
110
|
-
exitTimer = effect.armed ? setTimeout(() => { dispatch({ type: 'disarm-exit' }); },
|
|
110
|
+
exitTimer = effect.armed ? setTimeout(() => { dispatch({ type: 'disarm-exit' }); }, 1_500) : undefined;
|
|
111
111
|
return;
|
|
112
112
|
case 'external-edit': {
|
|
113
113
|
try {
|
|
@@ -176,6 +176,7 @@ export function createTerminalShell(options) {
|
|
|
176
176
|
return;
|
|
177
177
|
failed = true;
|
|
178
178
|
accepting = false;
|
|
179
|
+
renderer.discardSecretComposer();
|
|
179
180
|
const detail = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
180
181
|
writeError(`dashi: terminal failure\n${detail}\n`);
|
|
181
182
|
try {
|
|
@@ -201,6 +202,9 @@ export function createTerminalShell(options) {
|
|
|
201
202
|
function dispatch(action) {
|
|
202
203
|
if (!accepting)
|
|
203
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();
|
|
204
208
|
const [next, effects] = reduce(state, action);
|
|
205
209
|
state = next;
|
|
206
210
|
if (effects.length === 0)
|
|
@@ -224,6 +228,7 @@ export function createTerminalShell(options) {
|
|
|
224
228
|
},
|
|
225
229
|
dispatch,
|
|
226
230
|
async dispose() {
|
|
231
|
+
renderer.discardSecretComposer();
|
|
227
232
|
const [next, effects] = reduce(state, { type: 'terminal-lost' });
|
|
228
233
|
state = next;
|
|
229
234
|
for (const effect of effects)
|
package/lib/catalogs.d.ts
CHANGED
|
@@ -6,16 +6,21 @@ 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. */
|
|
12
14
|
export declare function modelOverlay(catalog: ModelCatalog, selected?: ModelSelection): Overlay;
|
|
13
15
|
/** Build the preset picker from the native preset service. */
|
|
14
16
|
export declare function permissionOverlay(ctx: Context, agent: Agent): Overlay;
|
|
17
|
+
export declare function sessionStatusLine(ctx: Context, agent: Agent, root: RootView, repo?: string): string;
|
|
15
18
|
/** Read DSH-owned root facts for /status in one projection cut. */
|
|
16
19
|
export declare function statusOverlay(ctx: Context, agent: Agent, root: RootView): Overlay;
|
|
17
20
|
/** Show DSH's heuristic next-request composition without treating it as an exact total. */
|
|
18
21
|
export declare function contextOverlay(ctx: Context, agent: Agent): Overlay;
|
|
22
|
+
/** Read DSH's whole-log token and timing projections without folding session history. */
|
|
23
|
+
export declare function usageOverlay(ctx: Context, agent: Agent): Overlay;
|
|
19
24
|
/** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
|
|
20
25
|
export declare function agentPresetOverlay(presets: readonly AgentPreset[], current: string, blank: boolean): Overlay;
|
|
21
26
|
/** Build bounded same-cwd prompt-search rows from DSH query hits. */
|
package/lib/catalogs.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
1
4
|
import { formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar';
|
|
2
5
|
import { isUserInvocable } from '@deepseek-ai/dsh-skill';
|
|
3
6
|
import { detectAtTrigger } from './at-trigger.js';
|
|
4
|
-
import { slashCompletionQuery } from './completion-trigger.js';
|
|
7
|
+
import { pluginCompletionQuery, slashCompletionQuery } from './completion-trigger.js';
|
|
5
8
|
import { isImagePath } from './image-input.js';
|
|
6
9
|
import { sessionTitle } from './session-list.js';
|
|
7
10
|
function matchRank(name, query) {
|
|
@@ -15,6 +18,24 @@ function selectedModel(ctx, agent) {
|
|
|
15
18
|
}
|
|
16
19
|
/** Build command and skill completion rows for one exact root. */
|
|
17
20
|
export async function completionOptions(ctx, agent, input, caret = input.length, signal) {
|
|
21
|
+
const plugin = pluginCompletionQuery(input, caret);
|
|
22
|
+
if (plugin !== undefined) {
|
|
23
|
+
let candidates = ['add', 'remove', 'update', 'outdated', 'list', 'why', 'exec', 'licenses'];
|
|
24
|
+
if (plugin.command !== undefined) {
|
|
25
|
+
const profile = fileURLToPath(new URL('.', ctx.baseUrl));
|
|
26
|
+
if (plugin.command === 'exec') {
|
|
27
|
+
candidates = (await readdir(join(profile, 'node_modules', '.bin')).catch((error) => error.code === 'ENOENT' ? [] : Promise.reject(error))).sort();
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const manifest = JSON.parse(await readFile(join(profile, 'package.json'), 'utf8'));
|
|
31
|
+
candidates = Object.keys(manifest.dependencies ?? {}).sort();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return candidates.filter(name => matchRank(name, plugin.query) < 2).map(name => ({
|
|
35
|
+
group: 'Commands', label: name,
|
|
36
|
+
value: { kind: 'insert', text: `/plugin ${plugin.command === undefined ? '' : `${plugin.command} `}${name} ` },
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
18
39
|
const at = detectAtTrigger(input, caret);
|
|
19
40
|
if (at !== undefined) {
|
|
20
41
|
const service = ctx.get('fileReferences');
|
|
@@ -69,6 +90,23 @@ export async function completionOptions(ctx, agent, input, caret = input.length,
|
|
|
69
90
|
}));
|
|
70
91
|
return [...commands, ...skills];
|
|
71
92
|
}
|
|
93
|
+
/** Build a filtered view of the skills resolved for one live session. */
|
|
94
|
+
export async function skillOverlay(ctx, agent, filter, signal) {
|
|
95
|
+
const lookup = { cwd: agent.session.header.cwd, scope: agent, signal };
|
|
96
|
+
const needle = filter.trim().toLocaleLowerCase();
|
|
97
|
+
const skills = (await ctx.skills.list(lookup)).filter(skill => needle === ''
|
|
98
|
+
|| skill.name.toLocaleLowerCase().includes(needle)
|
|
99
|
+
|| skill.description.toLocaleLowerCase().includes(needle));
|
|
100
|
+
const definitions = await Promise.all(skills.map(skill => ctx.skills.get(skill.name, lookup)));
|
|
101
|
+
return {
|
|
102
|
+
cursor: 0, kind: 'list', purpose: 'skills', title: needle === '' ? 'Skills' : `Skills · ${filter.trim()}`,
|
|
103
|
+
options: skills.map((skill, index) => ({
|
|
104
|
+
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'}`,
|
|
105
|
+
label: `/${skill.name}`,
|
|
106
|
+
value: { kind: 'insert', text: `/${skill.name} ` },
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
72
110
|
/** Build the cold session picker without adopting any candidate. */
|
|
73
111
|
export function sessionOverlay(items, currentId, cwd) {
|
|
74
112
|
return {
|
|
@@ -136,6 +174,37 @@ export function permissionOverlay(ctx, agent) {
|
|
|
136
174
|
function number(value) {
|
|
137
175
|
return typeof value === 'number' ? value.toLocaleString('en-US') : 'unavailable';
|
|
138
176
|
}
|
|
177
|
+
function milliseconds(value) {
|
|
178
|
+
return typeof value === 'number' ? `${number(value)} ms` : 'unavailable';
|
|
179
|
+
}
|
|
180
|
+
function compactTokens(value) {
|
|
181
|
+
return Intl.NumberFormat('en-US', { maximumFractionDigits: 1, notation: 'compact' }).format(value).replace('K', 'k');
|
|
182
|
+
}
|
|
183
|
+
export function sessionStatusLine(ctx, agent, root, repo) {
|
|
184
|
+
const { modelSelection, permissions, tokenUsage, contextPressure } = ctx.sessionProjections
|
|
185
|
+
.snapshot(agent.session, ['modelSelection', 'permissions', 'tokenUsage', 'contextPressure']).values;
|
|
186
|
+
const model = modelSelection?.next ?? modelSelection?.lastUsed;
|
|
187
|
+
const input = tokenUsage === undefined ? 0 : tokenUsage.uncachedInputTokens + tokenUsage.cacheReadTokens + tokenUsage.cacheWriteTokens;
|
|
188
|
+
const total = input + (tokenUsage?.outputTokens ?? 0);
|
|
189
|
+
const used = contextPressure?.projectedTokens ?? contextPressure?.pressureTokens;
|
|
190
|
+
const context = used === undefined ? undefined
|
|
191
|
+
: contextPressure?.contextWindow === undefined ? `ctx ${compactTokens(used)}`
|
|
192
|
+
: `ctx ${compactTokens(used)}/${compactTokens(contextPressure.contextWindow)} ${String(Math.min(100, Math.round(used / contextPressure.contextWindow * 100)))}%`;
|
|
193
|
+
const agents = root.subagents?.filter(item => item.state === 'running').length ?? 0;
|
|
194
|
+
const jobs = root.jobs?.filter(item => item.status === 'running' || item.status === 'stopping').length ?? 0;
|
|
195
|
+
return [
|
|
196
|
+
model?.model ?? root.model,
|
|
197
|
+
...(model?.reasoningEffort === undefined ? [] : [model.reasoningEffort]),
|
|
198
|
+
permissions?.currentValue ?? root.permission ?? 'unknown',
|
|
199
|
+
...(context === undefined ? [] : [context]),
|
|
200
|
+
...(input === 0 ? [] : [`cache ${String(Math.min(tokenUsage?.cacheReadTokens === input ? 100 : 99, Math.round((tokenUsage?.cacheReadTokens ?? 0) / input * 100)))}%`]),
|
|
201
|
+
...(tokenUsage === undefined ? [] : [`${compactTokens(total)} tok`]),
|
|
202
|
+
...(agents === 0 ? [] : [`${String(agents)} agents`]),
|
|
203
|
+
...(jobs === 0 ? [] : [`${String(jobs)} jobs`]),
|
|
204
|
+
...(root.title === undefined ? [] : [root.title]),
|
|
205
|
+
...(repo === undefined ? [] : [repo]),
|
|
206
|
+
].join(' · ');
|
|
207
|
+
}
|
|
139
208
|
/** Read DSH-owned root facts for /status in one projection cut. */
|
|
140
209
|
export function statusOverlay(ctx, agent, root) {
|
|
141
210
|
const values = ctx.sessionProjections.snapshot(agent.session, [
|
|
@@ -179,6 +248,27 @@ export function contextOverlay(ctx, agent) {
|
|
|
179
248
|
title: 'Context',
|
|
180
249
|
};
|
|
181
250
|
}
|
|
251
|
+
/** Read DSH's whole-log token and timing projections without folding session history. */
|
|
252
|
+
export function usageOverlay(ctx, agent) {
|
|
253
|
+
const values = ctx.sessionProjections.snapshot(agent.session, ['tokenUsage', 'sessionStats']).values;
|
|
254
|
+
const tokens = values.tokenUsage;
|
|
255
|
+
const stats = values.sessionStats;
|
|
256
|
+
const measured = stats === undefined ? undefined : stats.llmMs + stats.toolMs;
|
|
257
|
+
return {
|
|
258
|
+
kind: 'info',
|
|
259
|
+
lines: [
|
|
260
|
+
`Input tokens: ${number(tokens?.uncachedInputTokens)}`,
|
|
261
|
+
`Output tokens: ${number(tokens?.outputTokens)}`,
|
|
262
|
+
`Cache read: ${number(tokens?.cacheReadTokens)}`,
|
|
263
|
+
`Cache write: ${number(tokens?.cacheWriteTokens)}`,
|
|
264
|
+
`Turns: ${number(stats?.turns)} · Steps: ${number(stats?.steps)}`,
|
|
265
|
+
`Model time: ${milliseconds(stats?.llmMs)}`,
|
|
266
|
+
`Tool time: ${milliseconds(stats?.toolMs)}`,
|
|
267
|
+
`Measured wall time: ${milliseconds(measured)}`,
|
|
268
|
+
],
|
|
269
|
+
title: 'Usage',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
182
272
|
/** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
|
|
183
273
|
export function agentPresetOverlay(presets, current, blank) {
|
|
184
274
|
const options = presets.filter(preset => preset.broken === undefined).map(preset => ({
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export declare function slashCompletionQuery(input: string): string | undefined;
|
|
2
|
+
export declare function pluginCompletionQuery(input: string, caret: number): {
|
|
3
|
+
readonly command?: string;
|
|
4
|
+
readonly query: string;
|
|
5
|
+
} | undefined;
|
|
2
6
|
export declare function hasCompletionTrigger(input: string, caret: number): boolean;
|
|
@@ -2,6 +2,18 @@ import { detectAtTrigger } from './at-trigger.js';
|
|
|
2
2
|
export function slashCompletionQuery(input) {
|
|
3
3
|
return /^\/([a-z0-9_-]*)$/u.exec(input)?.[1];
|
|
4
4
|
}
|
|
5
|
+
export function pluginCompletionQuery(input, caret) {
|
|
6
|
+
if (caret !== input.length)
|
|
7
|
+
return undefined;
|
|
8
|
+
const match = /^\/plugin +([^\s]*)(?: +([^\s]*))?$/u.exec(input);
|
|
9
|
+
if (match === null)
|
|
10
|
+
return undefined;
|
|
11
|
+
const command = match[1] ?? '';
|
|
12
|
+
if (match[2] === undefined)
|
|
13
|
+
return { query: command };
|
|
14
|
+
return /^(?:exec|remove|update|why)$/u.test(command) ? { command, query: match[2] } : undefined;
|
|
15
|
+
}
|
|
5
16
|
export function hasCompletionTrigger(input, caret) {
|
|
6
|
-
return slashCompletionQuery(input) !== undefined ||
|
|
17
|
+
return slashCompletionQuery(input) !== undefined || pluginCompletionQuery(input, caret) !== undefined
|
|
18
|
+
|| detectAtTrigger(input, caret) !== undefined;
|
|
7
19
|
}
|
package/lib/diff-view.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
import { HUMAN_SHELL_STREAM_BYTES } from './shell-command.js';
|
|
1
2
|
import { boundedBody } from './tool-presentation.js';
|
|
2
3
|
import { foldCells } from './transcript.js';
|
|
4
|
+
import { latestCompletedTurn } from './rewind.js';
|
|
3
5
|
export async function diffOverlay(ctx, agent, presenter, mode, signal) {
|
|
4
6
|
if (mode === 'turn') {
|
|
5
7
|
const events = agent.session.snapshotEvents();
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const boundary = latestCompletedTurn(events);
|
|
9
|
+
const end = boundary.atSeq === undefined ? -1 : events.findIndex(event => event.seq === boundary.atSeq);
|
|
10
|
+
let start = end;
|
|
11
|
+
while (start >= 0 && events[start]?.type !== 'turn/start')
|
|
12
|
+
start--;
|
|
13
|
+
const cells = (start < 0 ? [] : foldCells(events.slice(start, end + 1), presenter)).filter(cell => cell.pending !== true && cell.tool?.card === 'diff' && (cell.tool.diffs?.length ?? 0) > 0);
|
|
14
|
+
return { cells, kind: 'info', lines: cells.length === 0 ? ['No write/edit changes in the last completed turn.'] : [],
|
|
15
|
+
title: `Last completed turn diff${boundary.open ? ' · trailing turn open' : ''}` };
|
|
9
16
|
}
|
|
10
17
|
const command = 'git diff --no-ext-diff --no-color HEAD --';
|
|
11
18
|
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
12
19
|
command, sandboxPolicy: ctx.sandboxPolicy.resolve({ session: agent.session }), signal,
|
|
13
|
-
stdoutMaxBytes:
|
|
20
|
+
stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES, timeoutMs: 30_000, workdir: agent.session.header.cwd,
|
|
14
21
|
}));
|
|
15
22
|
const output = [result.stdout.text, result.stderr.text].filter(Boolean).join('\n');
|
|
16
23
|
const cell = {
|
package/lib/help.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
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 · --
|
|
2
|
+
export declare const FLAG_HELP: readonly ["Launch flags: --inline | --fullscreen · --accessible · --verbose · -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 · --tools NAMES · --disallowedTools NAMES · --yolo | --dangerously-skip-permissions · --image PATH", " --system-prompt TEXT | --system-prompt-file PATH · --append-system-prompt TEXT | --append-system-prompt-file PATH · -h, --help"];
|
package/lib/help.js
CHANGED
|
@@ -2,5 +2,5 @@ import dashi from '../package.json' with { type: 'json' };
|
|
|
2
2
|
import dsh from '@deepseek-ai/dsh-cmdline/package.json' with { type: 'json' };
|
|
3
3
|
export const VERSION_LINE = `dashi ${dashi.version} on DSH ${dsh.version}`;
|
|
4
4
|
export const FLAG_HELP = [
|
|
5
|
-
'Launch flags: --inline | --fullscreen · --accessible · -r, --resume [NAME|UUID] · -c, --continue · --
|
|
5
|
+
'Launch flags: --inline | --fullscreen · --accessible · --verbose · -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 · --tools NAMES · --disallowedTools NAMES · --yolo | --dangerously-skip-permissions · --image PATH', ' --system-prompt TEXT | --system-prompt-file PATH · --append-system-prompt TEXT | --append-system-prompt-file PATH · -h, --help'
|
|
6
6
|
];
|
package/lib/history-records.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { SessionHistoryRecord } from '@deepseek-ai/dsh-api-session-controller';
|
|
2
|
-
import {
|
|
3
|
-
/**
|
|
2
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
3
|
+
/** Read the Controller's canonical Session event history. */
|
|
4
4
|
export declare function eventsFromRecords(records: readonly SessionHistoryRecord[]): SessionEvent[];
|
package/lib/history-records.js
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
/** Expand the Controller's compact history wire records to canonical Session events. */
|
|
1
|
+
/** Read the Controller's canonical Session event history. */
|
|
3
2
|
export function eventsFromRecords(records) {
|
|
4
|
-
return records.
|
|
5
|
-
if (record.type === 'event')
|
|
6
|
-
return [record.event];
|
|
7
|
-
const encoded = record.event.type === 'chunkrow/text-chunks'
|
|
8
|
-
? { type: 'text-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data }
|
|
9
|
-
: record.event.type === 'chunkrow/reasoning-chunks'
|
|
10
|
-
? { type: 'reasoning-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data }
|
|
11
|
-
: { type: 'tool-call-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data };
|
|
12
|
-
return decodeStorageRecord(encoded);
|
|
13
|
-
});
|
|
3
|
+
return records.map(record => record.event);
|
|
14
4
|
}
|
package/lib/index.js
CHANGED
|
@@ -8,26 +8,32 @@ import { readClipboardImage } from './clipboard-image.js';
|
|
|
8
8
|
import { editDraftExternally } from './external-editor.js';
|
|
9
9
|
import { FLAG_HELP, VERSION_LINE } from './help.js';
|
|
10
10
|
import { formatSessionList, isSessionId, parseSessionListArgs, sessionMatches, sessionNotFound, } from './session-list.js';
|
|
11
|
-
import { createSessionRuntime } from './session-runtime.js';
|
|
11
|
+
import { createSessionRuntime, LaunchArgumentError, } from './session-runtime.js';
|
|
12
12
|
import { TuiRoot } from './tui-root.js';
|
|
13
13
|
export const name = 'dashi';
|
|
14
14
|
export const inject = [
|
|
15
|
-
'agentPresets', 'agents', 'attachments', 'cmdlineArgs', 'commands', 'fs', 'loader', 'permissionPresets',
|
|
16
|
-
'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'tools',
|
|
15
|
+
'agentPresets', 'agents', 'attachments', 'authorization', 'cmdlineArgs', 'commands', 'credentials', 'fs', 'jobs', 'loader', 'permissionPresets', 'subagents',
|
|
16
|
+
'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'systemPrompt', 'tools',
|
|
17
17
|
];
|
|
18
|
-
const validatedVersions = JSON.parse(readFileSync(new URL('../validated-dsh-versions.json', import.meta.url), 'utf8'));
|
|
19
18
|
function parseArgs(args) {
|
|
20
19
|
let accessible = false;
|
|
20
|
+
let argumentError;
|
|
21
|
+
let agentPreset, sessionId;
|
|
22
|
+
let forkSession = false;
|
|
21
23
|
let inline = true;
|
|
22
24
|
let name;
|
|
23
25
|
let effort;
|
|
24
26
|
let model;
|
|
25
27
|
let permission;
|
|
26
28
|
let provider;
|
|
29
|
+
let tools;
|
|
30
|
+
let disallowedTools;
|
|
27
31
|
let resume;
|
|
28
32
|
let resumeAll = false;
|
|
29
33
|
let resumePicker = false;
|
|
34
|
+
let systemPrompt, appendSystemPrompt;
|
|
30
35
|
let useContinue = false;
|
|
36
|
+
let verbose = false;
|
|
31
37
|
const images = [];
|
|
32
38
|
const prompt = [];
|
|
33
39
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -38,13 +44,16 @@ function parseArgs(args) {
|
|
|
38
44
|
inline = true;
|
|
39
45
|
else if (argument === '--fullscreen')
|
|
40
46
|
inline = false;
|
|
41
|
-
else if (
|
|
42
|
-
|
|
47
|
+
else if (['--name', '-n', '--agent', '--image', '--effort', '--model', '--permission', '--provider', '--tools', '--disallowedTools',
|
|
48
|
+
'--system-prompt', '--system-prompt-file', '--append-system-prompt', '--append-system-prompt-file',
|
|
49
|
+
'--session-id'].includes(argument ?? '')) {
|
|
43
50
|
const value = args[++index];
|
|
44
51
|
if (value === undefined || value === '')
|
|
45
52
|
return undefined;
|
|
46
|
-
if (argument === '--name')
|
|
53
|
+
if (argument === '--name' || argument === '-n')
|
|
47
54
|
name = value;
|
|
55
|
+
else if (argument === '--agent')
|
|
56
|
+
agentPreset = value;
|
|
48
57
|
else if (argument === '--image')
|
|
49
58
|
images.push(value);
|
|
50
59
|
else if (argument === '--effort')
|
|
@@ -53,6 +62,32 @@ function parseArgs(args) {
|
|
|
53
62
|
model = value;
|
|
54
63
|
else if (argument === '--permission')
|
|
55
64
|
permission = value;
|
|
65
|
+
else if (argument === '--session-id')
|
|
66
|
+
sessionId = value;
|
|
67
|
+
else if (argument === '--tools' || argument === '--disallowedTools') {
|
|
68
|
+
const names = value.split(',').map(name => name.trim());
|
|
69
|
+
if (names.includes(''))
|
|
70
|
+
return undefined;
|
|
71
|
+
if (argument === '--tools')
|
|
72
|
+
tools = names;
|
|
73
|
+
else
|
|
74
|
+
disallowedTools = names;
|
|
75
|
+
}
|
|
76
|
+
else if (argument?.includes('system-prompt') === true) {
|
|
77
|
+
let text = value;
|
|
78
|
+
if (argument.endsWith('-file')) {
|
|
79
|
+
try {
|
|
80
|
+
text = readFileSync(value, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
argumentError = error instanceof Error ? error.message : String(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (argument.startsWith('--append'))
|
|
87
|
+
appendSystemPrompt = text;
|
|
88
|
+
else
|
|
89
|
+
systemPrompt = text;
|
|
90
|
+
}
|
|
56
91
|
else
|
|
57
92
|
provider = value;
|
|
58
93
|
}
|
|
@@ -69,6 +104,10 @@ function parseArgs(args) {
|
|
|
69
104
|
resumeAll = true;
|
|
70
105
|
else if (argument === '--continue' || argument === '-c')
|
|
71
106
|
useContinue = true;
|
|
107
|
+
else if (argument === '--fork-session')
|
|
108
|
+
forkSession = true;
|
|
109
|
+
else if (argument === '--verbose')
|
|
110
|
+
verbose = true;
|
|
72
111
|
else if (argument === '--yolo' || argument === '--dangerously-skip-permissions') {
|
|
73
112
|
permission = 'danger-full-access';
|
|
74
113
|
}
|
|
@@ -80,12 +119,17 @@ function parseArgs(args) {
|
|
|
80
119
|
if (resume !== undefined && (resumePicker || useContinue) || resumePicker && useContinue
|
|
81
120
|
|| name !== undefined && (resume !== undefined || resumePicker || useContinue)
|
|
82
121
|
|| resumeAll && resume === undefined && !resumePicker || resumeAll && useContinue
|
|
83
|
-
|| provider !== undefined && model === undefined
|
|
122
|
+
|| provider !== undefined && model === undefined
|
|
123
|
+
|| sessionId !== undefined && (!isSessionId(sessionId) || resume !== undefined || resumePicker || useContinue)
|
|
124
|
+
|| agentPreset !== undefined && (resume !== undefined || resumePicker || useContinue)
|
|
125
|
+
|| forkSession && resume === undefined && !resumePicker && !useContinue)
|
|
84
126
|
return undefined;
|
|
85
127
|
return {
|
|
86
128
|
accessible,
|
|
129
|
+
...(argumentError === undefined ? {} : { argumentError }),
|
|
130
|
+
...(appendSystemPrompt === undefined ? {} : { appendSystemPrompt }),
|
|
87
131
|
continue: useContinue,
|
|
88
|
-
inline,
|
|
132
|
+
...(agentPreset === undefined ? {} : { agentPreset }), forkSession, inline,
|
|
89
133
|
resumeAll,
|
|
90
134
|
resumePicker,
|
|
91
135
|
...(images.length === 0 ? {} : { images }),
|
|
@@ -95,7 +139,10 @@ function parseArgs(args) {
|
|
|
95
139
|
...(permission === undefined ? {} : { permission }),
|
|
96
140
|
...(prompt.length === 0 ? {} : { prompt: prompt.join(' ') }),
|
|
97
141
|
...(provider === undefined ? {} : { provider }),
|
|
98
|
-
...(
|
|
142
|
+
...(tools === undefined ? {} : { tools }),
|
|
143
|
+
...(disallowedTools === undefined ? {} : { disallowedTools }),
|
|
144
|
+
...(resume === undefined ? {} : { resume }), ...(sessionId === undefined ? {} : { sessionId }),
|
|
145
|
+
...(systemPrompt === undefined ? {} : { systemPrompt }), verbose,
|
|
99
146
|
};
|
|
100
147
|
}
|
|
101
148
|
function packageVersion(path) {
|
|
@@ -123,9 +170,6 @@ function warnDshVersion() {
|
|
|
123
170
|
if (baseVersion !== version) {
|
|
124
171
|
process.stderr.write(`dashi: warning: DSH ${version} loads @deepseek-ai/dsh-base ${baseVersion}; versions must match\n`);
|
|
125
172
|
}
|
|
126
|
-
if (!validatedVersions.includes(version)) {
|
|
127
|
-
process.stderr.write(`dashi: warning: DSH ${version} is not validated; validated: ${validatedVersions.join(', ')}\n`);
|
|
128
|
-
}
|
|
129
173
|
}
|
|
130
174
|
function installSessionList(ctx, options, ready, exit) {
|
|
131
175
|
const abort = new AbortController();
|
|
@@ -189,6 +233,8 @@ export function apply(ctx) {
|
|
|
189
233
|
const announce = ready.onReady(() => {
|
|
190
234
|
warnDshVersion();
|
|
191
235
|
boot = (async () => {
|
|
236
|
+
if (parsed.argumentError !== undefined)
|
|
237
|
+
throw new LaunchArgumentError(parsed.argumentError);
|
|
192
238
|
const cwd = process.cwd();
|
|
193
239
|
const makeRuntime = (options) => createSessionRuntime(ctx, options, tuiRoot, action => { shell?.dispatch(action); }, {
|
|
194
240
|
cancel: id => { shell?.cancelDecision(id); },
|
|
@@ -242,12 +288,14 @@ export function apply(ctx) {
|
|
|
242
288
|
initialAttachments: initial.initialAttachments, initialCells: initial.initialCells, initialHasMore: initial.initialHasMore,
|
|
243
289
|
initialPrompts: initial.initialPrompts, initialRoot: initial.root,
|
|
244
290
|
}),
|
|
291
|
+
...(parsed.verbose ? { initialToolMode: 'expanded' } : {}),
|
|
245
292
|
inline: parsed.inline || parsed.accessible,
|
|
246
293
|
interrupt: () => { runtime?.interrupt(); },
|
|
247
294
|
loadHistory: rootId => runtime?.loadHistory(rootId) ?? Promise.resolve(),
|
|
248
295
|
onReleased: () => { process.stdout.write(`${runtime?.summary ?? ''}\n`); },
|
|
249
296
|
pasteImage: readClipboardImage,
|
|
250
297
|
search: (query, rootId) => runtime?.search(query, rootId) ?? Promise.resolve(),
|
|
298
|
+
statusLine: () => runtime?.statusLine,
|
|
251
299
|
submit: (text, mode, attachments) => runtime?.submit(text, mode, attachments),
|
|
252
300
|
});
|
|
253
301
|
shell.start();
|
|
@@ -262,7 +310,7 @@ export function apply(ctx) {
|
|
|
262
310
|
process.stderr.write(`dashi: startup failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
263
311
|
await runtime?.shutdown();
|
|
264
312
|
await shell?.dispose();
|
|
265
|
-
exit(1);
|
|
313
|
+
exit(error instanceof LaunchArgumentError ? 2 : 1);
|
|
266
314
|
});
|
|
267
315
|
});
|
|
268
316
|
ctx.effect(() => async () => {
|
package/lib/input.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { type TUI } from '@earendil-works/pi-tui';
|
|
2
|
-
import type
|
|
2
|
+
import { type ComposerCursor, type UiAction, type ViewState } from './state.js';
|
|
3
3
|
export interface InputBindings {
|
|
4
4
|
readonly dispatch: (action: UiAction) => void;
|
|
5
5
|
readonly insertNewline?: () => void;
|
|
6
|
+
readonly insertText?: (text: string) => void;
|
|
6
7
|
readonly readComposerCursor?: () => ComposerCursor;
|
|
7
8
|
readonly readState: () => ViewState;
|
|
8
9
|
}
|
|
10
|
+
export declare function droppedPaths(data: string, cwd: string, inspect?: (path: string) => {
|
|
11
|
+
isDirectory(): boolean;
|
|
12
|
+
}): {
|
|
13
|
+
readonly images: readonly string[];
|
|
14
|
+
readonly text: string;
|
|
15
|
+
} | undefined;
|
|
9
16
|
export declare function caretOffset(text: string, cursor: ComposerCursor): number;
|
|
10
17
|
export declare function installInput(tui: TUI, bindings: InputBindings): () => void;
|
package/lib/input.js
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar';
|
|
1
4
|
import { isKeyRelease, matchesKey } from '@earendil-works/pi-tui';
|
|
5
|
+
import { isImagePath } from './image-input.js';
|
|
6
|
+
import { PAGE_CELLS } from './state.js';
|
|
7
|
+
export function droppedPaths(data, cwd, inspect = statSync) {
|
|
8
|
+
const [start, end] = ['\u001B[200~', '\u001B[201~'];
|
|
9
|
+
if (!data.startsWith(start) || !data.endsWith(end))
|
|
10
|
+
return undefined;
|
|
11
|
+
const token = /(?:[^\s'\\]+|'[^']*'|\\[\s\S])+/gu;
|
|
12
|
+
const source = data.slice(start.length, -end.length);
|
|
13
|
+
const chunks = source.match(token) ?? [];
|
|
14
|
+
if (chunks.length === 0 || source.replace(token, '').trim() !== '')
|
|
15
|
+
return undefined;
|
|
16
|
+
const paths = chunks.map(chunk => chunk.replace(/'([^']*)'|\\([\s\S])/gu, (_match, quoted, escaped) => quoted ?? escaped ?? ''));
|
|
17
|
+
try {
|
|
18
|
+
const entries = paths.map(value => {
|
|
19
|
+
const absolute = resolve(cwd, value);
|
|
20
|
+
const directory = inspect(absolute).isDirectory();
|
|
21
|
+
if (!directory && isImagePath(absolute))
|
|
22
|
+
return { absolute };
|
|
23
|
+
const local = relative(resolve(cwd), absolute);
|
|
24
|
+
const mention = formatFileMention({ kind: directory ? 'directory' : 'file', path: local !== '' && !isAbsolute(local) && local !== '..' && !local.startsWith(`..${sep}`) ? local : absolute }, false);
|
|
25
|
+
if (mention === undefined)
|
|
26
|
+
throw new Error('path cannot be mentioned');
|
|
27
|
+
return { absolute, mention };
|
|
28
|
+
});
|
|
29
|
+
const mentions = entries.flatMap(entry => entry.mention === undefined ? [] : [entry.mention]);
|
|
30
|
+
return { images: entries.flatMap(entry => entry.mention === undefined ? [entry.absolute] : []), text: mentions.length === 0 ? '' : `${mentions.join(' ')} ` };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
2
36
|
export function caretOffset(text, cursor) {
|
|
3
37
|
const lines = text.split('\n');
|
|
4
38
|
let offset = 0;
|
|
@@ -15,6 +49,13 @@ export function installInput(tui, bindings) {
|
|
|
15
49
|
const customQuestion = question !== undefined && decision?.kind === 'question'
|
|
16
50
|
&& decision.cursor === question.options.length;
|
|
17
51
|
const completion = state.overlay?.kind === 'list' && state.overlay.purpose === 'completion';
|
|
52
|
+
const dropped = decision === undefined && state.overlay === undefined && state.search === undefined && droppedPaths(data, state.root?.cwd ?? state.cwd);
|
|
53
|
+
if (dropped) {
|
|
54
|
+
bindings.insertText?.(dropped.text);
|
|
55
|
+
for (const path of dropped.images)
|
|
56
|
+
bindings.dispatch({ type: 'attachment-path', path });
|
|
57
|
+
return { consume: true };
|
|
58
|
+
}
|
|
18
59
|
return customQuestion || state.search !== undefined || completion
|
|
19
60
|
|| decision === undefined && state.overlay === undefined ? undefined : { consume: true };
|
|
20
61
|
}
|
|
@@ -98,6 +139,10 @@ export function installInput(tui, bindings) {
|
|
|
98
139
|
bindings.dispatch({ type: 'search-open', scope: 'history' });
|
|
99
140
|
else if (overlay.kind === 'history' && matchesKey(data, 'y'))
|
|
100
141
|
bindings.dispatch({ type: 'history-copy' });
|
|
142
|
+
else if (overlay.kind === 'info' && matchesKey(data, 'pageUp'))
|
|
143
|
+
bindings.dispatch({ type: 'overlay-move', offset: -PAGE_CELLS });
|
|
144
|
+
else if (overlay.kind === 'info' && matchesKey(data, 'pageDown'))
|
|
145
|
+
bindings.dispatch({ type: 'overlay-move', offset: PAGE_CELLS });
|
|
101
146
|
else if (matchesKey(data, 'up'))
|
|
102
147
|
bindings.dispatch({ type: 'overlay-move', offset: -1 });
|
|
103
148
|
else if (matchesKey(data, 'down'))
|
package/lib/renderer.d.ts
CHANGED
|
@@ -6,12 +6,14 @@ 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
|
}
|
|
12
13
|
export interface RendererOptions extends InputBindings {
|
|
13
14
|
readonly accessible?: boolean;
|
|
14
15
|
readonly inline: boolean;
|
|
16
|
+
readonly statusLine?: () => string | undefined;
|
|
15
17
|
readonly terminal?: Terminal;
|
|
16
18
|
readonly terminalType?: string;
|
|
17
19
|
}
|