@antst/dashi 0.1.0-alpha.11 → 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.
@@ -27,6 +27,7 @@ export interface TerminalShellOptions {
27
27
  readonly onReleased?: () => void;
28
28
  readonly pasteImage?: () => Promise<DraftAttachment>;
29
29
  readonly search?: (query: string, rootId: string) => Promise<void>;
30
+ readonly statusLine?: () => string | undefined;
30
31
  readonly submit?: (text: string, mode: 'next-turn' | 'steer', attachments: readonly DraftAttachment[]) => Promise<void> | void;
31
32
  readonly suspend?: () => Promise<void>;
32
33
  readonly writeError?: (message: string) => void;
@@ -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); });
package/lib/catalogs.d.ts CHANGED
@@ -14,6 +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
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. */
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,6 +177,20 @@ function number(value) {
156
177
  function milliseconds(value) {
157
178
  return typeof value === 'number' ? `${number(value)} ms` : 'unavailable';
158
179
  }
180
+ export function sessionStatusLine(ctx, agent, root, branch) {
181
+ const { modelSelection, permissions, tokenUsage, contextPressure } = ctx.sessionProjections
182
+ .snapshot(agent.session, ['modelSelection', 'permissions', 'tokenUsage', 'contextPressure']).values;
183
+ const model = modelSelection?.next ?? modelSelection?.lastUsed;
184
+ const input = tokenUsage === undefined ? 0 : tokenUsage.uncachedInputTokens + tokenUsage.cacheReadTokens + tokenUsage.cacheWriteTokens;
185
+ const used = contextPressure?.projectedTokens ?? contextPressure?.pressureTokens;
186
+ return [
187
+ `model ${model == null ? `${root.provider === undefined ? '' : `${root.provider}/`}${root.model}` : `${model.provider}/${model.model}`}`,
188
+ `permission ${permissions?.currentValue ?? root.permission ?? 'unknown'}`,
189
+ ...(used === undefined || contextPressure?.contextWindow === undefined ? [] : [`context ${number(used)}/${number(contextPressure.contextWindow)}`]),
190
+ ...(input === 0 ? [] : [`cache ${String(Math.min(tokenUsage?.cacheReadTokens === input ? 100 : 99, Math.round((tokenUsage?.cacheReadTokens ?? 0) / input * 100)))}%`]),
191
+ ...(branch === undefined ? [] : [`branch ${branch}`]),
192
+ ].join(' · ');
193
+ }
159
194
  /** Read DSH-owned root facts for /status in one projection cut. */
160
195
  export function statusOverlay(ctx, agent, root) {
161
196
  const values = ctx.sessionProjections.snapshot(agent.session, [
@@ -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/index.js CHANGED
@@ -299,6 +299,7 @@ export function apply(ctx) {
299
299
  onReleased: () => { process.stdout.write(`${runtime?.summary ?? ''}\n`); },
300
300
  pasteImage: readClipboardImage,
301
301
  search: (query, rootId) => runtime?.search(query, rootId) ?? Promise.resolve(),
302
+ statusLine: () => runtime?.statusLine,
302
303
  submit: (text, mode, attachments) => runtime?.submit(text, mode, attachments),
303
304
  });
304
305
  shell.start();
package/lib/input.d.ts CHANGED
@@ -1,5 +1,5 @@
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;
package/lib/input.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isKeyRelease, matchesKey } from '@earendil-works/pi-tui';
2
+ import { PAGE_CELLS } from './state.js';
2
3
  export function caretOffset(text, cursor) {
3
4
  const lines = text.split('\n');
4
5
  let offset = 0;
@@ -98,6 +99,10 @@ export function installInput(tui, bindings) {
98
99
  bindings.dispatch({ type: 'search-open', scope: 'history' });
99
100
  else if (overlay.kind === 'history' && matchesKey(data, 'y'))
100
101
  bindings.dispatch({ type: 'history-copy' });
102
+ else if (overlay.kind === 'info' && matchesKey(data, 'pageUp'))
103
+ bindings.dispatch({ type: 'overlay-move', offset: -PAGE_CELLS });
104
+ else if (overlay.kind === 'info' && matchesKey(data, 'pageDown'))
105
+ bindings.dispatch({ type: 'overlay-move', offset: PAGE_CELLS });
101
106
  else if (matchesKey(data, 'up'))
102
107
  bindings.dispatch({ type: 'overlay-move', offset: -1 });
103
108
  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
  }
package/lib/renderer.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Editor, TuiAltScreen, TuiMainScreen, stripTerminalSequences, truncateToWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
1
+ import { Editor, Markdown, TuiAltScreen, TuiMainScreen, stripTerminalSequences, truncateToWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
2
2
  import { caretOffset, installInput } from './input.js';
3
3
  import { hasCompletionTrigger } from './completion-trigger.js';
4
4
  import { createProcessTerminal } from './terminal-guard.js';
@@ -15,6 +15,22 @@ const THEME_GENERATION = 1;
15
15
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
16
16
  const SPINNER_INTERVAL_MS = 80;
17
17
  const MASK_TOKEN = new RegExp(String.raw `\u001B(?:\[[0-?]*[ -/]*[@-~]|[_P^X][^\u0007]*(?:\u0007|\u001B\\))|[^\s]`, 'gu');
18
+ const markdownTheme = {
19
+ heading: text => `\u001B[36m${text}${RESET}`,
20
+ link: text => `\u001B[36m${text}${RESET}`,
21
+ linkUrl: text => `${DIM}${text}${RESET}`,
22
+ code: text => `${YELLOW}${text}${RESET}`,
23
+ codeBlock: text => `${GREEN}${text}${RESET}`,
24
+ codeBlockBorder: text => `${DIM}${text}${RESET}`,
25
+ quote: text => `${DIM}${text}${RESET}`,
26
+ quoteBorder: text => `${DIM}${text}${RESET}`,
27
+ hr: text => `${DIM}${text}${RESET}`,
28
+ listBullet: text => `\u001B[36m${text}${RESET}`,
29
+ bold: text => `\u001B[1m${text}\u001B[22m`,
30
+ italic: text => `\u001B[3m${text}\u001B[23m`,
31
+ strikethrough: text => `\u001B[9m${text}\u001B[29m`,
32
+ underline: text => `\u001B[4m${text}\u001B[24m`,
33
+ };
18
34
  export function supportsOsc52(terminalType) {
19
35
  return terminalType?.toLocaleLowerCase() !== 'dumb';
20
36
  }
@@ -138,6 +154,22 @@ function plain(text, preserveNewlines = false) {
138
154
  function line(text, width) {
139
155
  return truncateToWidth(text, Math.max(1, width), '…');
140
156
  }
157
+ function windowRows(values, size, position, follow) {
158
+ if (values.length <= size)
159
+ return [...values];
160
+ if (size < 3)
161
+ return [values[Math.max(0, Math.min(values.length - 1, position))] ?? ''];
162
+ const start = Math.max(0, Math.min(values.length - Math.max(1, size - 1), follow ? position - Math.floor((size - 2) / 2) : position));
163
+ const above = start > 0;
164
+ let end = Math.min(values.length, start + size - Number(above));
165
+ if (end < values.length)
166
+ end--;
167
+ return [
168
+ ...(above ? [`${DIM}↑ ${String(start)} more${RESET}`] : []),
169
+ ...values.slice(start, end),
170
+ ...(end < values.length ? [`${DIM}↓ ${String(values.length - end)} more${RESET}`] : []),
171
+ ];
172
+ }
141
173
  function theme() {
142
174
  const identity = (text) => text;
143
175
  return {
@@ -160,7 +192,9 @@ export function createRenderer(options) {
160
192
  const osc52 = supportsOsc52(terminalType);
161
193
  const tui = options.inline
162
194
  ? new TuiMainScreen(terminal, true)
163
- : new TuiAltScreen(terminal, true, undefined, { mouse: true });
195
+ : new class extends TuiAltScreen {
196
+ isOverlayFocused() { return options.readState().overlay !== undefined || super.isOverlayFocused(); }
197
+ }(terminal, true, undefined, { mouse: true });
164
198
  let editor = new Editor(tui, theme(), { paddingX: 1 });
165
199
  editor.disableSubmit = true;
166
200
  editor.onChange = text => {
@@ -184,7 +218,11 @@ export function createRenderer(options) {
184
218
  const cached = cell.pending === true ? undefined : lineCache.get(key);
185
219
  if (cached !== undefined)
186
220
  return cached;
187
- const rendered = wrapTextWithAnsi(cellText(cell, toolMode, accessible, now), Math.max(1, width - 2)).map(value => ` ${value}`);
221
+ const cellWidth = Math.max(1, width - 2);
222
+ const lines = !accessible && cell.kind === 'assistant' && cell.pending !== true
223
+ ? [`${BOLD_CYAN}Assistant${RESET}`, ...new Markdown(plain(cell.text, true), 0, 0, markdownTheme).render(cellWidth).map(value => value.trimEnd())]
224
+ : wrapTextWithAnsi(cellText(cell, toolMode, accessible, now), cellWidth);
225
+ const rendered = lines.map(value => ` ${value}`);
188
226
  if (cell.pending !== true) {
189
227
  lineCache.set(key, rendered);
190
228
  if (lineCache.size > 512)
@@ -275,19 +313,19 @@ export function createRenderer(options) {
275
313
  result.push(`${DIM}↑↓ select · Enter details · Esc close${RESET}`);
276
314
  return result.slice(0, Math.max(2, rows - 3));
277
315
  };
316
+ const infoRows = (overlay, state, width) => overlay.lines.flatMap(value => wrapTextWithAnsi(plain(value, true), width)).concat(overlay.cells?.flatMap(cell => wrapTextWithAnsi(cellText(cell, state.toolMode, accessible), width)) ?? []);
278
317
  const overlayPanel = (overlay, state, width, rows) => {
279
318
  if (overlay.kind === 'history')
280
319
  return historyPanel(overlay, state, width, rows);
281
320
  if (overlay.kind === 'details')
282
321
  return detailsPanel(overlay, state, width, rows);
283
322
  if (overlay.kind === 'info') {
284
- const cards = overlay.cells?.flatMap(cell => wrapTextWithAnsi(cellText(cell, state.toolMode, accessible), width)) ?? [];
323
+ const body = infoRows(overlay, state, width);
285
324
  return [
286
325
  `${BOLD_CYAN}${announcement(plain(overlay.title), overlay.lines.length + (overlay.cells?.length ?? 0))}${RESET}`,
287
- ...overlay.lines.flatMap(value => wrapTextWithAnsi(plain(value, true), width)),
288
- ...cards,
289
- `${DIM}Enter or Esc to close${RESET}`,
290
- ].slice(0, Math.max(2, rows - 3));
326
+ ...windowRows(body, Math.max(1, rows - 5), overlay.scrollOffset ?? 0, false),
327
+ `${DIM}↑↓ scroll · PageUp/PageDown · Enter or Esc close${RESET}`,
328
+ ];
291
329
  }
292
330
  if (overlay.kind === 'confirm') {
293
331
  const choices = [overlay.acceptLabel, 'Cancel'];
@@ -303,18 +341,23 @@ export function createRenderer(options) {
303
341
  line(`${BOLD_CYAN}${announcement(plain(overlay.title), overlay.options.length, selected === undefined ? undefined : plain(selected.label), overlay.cursor)}${RESET}`, width),
304
342
  ...(overlay.notice === undefined ? [] : [`${DIM}${plain(overlay.notice)}${RESET}`]),
305
343
  ];
344
+ const body = [];
345
+ let selectedRow = 0;
306
346
  let group;
307
347
  for (const [index, option] of overlay.options.entries()) {
308
348
  if (option.group !== undefined && option.group !== group) {
309
349
  group = option.group;
310
- result.push(`${DIM}${plain(group)}${RESET}`);
350
+ body.push(`${DIM}${plain(group)}${RESET}`);
311
351
  }
312
352
  const detail = option.detail === undefined ? '' : ` — ${plain(option.detail)}`;
313
353
  const active = option.active === true ? accessible ? ' [active]' : ' ●' : '';
314
354
  const danger = option.danger === true ? ` ${YELLOW}${accessible ? '[danger]' : '!'}${RESET}` : '';
315
- result.push(line(`${index === overlay.cursor ? BOLD_CYAN : DIM}${index === overlay.cursor ? accessible ? '>' : '›' : ' '} ${String(index + 1)} ${plain(option.label)}${active}${detail}${RESET}${danger}`, width));
355
+ if (index === overlay.cursor)
356
+ selectedRow = body.length;
357
+ body.push(line(`${index === overlay.cursor ? BOLD_CYAN : DIM}${index === overlay.cursor ? accessible ? '>' : '›' : ' '} ${String(index + 1)} ${plain(option.label)}${active}${detail}${RESET}${danger}`, width));
316
358
  }
317
- return result.slice(0, Math.max(2, rows - (overlay.purpose === 'completion' ? 6 : 3)));
359
+ const limit = Math.max(2, rows - (overlay.purpose === 'completion' ? 6 : 3));
360
+ return [...result, ...windowRows(body, Math.max(1, limit - result.length), selectedRow, true)];
318
361
  };
319
362
  const document = {
320
363
  invalidate() { editor.invalidate(); },
@@ -368,6 +411,11 @@ export function createRenderer(options) {
368
411
  const attachmentRows = state.attachments.map((attachment, index) => line(`${YELLOW}[image ${String(index + 1)}]${RESET} ${plain(attachment.name)} · ${attachment.mediaType} · ${String(attachment.bytes)} B · ${plain(attachment.path)}`, width));
369
412
  const activityRows = decision !== undefined || state.search !== undefined || state.overlay !== undefined
370
413
  ? [] : activities(state).map(item => line(`${DIM}${activityText(item, now)}${RESET}`, width));
414
+ const persistentStatus = accessible || state.exitArmed ? undefined : options.statusLine?.();
415
+ const composer = () => [
416
+ ...(persistentStatus === undefined ? [] : [line(`${DIM}${plain(persistentStatus)}${RESET}`, width)]),
417
+ ...editor.render(width),
418
+ ];
371
419
  const control = decision !== undefined
372
420
  ? decisionPanel(decision, width, rows)
373
421
  : state.search !== undefined
@@ -379,11 +427,11 @@ export function createRenderer(options) {
379
427
  `${DIM}${state.search.matches.length === 0 ? 'no matches' : `${String(state.search.cursor + 1)}/${String(state.search.matches.length)}`} · Enter next · Shift+Enter previous${RESET}`,
380
428
  ]
381
429
  : state.overlay === undefined
382
- ? [...activityRows, ...attachmentRows, ...editor.render(width)]
430
+ ? [...activityRows, ...attachmentRows, ...composer()]
383
431
  : [
384
432
  ...overlayPanel(state.overlay, state, width, rows),
385
433
  ...(state.overlay.kind === 'list' && state.overlay.purpose === 'completion'
386
- ? [...attachmentRows, ...editor.render(width)]
434
+ ? [...attachmentRows, ...composer()]
387
435
  : []),
388
436
  ];
389
437
  const fixedRows = header.length + control.length + 2;
@@ -405,6 +453,13 @@ export function createRenderer(options) {
405
453
  tui.setFocus(editor);
406
454
  installInput(tui, {
407
455
  ...options,
456
+ dispatch: action => {
457
+ const overlay = options.readState().overlay;
458
+ const size = Math.max(1, terminal.rows - 5);
459
+ options.dispatch(action.type === 'overlay-move' && overlay?.kind === 'info'
460
+ ? { ...action, limit: Math.max(0, infoRows(overlay, options.readState(), terminal.columns).length - Math.max(1, size - 1)) }
461
+ : action);
462
+ },
408
463
  insertNewline: () => { editor.insertTextAtCursor('\n'); },
409
464
  readComposerCursor: () => editor.getCursor(),
410
465
  });
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
  }
@@ -28,6 +28,7 @@ export interface SessionRuntime {
28
28
  readonly initialHasMore: boolean;
29
29
  readonly initialPrompts: readonly string[];
30
30
  readonly root: RootView;
31
+ readonly statusLine: string;
31
32
  readonly summary: string;
32
33
  activate(value: ActivatableOverlayValue): Promise<void>;
33
34
  attach(path: string): Promise<DraftAttachment | undefined>;
@@ -4,12 +4,12 @@ import { parseCredentialKey } from '@deepseek-ai/dsh-credentials';
4
4
  import { JobId } from '@deepseek-ai/dsh-jobs';
5
5
  import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm';
6
6
  import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
7
- import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, skillOverlay, statusOverlay, usageOverlay, } from './catalogs.js';
7
+ import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionStatusLine, sessionOverlay, skillOverlay, statusOverlay, usageOverlay, } from './catalogs.js';
8
8
  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,6 +80,18 @@ async function firstControl(iterator) {
80
80
  }
81
81
  return first.value;
82
82
  }
83
+ async function gitBranch(ctx, agent, signal) {
84
+ try {
85
+ 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,
88
+ }));
89
+ return result.exitCode === 0 && result.stdout.text.trim() !== '' ? result.stdout.text.trim() : undefined;
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
83
95
  function usage(raw, expected) {
84
96
  return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
85
97
  }
@@ -153,8 +165,9 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
153
165
  address: { kind: 'session', sessionId }, maxMessages: 50,
154
166
  }, abort.signal)[Symbol.asyncIterator]();
155
167
  const controlIterator = ctx.sessionController.control(abort.signal)[Symbol.asyncIterator]();
156
- const [opening, controlOpening, subagents] = await Promise.all([
168
+ const [opening, controlOpening, subagents, branch] = await Promise.all([
157
169
  firstSnapshot(iterator), firstControl(controlIterator), subagentViews(ctx, sessionId, abort.signal),
170
+ gitBranch(ctx, agent, abort.signal),
158
171
  ]);
159
172
  title ??= projectionString(opening.projections, 'title');
160
173
  const selected = projectionModel(opening.projections);
@@ -166,6 +179,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
166
179
  return {
167
180
  abort,
168
181
  agent,
182
+ branch,
169
183
  controlIterator,
170
184
  cursor: opening.cursor,
171
185
  disposers,
@@ -275,12 +289,20 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
275
289
  });
276
290
  }, 34);
277
291
  };
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)
295
+ return;
296
+ bound.branch = branch;
297
+ dispatch({ type: 'redraw' });
298
+ };
278
299
  const consume = (bound, frame) => {
279
300
  const [event] = eventsFromRecords([frame]);
280
301
  if (event === undefined)
281
302
  return;
282
303
  bound.events.push(event);
283
304
  if (event.type === 'turn/end') {
305
+ void refreshBranch(bound);
284
306
  const startedAt = turnStartTime(bound.events, event.data.turn);
285
307
  if (startedAt !== undefined) {
286
308
  dispatch({
@@ -305,7 +327,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
305
327
  ...(model.reasoningEffort === undefined ? {} : { effort: model.reasoningEffort }),
306
328
  });
307
329
  }
308
- const prompt = humanPrompt(event);
330
+ const prompt = historyInput(event);
309
331
  if (prompt !== undefined)
310
332
  dispatch({ type: 'prompt-recorded', rootId: bound.root.id, text: prompt });
311
333
  schedule(bound);
@@ -384,7 +406,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
384
406
  tuiRoot.bind(candidate.agent);
385
407
  dispatch({
386
408
  type: 'root-bound', cells: transcriptCells(candidate),
387
- hasMore: candidate.hasMore, prompts: humanPrompts(candidate.events), root: candidate.root,
409
+ hasMore: candidate.hasMore, prompts: historyInputs(candidate.events), root: candidate.root,
388
410
  });
389
411
  if (started) {
390
412
  candidate.pump = follow(candidate);
@@ -483,7 +505,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
483
505
  const roller = ctx.commands.list(bound.agent).some(command => command.name === 'roller-restore');
484
506
  const overlay = rewindOverlay(bound.agent.session.snapshotEvents(), roller);
485
507
  if (overlay.kind !== 'list' || overlay.options.length === 0) {
486
- throw new Error('the current session has no human prompt to rewind');
508
+ throw new Error('the current session has no input to rewind');
487
509
  }
488
510
  dispatch({ type: 'open-overlay', overlay });
489
511
  return;
@@ -773,7 +795,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
773
795
  if (invocation.rawInput.trim() === '')
774
796
  return { kind: 'error', text: 'usage: /plugin ARGS' };
775
797
  const command = `dsh plugin --profile ${quoteShellWord(decodeURIComponent(new URL(ctx.baseUrl).pathname).split('/').at(-2))}${invocation.rawInput}`;
776
- 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));
777
799
  bound.agent.inject(message);
778
800
  await ctx.sessions.flush(bound.agent.session);
779
801
  publish(bound);
@@ -1255,8 +1277,9 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
1255
1277
  initialAttachments,
1256
1278
  get initialCells() { return transcriptCells(current()); },
1257
1279
  get initialHasMore() { return current().hasMore; },
1258
- get initialPrompts() { return humanPrompts(current().events); },
1280
+ get initialPrompts() { return historyInputs(current().events); },
1259
1281
  get root() { return current().root; },
1282
+ get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.branch); },
1260
1283
  get summary() { return `Resume with: dsh --profile dashi --resume ${current().root.id}`; },
1261
1284
  activate,
1262
1285
  async attach(path) {
package/lib/state.d.ts CHANGED
@@ -216,6 +216,7 @@ export type Overlay = {
216
216
  readonly cells?: readonly TerminalCell[];
217
217
  readonly kind: 'info';
218
218
  readonly lines: readonly string[];
219
+ readonly scrollOffset?: number;
219
220
  readonly title: string;
220
221
  } | {
221
222
  readonly cursor: number;
@@ -346,6 +347,7 @@ export type UiAction = {
346
347
  } | {
347
348
  readonly type: 'overlay-close';
348
349
  } | {
350
+ readonly limit?: number;
349
351
  readonly type: 'overlay-move';
350
352
  readonly offset: number;
351
353
  } | {
@@ -508,4 +510,5 @@ export type UiEffect = {
508
510
  };
509
511
  export declare function initialViewState(cwd: string, inline: boolean, root?: RootView, cells?: readonly TerminalCell[], prompts?: readonly string[], historyHasMore?: boolean, attachments?: readonly DraftAttachment[], toolMode?: ToolMode): ViewState;
510
512
  export declare const TURN_BELL_THRESHOLD_MS = 10000;
513
+ export declare const PAGE_CELLS = 8;
511
514
  export declare function reduce(state: ViewState, action: UiAction): readonly [ViewState, readonly UiEffect[]];
package/lib/state.js CHANGED
@@ -55,7 +55,7 @@ function moveOverlay(overlay, cursor, detailsCount = 0) {
55
55
  : Math.max(0, overlay.options.length - 1);
56
56
  return { ...overlay, cursor: Math.max(0, Math.min(limit, cursor)) };
57
57
  }
58
- const PAGE_CELLS = 8;
58
+ export const PAGE_CELLS = 8;
59
59
  function matches(cells, query) {
60
60
  const needle = query.trim().toLocaleLowerCase();
61
61
  if (needle === '')
@@ -376,8 +376,14 @@ export function reduce(state, action) {
376
376
  case 'overlay-move': {
377
377
  if (state.overlay === undefined)
378
378
  return [state, []];
379
+ if (state.overlay.kind === 'info') {
380
+ const current = state.overlay.scrollOffset ?? 0;
381
+ const scrollOffset = Math.max(0, Math.min(action.limit ?? current, current + action.offset));
382
+ return scrollOffset === current ? [state, []]
383
+ : [{ ...state, overlay: { ...state.overlay, scrollOffset } }, redraw()];
384
+ }
379
385
  const count = (state.root?.jobs?.length ?? 0) + (state.root?.subagents?.length ?? 0);
380
- const overlay = moveOverlay(state.overlay, state.overlay.kind === 'info' ? 0 : state.overlay.cursor + action.offset, count);
386
+ const overlay = moveOverlay(state.overlay, state.overlay.cursor + action.offset, count);
381
387
  return overlay === state.overlay ? [state, []] : [{ ...state, overlay }, redraw()];
382
388
  }
383
389
  case 'overlay-number': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antst/dashi",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "A terminal UI for DeepSeek Harness",
5
5
  "publishConfig": {
6
6
  "access": "public"