@antst/dashi 0.1.0-alpha.12 → 0.1.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/catalogs.d.ts CHANGED
@@ -14,7 +14,7 @@ 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, branch?: string): string;
17
+ export declare function sessionStatusLine(ctx: Context, agent: Agent, root: RootView, repo?: string): string;
18
18
  /** Read DSH-owned root facts for /status in one projection cut. */
19
19
  export declare function statusOverlay(ctx: Context, agent: Agent, root: RootView): Overlay;
20
20
  /** Show DSH's heuristic next-request composition without treating it as an exact total. */
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');
@@ -156,18 +177,32 @@ function number(value) {
156
177
  function milliseconds(value) {
157
178
  return typeof value === 'number' ? `${number(value)} ms` : 'unavailable';
158
179
  }
159
- export function sessionStatusLine(ctx, agent, root, branch) {
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) {
160
184
  const { modelSelection, permissions, tokenUsage, contextPressure } = ctx.sessionProjections
161
185
  .snapshot(agent.session, ['modelSelection', 'permissions', 'tokenUsage', 'contextPressure']).values;
162
186
  const model = modelSelection?.next ?? modelSelection?.lastUsed;
163
187
  const input = tokenUsage === undefined ? 0 : tokenUsage.uncachedInputTokens + tokenUsage.cacheReadTokens + tokenUsage.cacheWriteTokens;
188
+ const total = input + (tokenUsage?.outputTokens ?? 0);
164
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;
165
195
  return [
166
- `model ${model == null ? `${root.provider === undefined ? '' : `${root.provider}/`}${root.model}` : `${model.provider}/${model.model}`}`,
167
- `permission ${permissions?.currentValue ?? root.permission ?? 'unknown'}`,
168
- ...(used === undefined || contextPressure?.contextWindow === undefined ? [] : [`context ${number(used)}/${number(contextPressure.contextWindow)}`]),
196
+ model?.model ?? root.model,
197
+ ...(model?.reasoningEffort === undefined ? [] : [model.reasoningEffort]),
198
+ permissions?.currentValue ?? root.permission ?? 'unknown',
199
+ ...(context === undefined ? [] : [context]),
169
200
  ...(input === 0 ? [] : [`cache ${String(Math.min(tokenUsage?.cacheReadTokens === input ? 100 : 99, Math.round((tokenUsage?.cacheReadTokens ?? 0) / input * 100)))}%`]),
170
- ...(branch === undefined ? [] : [`branch ${branch}`]),
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]),
171
206
  ].join(' · ');
172
207
  }
173
208
  /** Read DSH-owned root facts for /status in one projection cut. */
