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

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.
@@ -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); });
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, 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. */
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
@@ -153,6 +153,23 @@ export function permissionOverlay(ctx, agent) {
153
153
  function number(value) {
154
154
  return typeof value === 'number' ? value.toLocaleString('en-US') : 'unavailable';
155
155
  }
156
+ function milliseconds(value) {
157
+ return typeof value === 'number' ? `${number(value)} ms` : 'unavailable';
158
+ }
159
+ export function sessionStatusLine(ctx, agent, root, branch) {
160
+ const { modelSelection, permissions, tokenUsage, contextPressure } = ctx.sessionProjections
161
+ .snapshot(agent.session, ['modelSelection', 'permissions', 'tokenUsage', 'contextPressure']).values;
162
+ const model = modelSelection?.next ?? modelSelection?.lastUsed;
163
+ const input = tokenUsage === undefined ? 0 : tokenUsage.uncachedInputTokens + tokenUsage.cacheReadTokens + tokenUsage.cacheWriteTokens;
164
+ const used = contextPressure?.projectedTokens ?? contextPressure?.pressureTokens;
165
+ 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)}`]),
169
+ ...(input === 0 ? [] : [`cache ${String(Math.min(tokenUsage?.cacheReadTokens === input ? 100 : 99, Math.round((tokenUsage?.cacheReadTokens ?? 0) / input * 100)))}%`]),
170
+ ...(branch === undefined ? [] : [`branch ${branch}`]),
171
+ ].join(' · ');
172
+ }
156
173
  /** Read DSH-owned root facts for /status in one projection cut. */
157
174
  export function statusOverlay(ctx, agent, root) {
158
175
  const values = ctx.sessionProjections.snapshot(agent.session, [
@@ -196,6 +213,27 @@ export function contextOverlay(ctx, agent) {
196
213
  title: 'Context',
197
214
  };
198
215
  }
216
+ /** Read DSH's whole-log token and timing projections without folding session history. */
217
+ export function usageOverlay(ctx, agent) {
218
+ const values = ctx.sessionProjections.snapshot(agent.session, ['tokenUsage', 'sessionStats']).values;
219
+ const tokens = values.tokenUsage;
220
+ const stats = values.sessionStats;
221
+ const measured = stats === undefined ? undefined : stats.llmMs + stats.toolMs;
222
+ return {
223
+ kind: 'info',
224
+ lines: [
225
+ `Input tokens: ${number(tokens?.uncachedInputTokens)}`,
226
+ `Output tokens: ${number(tokens?.outputTokens)}`,
227
+ `Cache read: ${number(tokens?.cacheReadTokens)}`,
228
+ `Cache write: ${number(tokens?.cacheWriteTokens)}`,
229
+ `Turns: ${number(stats?.turns)} · Steps: ${number(stats?.steps)}`,
230
+ `Model time: ${milliseconds(stats?.llmMs)}`,
231
+ `Tool time: ${milliseconds(stats?.toolMs)}`,
232
+ `Measured wall time: ${milliseconds(measured)}`,
233
+ ],
234
+ title: 'Usage',
235
+ };
236
+ }
199
237
  /** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
200
238
  export function agentPresetOverlay(presets, current, blank) {
201
239
  const options = presets.filter(preset => preset.broken === undefined).map(preset => ({
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 · --tools NAMES · --disallowedTools NAMES · --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 · --tools NAMES · --disallowedTools NAMES · --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
  ];
package/lib/index.js CHANGED
@@ -13,11 +13,12 @@ import { TuiRoot } from './tui-root.js';
13
13
  export const name = 'dashi';
14
14
  export const inject = [
15
15
  'agentPresets', 'agents', 'attachments', 'authorization', 'cmdlineArgs', 'commands', 'credentials', 'fs', 'jobs', 'loader', 'permissionPresets', 'subagents',
16
- 'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'tools',
16
+ 'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'systemPrompt', 'tools',
17
17
  ];
18
18
  const validatedVersions = JSON.parse(readFileSync(new URL('../validated-dsh-versions.json', import.meta.url), 'utf8'));
19
19
  function parseArgs(args) {
20
20
  let accessible = false;
21
+ let argumentError;
21
22
  let agentPreset, sessionId;
22
23
  let forkSession = false;
23
24
  let inline = true;
@@ -31,7 +32,9 @@ function parseArgs(args) {
31
32
  let resume;
32
33
  let resumeAll = false;
33
34
  let resumePicker = false;
35
+ let systemPrompt, appendSystemPrompt;
34
36
  let useContinue = false;
37
+ let verbose = false;
35
38
  const images = [];
36
39
  const prompt = [];
37
40
  for (let index = 0; index < args.length; index++) {
@@ -43,6 +46,7 @@ function parseArgs(args) {
43
46
  else if (argument === '--fullscreen')
44
47
  inline = false;
45
48
  else if (['--name', '-n', '--agent', '--image', '--effort', '--model', '--permission', '--provider', '--tools', '--disallowedTools',
49
+ '--system-prompt', '--system-prompt-file', '--append-system-prompt', '--append-system-prompt-file',
46
50
  '--session-id'].includes(argument ?? '')) {
47
51
  const value = args[++index];
48
52
  if (value === undefined || value === '')
@@ -70,6 +74,21 @@ function parseArgs(args) {
70
74
  else
71
75
  disallowedTools = names;
72
76
  }
77
+ else if (argument?.includes('system-prompt') === true) {
78
+ let text = value;
79
+ if (argument.endsWith('-file')) {
80
+ try {
81
+ text = readFileSync(value, 'utf8');
82
+ }
83
+ catch (error) {
84
+ argumentError = error instanceof Error ? error.message : String(error);
85
+ }
86
+ }
87
+ if (argument.startsWith('--append'))
88
+ appendSystemPrompt = text;
89
+ else
90
+ systemPrompt = text;
91
+ }
73
92
  else
74
93
  provider = value;
75
94
  }
@@ -88,6 +107,8 @@ function parseArgs(args) {
88
107
  useContinue = true;
89
108
  else if (argument === '--fork-session')
90
109
  forkSession = true;
110
+ else if (argument === '--verbose')
111
+ verbose = true;
91
112
  else if (argument === '--yolo' || argument === '--dangerously-skip-permissions') {
92
113
  permission = 'danger-full-access';
93
114
  }
@@ -106,6 +127,8 @@ function parseArgs(args) {
106
127
  return undefined;
107
128
  return {
108
129
  accessible,
130
+ ...(argumentError === undefined ? {} : { argumentError }),
131
+ ...(appendSystemPrompt === undefined ? {} : { appendSystemPrompt }),
109
132
  continue: useContinue,
110
133
  ...(agentPreset === undefined ? {} : { agentPreset }), forkSession, inline,
111
134
  resumeAll,
@@ -120,6 +143,7 @@ function parseArgs(args) {
120
143
  ...(tools === undefined ? {} : { tools }),
121
144
  ...(disallowedTools === undefined ? {} : { disallowedTools }),
122
145
  ...(resume === undefined ? {} : { resume }), ...(sessionId === undefined ? {} : { sessionId }),
146
+ ...(systemPrompt === undefined ? {} : { systemPrompt }), verbose,
123
147
  };
124
148
  }
125
149
  function packageVersion(path) {
@@ -213,6 +237,8 @@ export function apply(ctx) {
213
237
  const announce = ready.onReady(() => {
214
238
  warnDshVersion();
215
239
  boot = (async () => {
240
+ if (parsed.argumentError !== undefined)
241
+ throw new LaunchArgumentError(parsed.argumentError);
216
242
  const cwd = process.cwd();
217
243
  const makeRuntime = (options) => createSessionRuntime(ctx, options, tuiRoot, action => { shell?.dispatch(action); }, {
218
244
  cancel: id => { shell?.cancelDecision(id); },
@@ -266,12 +292,14 @@ export function apply(ctx) {
266
292
  initialAttachments: initial.initialAttachments, initialCells: initial.initialCells, initialHasMore: initial.initialHasMore,
267
293
  initialPrompts: initial.initialPrompts, initialRoot: initial.root,
268
294
  }),
295
+ ...(parsed.verbose ? { initialToolMode: 'expanded' } : {}),
269
296
  inline: parsed.inline || parsed.accessible,
270
297
  interrupt: () => { runtime?.interrupt(); },
271
298
  loadHistory: rootId => runtime?.loadHistory(rootId) ?? Promise.resolve(),
272
299
  onReleased: () => { process.stdout.write(`${runtime?.summary ?? ''}\n`); },
273
300
  pasteImage: readClipboardImage,
274
301
  search: (query, rootId) => runtime?.search(query, rootId) ?? Promise.resolve(),
302
+ statusLine: () => runtime?.statusLine,
275
303
  submit: (text, mode, attachments) => runtime?.submit(text, mode, attachments),
276
304
  });
277
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
  });
@@ -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;
@@ -17,6 +18,7 @@ export interface RootLaunchOptions {
17
18
  readonly disallowedTools?: readonly string[];
18
19
  readonly resume?: string;
19
20
  readonly sessionId?: string;
21
+ readonly systemPrompt?: string;
20
22
  }
21
23
  export declare class LaunchArgumentError extends Error {
22
24
  }
@@ -26,6 +28,7 @@ export interface SessionRuntime {
26
28
  readonly initialHasMore: boolean;
27
29
  readonly initialPrompts: readonly string[];
28
30
  readonly root: RootView;
31
+ readonly statusLine: string;
29
32
  readonly summary: string;
30
33
  activate(value: ActivatableOverlayValue): Promise<void>;
31
34
  attach(path: string): Promise<DraftAttachment | undefined>;
@@ -2,9 +2,9 @@ import { basename } from 'node:path';
2
2
  import { AuthorizationDeclinedError } from '@deepseek-ai/dsh-authorization';
3
3
  import { parseCredentialKey } from '@deepseek-ai/dsh-credentials';
4
4
  import { JobId } from '@deepseek-ai/dsh-jobs';
5
- import { createUserMessage } from '@deepseek-ai/dsh-llm';
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, } 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';
@@ -21,6 +21,7 @@ export class LaunchArgumentError extends Error {
21
21
  function starterAgents(cwd) {
22
22
  return `# ${basename(cwd)}\n\n## Working agreement\n\n- Add project-specific architecture guidance.\n- Add required build, test, and lint commands.\n- Add repository conventions and constraints.\n`;
