@antst/dashi 0.1.0-alpha.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/lib/application.d.ts +2 -0
- package/lib/application.js +3 -3
- package/lib/catalogs.d.ts +3 -0
- package/lib/catalogs.js +74 -1
- package/lib/completion-trigger.d.ts +4 -0
- package/lib/completion-trigger.js +13 -1
- package/lib/diff-view.js +9 -3
- package/lib/help.d.ts +1 -1
- package/lib/help.js +1 -1
- package/lib/history-records.d.ts +2 -2
- package/lib/history-records.js +2 -12
- package/lib/index.js +46 -9
- package/lib/input.d.ts +8 -1
- package/lib/input.js +45 -0
- package/lib/renderer.d.ts +1 -0
- package/lib/renderer.js +71 -15
- package/lib/rewind.d.ts +15 -2
- package/lib/rewind.js +46 -16
- package/lib/session-runtime.d.ts +7 -0
- package/lib/session-runtime.js +284 -35
- package/lib/shell-command.d.ts +1 -1
- package/lib/shell-command.js +3 -2
- package/lib/state.d.ts +13 -1
- package/lib/state.js +36 -16
- package/lib/transcript.d.ts +3 -1
- package/lib/transcript.js +57 -44
- package/package.json +38 -35
- package/validated-dsh-versions.json +0 -3
package/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 {
|
package/lib/catalogs.d.ts
CHANGED
|
@@ -14,10 +14,13 @@ export declare function sessionOverlay(items: readonly SessionSummary[], current
|
|
|
14
14
|
export declare function modelOverlay(catalog: ModelCatalog, selected?: ModelSelection): Overlay;
|
|
15
15
|
/** Build the preset picker from the native preset service. */
|
|
16
16
|
export declare function permissionOverlay(ctx: Context, agent: Agent): Overlay;
|
|
17
|
+
export declare function sessionStatusLine(ctx: Context, agent: Agent, root: RootView, repo?: string): string;
|
|
17
18
|
/** Read DSH-owned root facts for /status in one projection cut. */
|
|
18
19
|
export declare function statusOverlay(ctx: Context, agent: Agent, root: RootView): Overlay;
|
|
19
20
|
/** Show DSH's heuristic next-request composition without treating it as an exact total. */
|
|
20
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;
|
|
21
24
|
/** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
|
|
22
25
|
export declare function agentPresetOverlay(presets: readonly AgentPreset[], current: string, blank: boolean): Overlay;
|
|
23
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');
|
|
@@ -153,6 +174,37 @@ export function permissionOverlay(ctx, agent) {
|
|
|
153
174
|
function number(value) {
|
|
154
175
|
return typeof value === 'number' ? value.toLocaleString('en-US') : 'unavailable';
|
|
155
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
|
+
}
|
|
156
208
|
/** Read DSH-owned root facts for /status in one projection cut. */
|
|
157
209
|
export function statusOverlay(ctx, agent, root) {
|
|
158
210
|
const values = ctx.sessionProjections.snapshot(agent.session, [
|
|
@@ -196,6 +248,27 @@ export function contextOverlay(ctx, agent) {
|
|
|
196
248
|
title: 'Context',
|
|
197
249
|
};
|
|
198
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
|
+
}
|
|
199
272
|
/** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
|
|
200
273
|
export function agentPresetOverlay(presets, current, blank) {
|
|
201
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,12 +1,18 @@
|
|
|
1
1
|
import { HUMAN_SHELL_STREAM_BYTES } from './shell-command.js';
|
|
2
2
|
import { boundedBody } from './tool-presentation.js';
|
|
3
3
|
import { foldCells } from './transcript.js';
|
|
4
|
+
import { latestCompletedTurn } from './rewind.js';
|
|
4
5
|
export async function diffOverlay(ctx, agent, presenter, mode, signal) {
|
|
5
6
|
if (mode === 'turn') {
|
|
6
7
|
const events = agent.session.snapshotEvents();
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
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' : ''}` };
|
|
10
16
|
}
|
|
11
17
|
const command = 'git diff --no-ext-diff --no-color HEAD --';
|
|
12
18
|
const result = await ctx.shell.run(ctx.shell.resolve({
|
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 · --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"];
|
|
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 · --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'
|
|
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,16 +8,16 @@ 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', 'authorization', 'cmdlineArgs', 'commands', 'credentials', '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
21
|
let agentPreset, sessionId;
|
|
22
22
|
let forkSession = false;
|
|
23
23
|
let inline = true;
|
|
@@ -26,10 +26,14 @@ function parseArgs(args) {
|
|
|
26
26
|
let model;
|
|
27
27
|
let permission;
|
|
28
28
|
let provider;
|
|
29
|
+
let tools;
|
|
30
|
+
let disallowedTools;
|
|
29
31
|
let resume;
|
|
30
32
|
let resumeAll = false;
|
|
31
33
|
let resumePicker = false;
|
|
34
|
+
let systemPrompt, appendSystemPrompt;
|
|
32
35
|
let useContinue = false;
|
|
36
|
+
let verbose = false;
|
|
33
37
|
const images = [];
|
|
34
38
|
const prompt = [];
|
|
35
39
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -40,7 +44,8 @@ function parseArgs(args) {
|
|
|
40
44
|
inline = true;
|
|
41
45
|
else if (argument === '--fullscreen')
|
|
42
46
|
inline = false;
|
|
43
|
-
else if (['--name', '-n', '--agent', '--image', '--effort', '--model', '--permission', '--provider',
|
|
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',
|
|
44
49
|
'--session-id'].includes(argument ?? '')) {
|
|
45
50
|
const value = args[++index];
|
|
46
51
|
if (value === undefined || value === '')
|
|
@@ -59,6 +64,30 @@ function parseArgs(args) {
|
|
|
59
64
|
permission = value;
|
|
60
65
|
else if (argument === '--session-id')
|
|
61
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
|
+
}
|
|
62
91
|
else
|
|
63
92
|
provider = value;
|
|
64
93
|
}
|
|
@@ -77,6 +106,8 @@ function parseArgs(args) {
|
|
|
77
106
|
useContinue = true;
|
|
78
107
|
else if (argument === '--fork-session')
|
|
79
108
|
forkSession = true;
|
|
109
|
+
else if (argument === '--verbose')
|
|
110
|
+
verbose = true;
|
|
80
111
|
else if (argument === '--yolo' || argument === '--dangerously-skip-permissions') {
|
|
81
112
|
permission = 'danger-full-access';
|
|
82
113
|
}
|
|
@@ -95,6 +126,8 @@ function parseArgs(args) {
|
|
|
95
126
|
return undefined;
|
|
96
127
|
return {
|
|
97
128
|
accessible,
|
|
129
|
+
...(argumentError === undefined ? {} : { argumentError }),
|
|
130
|
+
...(appendSystemPrompt === undefined ? {} : { appendSystemPrompt }),
|
|
98
131
|
continue: useContinue,
|
|
99
132
|
...(agentPreset === undefined ? {} : { agentPreset }), forkSession, inline,
|
|
100
133
|
resumeAll,
|
|
@@ -106,7 +139,10 @@ function parseArgs(args) {
|
|
|
106
139
|
...(permission === undefined ? {} : { permission }),
|
|
107
140
|
...(prompt.length === 0 ? {} : { prompt: prompt.join(' ') }),
|
|
108
141
|
...(provider === undefined ? {} : { provider }),
|
|
142
|
+
...(tools === undefined ? {} : { tools }),
|
|
143
|
+
...(disallowedTools === undefined ? {} : { disallowedTools }),
|
|
109
144
|
...(resume === undefined ? {} : { resume }), ...(sessionId === undefined ? {} : { sessionId }),
|
|
145
|
+
...(systemPrompt === undefined ? {} : { systemPrompt }), verbose,
|
|
110
146
|
};
|
|
111
147
|
}
|
|
112
148
|
function packageVersion(path) {
|
|
@@ -134,9 +170,6 @@ function warnDshVersion() {
|
|
|
134
170
|
if (baseVersion !== version) {
|
|
135
171
|
process.stderr.write(`dashi: warning: DSH ${version} loads @deepseek-ai/dsh-base ${baseVersion}; versions must match\n`);
|
|
136
172
|
}
|
|
137
|
-
if (!validatedVersions.includes(version)) {
|
|
138
|
-
process.stderr.write(`dashi: warning: DSH ${version} is not validated; validated: ${validatedVersions.join(', ')}\n`);
|
|
139
|
-
}
|
|
140
173
|
}
|
|
141
174
|
function installSessionList(ctx, options, ready, exit) {
|
|
142
175
|
const abort = new AbortController();
|
|
@@ -200,6 +233,8 @@ export function apply(ctx) {
|
|
|
200
233
|
const announce = ready.onReady(() => {
|
|
201
234
|
warnDshVersion();
|
|
202
235
|
boot = (async () => {
|
|
236
|
+
if (parsed.argumentError !== undefined)
|
|
237
|
+
throw new LaunchArgumentError(parsed.argumentError);
|
|
203
238
|
const cwd = process.cwd();
|
|
204
239
|
const makeRuntime = (options) => createSessionRuntime(ctx, options, tuiRoot, action => { shell?.dispatch(action); }, {
|
|
205
240
|
cancel: id => { shell?.cancelDecision(id); },
|
|
@@ -253,12 +288,14 @@ export function apply(ctx) {
|
|
|
253
288
|
initialAttachments: initial.initialAttachments, initialCells: initial.initialCells, initialHasMore: initial.initialHasMore,
|
|
254
289
|
initialPrompts: initial.initialPrompts, initialRoot: initial.root,
|
|
255
290
|
}),
|
|
291
|
+
...(parsed.verbose ? { initialToolMode: 'expanded' } : {}),
|
|
256
292
|
inline: parsed.inline || parsed.accessible,
|
|
257
293
|
interrupt: () => { runtime?.interrupt(); },
|
|
258
294
|
loadHistory: rootId => runtime?.loadHistory(rootId) ?? Promise.resolve(),
|
|
259
295
|
onReleased: () => { process.stdout.write(`${runtime?.summary ?? ''}\n`); },
|
|
260
296
|
pasteImage: readClipboardImage,
|
|
261
297
|
search: (query, rootId) => runtime?.search(query, rootId) ?? Promise.resolve(),
|
|
298
|
+
statusLine: () => runtime?.statusLine,
|
|
262
299
|
submit: (text, mode, attachments) => runtime?.submit(text, mode, attachments),
|
|
263
300
|
});
|
|
264
301
|
shell.start();
|
|
@@ -273,7 +310,7 @@ export function apply(ctx) {
|
|
|
273
310
|
process.stderr.write(`dashi: startup failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
274
311
|
await runtime?.shutdown();
|
|
275
312
|
await shell?.dispose();
|
|
276
|
-
exit(1);
|
|
313
|
+
exit(error instanceof LaunchArgumentError ? 2 : 1);
|
|
277
314
|
});
|
|
278
315
|
});
|
|
279
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
|
@@ -13,6 +13,7 @@ export interface Renderer extends TerminalSession {
|
|
|
13
13
|
export interface RendererOptions extends InputBindings {
|
|
14
14
|
readonly accessible?: boolean;
|
|
15
15
|
readonly inline: boolean;
|
|
16
|
+
readonly statusLine?: () => string | undefined;
|
|
16
17
|
readonly terminal?: Terminal;
|
|
17
18
|
readonly terminalType?: string;
|
|
18
19
|
}
|