@@ -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/renderer.js CHANGED
@@ -401,8 +401,8 @@ export function createRenderer(options) {
401
401
  : root === undefined
402
402
  ? `${DIM}no session · cards ${state.toolMode} · Ctrl+J newline · Ctrl+L redraw${RESET}`
403
403
  : root.status === 'running'
404
- ? `${DIM}running · ${plain(root.model)} · Enter ${state.sendMode === 'steer' ? 'steer' : 'queue next turn'} · Ctrl+T toggle · cards ${state.toolMode} · Esc interrupt${RESET}`
405
- : `${DIM}idle · ${plain(root.model)} · Enter send · cards ${state.toolMode}${RESET}`;
404
+ ? `${DIM}running${accessible ? ` · ${plain(root.model)}` : ''} · Enter ${state.sendMode === 'steer' ? 'steer' : 'queue next turn'} · Ctrl+T toggle · cards ${state.toolMode} · Esc interrupt${RESET}`
405
+ : `${DIM}idle${accessible ? ` · ${plain(root.model)}` : ''} · Enter send · cards ${state.toolMode}${RESET}`;
406
406
  const activityCount = activities(state).length;
407
407
  const baseStatus = `${baseStatusText}${root?.contextPercent === undefined ? '' : ` · ${String(root.contextPercent)}% context`}${activityCount === 0 ? '' : ' · Ctrl+B activity'}`;
408
408
  const status = state.newOutput === 0 ? baseStatus
package/lib/rewind.d.ts CHANGED
@@ -7,8 +7,10 @@ export interface RewindBoundary {
7
7
  }
8
8
  /** Return the text of a direct human message, excluding injected context. */
9
9
  export declare function humanPrompt(event: SessionEvent): string | undefined;
10
- export declare function humanPrompts(events: readonly SessionEvent[]): readonly string[];
11
- /** Derive each human prompt and its controller-valid containing-turn cut. */
10
+ /** Recover one composer input from DSH's durable log. */
11
+ export declare function historyInput(event: SessionEvent): string | undefined;
12
+ export declare function historyInputs(events: readonly SessionEvent[]): readonly string[];
13
+ /** Derive each recalled input and its controller-valid preceding-turn cut. */
12
14
  export declare function rewindBoundaries(events: readonly SessionEvent[]): readonly RewindBoundary[];
13
15
  export declare function rewindOverlay(events: readonly SessionEvent[], roller: boolean): Overlay;
14
16
  export declare function rewindActionOverlay(boundary: Extract<OverlayValue, {
package/lib/rewind.js CHANGED
@@ -7,10 +7,29 @@ export function humanPrompt(event) {
7
7
  .map(block => block.text).join('');
8
8
  return text === '' ? undefined : text;
9
9
  }
10
- export function humanPrompts(events) {
11
- return events.flatMap(event => humanPrompt(event) ?? []);
10
+ function shellInput(message) {
11
+ if (message.source.kind !== 'plugin' || message.source.plugin !== 'dashi'
12
+ || message.source.form !== 'notice')
13
+ return undefined;
14
+ const first = message.content.find(block => block.type === 'text')?.text.split(/\r?\n/u)[0];
15
+ return first?.startsWith('$ ') === true ? `!${first.slice(2)}` : undefined;
16
+ }
17
+ /** Recover one composer input from DSH's durable log. */
18
+ export function historyInput(event) {
19
+ const prompt = humanPrompt(event);
20
+ if (prompt !== undefined)
21
+ return prompt;
22
+ if (event.type === 'command/run' && event.data.args !== undefined) {
23
+ return `/${event.data.name}${event.data.args}`;
24
+ }
25
+ if (event.type !== 'agent/inbox/spliced')
26
+ return undefined;
27
+ return event.data.inserted.map(shellInput).find(input => input !== undefined);
28
+ }
29
+ export function historyInputs(events) {
30
+ return events.flatMap(event => historyInput(event) ?? []);
12
31
  }
13
- /** Derive each human prompt and its controller-valid containing-turn cut. */
32
+ /** Derive each recalled input and its controller-valid preceding-turn cut. */
14
33
  export function rewindBoundaries(events) {
15
34
  const boundaries = [];
16
35
  let previousEnd;
@@ -21,22 +40,22 @@ export function rewindBoundaries(events) {
21
40
  openTurn = event.data.turn;
22
41
  prompted = false;
23
42
  }
24
- else if (event.type === 'user/message' && openTurn !== undefined) {
25
- const prompt = humanPrompt(event);
26
- if (prompt !== undefined) {
27
- boundaries.push({
28
- ...(previousEnd === undefined ? {} : { atSeq: previousEnd.seq }),
29
- label: `${prompt.replaceAll(/\s+/gu, ' ')}${prompted ? ' · mid-turn' : ''}`,
30
- prompt,
31
- });
32
- prompted = true;
33
- }
34
- }
35
43
  else if (event.type === 'turn/end') {
36
44
  previousEnd = event;
37
45
  openTurn = undefined;
38
46
  prompted = false;
39
47
  }
48
+ const prompt = historyInput(event);
49
+ if (prompt === undefined)
50
+ continue;
51
+ const human = humanPrompt(event) !== undefined;
52
+ boundaries.push({
53
+ ...(previousEnd === undefined ? {} : { atSeq: previousEnd.seq }),
54
+ label: `${prompt.replaceAll(/\s+/gu, ' ')}${human && prompted ? ' · mid-turn' : ''}`,
55
+ prompt,
56
+ });
57
+ if (human && openTurn !== undefined)
58
+ prompted = true;
40
59
  }
41
60
  return boundaries;
42
61
  }
@@ -9,7 +9,7 @@ import { diffOverlay } from './diff-view.js';
9
9
  import { eventsFromRecords } from './history-records.js';
10
10
  import { admitDraftImages, encodeDraftImages, readDraftImages } from './image-input.js';
11
11
  import { contextPercent, jobViews, subagentViews } from './presentation.js';
12
- import { humanPrompt, humanPrompts, inheritedTurn, rewindActionOverlay, rewindOverlay, } from './rewind.js';
12
+ import { historyInput, historyInputs, humanPrompt, inheritedTurn, rewindActionOverlay, rewindOverlay, } from './rewind.js';
13
13
  import { createToolPresenter } from './tool-presentation.js';
14
14
  import { quoteShellWord, runHumanShell } from './shell-command.js';
15
15
  import { markdownTranscript } from './session-export.js';
@@ -80,16 +80,18 @@ async function firstControl(iterator) {
80
80
  }
81
81
  return first.value;
82
82
  }
83
- async function gitBranch(ctx, agent, signal) {
83
+ async function gitRepo(ctx, agent, signal) {
84
+ const fallback = basename(agent.session.header.cwd ?? '');
84
85
  try {
85
86
  const result = await ctx.shell.run(ctx.shell.resolve({
86
- command: 'git branch --show-current', sandboxPolicy: ctx.sandboxPolicy.resolve({ mode: 'read-only' }),
87
- signal, stdoutMaxBytes: 512, timeoutMs: 2_000, workdir: agent.session.header.cwd,
87
+ command: 'git rev-parse --show-toplevel && git branch --show-current', sandboxPolicy: ctx.sandboxPolicy.resolve({ mode: 'read-only' }),
88
+ signal, stdoutMaxBytes: 1_024, timeoutMs: 2_000, workdir: agent.session.header.cwd,
88
89
  }));
89
- return result.exitCode === 0 && result.stdout.text.trim() !== '' ? result.stdout.text.trim() : undefined;
90
+ const [root, branch] = result.stdout.text.trim().split(/\r?\n/u);
91
+ return result.exitCode === 0 && root !== undefined && branch !== undefined ? `${basename(root)}/${branch}` : fallback;
90
92
  }
91
93
  catch {
92
- return undefined;
94
+ return fallback;
93
95
  }
94
96
  }
95
97
  function usage(raw, expected) {
@@ -165,9 +167,9 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
165
167
  address: { kind: 'session', sessionId }, maxMessages: 50,
166
168
  }, abort.signal)[Symbol.asyncIterator]();
167
169
  const controlIterator = ctx.sessionController.control(abort.signal)[Symbol.asyncIterator]();
168
- const [opening, controlOpening, subagents, branch] = await Promise.all([
170
+ const [opening, controlOpening, subagents, repo] = await Promise.all([
169
171
  firstSnapshot(iterator), firstControl(controlIterator), subagentViews(ctx, sessionId, abort.signal),
170
- gitBranch(ctx, agent, abort.signal),
172
+ gitRepo(ctx, agent, abort.signal),
171
173
  ]);
172
174
  title ??= projectionString(opening.projections, 'title');
173
175
  const selected = projectionModel(opening.projections);
@@ -179,7 +181,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
179
181
  return {
180
182
  abort,
181
183
  agent,
182
- branch,
184
+ repo,
183
185
  controlIterator,
184
186
  cursor: opening.cursor,
185
187
  disposers,
@@ -289,11 +291,11 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
289
291
  });
290
292
  }, 34);
291
293
  };