23
23
  }
24
+ const RECAP_PROMPT = 'Summarize this conversation so far in ten lines: goal, decisions, open items.';
24
25
  function detail(error) {
25
26
  return error instanceof Error ? error.message : String(error);
26
27
  }
@@ -79,9 +80,31 @@ async function firstControl(iterator) {
79
80
  }
80
81
  return first.value;
81
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
+ }
82
95
  function usage(raw, expected) {
83
96
  return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
84
97
  }
98
+ function loopInterval(seconds) {
99
+ if (seconds % 3_600 === 0)
100
+ return `${String(seconds / 3_600)}h`;
101
+ return seconds % 60 === 0 ? `${String(seconds / 60)}m` : `${String(seconds)}s`;
102
+ }
103
+ function loopLine(schedule) {
104
+ const rule = schedule.kind === 'every' ? `every ${loopInterval(schedule.everySeconds)}`
105
+ : schedule.kind === 'after' ? `after ${String(schedule.afterSeconds)}s` : `at ${schedule.scheduledAt}`;
106
+ return `${String(schedule.id)} · ${rule} · ${schedule.prompt}`;
107
+ }
85
108
  function configValue(raw) {
86
109
  try {
87
110
  return JSON.parse(raw);
@@ -114,18 +137,26 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
114
137
  if ('error' in resolved)
115
138
  throw resolved.error;
116
139
  const agent = resolved.agent;
117
- let disposeRestriction;
140
+ const disposers = [];
118
141
  if (options.tools !== undefined || options.disallowedTools !== undefined) {
119
142
  try {
120
- disposeRestriction = agent.ctx.tools.restrict({
143
+ disposers.push(agent.ctx.tools.restrict({
121
144
  ...(options.tools === undefined ? {} : { allow: options.tools }),
122
145
  ...(options.disallowedTools === undefined ? {} : { deny: options.disallowedTools }),
123
- });
146
+ }));
124
147
  }
125
148
  catch (error) {
126
149
  throw new LaunchArgumentError(detail(error));
127
150
  }
128
151
  }
152
+ if (options.systemPrompt !== undefined)
153
+ disposers.push(agent.ctx.systemPrompt.section({
154
+ complete: true, name: 'dashi:system-prompt', order: Number.MAX_SAFE_INTEGER, text: options.systemPrompt,
155
+ }));
156
+ if (options.appendSystemPrompt !== undefined)
157
+ disposers.push(agent.ctx.systemPrompt.section({
158
+ name: 'dashi:append-system-prompt', order: Number.MAX_SAFE_INTEGER, text: options.appendSystemPrompt,
159
+ }));
129
160
  let title;
130
161
  if (name !== undefined)
131
162
  title = (await ctx.sessionController.rename({ sessionId, title: name })).title;
@@ -134,8 +165,9 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
134
165
  address: { kind: 'session', sessionId }, maxMessages: 50,
135
166
  }, abort.signal)[Symbol.asyncIterator]();
