@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 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 {
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 || 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,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 start = events.map(event => event.type).lastIndexOf('turn/start');
8
- const cells = (start < 0 ? [] : foldCells(events.slice(start), presenter)).filter(cell => cell.pending !== true && cell.tool?.card === 'diff' && (cell.tool.diffs?.length ?? 0) > 0);
9
- return { cells, kind: 'info', lines: cells.length === 0 ? ['No write/edit changes in the last turn.'] : [], title: 'Last turn diff' };
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
  ];
@@ -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,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 { 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
@@ -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
  }