@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/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(); },
@@ -358,8 +401,8 @@ export function createRenderer(options) {
358
401
  : root === undefined
359
402
  ? `${DIM}no session · cards ${state.toolMode} · Ctrl+J newline · Ctrl+L redraw${RESET}`
360
403
  : root.status === 'running'
361
- ? `${DIM}running · ${plain(root.model)} · Enter ${state.sendMode === 'steer' ? 'steer' : 'queue next turn'} · Ctrl+T toggle · cards ${state.toolMode} · Esc interrupt${RESET}`
362
- : `${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}`;
363
406
  const activityCount = activities(state).length;
364
407
  const baseStatus = `${baseStatusText}${root?.contextPercent === undefined ? '' : ` · ${String(root.contextPercent)}% context`}${activityCount === 0 ? '' : ' · Ctrl+B activity'}`;
365
408
  const status = state.newOutput === 0 ? baseStatus
@@ -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,7 +453,15 @@ 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'); },
464
+ insertText: text => { editor.insertTextAtCursor(text); },
409
465
  readComposerCursor: () => editor.getCursor(),
410
466
  });
411
467
  return {
package/lib/rewind.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ModelSelection } from '@deepseek-ai/dsh-api-session-controller';
1
2
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
2
3
  import type { Overlay, OverlayValue } from './state.js';