136
167
  const controlIterator = ctx.sessionController.control(abort.signal)[Symbol.asyncIterator]();
137
- const [opening, controlOpening, subagents] = await Promise.all([
168
+ const [opening, controlOpening, subagents, branch] = await Promise.all([
138
169
  firstSnapshot(iterator), firstControl(controlIterator), subagentViews(ctx, sessionId, abort.signal),
170
+ gitBranch(ctx, agent, abort.signal),
139
171
  ]);
140
172
  title ??= projectionString(opening.projections, 'title');
141
173
  const selected = projectionModel(opening.projections);
@@ -147,9 +179,10 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
147
179
  return {
148
180
  abort,
149
181
  agent,
182
+ branch,
150
183
  controlIterator,
151
184
  cursor: opening.cursor,
152
- disposers: disposeRestriction === undefined ? [] : [disposeRestriction],
185
+ disposers,
153
186
  events: eventsFromRecords(opening.records),
154
187
  hasMore: opening.hasMore,
155
188
  iterator,
@@ -256,12 +289,20 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
256
289
  });
257
290
  }, 34);
258
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
+ };
259
299
  const consume = (bound, frame) => {
260
300
  const [event] = eventsFromRecords([frame]);
261
301
  if (event === undefined)
262
302
  return;
263
303
  bound.events.push(event);
264
304
  if (event.type === 'turn/end') {
305
+ void refreshBranch(bound);
265
306
  const startedAt = turnStartTime(bound.events, event.data.turn);
266
307
  if (startedAt !== undefined) {
267
308
  dispatch({
@@ -511,6 +552,79 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
511
552
  const result = ctx.jobs.kill(JobId(match[1]), bound.agent, 'stopped by /tasks');
512
553
  return { kind: 'success', text: result === 'requested' ? `stopping ${match[1]}` : `${match[1]} already finished` };
513
554
  };
555
+ const loop = async (invocation) => {
556
+ const stale = ensureCurrent(invocation);
557
+ if (stale !== undefined)
558
+ return stale;
559
+ const raw = invocation.rawInput.trim();
560
+ const stop = /^stop\s+(\S+)$/u.exec(raw);
561
+ const create = /^(\d+)([mh])\s+(.+)$/su.exec(raw);
562
+ if (raw !== '' && stop === null && create === null) {
563
+ return { kind: 'error', text: 'usage: /loop [INTERVAL TEXT | stop ID]' };
564
+ }
565
+ const name = raw === '' ? 'schedule_list' : stop === null ? 'schedule_create' : 'schedule_delete';
566
+ const args = raw === '' ? {} : stop === null ? {
567
+ every_seconds: Number(create[1]) * (create[2] === 'h' ? 3_600 : 60), prompt: create[3],
568
+ } : { id: stop[1] };
569
+ const result = await ctx.agents.withInitiator(bound.agent, () => ctx.tools.execute({
570
+ agent: bound.agent, arguments: args, callId: ToolCallId(`${String(invocation.commandId)}:loop`),
571
+ name, signal: invocation.signal,
572
+ }));
573
+ if (result.isError)
574
+ return { kind: 'error', text: result.error.message };
575
+ const value = result.value;
576
+ if (!Array.isArray(value) && 'message' in value)
577
+ return { kind: 'error', text: value.message };
578
+ if (Array.isArray(value))
579
+ return { kind: 'success', text: value.length === 0 ? 'no active schedules' : value.map(loopLine).join('\n') };
580
+ if ('deleted' in value)
581
+ return value.deleted ? { kind: 'success', text: `stopped ${String(value.id)}` }
582
+ : { kind: 'error', text: `schedule ${String(value.id)} not found` };
583
+ return { kind: 'success', text: `scheduled ${loopLine(value)}` };
584
+ };
585
+ const askAside = async (invocation, prompt = invocation.rawInput.trim()) => {
586
+ const stale = ensureCurrent(invocation);
587
+ if (stale !== undefined)
588
+ return stale;
589
+ if (prompt === '')
590
+ return { kind: 'error', text: 'usage: /btw TEXT' };
591
+ const forked = await ctx.sessionController.fork({ sessionId: bound.agent.id });
592
+ const resolved = await ctx.sessionController.resolveAgent(forked.sessionId);
593
+ if ('error' in resolved)
594
+ throw resolved.error;
595
+ const child = resolved.agent;
596
+ const turn = inheritedTurn(child.session.snapshotEvents(), child.session.inheritedEventCount);
597
+ if (turn === undefined)
598
+ throw new Error('the fork contains no completed turn');
599
+ const iterator = ctx.sessionController.follow({
600
+ address: { kind: 'session', sessionId: child.id }, maxMessages: 50,
601
+ }, invocation.signal)[Symbol.asyncIterator]();
602
+ await firstSnapshot(iterator);
603
+ await ctx.sessionController.rename({
604
+ sessionId: child.id, title: `btw · ${prompt.split(/\s+/u).slice(0, 6).join(' ')}`,
605
+ });
606
+ const from = child.session.seq;
607
+ child.steer(createUserMessage({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } }));
608
+ try {
609
+ for (;;) {
610
+ const next = await iterator.next();
611
+ if (next.done === true)
612
+ throw new Error('DSH session follow ended before the side turn completed');
613
+ if (next.value.type !== 'snapshot' && eventsFromRecords([next.value])[0]?.type === 'turn/end')
614
+ break;
615
+ }
616
+ }
617
+ finally {
618
+ await iterator.return?.();
619
+ }
620
+ await ctx.sessions.flush(child.session);
621
+ const cells = foldCells(child.session.snapshotEvents(from), createToolPresenter(tool => ctx.tools.get(tool, child)))
622
+ .filter(cell => cell.kind !== 'user');
623
+ dispatch({ type: 'open-overlay', overlay: {
624
+ cells, kind: 'info', lines: [], title: `Btw · turn ${String(turn)}`,
625
+ } });
626
+ return { kind: 'success' };
627
+ };
514
628
  const definitions = [
515
629
  {
516
630
  name: 'help', description: 'Show keyboard and command help',
@@ -551,6 +665,16 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
551
665
  return { kind: 'success' };
552
666
  },
553
667
  },
668
+ {
669
+ name: 'usage', description: 'Show session token and timing totals',
670
+ handler: (invocation) => {
671
+ const invalid = ensureCurrent(invocation) ?? usage(invocation.rawInput, '/usage');
672
+ if (invalid !== undefined)
673
+ return invalid;
674
+ dispatch({ type: 'open-overlay', overlay: usageOverlay(ctx, bound.agent) });
675
+ return { kind: 'success' };
676
+ },
677
+ },
554
678
  {
555
679
  name: 'memory', description: 'Open an instruction file loaded by DSH',
556
680
  handler: (invocation) => {
@@ -671,7 +795,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
671
795
  if (invocation.rawInput.trim() === '')
672
796
  return { kind: 'error', text: 'usage: /plugin ARGS' };
673
797
  const command = `dsh plugin --profile ${quoteShellWord(decodeURIComponent(new URL(ctx.baseUrl).pathname).split('/').at(-2))}${invocation.rawInput}`;
674
- 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'));
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));
675
799
  bound.agent.inject(message);
676
800
  await ctx.sessions.flush(bound.agent.session);
677
801
  publish(bound);
@@ -679,29 +803,35 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
679
803
  },
680
804
  },
681
805
  {
682
- name: 'new', description: 'Start a new session', input: { hint: '[--name TITLE]' },
806
+ name: 'new', description: 'Start a new session', input: { hint: '[NAME | --name TITLE]' },
683
807
  handler: async (invocation) => {
684
808
  const stale = ensureCurrent(invocation);
685
809
  if (stale !== undefined)
686
810
  return stale;
687
811
  const raw = invocation.rawInput.trim();
688
812
  const match = /^--name\s+(.+)$/su.exec(raw);
689
- if (raw !== '' && match === null)
690
- return { kind: 'error', text: 'usage: /new [--name TITLE]' };
813
+ if (raw.startsWith('--') && match === null)
814
+ return { kind: 'error', text: 'usage: /new [NAME | --name TITLE]' };
691
815
  const title = match?.[1]?.trim();
816
+ if (raw !== '' && match === null) {
817
+ await ctx.sessionController.rename({ sessionId: bound.agent.id, title: raw });
818
+ await ctx.sessions.flush(bound.agent.session);
819
+ }
692
820
  await runRootOperation({ kind: 'new', ...(title === undefined ? {} : { title }) });
693
821
  return { kind: 'success' };
694
822
  },
695
823
  },
696
824
  {
697
- name: 'clear', description: 'Start a new session',
825
+ name: 'clear', description: 'Start a new session', input: { hint: '[NAME]' },
698
826
  handler: async (invocation) => {
699
827
  const stale = ensureCurrent(invocation);
700
828
  if (stale !== undefined)
701
829
  return stale;
702
- const invalid = usage(invocation.rawInput, '/clear');
703
- if (invalid !== undefined)
704
- return invalid;
830
+ const title = invocation.rawInput.trim();
831
+ if (title !== '') {
832
+ await ctx.sessionController.rename({ sessionId: bound.agent.id, title });
833
+ await ctx.sessions.flush(bound.agent.session);
834
+ }
705
835
  await runRootOperation({ kind: 'new' });
706
836
  return { kind: 'success' };
707
837
  },
@@ -787,17 +917,46 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
787
917
  },
788
918
  },
