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

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.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');
@@ -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/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,24 @@ 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
+ /** Recover one composer input from DSH's durable log. */
11
+ export function historyInput(event) {
12
+ const prompt = humanPrompt(event);
13
+ if (prompt !== undefined)
14
+ return prompt;
15
+ if (event.type === 'command/run' && event.data.args !== undefined) {
16
+ return `/${event.data.name}${event.data.args}`;
17
+ }
18
+ if (event.type !== 'user/message' || event.data.source.kind !== 'plugin'
19
+ || event.data.source.plugin !== 'dashi' || event.data.source.form !== 'notice')
20
+ return undefined;
21
+ const first = event.data.content.find(block => block.type === 'text')?.text.split(/\r?\n/u)[0];
22
+ return first?.startsWith('$ ') === true ? `!${first.slice(2)}` : undefined;
12
23
  }
13
- /** Derive each human prompt and its controller-valid containing-turn cut. */
24
+ export function historyInputs(events) {
25
+ return events.flatMap(event => historyInput(event) ?? []);
26
+ }
27
+ /** Derive each recalled input and its controller-valid preceding-turn cut. */
14
28
  export function rewindBoundaries(events) {
15
29
  const boundaries = [];
16
30
  let previousEnd;
@@ -21,22 +35,22 @@ export function rewindBoundaries(events) {
21
35
  openTurn = event.data.turn;
22
36
  prompted = false;
23
37
  }
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
38
  else if (event.type === 'turn/end') {
36
39
  previousEnd = event;
37
40
  openTurn = undefined;
38
41
  prompted = false;
39
42
  }
43
+ const prompt = historyInput(event);
44
+ if (prompt === undefined)
45
+ continue;
46
+ const human = humanPrompt(event) !== undefined;
47
+ boundaries.push({
48
+ ...(previousEnd === undefined ? {} : { atSeq: previousEnd.seq }),
49
+ label: `${prompt.replaceAll(/\s+/gu, ' ')}${human && prompted ? ' · mid-turn' : ''}`,
50
+ prompt,
51
+ });
52
+ if (human && openTurn !== undefined)
53
+ prompted = true;
40
54
  }
41
55
  return boundaries;
42
56
  }
@@ -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';
@@ -327,7 +327,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
327
327
  ...(model.reasoningEffort === undefined ? {} : { effort: model.reasoningEffort }),
328
328
  });
329
329
  }
330
- const prompt = humanPrompt(event);
330
+ const prompt = historyInput(event);
331
331
  if (prompt !== undefined)
332
332
  dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: prompt });
333
333
  schedule(bound);
@@ -406,7 +406,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
406
406
  tuiRoot.bind(candidate.agent);
407
407
  dispatch({
408
408
  type: 'root-bound', cells: transcriptCells(candidate),
409
- hasMore: candidate.hasMore, prompts: humanPrompts(candidate.events), root: candidate.root,
409
+ hasMore: candidate.hasMore, prompts: historyInputs(candidate.events), root: candidate.root,
410
410
  });
411
411
  if (started) {
412
412
  candidate.pump = follow(candidate);
@@ -505,7 +505,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
505
505
  const roller = ctx.commands.list(bound.agent).some(command => command.name === 'roller-restore');
506
506
  const overlay = rewindOverlay(bound.agent.session.snapshotEvents(), roller);
507
507
  if (overlay.kind !== 'list' || overlay.options.length === 0) {
508
- throw new Error('the current session has no human prompt to rewind');
508
+ throw new Error('the current session has no input to rewind');
509
509
  }
510
510
  dispatch({ type: 'open-overlay', overlay });
511
511
  return;
@@ -795,7 +795,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
795
795
  if (invocation.rawInput.trim() === '')
796
796
  return { kind: 'error', text: 'usage: /plugin ARGS' };
797
797
  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));
798
+ 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
799
  bound.agent.inject(message);
800
800
  await ctx.sessions.flush(bound.agent.session);
801
801
  publish(bound);
@@ -1277,7 +1277,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
1277
1277
  initialAttachments,
1278
1278
  get initialCells() { return transcriptCells(current()); },
1279
1279
  get initialHasMore() { return current().hasMore; },
1280
- get initialPrompts() { return humanPrompts(current().events); },
1280
+ get initialPrompts() { return historyInputs(current().events); },
1281
1281
  get root() { return current().root; },
1282
1282
  get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.branch); },
1283
1283
  get summary() { return `Resume with: dsh --profile dashi --resume ${current().root.id}`; },
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.13",
4
4
  "description": "A terminal UI for DeepSeek Harness",
5
5
  "publishConfig": {
6
6
  "access": "public"