3
4
  export interface RewindBoundary {
@@ -5,10 +6,22 @@ export interface RewindBoundary {
5
6
  readonly label: string;
6
7
  readonly prompt: string;
7
8
  }
9
+ export declare function latestCompletedTurn(events: readonly SessionEvent[]): {
10
+ readonly atSeq?: number;
11
+ readonly open: boolean;
12
+ readonly turn?: number;
13
+ };
14
+ /** Return only a selection that DSH recorded for the source session. */
15
+ export declare function rewindModelSelection(projection: {
16
+ readonly lastUsed: ModelSelection | null;
17
+ readonly next: ModelSelection | null;
18
+ } | undefined): ModelSelection | undefined;
8
19
  /** Return the text of a direct human message, excluding injected context. */
9
20
  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. */
21
+ /** Recover one composer input from DSH's durable log. */
22
+ export declare function historyInput(event: SessionEvent): string | undefined;
23
+ export declare function historyInputs(events: readonly SessionEvent[]): readonly string[];
24
+ /** Derive each recalled input and its controller-valid preceding-turn cut. */
12
25
  export declare function rewindBoundaries(events: readonly SessionEvent[]): readonly RewindBoundary[];
13
26
  export declare function rewindOverlay(events: readonly SessionEvent[], roller: boolean): Overlay;
14
27
  export declare function rewindActionOverlay(boundary: Extract<OverlayValue, {
package/lib/rewind.js CHANGED
@@ -1,3 +1,13 @@
1
+ export function latestCompletedTurn(events) {
2
+ const end = events.filter((event) => event.type === 'turn/end' && event.data.reason.kind !== 'interrupted').at(-1);
3
+ const start = events.filter((event) => event.type === 'turn/start').at(-1);
4
+ return { open: start !== undefined && (end === undefined || start.seq > end.seq),
5
+ ...(end === undefined ? {} : { atSeq: end.seq, turn: end.data.turn }) };
6
+ }
7
+ /** Return only a selection that DSH recorded for the source session. */
8
+ export function rewindModelSelection(projection) {
9
+ return projection?.next ?? projection?.lastUsed ?? undefined;
10
+ }
1
11
  /** Return the text of a direct human message, excluding injected context. */
2
12
  export function humanPrompt(event) {
3
13
  if (event.type !== 'user/message' || event.data.source.kind !== 'user')
@@ -7,10 +17,29 @@ export function humanPrompt(event) {
7
17
  .map(block => block.text).join('');
8
18
  return text === '' ? undefined : text;
9
19
  }
10
- export function humanPrompts(events) {
11
- return events.flatMap(event => humanPrompt(event) ?? []);
20
+ function shellInput(message) {
21
+ if (message.source.kind !== 'plugin' || message.source.plugin !== 'dashi'
22
+ || message.source.form !== 'notice')
23
+ return undefined;
24
+ const first = message.content.find(block => block.type === 'text')?.text.split(/\r?\n/u)[0];
25
+ return first?.startsWith('$ ') === true ? `!${first.slice(2)}` : undefined;
26
+ }
27
+ /** Recover one composer input from DSH's durable log. */
28
+ export function historyInput(event) {
29
+ const prompt = humanPrompt(event);
30
+ if (prompt !== undefined)
31
+ return prompt;
32
+ if (event.type === 'command/run' && event.data.args !== undefined) {
33
+ return `/${event.data.name}${event.data.args}`;
34
+ }
35
+ if (event.type !== 'agent/inbox/spliced')
36
+ return undefined;
37
+ return event.data.inserted.map(shellInput).find(input => input !== undefined);
12
38
  }
13
- /** Derive each human prompt and its controller-valid containing-turn cut. */
39
+ export function historyInputs(events) {
40
+ return events.flatMap(event => historyInput(event) ?? []);
41
+ }
42
+ /** Derive each recalled input and its controller-valid preceding-turn cut. */
14
43
  export function rewindBoundaries(events) {
15
44
  const boundaries = [];
16
45
  let previousEnd;
@@ -21,27 +50,28 @@ export function rewindBoundaries(events) {
21
50
  openTurn = event.data.turn;
22
51
  prompted = false;
23
52
  }
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
- else if (event.type === 'turn/end') {
53
+ else if (event.type === 'turn/end' && event.data.reason.kind !== 'interrupted') {
36
54
  previousEnd = event;
37
55
  openTurn = undefined;
38
56
  prompted = false;
39
57
  }
58
+ const prompt = historyInput(event);
59
+ if (prompt === undefined)
60
+ continue;
61
+ const human = humanPrompt(event) !== undefined;
62
+ boundaries.push({
63
+ ...(previousEnd === undefined ? {} : { atSeq: previousEnd.seq }),
64
+ label: `${prompt.replaceAll(/\s+/gu, ' ')}${human && prompted ? ' · mid-turn' : ''}`,
65
+ prompt,
66
+ });
67
+ if (human && openTurn !== undefined)
68
+ prompted = true;
40
69
  }
41
70
  return boundaries;
42
71
  }
43
72
  export function rewindOverlay(events, roller) {
44
73
  const boundaries = rewindBoundaries(events);
74
+ const trailing = latestCompletedTurn(events);
45
75
  return {
46
76
  cursor: Math.max(0, boundaries.length - 1),
47
77
  kind: 'list',
@@ -53,7 +83,7 @@ export function rewindOverlay(events, roller) {
53
83
  },
54
84
  })),
55
85
  purpose: 'rewind',
56
- title: 'Rewind to a prompt',
86
+ title: `Rewind to a prompt${trailing.open ? ' · trailing turn open; completed boundaries only' : ''}`,
57
87
  };
58
88
  }
59
89
  export function rewindActionOverlay(boundary) {
@@ -4,6 +4,7 @@ import type { TuiRoot } from './tui-root.js';
4
4
  export interface RootLaunchOptions {
5
5
  readonly agentPreset?: string;
6
6
  readonly continue: boolean;
7
+ readonly appendSystemPrompt?: string;
7
8
  readonly cwd: string;
8
9
  readonly effort?: string;
9
10
  readonly forkSession: boolean;
@@ -13,8 +14,13 @@ export interface RootLaunchOptions {
13
14
  readonly permission?: string;
14
15
  readonly prompt?: string;
15
16
  readonly provider?: string;
17
+ readonly tools?: readonly string[];
18
+ readonly disallowedTools?: readonly string[];
16
19
  readonly resume?: string;
17
20
  readonly sessionId?: string;
21
+ readonly systemPrompt?: string;
22
+ }
23
+ export declare class LaunchArgumentError extends Error {
18
24
  }
19
25
  export interface SessionRuntime {
20
26
  readonly initialAttachments: readonly DraftAttachment[];
@@ -22,6 +28,7 @@ export interface SessionRuntime {
22
28
  readonly initialHasMore: boolean;
23
29
  readonly initialPrompts: readonly string[];
24
30
  readonly root: RootView;
31
+ readonly statusLine: string;
25
32
  readonly summary: string;
26
33
  activate(value: ActivatableOverlayValue): Promise<void>;
27
34
  attach(path: string): Promise<DraftAttachment | undefined>;