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

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,6 +20,7 @@ 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>;
@@ -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();
package/lib/catalogs.d.ts CHANGED
@@ -18,6 +18,8 @@ export declare function permissionOverlay(ctx: Context, agent: Agent): Overlay;
18
18
  export declare function statusOverlay(ctx: Context, agent: Agent, root: RootView): Overlay;
19
19
  /** Show DSH's heuristic next-request composition without treating it as an exact total. */
20
20
  export declare function contextOverlay(ctx: Context, agent: Agent): Overlay;
21
+ /** Read DSH's whole-log token and timing projections without folding session history. */
22
+ export declare function usageOverlay(ctx: Context, agent: Agent): Overlay;
21
23
  /** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
22
24
  export declare function agentPresetOverlay(presets: readonly AgentPreset[], current: string, blank: boolean): Overlay;
23
25
  /** Build bounded same-cwd prompt-search rows from DSH query hits. */
package/lib/catalogs.js CHANGED
@@ -153,6 +153,9 @@ 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
+ }
156
159
  /** Read DSH-owned root facts for /status in one projection cut. */
157
160
  export function statusOverlay(ctx, agent, root) {
158
161
  const values = ctx.sessionProjections.snapshot(agent.session, [
@@ -196,6 +199,27 @@ export function contextOverlay(ctx, agent) {
196
199
  title: 'Context',
197
200
  };
198
201
  }
202
+ /** Read DSH's whole-log token and timing projections without folding session history. */
203
+ export function usageOverlay(ctx, agent) {
204
+ const values = ctx.sessionProjections.snapshot(agent.session, ['tokenUsage', 'sessionStats']).values;
205
+ const tokens = values.tokenUsage;
206
+ const stats = values.sessionStats;
207
+ const measured = stats === undefined ? undefined : stats.llmMs + stats.toolMs;
208
+ return {
209
+ kind: 'info',
210
+ lines: [
211
+ `Input tokens: ${number(tokens?.uncachedInputTokens)}`,
212
+ `Output tokens: ${number(tokens?.outputTokens)}`,
213
+ `Cache read: ${number(tokens?.cacheReadTokens)}`,
214
+ `Cache write: ${number(tokens?.cacheWriteTokens)}`,
215
+ `Turns: ${number(stats?.turns)} · Steps: ${number(stats?.steps)}`,
216
+ `Model time: ${milliseconds(stats?.llmMs)}`,
217
+ `Tool time: ${milliseconds(stats?.toolMs)}`,
218
+ `Measured wall time: ${milliseconds(measured)}`,
219
+ ],
220
+ title: 'Usage',
221
+ };
222
+ }
199
223
  /** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
200
224
  export function agentPresetOverlay(presets, current, blank) {
201
225
  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,6 +292,7 @@ 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(),
@@ -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
  }
@@ -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, 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
  }
@@ -82,6 +83,16 @@ async function firstControl(iterator) {
82
83
  function usage(raw, expected) {
83
84
  return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
84
85
  }
86
+ function loopInterval(seconds) {
87
+ if (seconds % 3_600 === 0)
88
+ return `${String(seconds / 3_600)}h`;
89
+ return seconds % 60 === 0 ? `${String(seconds / 60)}m` : `${String(seconds)}s`;
90
+ }
91
+ function loopLine(schedule) {
92
+ const rule = schedule.kind === 'every' ? `every ${loopInterval(schedule.everySeconds)}`
93
+ : schedule.kind === 'after' ? `after ${String(schedule.afterSeconds)}s` : `at ${schedule.scheduledAt}`;
94
+ return `${String(schedule.id)} · ${rule} · ${schedule.prompt}`;
95
+ }
85
96
  function configValue(raw) {
86
97
  try {
87
98
  return JSON.parse(raw);
@@ -114,18 +125,26 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
114
125
  if ('error' in resolved)
115
126
  throw resolved.error;
116
127
  const agent = resolved.agent;
117
- let disposeRestriction;
128
+ const disposers = [];
118
129
  if (options.tools !== undefined || options.disallowedTools !== undefined) {
119
130
  try {
120
- disposeRestriction = agent.ctx.tools.restrict({
131
+ disposers.push(agent.ctx.tools.restrict({
121
132
  ...(options.tools === undefined ? {} : { allow: options.tools }),
122
133
  ...(options.disallowedTools === undefined ? {} : { deny: options.disallowedTools }),
123
- });
134
+ }));
124
135
  }
125
136
  catch (error) {
126
137
  throw new LaunchArgumentError(detail(error));
127
138
  }
128
139
  }
140
+ if (options.systemPrompt !== undefined)
141
+ disposers.push(agent.ctx.systemPrompt.section({
142
+ complete: true, name: 'dashi:system-prompt', order: Number.MAX_SAFE_INTEGER, text: options.systemPrompt,
143
+ }));
144
+ if (options.appendSystemPrompt !== undefined)
145
+ disposers.push(agent.ctx.systemPrompt.section({
146
+ name: 'dashi:append-system-prompt', order: Number.MAX_SAFE_INTEGER, text: options.appendSystemPrompt,
147
+ }));
129
148
  let title;
130
149
  if (name !== undefined)
131
150
  title = (await ctx.sessionController.rename({ sessionId, title: name })).title;
@@ -149,7 +168,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
149
168
  agent,
150
169
  controlIterator,
151
170
  cursor: opening.cursor,
152
- disposers: disposeRestriction === undefined ? [] : [disposeRestriction],
171
+ disposers,
153
172
  events: eventsFromRecords(opening.records),
154
173
  hasMore: opening.hasMore,
155
174
  iterator,
@@ -511,6 +530,79 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
511
530
  const result = ctx.jobs.kill(JobId(match[1]), bound.agent, 'stopped by /tasks');
512
531
  return { kind: 'success', text: result === 'requested' ? `stopping ${match[1]}` : `${match[1]} already finished` };
513
532
  };
533
+ const loop = async (invocation) => {
534
+ const stale = ensureCurrent(invocation);
535
+ if (stale !== undefined)
536
+ return stale;
537
+ const raw = invocation.rawInput.trim();
538
+ const stop = /^stop\s+(\S+)$/u.exec(raw);
539
+ const create = /^(\d+)([mh])\s+(.+)$/su.exec(raw);
540
+ if (raw !== '' && stop === null && create === null) {
541
+ return { kind: 'error', text: 'usage: /loop [INTERVAL TEXT | stop ID]' };
542
+ }
543
+ const name = raw === '' ? 'schedule_list' : stop === null ? 'schedule_create' : 'schedule_delete';
544
+ const args = raw === '' ? {} : stop === null ? {
545
+ every_seconds: Number(create[1]) * (create[2] === 'h' ? 3_600 : 60), prompt: create[3],
546
+ } : { id: stop[1] };
547
+ const result = await ctx.agents.withInitiator(bound.agent, () => ctx.tools.execute({
548
+ agent: bound.agent, arguments: args, callId: ToolCallId(`${String(invocation.commandId)}:loop`),
549
+ name, signal: invocation.signal,
550
+ }));
551
+ if (result.isError)
552
+ return { kind: 'error', text: result.error.message };
553
+ const value = result.value;
554
+ if (!Array.isArray(value) && 'message' in value)
555
+ return { kind: 'error', text: value.message };
556
+ if (Array.isArray(value))
557
+ return { kind: 'success', text: value.length === 0 ? 'no active schedules' : value.map(loopLine).join('\n') };
558
+ if ('deleted' in value)
559
+ return value.deleted ? { kind: 'success', text: `stopped ${String(value.id)}` }
560
+ : { kind: 'error', text: `schedule ${String(value.id)} not found` };
561
+ return { kind: 'success', text: `scheduled ${loopLine(value)}` };
562
+ };
563
+ const askAside = async (invocation, prompt = invocation.rawInput.trim()) => {
564
+ const stale = ensureCurrent(invocation);
565
+ if (stale !== undefined)
566
+ return stale;
567
+ if (prompt === '')
568
+ return { kind: 'error', text: 'usage: /btw TEXT' };
569
+ const forked = await ctx.sessionController.fork({ sessionId: bound.agent.id });
570
+ const resolved = await ctx.sessionController.resolveAgent(forked.sessionId);
571
+ if ('error' in resolved)
572
+ throw resolved.error;
573
+ const child = resolved.agent;
574
+ const turn = inheritedTurn(child.session.snapshotEvents(), child.session.inheritedEventCount);
575
+ if (turn === undefined)
576
+ throw new Error('the fork contains no completed turn');
577
+ const iterator = ctx.sessionController.follow({
578
+ address: { kind: 'session', sessionId: child.id }, maxMessages: 50,
579
+ }, invocation.signal)[Symbol.asyncIterator]();
580
+ await firstSnapshot(iterator);
581
+ await ctx.sessionController.rename({
582
+ sessionId: child.id, title: `btw · ${prompt.split(/\s+/u).slice(0, 6).join(' ')}`,
583
+ });
584
+ const from = child.session.seq;
585
+ child.steer(createUserMessage({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } }));
586
+ try {
587
+ for (;;) {
588
+ const next = await iterator.next();
589
+ if (next.done === true)
590
+ throw new Error('DSH session follow ended before the side turn completed');
591
+ if (next.value.type !== 'snapshot' && eventsFromRecords([next.value])[0]?.type === 'turn/end')
592
+ break;
593
+ }
594
+ }
595
+ finally {
596
+ await iterator.return?.();
597
+ }
598
+ await ctx.sessions.flush(child.session);
599
+ const cells = foldCells(child.session.snapshotEvents(from), createToolPresenter(tool => ctx.tools.get(tool, child)))
600
+ .filter(cell => cell.kind !== 'user');
601
+ dispatch({ type: 'open-overlay', overlay: {
602
+ cells, kind: 'info', lines: [], title: `Btw · turn ${String(turn)}`,
603
+ } });
604
+ return { kind: 'success' };
605
+ };
514
606
  const definitions = [
515
607
  {
516
608
  name: 'help', description: 'Show keyboard and command help',
@@ -551,6 +643,16 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
551
643
  return { kind: 'success' };
552
644
  },
553
645
  },
646
+ {
647
+ name: 'usage', description: 'Show session token and timing totals',
648
+ handler: (invocation) => {
649
+ const invalid = ensureCurrent(invocation) ?? usage(invocation.rawInput, '/usage');
650
+ if (invalid !== undefined)
651
+ return invalid;
652
+ dispatch({ type: 'open-overlay', overlay: usageOverlay(ctx, bound.agent) });
653
+ return { kind: 'success' };
654
+ },
655
+ },
554
656
  {
555
657
  name: 'memory', description: 'Open an instruction file loaded by DSH',
556
658
  handler: (invocation) => {
@@ -671,7 +773,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
671
773
  if (invocation.rawInput.trim() === '')
672
774
  return { kind: 'error', text: 'usage: /plugin ARGS' };
673
775
  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'));
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));
675
777
  bound.agent.inject(message);
676
778
  await ctx.sessions.flush(bound.agent.session);
677
779
  publish(bound);
@@ -679,29 +781,35 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
679
781
  },
680
782
  },
681
783
  {
682
- name: 'new', description: 'Start a new session', input: { hint: '[--name TITLE]' },
784
+ name: 'new', description: 'Start a new session', input: { hint: '[NAME | --name TITLE]' },
683
785
  handler: async (invocation) => {
684
786
  const stale = ensureCurrent(invocation);
685
787
  if (stale !== undefined)
686
788
  return stale;
687
789
  const raw = invocation.rawInput.trim();
688
790
  const match = /^--name\s+(.+)$/su.exec(raw);
689
- if (raw !== '' && match === null)
690
- return { kind: 'error', text: 'usage: /new [--name TITLE]' };
791
+ if (raw.startsWith('--') && match === null)
792
+ return { kind: 'error', text: 'usage: /new [NAME | --name TITLE]' };
691
793
  const title = match?.[1]?.trim();
794
+ if (raw !== '' && match === null) {
795
+ await ctx.sessionController.rename({ sessionId: bound.agent.id, title: raw });
796
+ await ctx.sessions.flush(bound.agent.session);
797
+ }
692
798
  await runRootOperation({ kind: 'new', ...(title === undefined ? {} : { title }) });
693
799
  return { kind: 'success' };
694
800
  },
695
801
  },
696
802
  {
697
- name: 'clear', description: 'Start a new session',
803
+ name: 'clear', description: 'Start a new session', input: { hint: '[NAME]' },
698
804
  handler: async (invocation) => {
699
805
  const stale = ensureCurrent(invocation);
700
806
  if (stale !== undefined)
701
807
  return stale;
702
- const invalid = usage(invocation.rawInput, '/clear');
703
- if (invalid !== undefined)
704
- return invalid;
808
+ const title = invocation.rawInput.trim();
809
+ if (title !== '') {
810
+ await ctx.sessionController.rename({ sessionId: bound.agent.id, title });
811
+ await ctx.sessions.flush(bound.agent.session);
812
+ }
705
813
  await runRootOperation({ kind: 'new' });
706
814
  return { kind: 'success' };
707
815
  },
@@ -787,17 +895,46 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
787
895
  },
788
896
  },
789
897
  {
790
- name: 'agents', description: 'Choose the native DSH agent preset',
898
+ name: 'loop', description: 'List, schedule, or stop a fixed-rate reminder',
899
+ input: { hint: '[INTERVAL TEXT | stop ID]' }, handler: loop,
900
+ },
901
+ {
902
+ name: 'btw', description: 'Ask a side question in a fork', input: { hint: 'TEXT' }, recordInput: false,
903
+ handler: invocation => askAside(invocation),
904
+ },
905
+ {
906
+ name: 'recap', description: 'Summarize the conversation in a fork', recordInput: false,
907
+ handler: invocation => usage(invocation.rawInput, '/recap') ?? askAside(invocation, RECAP_PROMPT),
908
+ },
909
+ {
910
+ name: 'agents', description: 'Choose or author a native DSH agent preset',
911
+ input: { hint: '[new NAME | copy SRC DEST | delete NAME]' },
791
912
  handler: async (invocation) => {
792
913
  const stale = ensureCurrent(invocation);
793
914
  if (stale !== undefined)
794
915
  return stale;
795
- const invalid = usage(invocation.rawInput, '/agents');
796
- if (invalid !== undefined)
797
- return invalid;
916
+ const raw = invocation.rawInput.trim();
798
917
  const blank = !bound.agent.session.snapshotEvents().some(event => event.type === 'turn/start');
799
918
  const selected = ctx.sessionProjections.stateOf(bound.agent.session, 'agentPreset')
800
919
  ?? ctx.agentPresets.defaultId;
920
+ const create = /^new\s+(\S+)$/u.exec(raw);
921
+ const copy = /^copy\s+(\S+)\s+(\S+)$/u.exec(raw);
922
+ const remove = /^delete\s+(\S+)$/u.exec(raw);
923
+ if (create?.[1] !== undefined) {
924
+ await ctx.agentPresets.copy(selected, create[1]);
925
+ dispatch({ type: 'open-file', path: (await ctx.agentPresets.resolve(create[1])).path });
926
+ return { kind: 'success', text: `created ${create[1]} from ${selected}` };
927
+ }
928
+ if (copy?.[1] !== undefined && copy[2] !== undefined) {
929
+ await ctx.agentPresets.copy(copy[1], copy[2]);
930
+ return { kind: 'success', text: `copied ${copy[1]} to ${copy[2]}` };
931
+ }
932
+ if (remove?.[1] !== undefined) {
933
+ await ctx.agentPresets.remove(remove[1]);
934
+ return { kind: 'success', text: `deleted ${remove[1]}` };
935
+ }
936
+ if (raw !== '')
937
+ return { kind: 'error', text: 'usage: /agents [new NAME | copy SRC DEST | delete NAME]' };
801
938
  dispatch({
802
939
  type: 'open-overlay',
803
940
  overlay: agentPresetOverlay(await ctx.agentPresets.list(), selected, blank),
@@ -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
@@ -333,6 +333,9 @@ export type UiAction = {
333
333
  readonly type: 'open-history';
334
334
  } | {
335
335
  readonly type: 'open-details';
336
+ } | {
337
+ readonly path: string;
338
+ readonly type: 'open-file';
336
339
  } | {
337
340
  readonly type: 'completion-ready';
338
341
  readonly options: readonly OverlayOption[];
@@ -503,6 +506,6 @@ export type UiEffect = {
503
506
  } | {
504
507
  readonly type: 'suspend';
505
508
  };
506
- export declare function initialViewState(cwd: string, inline: boolean, root?: RootView, cells?: readonly TerminalCell[], prompts?: readonly string[], historyHasMore?: boolean, attachments?: readonly DraftAttachment[]): ViewState;
509
+ 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
510
  export declare const TURN_BELL_THRESHOLD_MS = 10000;
508
511
  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
  };
@@ -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, []];
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.11",
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",