789
919
  {
790
- name: 'agents', description: 'Choose the native DSH agent preset',
920
+ name: 'loop', description: 'List, schedule, or stop a fixed-rate reminder',
921
+ input: { hint: '[INTERVAL TEXT | stop ID]' }, handler: loop,
922
+ },
923
+ {
924
+ name: 'btw', description: 'Ask a side question in a fork', input: { hint: 'TEXT' }, recordInput: false,
925
+ handler: invocation => askAside(invocation),
926
+ },
927
+ {
928
+ name: 'recap', description: 'Summarize the conversation in a fork', recordInput: false,
929
+ handler: invocation => usage(invocation.rawInput, '/recap') ?? askAside(invocation, RECAP_PROMPT),
930
+ },
931
+ {
932
+ name: 'agents', description: 'Choose or author a native DSH agent preset',
933
+ input: { hint: '[new NAME | copy SRC DEST | delete NAME]' },
791
934
  handler: async (invocation) => {
792
935
  const stale = ensureCurrent(invocation);
793
936
  if (stale !== undefined)
794
937
  return stale;
795
- const invalid = usage(invocation.rawInput, '/agents');
796
- if (invalid !== undefined)
797
- return invalid;
938
+ const raw = invocation.rawInput.trim();
798
939
  const blank = !bound.agent.session.snapshotEvents().some(event => event.type === 'turn/start');
799
940
  const selected = ctx.sessionProjections.stateOf(bound.agent.session, 'agentPreset')
800
941
  ?? ctx.agentPresets.defaultId;
942
+ const create = /^new\s+(\S+)$/u.exec(raw);
943
+ const copy = /^copy\s+(\S+)\s+(\S+)$/u.exec(raw);
944
+ const remove = /^delete\s+(\S+)$/u.exec(raw);
945
+ if (create?.[1] !== undefined) {
946
+ await ctx.agentPresets.copy(selected, create[1]);
947
+ dispatch({ type: 'open-file', path: (await ctx.agentPresets.resolve(create[1])).path });
948
+ return { kind: 'success', text: `created ${create[1]} from ${selected}` };
949
+ }
950
+ if (copy?.[1] !== undefined && copy[2] !== undefined) {
951
+ await ctx.agentPresets.copy(copy[1], copy[2]);
952
+ return { kind: 'success', text: `copied ${copy[1]} to ${copy[2]}` };
953
+ }
954
+ if (remove?.[1] !== undefined) {
955
+ await ctx.agentPresets.remove(remove[1]);
956
+ return { kind: 'success', text: `deleted ${remove[1]}` };
957
+ }
958
+ if (raw !== '')
959
+ return { kind: 'error', text: 'usage: /agents [new NAME | copy SRC DEST | delete NAME]' };
801
960
  dispatch({
802
961
  type: 'open-overlay',
803
962
  overlay: agentPresetOverlay(await ctx.agentPresets.list(), selected, blank),
@@ -1120,6 +1279,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
1120
1279
  get initialHasMore() { return current().hasMore; },
1121
1280
  get initialPrompts() { return humanPrompts(current().events); },
1122
1281
  get root() { return current().root; },
1282
+ get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.branch); },
1123
1283
  get summary() { return `Resume with: dsh --profile dashi --resume ${current().root.id}`; },
1124
1284
  activate,
1125
1285
  async attach(path) {
@@ -7,4 +7,4 @@ export declare const HUMAN_SHELL_STREAM_BYTES: number;
7
7
  export declare function shellResultText(command: string, result: ShellRunResult): string;
8
8
  export declare function quoteShellWord(value: string): string;
9
9
  /** Run an explicit human command through DSH and identify its next-step context. */
10
- export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal, success?: string): Promise<readonly [UserMessage, ShellRunResult]>;
10
+ export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal, success?: string, useSessionSandbox?: boolean): Promise<readonly [UserMessage, ShellRunResult]>;
@@ -45,11 +45,12 @@ export function shellResultText(command, result) {
45
45
  }
46
46
  export function quoteShellWord(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
47
47
  /** Run an explicit human command through DSH and identify its next-step context. */
48
- export async function runHumanShell(ctx, agent, command, signal, success) {
48
+ export async function runHumanShell(ctx, agent, command, signal, success, useSessionSandbox = true) {
49
49
  const result = await ctx.shell.run(ctx.shell.resolve({
50
50
  command,
51
51
  env: { DSH_HOME: resolveDshHome() },
52
- sandboxPolicy: ctx.sandboxPolicy.resolve({ session: agent.session }),
52
+ sandboxPolicy: ctx.sandboxPolicy.resolve(useSessionSandbox
53
+ ? { session: agent.session } : { mode: 'danger-full-access' }),
53
54
  signal,
54
55
  stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES,
55
56
  timeoutMs: HUMAN_SHELL_TIMEOUT_MS,
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;
@@ -333,6 +334,9 @@ export type UiAction = {
333
334
  readonly type: 'open-history';
334
335
  } | {
335
336
  readonly type: 'open-details';
337
+ } | {
338
+ readonly path: string;
339
+ readonly type: 'open-file';
336
340
  } | {
337
341
  readonly type: 'completion-ready';
338
342
  readonly options: readonly OverlayOption[];
@@ -343,6 +347,7 @@ export type UiAction = {
343
347
  } | {
344
348
  readonly type: 'overlay-close';
345
349
  } | {
350
+ readonly limit?: number;
346
351
  readonly type: 'overlay-move';
347
352
  readonly offset: number;
348
353
  } | {
@@ -503,6 +508,7 @@ export type UiEffect = {
503
508
  } | {
504
509
  readonly type: 'suspend';
505
510
  };
506
- export declare function initialViewState(cwd: string, inline: boolean, root?: RootView, cells?: readonly TerminalCell[], prompts?: readonly string[], historyHasMore?: boolean, attachments?: readonly DraftAttachment[]): ViewState;
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;
507
512
  export declare const TURN_BELL_THRESHOLD_MS = 10000;
513
+ export declare const PAGE_CELLS = 8;
508
514
  export declare function reduce(state: ViewState, action: UiAction): readonly [ViewState, readonly UiEffect[]];
package/lib/state.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { FLAG_HELP, VERSION_LINE } from './help.js';
2
- export function initialViewState(cwd, inline, root, cells = [], prompts = [], historyHasMore = false, attachments = []) {
2
+ export function initialViewState(cwd, inline, root, cells = [], prompts = [], historyHasMore = false, attachments = [], toolMode = 'collapsed') {
3
3
  return {
4
4
  attachments, cells, composer: '', cwd, decisions: [], exitArmed: false, historyHasMore, inline,
5
5
  newOutput: 0, prompts, rewindArmed: false, scrollOffset: 0,
6
- sendMode: 'steer', toolMode: 'collapsed',
6
+ sendMode: 'steer', toolMode,
7
7
  ...(root === undefined ? {} : { root }),
8
8
  };
9
9
  }
@@ -41,8 +41,8 @@ function help() {
41
41
  'Ctrl+B activity details for jobs and subagents',
42
42
  '@ paths require DSH file-reference · Ctrl+V pastes an image · Backspace removes it',
43
43
  'Ctrl+S stash · Ctrl+G editor · F1 help · Ctrl+L redraw · Ctrl+Z suspend',
44
- '/new /clear /reset /resume /continue [--all] [NAME|UUID] /fork /branch /rewind /rename /model /effort /permission /agents /queue /status /context',
45
- '/init /memory /skills /config /login /logout /diff /plugins /plugin /tasks /bashes /subtask /history /export /copy /exit /quit',
44
+ '/new /clear /reset /resume /continue [--all] [NAME|UUID] /fork /branch /rewind /rename /model /effort /permission /agents /queue /status /usage /context',
45
+ '/init /memory /skills /config /login /logout /diff /plugins /plugin /tasks /bashes /subtask /loop /btw /recap /history /export /copy /exit /quit',
46
46
  ...FLAG_HELP,
47
47
  ],
48
48
  };
@@ -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 === '')
@@ -366,6 +366,7 @@ export function reduce(state, action) {
366
366
  return head === undefined
367
367
  ? [{ ...state, overlay: action.overlay, exitArmed: false, rewindArmed: false }, redraw()]
368
368
  : [state, []];
369
+ case 'open-file': return [state, [{ type: 'activate-overlay', value: { kind: 'open-file', path: action.path } }]];
369
370
  case 'overlay-close': {
370
371
  if (state.overlay === undefined)
371
372
  return [state, []];
@@ -375,8 +376,14 @@ export function reduce(state, action) {
375
376
  case 'overlay-move': {
376
377
  if (state.overlay === undefined)
377
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
+ }
378
385
  const count = (state.root?.jobs?.length ?? 0) + (state.root?.subagents?.length ?? 0);
379
- 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);
380
387
  return overlay === state.overlay ? [state, []] : [{ ...state, overlay }, redraw()];
381
388
  }
382
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.10",
3
+ "version": "0.1.0-alpha.12",
4
4
  "description": "A terminal UI for DeepSeek Harness",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -57,13 +57,16 @@
57
57
  "@deepseek-ai/dsh-permission-presets": "0.1.2-rc.1",
58
58
  "@deepseek-ai/dsh-plan-mode": "0.1.2-rc.1",
59
59
  "@deepseek-ai/dsh-sandbox-policy": "0.1.2-rc.1",
60
+ "@deepseek-ai/dsh-schedule": "0.1.2-rc.1",
60
61
  "@deepseek-ai/dsh-session": "0.1.2-rc.1",
61
62
  "@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
62
63
  "@deepseek-ai/dsh-session-query": "0.1.2-rc.1",
64
+ "@deepseek-ai/dsh-session-stats": "0.1.2-rc.1",
63
65
  "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
64
66
  "@deepseek-ai/dsh-shell": "0.1.2-rc.1",
65
67
  "@deepseek-ai/dsh-skill": "0.1.2-rc.1",
66
68
  "@deepseek-ai/dsh-subagent": "0.1.2-rc.1",
69
+ "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
67
70
  "@deepseek-ai/dsh-token-meter": "0.1.2-rc.1",
68
71
  "@deepseek-ai/dsh-tool-todo": "0.1.2-rc.1",
69
72
  "@deepseek-ai/dsh-tools": "0.1.2-rc.1",