@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 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 the exact validated DSH release and start dashi:
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.2-rc.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
- dashi validates each DSH release before adopting it, and its packages pin DSH
19
- dependencies exactly.
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
@@ -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;
@@ -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' }); }, 2_000) : undefined;
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 || detectAtTrigger(input, caret) !== 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 start = events.map(event => event.type).lastIndexOf('turn/start');
7
- const cells = (start < 0 ? [] : foldCells(events.slice(start), presenter)).filter(cell => cell.pending !== true && cell.tool?.card === 'diff' && (cell.tool.diffs?.length ?? 0) > 0);
8
- return { cells, kind: 'info', lines: cells.length === 0 ? ['No write/edit changes in the last turn.'] : [], title: 'Last turn diff' };
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: 64 * 1024, timeoutMs: 30_000, workdir: agent.session.header.cwd,
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 · --all · --name TITLE", " --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 · --all · --name TITLE', ' --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
  ];
@@ -1,4 +1,4 @@
1
1
  import type { SessionHistoryRecord } from '@deepseek-ai/dsh-api-session-controller';
2
- import { type SessionEvent } from '@deepseek-ai/dsh-session';
3
- /** Expand the Controller's compact history wire records to canonical Session events. */
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[];
@@ -1,14 +1,4 @@
1
- import { decodeStorageRecord } from '@deepseek-ai/dsh-session';
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.flatMap((record) => {
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 (argument === '--name' || argument === '--image' || argument === '--effort'
42
- || argument === '--model' || argument === '--permission' || argument === '--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',
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
- ...(resume === undefined ? {} : { resume }),
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 { ComposerCursor, UiAction, ViewState } from './state.js';
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
  }