@antst/dashi 0.1.0-alpha.6 → 0.1.0-alpha.8

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.
@@ -1,4 +1,5 @@
1
1
  import { createRenderer } from './renderer.js';
2
+ import { runEditor } from './external-editor.js';
2
3
  import { initialViewState, reduce, } from './state.js';
3
4
  import { createTerminalGuard } from './terminal-guard.js';
4
5
  function suspendProcess() {
@@ -44,7 +45,14 @@ export function createTerminalShell(options) {
44
45
  async function run(effect) {
45
46
  switch (effect.type) {
46
47
  case 'activate-overlay':
47
- await options.activateOverlay?.(effect.value);
48
+ if (effect.value.kind === 'open-file') {
49
+ const path = effect.value.path;
50
+ await guard.withCookedTerminal(() => runEditor(process.env.EDITOR?.trim() || 'vi', path, options.cwd)).catch(error => {
51
+ dispatch({ type: 'runtime-error', message: error instanceof Error ? error.message : String(error) });
52
+ });
53
+ }
54
+ else
55
+ await options.activateOverlay?.(effect.value);
48
56
  return;
49
57
  case 'attach': {
50
58
  try {
@@ -0,0 +1,5 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ import type { Overlay } from './state.js';
4
+ import { type ToolPresenter } from './tool-presentation.js';
5
+ export declare function diffOverlay(ctx: Context, agent: Agent, presenter: ToolPresenter, mode: '' | 'turn', signal: AbortSignal): Promise<Overlay>;
@@ -0,0 +1,23 @@
1
+ import { boundedBody } from './tool-presentation.js';
2
+ import { foldCells } from './transcript.js';
3
+ export async function diffOverlay(ctx, agent, presenter, mode, signal) {
4
+ if (mode === 'turn') {
5
+ const events = agent.session.snapshotEvents();
6
+ const start = events.map(event => event.type).lastIndexOf('turn/start');
7
+ const cells = (start < 0 ? [] : foldCells(events.slice(start), presenter)).filter(cell => cell.pending !== true && cell.tool?.card === 'diff' && (cell.tool.diffs?.length ?? 0) > 0);
8
+ return { cells, kind: 'info', lines: cells.length === 0 ? ['No write/edit changes in the last turn.'] : [], title: 'Last turn diff' };
9
+ }
10
+ const command = 'git diff --no-ext-diff --no-color HEAD --';
11
+ const result = await ctx.shell.run(ctx.shell.resolve({
12
+ command, sandboxPolicy: ctx.sandboxPolicy.resolve({ session: agent.session }), signal,
13
+ stdoutMaxBytes: 64 * 1024, timeoutMs: 30_000, workdir: agent.session.header.cwd,
14
+ }));
15
+ const output = [result.stdout.text, result.stderr.text].filter(Boolean).join('\n');
16
+ const cell = {
17
+ key: 'diff:working', kind: result.exitCode === 0 ? 'tool' : 'error', text: 'git diff',
18
+ tool: { card: 'terminal', title: 'git diff', status: result.exitCode === 0 ? output === '' ? 'clean' : 'changed'
19
+ : result.exitCode === null ? result.signal ?? 'failed' : `exit ${String(result.exitCode)}`,
20
+ ...(output === '' ? {} : { body: boundedBody(output) }) },
21
+ };
22
+ return { cells: [cell], kind: 'info', lines: [], title: 'Working tree diff' };
23
+ }
@@ -1,2 +1,3 @@
1
+ export declare function runEditor(command: string, file: string, cwd: string): Promise<void>;
1
2
  /** Edit one draft through the user's terminal editor and always remove its file. */
2
3
  export declare function editDraftExternally(text: string, cwd: string): Promise<string>;
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { mkdtemp, readFile, rm, rmdir, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- function runEditor(command, file, cwd) {
5
+ export function runEditor(command, file, cwd) {
6
6
  return new Promise((resolve, reject) => {
7
7
  const child = spawn('/bin/sh', ['-c', `exec ${command} "$1"`, 'dashi-editor', file], {
8
8
  cwd, stdio: 'inherit',
package/lib/help.d.ts ADDED
@@ -0,0 +1,2 @@
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 · --all · --name TITLE", " --model ID [--provider ID] · --effort LEVEL · --permission PRESET", " --yolo | --dangerously-skip-permissions · --image PATH · -h, --help"];
package/lib/help.js ADDED
@@ -0,0 +1,6 @@
1
+ import dashi from '../package.json' with { type: 'json' };
2
+ import dsh from '@deepseek-ai/dsh-cmdline/package.json' with { type: 'json' };
3
+ export const VERSION_LINE = `dashi ${dashi.version} on DSH ${dsh.version}`;
4
+ export const FLAG_HELP = [
5
+ 'Launch flags: --inline | --fullscreen · --accessible · -r, --resume [NAME|UUID] · -c, --continue · --all · --name TITLE', ' --model ID [--provider ID] · --effort LEVEL · --permission PRESET', ' --yolo | --dangerously-skip-permissions · --image PATH · -h, --help'
6
+ ];
package/lib/index.js CHANGED
@@ -6,19 +6,24 @@ import { createTerminalShell } from './application.js';
6
6
  import { sessionOverlay } from './catalogs.js';
7
7
  import { readClipboardImage } from './clipboard-image.js';
8
8
  import { editDraftExternally } from './external-editor.js';
9
+ import { FLAG_HELP, VERSION_LINE } from './help.js';
9
10
  import { formatSessionList, isSessionId, parseSessionListArgs, sessionMatches, sessionNotFound, } from './session-list.js';
10
11
  import { createSessionRuntime } from './session-runtime.js';
11
12
  import { TuiRoot } from './tui-root.js';
12
13
  export const name = 'dashi';
13
14
  export const inject = [
14
- 'agentPresets', 'agents', 'attachments', 'cmdlineArgs', 'commands', 'fs', 'permissionPresets', 'sessionController',
15
- 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'shell', 'skills', 'tools',
15
+ 'agentPresets', 'agents', 'attachments', 'cmdlineArgs', 'commands', 'fs', 'loader', 'permissionPresets',
16
+ 'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'tools',
16
17
  ];
17
18
  const validatedVersions = JSON.parse(readFileSync(new URL('../validated-dsh-versions.json', import.meta.url), 'utf8'));
18
19
  function parseArgs(args) {
19
20
  let accessible = false;
20
21
  let inline = true;
21
22
  let name;
23
+ let effort;
24
+ let model;
25
+ let permission;
26
+ let provider;
22
27
  let resume;
23
28
  let resumeAll = false;
24
29
  let resumePicker = false;
@@ -33,14 +38,23 @@ function parseArgs(args) {
33
38
  inline = true;
34
39
  else if (argument === '--fullscreen')
35
40
  inline = false;
36
- else if (argument === '--name' || argument === '--image') {
41
+ else if (argument === '--name' || argument === '--image' || argument === '--effort'
42
+ || argument === '--model' || argument === '--permission' || argument === '--provider') {
37
43
  const value = args[++index];
38
44
  if (value === undefined || value === '')
39
45
  return undefined;
40
46
  if (argument === '--name')
41
47
  name = value;
42
- else
48
+ else if (argument === '--image')
43
49
  images.push(value);
50
+ else if (argument === '--effort')
51
+ effort = value;
52
+ else if (argument === '--model')
53
+ model = value;
54
+ else if (argument === '--permission')
55
+ permission = value;
56
+ else
57
+ provider = value;
44
58
  }
45
59
  else if (argument === '--resume' || argument === '-r') {
46
60
  const value = args[index + 1];
@@ -55,6 +69,9 @@ function parseArgs(args) {
55
69
  resumeAll = true;
56
70
  else if (argument === '--continue' || argument === '-c')
57
71
  useContinue = true;
72
+ else if (argument === '--yolo' || argument === '--dangerously-skip-permissions') {
73
+ permission = 'danger-full-access';
74
+ }
58
75
  else if (argument?.startsWith('-') === true)
59
76
  return undefined;
60
77
  else if (argument !== undefined)
@@ -62,7 +79,8 @@ function parseArgs(args) {
62
79
  }
63
80
  if (resume !== undefined && (resumePicker || useContinue) || resumePicker && useContinue
64
81
  || name !== undefined && (resume !== undefined || resumePicker || useContinue)
65
- || resumeAll && resume === undefined && !resumePicker || resumeAll && useContinue)
82
+ || resumeAll && resume === undefined && !resumePicker || resumeAll && useContinue
83
+ || provider !== undefined && model === undefined)
66
84
  return undefined;
67
85
  return {
68
86
  accessible,
@@ -71,8 +89,12 @@ function parseArgs(args) {
71
89
  resumeAll,
72
90
  resumePicker,
73
91
  ...(images.length === 0 ? {} : { images }),
92
+ ...(effort === undefined ? {} : { effort }),
93
+ ...(model === undefined ? {} : { model }),
74
94
  ...(name === undefined ? {} : { name }),
95
+ ...(permission === undefined ? {} : { permission }),
75
96
  ...(prompt.length === 0 ? {} : { prompt: prompt.join(' ') }),
97
+ ...(provider === undefined ? {} : { provider }),
76
98
  ...(resume === undefined ? {} : { resume }),
77
99
  };
78
100
  }
@@ -133,6 +155,11 @@ export function apply(ctx) {
133
155
  throw new Error('dashi: the launcher must provide ctx.appExit, ctx.appReady, and ctx.cmdlineArgs');
134
156
  }
135
157
  const args = cmdline.get();
158
+ if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
159
+ process.stdout.write(`${VERSION_LINE}\n\nUsage: dashi [options] [prompt]\n\n${FLAG_HELP.join('\n')}\n`);
160
+ exit(0);
161
+ return;
162
+ }
136
163
  if (args[0] === 'sessions') {
137
164
  const options = parseSessionListArgs(args);
138
165
  if (options === undefined) {
package/lib/input.js CHANGED
@@ -102,6 +102,8 @@ export function installInput(tui, bindings) {
102
102
  bindings.dispatch({ type: 'overlay-move', offset: -1 });
103
103
  else if (matchesKey(data, 'down'))
104
104
  bindings.dispatch({ type: 'overlay-move', offset: 1 });
105
+ else if (matchesKey(data, 'ctrl+o'))
106
+ bindings.dispatch({ type: 'toggle-tool-mode' });
105
107
  else if (matchesKey(data, 'enter'))
106
108
  bindings.dispatch({ type: 'overlay-submit', execute: true });
107
109
  else if (matchesKey(data, 'tab'))
@@ -0,0 +1,3 @@
1
+ import type { Agent } from '@deepseek-ai/dsh-agent';
2
+ import type { Overlay } from './state.js';
3
+ export declare function memoryOverlay(agent: Agent): Overlay;
package/lib/memory.js ADDED
@@ -0,0 +1,27 @@
1
+ import { resolve } from 'node:path';
2
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
3
+ export function memoryOverlay(agent) {
4
+ const cwd = agent.session.header.cwd ?? process.cwd();
5
+ let root = cwd;
6
+ const active = new Map();
7
+ for (const seq of agent.session.surface.nodes) {
8
+ const event = agent.session.eventAt(seq);
9
+ if (event?.type !== 'user/message' || event.data.source.kind !== 'agent-instructions')
10
+ continue;
11
+ const source = event.data.source, identity = source.baselineIdentity === undefined
12
+ ? undefined : JSON.parse(source.baselineIdentity);
13
+ if (typeof identity?.projectRoot === 'string')
14
+ root = resolve(cwd, identity.projectRoot);
15
+ for (const change of source.changes) {
16
+ if (change.action === 'remove')
17
+ active.delete(change.scope);
18
+ else
19
+ active.set(change.scope, { path: change.path, scope: change.scope.split('\0')[0] ?? change.scope });
20
+ }
21
+ }
22
+ return { cursor: 0, kind: 'list', options: [...active.values()].map(item => {
23
+ const homePath = /^(?:~\/\.dsh|\$DSH_HOME)\/(.*)$/u.exec(item.path)?.[1];
24
+ return { detail: `scope ${item.scope}`, label: item.path,
25
+ value: { kind: 'open-file', path: resolve(homePath === undefined ? root : resolveDshHome(), homePath ?? item.path) } };
26
+ }), purpose: 'memory', title: 'Memory' };
27
+ }
package/lib/renderer.js CHANGED
@@ -65,7 +65,12 @@ function cellText(cell, toolMode, accessible, now = Date.now()) {
65
65
  : accessible ? '[done]' : '◆';
66
66
  return `${cell.pending === true ? YELLOW : DIM}${marker} ${safe}${elapsed === undefined ? '' : ` · ${duration(elapsed)}`}${RESET}`;
67
67
  }
68
- case 'context': return `${DIM}Context · ${safe}${RESET}`;
68
+ case 'context': {
69
+ if (cell.collapsed !== true || cell.detail === undefined)
70
+ return `${DIM}Context · ${safe}${RESET}`;
71
+ const head = `${DIM}Context · ${plain(cell.detail, true)}${RESET}`;
72
+ return toolMode === 'expanded' ? `${head}\n${safe}` : head;
73
+ }
69
74
  case 'shell': return `${DIM}Shell${RESET}\n${safe}`;
70
75
  case 'compaction': return `${DIM}${accessible ? '[compacted]' : '↻'} ${safe}${RESET}`;
71
76
  case 'reasoning': return cell.collapsed
@@ -263,9 +268,11 @@ export function createRenderer(options) {
263
268
  if (overlay.kind === 'details')
264
269
  return detailsPanel(overlay, state, width, rows);
265
270
  if (overlay.kind === 'info') {
271
+ const cards = overlay.cells?.flatMap(cell => wrapTextWithAnsi(cellText(cell, state.toolMode, accessible), width)) ?? [];
266
272
  return [
267
- `${BOLD_CYAN}${announcement(plain(overlay.title), overlay.lines.length)}${RESET}`,
273
+ `${BOLD_CYAN}${announcement(plain(overlay.title), overlay.lines.length + (overlay.cells?.length ?? 0))}${RESET}`,
268
274
  ...overlay.lines.flatMap(value => wrapTextWithAnsi(plain(value, true), width)),
275
+ ...cards,
269
276
  `${DIM}Enter or Esc to close${RESET}`,
270
277
  ].slice(0, Math.max(2, rows - 3));
271
278
  }
@@ -4,9 +4,13 @@ import type { TuiRoot } from './tui-root.js';
4
4
  export interface RootLaunchOptions {
5
5
  readonly continue: boolean;
6
6
  readonly cwd: string;
7
+ readonly effort?: string;
7
8
  readonly name?: string;
8
9
  readonly images?: readonly string[];
10
+ readonly model?: string;
11
+ readonly permission?: string;
9
12
  readonly prompt?: string;
13
+ readonly provider?: string;
10
14
  readonly resume?: string;
11
15
  }
12
16
  export interface SessionRuntime {
@@ -1,14 +1,16 @@
1
1
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
2
  import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
3
3
  import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, statusOverlay, } from './catalogs.js';
4
+ import { diffOverlay } from './diff-view.js';
4
5
  import { eventsFromRecords } from './history-records.js';
5
6
  import { admitDraftImages, encodeDraftImages, readDraftImages } from './image-input.js';
6
7
  import { contextPercent, jobViews, subagentViews } from './presentation.js';
7
8
  import { humanPrompt, humanPrompts, inheritedTurn, rewindActionOverlay, rewindOverlay, } from './rewind.js';
8
9
  import { createToolPresenter } from './tool-presentation.js';
9
- import { runHumanShell } from './shell-command.js';
10
+ import { quoteShellWord, runHumanShell } from './shell-command.js';
10
11
  import { markdownTranscript } from './session-export.js';
11
12
  import { isSessionId, sessionMatches, sessionNotFound } from './session-list.js';
13
+ import { memoryOverlay } from './memory.js';
12
14
  import { foldCells, pendingShellCells } from './transcript.js';
13
15
  function detail(error) {
14
16
  return error instanceof Error ? error.message : String(error);
@@ -71,6 +73,17 @@ async function firstControl(iterator) {
71
73
  function usage(raw, expected) {
72
74
  return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
73
75
  }
76
+ function configValue(raw) {
77
+ try {
78
+ return JSON.parse(raw);
79
+ }
80
+ catch {
81
+ return raw;
82
+ }
83
+ }
84
+ function configLayer(value) {
85
+ return value === undefined ? '—' : JSON.stringify(value);
86
+ }
74
87
  /** Create or resume the exact current root and own its replaceable terminal binding. */
75
88
  export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, decisions) {
76
89
  const lifetime = new AbortController();
@@ -491,6 +504,82 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
491
504
  return { kind: 'success' };
492
505
  },
493
506
  },
507
+ {
508
+ name: 'memory', description: 'Open an instruction file loaded by DSH',
509
+ handler: (invocation) => {
510
+ const invalid = ensureCurrent(invocation) ?? usage(invocation.rawInput, '/memory');
511
+ if (invalid !== undefined)
512
+ return invalid;
513
+ dispatch({ type: 'open-overlay', overlay: memoryOverlay(bound.agent) });
514
+ return { kind: 'success' };
515
+ },
516
+ },
517
+ {
518
+ name: 'config', description: 'Show or update native DSH settings', input: { hint: '[NAMESPACE KEY=VALUE]' },
519
+ handler: async (invocation) => {
520
+ const stale = ensureCurrent(invocation);
521
+ if (stale !== undefined)
522
+ return stale;
523
+ const raw = invocation.rawInput.trim();
524
+ if (raw === '')
525
+ return { kind: 'success', text: ctx.settings.describe({ redactSecrets: true }).map(item => `${String(item.ns)}\n value: ${configLayer(item.value)}\n base: ${configLayer(item.base)}\n user: ${configLayer(item.user)}`).join('\n') };
526
+ const match = /^(\S+)\s+([^\s=]+)=(.*)$/su.exec(raw);
527
+ if (match?.[1] === undefined || match[2] === undefined || match[3] === undefined) {
528
+ return { kind: 'error', text: 'usage: /config [NAMESPACE KEY=VALUE]' };
529
+ }
530
+ const value = configValue(match[3]);
531
+ if (match[2].includes('.'))
532
+ await ctx.settings.mutate(match[1], [{ op: 'set', path: match[2].split('.'), value }]);
533
+ else
534
+ await ctx.settings.update(match[1], { [match[2]]: value });
535
+ return { kind: 'success', text: `updated ${match[1]}.${match[2]}` };
536
+ },
537
+ },
538
+ {
539
+ name: 'diff', description: 'Show working tree or last-turn file changes', input: { hint: '[turn]' }, handler: async (invocation) => {
540
+ const invalid = ensureCurrent(invocation);
541
+ if (invalid !== undefined)
542
+ return invalid;
543
+ const mode = invocation.rawInput.trim();
544
+ if (mode !== '' && mode !== 'turn')
545
+ return { kind: 'error', text: 'usage: /diff [turn]' };
546
+ dispatch({ type: 'open-overlay', overlay: await diffOverlay(ctx, bound.agent, bound.presenter, mode, lifetime.signal) });
547
+ return { kind: 'success' };
548
+ },
549
+ },
550
+ {
551
+ name: 'plugins', description: 'List the running profile plugins',
552
+ handler: async (invocation) => {
553
+ const stale = ensureCurrent(invocation);
554
+ if (stale !== undefined)
555
+ return stale;
556
+ if (invocation.rawInput.trim() !== '')
557
+ return { kind: 'error', text: 'usage: /plugins' };
558
+ const servers = new Map();
559
+ for (const entry of ctx.loader.entries())
560
+ if (entry.options.name === '@deepseek-ai/dsh-mcp-client'
561
+ && typeof entry.options.config?.serverName === 'string')
562
+ servers.set(entry.id, entry.options.config.serverName);
563
+ const snapshot = await ctx.get('pluginInventory').list();
564
+ return { kind: 'success', text: snapshot.entries.map(entry => `${String(entry.entryId)} · ${entry.moduleName}${servers.has(String(entry.entryId)) ? ` · ${servers.get(String(entry.entryId))}` : ''} · ${entry.enabled ? 'enabled' : 'disabled'} · ${entry.fiberPhase ?? 'inactive'}`).join('\n') };
565
+ },
566
+ },
567
+ {
568
+ name: 'plugin', description: 'Run DSH profile plugin management', input: { hint: 'ARGS' },
569
+ handler: async (invocation) => {
570
+ const stale = ensureCurrent(invocation);
571
+ if (stale !== undefined)
572
+ return stale;
573
+ if (invocation.rawInput.trim() === '')
574
+ return { kind: 'error', text: 'usage: /plugin ARGS' };
575
+ const command = `dsh plugin --profile ${quoteShellWord(decodeURIComponent(new URL(ctx.baseUrl).pathname).split('/').at(-2))}${invocation.rawInput}`;
576
+ 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'));
577
+ bound.agent.inject(message);
578
+ await ctx.sessions.flush(bound.agent.session);
579
+ publish(bound);
580
+ return result.exitCode === 0 ? { kind: 'success' } : { kind: 'error', text: `dsh plugin exited ${String(result.exitCode ?? result.signal ?? 'without status')}` };
581
+ },
582
+ },
494
583
  {
495
584
  name: 'new', description: 'Start a new session', input: { hint: '[--name TITLE]' },
496
585
  handler: async (invocation) => {
@@ -775,6 +864,34 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
775
864
  await bound.controlPump;
776
865
  }
777
866
  await install(binding);
867
+ if (options.model !== undefined || options.effort !== undefined) {
868
+ const catalog = await ctx.sessionController.modelCatalog();
869
+ const currentSelection = currentModel(ctx, binding.agent) ?? catalog.default;
870
+ const model = options.model ?? currentSelection.model;
871
+ const candidates = catalog.groups.filter(group => group.models.some(item => item.id === model));
872
+ if (options.provider === undefined && candidates.length > 1) {
873
+ throw new Error(`model "${model}" is available from multiple providers: ${candidates.map(group => group.id).join(', ')}; pass --provider`);
874
+ }
875
+ const result = await ctx.sessionController.selectModel({
876
+ sessionId: binding.agent.id,
877
+ model,
878
+ provider: options.provider ?? candidates[0]?.id ?? currentSelection.provider,
879
+ ...(options.effort === undefined ? {} : { reasoningEffort: options.effort }),
880
+ });
881
+ const { effort: _previousEffort, ...root } = binding.root;
882
+ binding.root = {
883
+ ...root, model: result.selected.model, provider: result.selected.provider,
884
+ ...(result.selected.reasoningEffort === undefined ? {} : { effort: result.selected.reasoningEffort }),
885
+ };
886
+ }
887
+ if (options.permission !== undefined) {
888
+ const execution = await ctx.commands.execute(binding.agent, `/permission ${options.permission}`, [], lifetime.signal);
889
+ if (execution === undefined)
890
+ throw new Error('DSH supplied no /permission command');
891
+ if (execution.result.kind === 'error')
892
+ throw new Error(execution.result.text ?? 'permission change failed');
893
+ binding.root = { ...binding.root, permission: options.permission };
894
+ }
778
895
  tuiRoot.bind(binding.agent);
779
896
  const activate = async (value) => {
780
897
  try {
@@ -999,7 +1116,8 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
999
1116
  if (attachments.length > 0)
1000
1117
  throw new Error('!command does not accept image attachments');
1001
1118
  await bound.agent.runMaintenance(async (signal) => {
1002
- bound.agent.inject(await runHumanShell(ctx, bound.agent, command, signal));
1119
+ const [message] = await runHumanShell(ctx, bound.agent, command, signal);
1120
+ bound.agent.inject(message);
1003
1121
  await ctx.sessions.flush(bound.agent.session);
1004
1122
  });
1005
1123
  publish(bound);
@@ -5,5 +5,6 @@ import type { ShellRunResult } from '@deepseek-ai/dsh-shell';
5
5
  export declare const HUMAN_SHELL_TIMEOUT_MS = 30000;
6
6
  export declare const HUMAN_SHELL_STREAM_BYTES: number;
7
7
  export declare function shellResultText(command: string, result: ShellRunResult): string;
8
+ export declare function quoteShellWord(value: string): string;
8
9
  /** Run an explicit human command through DSH and identify its next-step context. */
9
- export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal): Promise<UserMessage>;
10
+ export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal, success?: string): Promise<readonly [UserMessage, ShellRunResult]>;
@@ -1,4 +1,5 @@
1
1
  import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm';
2
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
2
3
  export const HUMAN_SHELL_TIMEOUT_MS = 30_000;
3
4
  export const HUMAN_SHELL_STREAM_BYTES = 32 * 1024;
4
5
  function tail(output) {
@@ -42,21 +43,23 @@ export function shellResultText(command, result) {
42
43
  }
43
44
  return parts.join('\n');
44
45
  }
46
+ export function quoteShellWord(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
45
47
  /** Run an explicit human command through DSH and identify its next-step context. */
46
- export async function runHumanShell(ctx, agent, command, signal) {
48
+ export async function runHumanShell(ctx, agent, command, signal, success) {
47
49
  const result = await ctx.shell.run(ctx.shell.resolve({
48
50
  command,
51
+ env: { DSH_HOME: resolveDshHome() },
49
52
  sandboxPolicy: ctx.sandboxPolicy.resolve({ session: agent.session }),
50
53
  signal,
51
54
  stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES,
52
55
  timeoutMs: HUMAN_SHELL_TIMEOUT_MS,
53
56
  workdir: agent.session.header.cwd,
54
57
  }));
55
- return createUserMessage({
56
- content: [{ type: 'text', text: shellResultText(command, result) }],
57
- source: {
58
- form: 'notice', kind: 'plugin', plugin: 'dashi',
59
- summary: boundContextSummary(`Shell: ${command.replace(/\s+/gu, ' ').trim()}`),
60
- },
61
- });
58
+ return [createUserMessage({
59
+ content: [{ type: 'text', text: shellResultText(command, result) + (result.exitCode === 0 && success ? `\n${success}` : '') }],
60
+ source: {
61
+ form: 'notice', kind: 'plugin', plugin: 'dashi',
62
+ summary: boundContextSummary(`Shell: ${command.replace(/\s+/gu, ' ').trim()}`),
63
+ },
64
+ }), result];
62
65
  }
package/lib/state.d.ts CHANGED
@@ -137,6 +137,9 @@ export type OverlayValue = {
137
137
  readonly effort?: string;
138
138
  readonly model: string;
139
139
  readonly provider: string;
140
+ } | {
141
+ readonly kind: 'open-file';
142
+ readonly path: string;
140
143
  } | {
141
144
  readonly interrupt?: true;
142
145
  readonly kind: 'fork';
@@ -197,9 +200,10 @@ export type Overlay = {
197
200
  readonly kind: 'list';
198
201
  readonly notice?: string;
199
202
  readonly options: readonly OverlayOption[];
200
- readonly purpose: 'agents' | 'completion' | 'model' | 'permission' | 'resume' | 'rewind' | 'rewind-action' | 'search';
203
+ readonly purpose: 'agents' | 'completion' | 'memory' | 'model' | 'permission' | 'resume' | 'rewind' | 'rewind-action' | 'search';
201
204
  readonly title: string;
202
205
  } | {
206
+ readonly cells?: readonly TerminalCell[];
203
207
  readonly kind: 'info';
204
208
  readonly lines: readonly string[];
205
209
  readonly title: string;
package/lib/state.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { FLAG_HELP, VERSION_LINE } from './help.js';
1
2
  export function initialViewState(cwd, inline, root, cells = [], prompts = [], historyHasMore = false, attachments = []) {
2
3
  return {
3
4
  attachments, cells, composer: '', cwd, decisions: [], exitArmed: false, historyHasMore, inline,
@@ -31,6 +32,7 @@ function help() {
31
32
  kind: 'info',
32
33
  title: 'Help',
33
34
  lines: [
35
+ VERSION_LINE,
34
36
  'Enter send · Ctrl+J newline · Ctrl+T steer/next turn',
35
37
  'Esc interrupt · Ctrl+C clear/interrupt · Ctrl+C/D again within 2s to exit',
36
38
  '/ and @ complete live · Tab insert · Shift+Tab permission · Ctrl+O cards',
@@ -39,8 +41,9 @@ function help() {
39
41
  'Ctrl+B activity details for jobs and subagents',
40
42
  '@ paths require DSH file-reference · Ctrl+V pastes an image · Backspace removes it',
41
43
  'Ctrl+S stash · Ctrl+G editor · F1 help · Ctrl+L redraw · Ctrl+Z suspend',
42
- '/new /clear /resume [--all] [NAME|UUID] /fork /rewind /rename /model /permission /agents /queue · launch -r/-c',
43
- '/status /context /tasks /history /export /copy /exit',
44
+ '/new /clear /resume [--all] [NAME|UUID] /fork /rewind /rename /model /permission /agents /queue',
45
+ '/status /context /memory /config /diff /plugins /plugin /tasks /history /export /copy /exit',
46
+ ...FLAG_HELP,
44
47
  ],
45
48
  };
46
49
  }
package/lib/transcript.js CHANGED
@@ -26,6 +26,24 @@ function shellCell(message, pending) {
26
26
  key: `shell:${String(message.id)}`, kind: 'shell', pending, text: messageText(message.content),
27
27
  };
28
28
  }
29
+ function collapsedContextCell(message, key) {
30
+ const source = message.source;
31
+ const form = source.form;
32
+ if (source.kind !== 'agent-instructions'
33
+ && (source.kind !== 'plugin' || !['instructions', 'catalog', 'snapshot', 'notice', 'recall'].includes(String(form)))) {
34
+ return undefined;
35
+ }
36
+ const text = messageText(message.content);
37
+ const lines = text.split(/\r?\n/u);
38
+ const first = form === 'notice' && typeof source.summary === 'string' ? source.summary : lines[0] ?? '';
39
+ return {
40
+ collapsed: true,
41
+ detail: `${String(form)} · ${first.trim()} · ${String(lines.length)} lines`,
42
+ key,
43
+ kind: 'context',
44
+ text,
45
+ };
46
+ }
29
47
  /** Project DSH-owned pending injections beside the durable transcript. */
30
48
  export function pendingShellCells(messages) {
31
49
  return messages.filter(isDashiShell).map(message => shellCell(message, true));
@@ -132,9 +150,10 @@ export function foldCells(events, presenter = createToolPresenter(() => undefine
132
150
  break;
133
151
  }
134
152
  const text = messageText(event.data.content);
153
+ const context = collapsedContextCell(event.data, `${event.seq}:context`);
135
154
  cells.push(isDashiShell(event.data)
136
155
  ? shellCell(event.data, false)
137
- : {
156
+ : context ?? {
138
157
  key: `${event.seq}:user`,
139
158
  kind: event.data.source.kind === 'user' ? 'user' : 'context',
140
159
  text,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antst/dashi",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.8",
4
4
  "description": "A terminal UI for DeepSeek Harness",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -37,7 +37,9 @@
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "4.0.2",
40
+ "@deepseek-ai/cordis-plugin-loader": "1.0.3",
40
41
  "@deepseek-ai/dsh-agent": "0.1.2-rc.1",
42
+ "@deepseek-ai/dsh-agent-instructions": "0.1.2-rc.1",
41
43
  "@deepseek-ai/dsh-agent-presets": "0.1.2-rc.1",
42
44
  "@deepseek-ai/dsh-api-session-controller": "0.1.2-rc.1",
43
45
  "@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
@@ -46,6 +48,8 @@
46
48
  "@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
47
49
  "@deepseek-ai/dsh-file-reference": "0.1.2-rc.1",
48
50
  "@deepseek-ai/dsh-fs": "0.1.2-rc.1",
51
+ "@deepseek-ai/dsh-home-paths": "0.1.2-rc.1",
52
+ "@deepseek-ai/dsh-host-plugin-inventory": "0.1.2-rc.1",
49
53
  "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
50
54
  "@deepseek-ai/dsh-permission-presets": "0.1.2-rc.1",
51
55
  "@deepseek-ai/dsh-plan-mode": "0.1.2-rc.1",
@@ -53,6 +57,7 @@
53
57
  "@deepseek-ai/dsh-session": "0.1.2-rc.1",
54
58
  "@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
55
59
  "@deepseek-ai/dsh-session-query": "0.1.2-rc.1",
60
+ "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
56
61
  "@deepseek-ai/dsh-shell": "0.1.2-rc.1",
57
62
  "@deepseek-ai/dsh-skill": "0.1.2-rc.1",
58
63
  "@deepseek-ai/dsh-subagent": "0.1.2-rc.1",