292
- const refreshBranch = async (bound) => {
293
- const branch = await gitBranch(ctx, bound.agent, bound.abort.signal);
294
- if (bound.abort.signal.aborted || current() !== bound || bound.branch === branch)
294
+ const refreshRepo = async (bound) => {
295
+ const repo = await gitRepo(ctx, bound.agent, bound.abort.signal);
296
+ if (bound.abort.signal.aborted || current() !== bound || bound.repo === repo)
295
297
  return;
296
- bound.branch = branch;
298
+ bound.repo = repo;
297
299
  dispatch({ type: 'redraw' });
298
300
  };
299
301
  const consume = (bound, frame) => {
@@ -302,7 +304,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
302
304
  return;
303
305
  bound.events.push(event);
304
306
  if (event.type === 'turn/end') {
305
- void refreshBranch(bound);
307
+ void refreshRepo(bound);
306
308
  const startedAt = turnStartTime(bound.events, event.data.turn);
307
309
  if (startedAt !== undefined) {
308
310
  dispatch({
@@ -327,7 +329,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
327
329
  ...(model.reasoningEffort === undefined ? {} : { effort: model.reasoningEffort }),
328
330
  });
329
331
  }
330
- const prompt = humanPrompt(event);
332
+ const prompt = event.type === 'agent/inbox/spliced' ? undefined : historyInput(event);
331
333
  if (prompt !== undefined)
332
334
  dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: prompt });
333
335
  schedule(bound);
@@ -406,7 +408,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
406
408
  tuiRoot.bind(candidate.agent);
407
409
  dispatch({
408
410
  type: 'root-bound', cells: transcriptCells(candidate),
409
- hasMore: candidate.hasMore, prompts: humanPrompts(candidate.events), root: candidate.root,
411
+ hasMore: candidate.hasMore, prompts: historyInputs(candidate.events), root: candidate.root,
410
412
  });
411
413
  if (started) {
412
414
  candidate.pump = follow(candidate);
@@ -505,7 +507,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
505
507
  const roller = ctx.commands.list(bound.agent).some(command => command.name === 'roller-restore');
506
508
  const overlay = rewindOverlay(bound.agent.session.snapshotEvents(), roller);
507
509
  if (overlay.kind !== 'list' || overlay.options.length === 0) {
508
- throw new Error('the current session has no human prompt to rewind');
510
+ throw new Error('the current session has no input to rewind');
509
511
  }
510
512
  dispatch({ type: 'open-overlay', overlay });
511
513
  return;
@@ -795,7 +797,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
795
797
  if (invocation.rawInput.trim() === '')
796
798
  return { kind: 'error', text: 'usage: /plugin ARGS' };
797
799
  const command = `dsh plugin --profile ${quoteShellWord(decodeURIComponent(new URL(ctx.baseUrl).pathname).split('/').at(-2))}${invocation.rawInput}`;
798
- const [message, result] = await bound.agent.runMaintenance(signal => runHumanShell(ctx, bound.agent, command, signal, 'changes load on the next launch; exit and run dashi again', false));
800
+ const [message, result] = await bound.agent.runMaintenance(signal => runHumanShell(ctx, bound.agent, command, signal, 'bundles load on the next launch; plain plugins need a patch row', false));
799
801
  bound.agent.inject(message);
800
802
  await ctx.sessions.flush(bound.agent.session);
801
803
  publish(bound);
@@ -1277,9 +1279,9 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
1277
1279
  initialAttachments,
1278
1280
  get initialCells() { return transcriptCells(current()); },
1279
1281
  get initialHasMore() { return current().hasMore; },
1280
- get initialPrompts() { return humanPrompts(current().events); },
1282
+ get initialPrompts() { return historyInputs(current().events); },
1281
1283
  get root() { return current().root; },
1282
- get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.branch); },
1284
+ get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.repo); },
1283
1285
  get summary() { return `Resume with: dsh --profile dashi --resume ${current().root.id}`; },
1284
1286
  activate,
1285
1287
  async attach(path) {
@@ -1428,6 +1430,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
1428
1430
  bound.agent.inject(message);
1429
1431
  await ctx.sessions.flush(bound.agent.session);
1430
1432
  });
1433
+ dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: `!${command}` });
1431
1434
  publish(bound);
1432
1435
  return;
1433
1436
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antst/dashi",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.14",
4
4
  "description": "A terminal UI for DeepSeek Harness",
5
5
  "publishConfig": {
6
6
  "access": "public"