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

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.
@@ -176,6 +176,7 @@ export function createTerminalShell(options) {
176
176
  return;
177
177
  failed = true;
178
178
  accepting = false;
179
+ renderer.discardSecretComposer();
179
180
  const detail = error instanceof Error ? error.stack ?? error.message : String(error);
180
181
  writeError(`dashi: terminal failure\n${detail}\n`);
181
182
  try {
@@ -201,6 +202,9 @@ export function createTerminalShell(options) {
201
202
  function dispatch(action) {
202
203
  if (!accepting)
203
204
  return;
205
+ const question = state.decisions[0]?.kind === 'question' ? state.decisions[0].questions[state.decisions[0].index] : undefined;
206
+ if (question?.secret === true && ['decision-submit', 'decision-cancel', 'decision-withdrawn', 'escape', 'terminal-lost'].includes(action.type))
207
+ renderer.discardSecretComposer();
204
208
  const [next, effects] = reduce(state, action);
205
209
  state = next;
206
210
  if (effects.length === 0)
@@ -224,6 +228,7 @@ export function createTerminalShell(options) {
224
228
  },
225
229
  dispatch,
226
230
  async dispose() {
231
+ renderer.discardSecretComposer();
227
232
  const [next, effects] = reduce(state, { type: 'terminal-lost' });
228
233
  state = next;
229
234
  for (const effect of effects)
package/lib/catalogs.d.ts CHANGED
@@ -6,6 +6,8 @@ import type { SessionSearchHit } from '@deepseek-ai/dsh-session-query';
6
6
  import type { Overlay, OverlayOption, RootView } from './state.js';
7
7
  /** Build command and skill completion rows for one exact root. */
8
8
  export declare function completionOptions(ctx: Context, agent: Agent, input: string, caret?: number, signal?: AbortSignal): Promise<readonly OverlayOption[]>;
9
+ /** Build a filtered view of the skills resolved for one live session. */
10
+ export declare function skillOverlay(ctx: Context, agent: Agent, filter: string, signal?: AbortSignal): Promise<Overlay>;
9
11
  /** Build the cold session picker without adopting any candidate. */
10
12
  export declare function sessionOverlay(items: readonly SessionSummary[], currentId: string, cwd?: string): Overlay;
11
13
  /** Flatten DSH's provider/model/effort catalog into one terminal picker. */
package/lib/catalogs.js CHANGED
@@ -69,6 +69,23 @@ export async function completionOptions(ctx, agent, input, caret = input.length,
69
69
  }));
70
70
  return [...commands, ...skills];
71
71
  }
72
+ /** Build a filtered view of the skills resolved for one live session. */
73
+ export async function skillOverlay(ctx, agent, filter, signal) {
74
+ const lookup = { cwd: agent.session.header.cwd, scope: agent, signal };
75
+ const needle = filter.trim().toLocaleLowerCase();
76
+ const skills = (await ctx.skills.list(lookup)).filter(skill => needle === ''
77
+ || skill.name.toLocaleLowerCase().includes(needle)
78
+ || skill.description.toLocaleLowerCase().includes(needle));
79
+ const definitions = await Promise.all(skills.map(skill => ctx.skills.get(skill.name, lookup)));
80
+ return {
81
+ cursor: 0, kind: 'list', purpose: 'skills', title: needle === '' ? 'Skills' : `Skills · ${filter.trim()}`,
82
+ options: skills.map((skill, index) => ({
83
+ detail: `${skill.description} · user ${skill.invocation.userInvocable ? 'yes' : 'no'} · model ${skill.invocation.modelInvocable ? 'yes' : 'no'} · source ${skill.source} · provider ${skill.provider} · path ${definitions[index]?.path ?? 'unavailable'}`,
84
+ label: `/${skill.name}`,
85
+ value: { kind: 'insert', text: `/${skill.name} ` },
86
+ })),
87
+ };
88
+ }
72
89
  /** Build the cold session picker without adopting any candidate. */
73
90
  export function sessionOverlay(items, currentId, cwd) {
74
91
  return {
package/lib/diff-view.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { HUMAN_SHELL_STREAM_BYTES } from './shell-command.js';
1
2
  import { boundedBody } from './tool-presentation.js';
2
3
  import { foldCells } from './transcript.js';
3
4
  export async function diffOverlay(ctx, agent, presenter, mode, signal) {
@@ -10,7 +11,7 @@ export async function diffOverlay(ctx, agent, presenter, mode, signal) {
10
11
  const command = 'git diff --no-ext-diff --no-color HEAD --';
11
12
  const result = await ctx.shell.run(ctx.shell.resolve({
12
13
  command, sandboxPolicy: ctx.sandboxPolicy.resolve({ session: agent.session }), signal,
13
- stdoutMaxBytes: 64 * 1024, timeoutMs: 30_000, workdir: agent.session.header.cwd,
14
+ stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES, timeoutMs: 30_000, workdir: agent.session.header.cwd,
14
15
  }));
15
16
  const output = [result.stdout.text, result.stderr.text].filter(Boolean).join('\n');
16
17
  const cell = {
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 · --all · --name TITLE", " --model ID [--provider ID] · --effort LEVEL · --permission PRESET", " --yolo | --dangerously-skip-permissions · --image PATH · -h, --help"];
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 · --yolo | --dangerously-skip-permissions · --image 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 · --all · --name TITLE', ' --model ID [--provider ID] · --effort LEVEL · --permission PRESET', ' --yolo | --dangerously-skip-permissions · --image PATH · -h, --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 · --yolo | --dangerously-skip-permissions · --image PATH · -h, --help'
6
6
  ];
package/lib/index.js CHANGED
@@ -12,12 +12,14 @@ import { createSessionRuntime } from './session-runtime.js';
12
12
  import { TuiRoot } from './tui-root.js';
13
13
  export const name = 'dashi';
14
14
  export const inject = [
15
- 'agentPresets', 'agents', 'attachments', 'cmdlineArgs', 'commands', 'fs', 'loader', 'permissionPresets',
15
+ 'agentPresets', 'agents', 'attachments', 'authorization', 'cmdlineArgs', 'commands', 'credentials', 'fs', 'loader', 'permissionPresets',
16
16
  'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', '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 agentPreset, sessionId;
22
+ let forkSession = false;
21
23
  let inline = true;
22
24
  let name;
23
25
  let effort;
@@ -38,13 +40,15 @@ function parseArgs(args) {
38
40
  inline = true;
39
41
  else if (argument === '--fullscreen')
40
42
  inline = false;
41
- else if (argument === '--name' || argument === '--image' || argument === '--effort'
42
- || argument === '--model' || argument === '--permission' || argument === '--provider') {
43
+ else if (['--name', '-n', '--agent', '--image', '--effort', '--model', '--permission', '--provider',
44
+ '--session-id'].includes(argument ?? '')) {
43
45
  const value = args[++index];
44
46
  if (value === undefined || value === '')
45
47
  return undefined;
46
- if (argument === '--name')
48
+ if (argument === '--name' || argument === '-n')
47
49
  name = value;
50
+ else if (argument === '--agent')
51
+ agentPreset = value;
48
52
  else if (argument === '--image')
49
53
  images.push(value);
50
54
  else if (argument === '--effort')
@@ -53,6 +57,8 @@ function parseArgs(args) {
53
57
  model = value;
54
58
  else if (argument === '--permission')
55
59
  permission = value;
60
+ else if (argument === '--session-id')
61
+ sessionId = value;
56
62
  else
57
63
  provider = value;
58
64
  }
@@ -69,6 +75,8 @@ function parseArgs(args) {
69
75
  resumeAll = true;
70
76
  else if (argument === '--continue' || argument === '-c')
71
77
  useContinue = true;
78
+ else if (argument === '--fork-session')
79
+ forkSession = true;
72
80
  else if (argument === '--yolo' || argument === '--dangerously-skip-permissions') {
73
81
  permission = 'danger-full-access';
74
82
  }
@@ -80,12 +88,15 @@ function parseArgs(args) {
80
88
  if (resume !== undefined && (resumePicker || useContinue) || resumePicker && useContinue
81
89
  || name !== undefined && (resume !== undefined || resumePicker || useContinue)
82
90
  || resumeAll && resume === undefined && !resumePicker || resumeAll && useContinue
83
- || provider !== undefined && model === undefined)
91
+ || provider !== undefined && model === undefined
92
+ || sessionId !== undefined && (!isSessionId(sessionId) || resume !== undefined || resumePicker || useContinue)
93
+ || agentPreset !== undefined && (resume !== undefined || resumePicker || useContinue)
94
+ || forkSession && resume === undefined && !resumePicker && !useContinue)
84
95
  return undefined;
85
96
  return {
86
97
  accessible,
87
98
  continue: useContinue,
88
- inline,
99
+ ...(agentPreset === undefined ? {} : { agentPreset }), forkSession, inline,
89
100
  resumeAll,
90
101
  resumePicker,
91
102
  ...(images.length === 0 ? {} : { images }),
@@ -95,7 +106,7 @@ function parseArgs(args) {
95
106
  ...(permission === undefined ? {} : { permission }),
96
107
  ...(prompt.length === 0 ? {} : { prompt: prompt.join(' ') }),
97
108
  ...(provider === undefined ? {} : { provider }),
98
- ...(resume === undefined ? {} : { resume }),
109
+ ...(resume === undefined ? {} : { resume }), ...(sessionId === undefined ? {} : { sessionId }),
99
110
  };
100
111
  }
101
112
  function packageVersion(path) {
package/lib/renderer.d.ts CHANGED
@@ -6,6 +6,7 @@ export type CopyResult = 'ok' | 'too-large' | 'unsupported';
6
6
  export interface Renderer extends TerminalSession {
7
7
  bell(): void;
8
8
  copy(text: string): CopyResult;
9
+ discardSecretComposer(): void;
9
10
  materializedCells(): number;
10
11
  setComposer(text: string, cursor?: ComposerCursor): void;
11
12
  }
package/lib/renderer.js CHANGED
@@ -14,6 +14,7 @@ const OVERSCAN_CELLS = 8;
14
14
  const THEME_GENERATION = 1;
15
15
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
16
16
  const SPINNER_INTERVAL_MS = 80;
17
+ const MASK_TOKEN = new RegExp(String.raw `\u001B(?:\[[0-?]*[ -/]*[@-~]|[_P^X][^\u0007]*(?:\u0007|\u001B\\))|[^\s]`, 'gu');
17
18
  export function supportsOsc52(terminalType) {
18
19
  return terminalType?.toLocaleLowerCase() !== 'dumb';
19
20
  }
@@ -160,14 +161,22 @@ export function createRenderer(options) {
160
161
  const tui = options.inline
161
162
  ? new TuiMainScreen(terminal, true)
162
163
  : new TuiAltScreen(terminal, true, undefined, { mouse: true });
163
- const editor = new Editor(tui, theme(), { paddingX: 1 });
164
+ let editor = new Editor(tui, theme(), { paddingX: 1 });
164
165
  editor.disableSubmit = true;
165
166
  editor.onChange = text => {
167
+ const decision = options.readState().decisions[0];
166
168
  const caret = caretOffset(text, editor.getCursor());
167
169
  options.dispatch({
168
- type: 'composer-changed', text, caret, completion: hasCompletionTrigger(text, caret),
170
+ type: 'composer-changed', text: decision?.kind === 'question' && decision.questions[decision.index]?.secret === true ? editor.getExpandedText() : text, caret, completion: hasCompletionTrigger(text, caret),
169
171
  });
170
172
  };
173
+ const replaceEditor = () => {
174
+ const discarded = editor;
175
+ editor = new Editor(tui, theme(), { paddingX: 1 });
176
+ editor.disableSubmit = true;
177
+ editor.onChange = discarded.onChange;
178
+ tui.setFocus(editor);
179
+ };
171
180
  const lineCache = new Map();
172
181
  let animationTimer;
173
182
  const cellLines = (cell, width, toolMode, now) => {
@@ -203,14 +212,18 @@ export function createRenderer(options) {
203
212
  return [`${RED}Invalid empty question batch${RESET}`];
204
213
  const planReview = question.intent?.kind === 'plan-review';
205
214
  const heading = planReview ? 'Plan review' : question.header ?? `Question ${String(decision.index + 1)}/${String(decision.questions.length)}`;
215
+ const customAllowed = question.allowCustom !== false;
206
216
  const selected = question.options[decision.cursor]?.label ?? 'Other (type an answer)';
207
- const announced = announcement(plain(heading), question.options.length + 1, selected, decision.cursor);
217
+ const announced = announcement(plain(heading), question.options.length + Number(customAllowed), selected, decision.cursor);
208
218
  const prompt = wrapTextWithAnsi(`${BOLD_CYAN}${announced}${RESET}\n${plain(question.question, true)}`, width);
209
219
  const choiceRows = question.options.map((option, index) => optionLine(option.description === undefined ? option.label : `${option.label} — ${option.description}`, index + 1, decision.cursor === index, decision.selected.includes(option.label)));
210
- choiceRows.push(optionLine('Other (type an answer)', question.options.length + 1, decision.cursor === question.options.length, false));
220
+ if (customAllowed)
221
+ choiceRows.push(optionLine('Other (type an answer)', question.options.length + 1, decision.cursor === question.options.length, false));
211
222
  const detailLimit = planReview ? Math.max(3, rows - prompt.length - choiceRows.length - 5) : 3;
212
223
  const detail = question.detail === undefined ? [] : wrapTextWithAnsi(`${DIM}${plain(question.detail, true)}${RESET}`, width).slice(0, detailLimit);
213
- const custom = decision.cursor === question.options.length ? editor.render(width) : [];
224
+ const editorLines = decision.cursor === question.options.length ? editor.render(width) : [];
225
+ const custom = question.secret === true
226
+ ? editorLines.map((value, index) => index === 0 || index === editorLines.length - 1 ? value : value.replace(MASK_TOKEN, token => token.startsWith('\u001B') ? token : '•')) : editorLines;
214
227
  const rendered = [...prompt, ...detail, ...choiceRows, ...custom];
215
228
  return rows < 12 ? rendered.slice(0, Math.max(2, rows - 4)) : rendered;
216
229
  };
@@ -406,6 +419,7 @@ export function createRenderer(options) {
406
419
  terminal.write(`\u001B]52;c;${Buffer.from(safe).toString('base64')}\u0007`);
407
420
  return 'ok';
408
421
  },
422
+ discardSecretComposer: replaceEditor,
409
423
  drainInput: () => terminal.drainInput(),
410
424
  materializedCells: () => materializedCount,
411
425
  render: force => { tui.renderNow(force); },
@@ -2,9 +2,11 @@ import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { ActivatableOverlayValue, DecisionAnswer, DraftAttachment, OverlayOption, PendingDecision, RootView, SendMode, TerminalCell, UiAction } from './state.js';
3
3
  import type { TuiRoot } from './tui-root.js';
4
4
  export interface RootLaunchOptions {
5
+ readonly agentPreset?: string;
5
6
  readonly continue: boolean;
6
7
  readonly cwd: string;
7
8
  readonly effort?: string;
9
+ readonly forkSession: boolean;
8
10
  readonly name?: string;
9
11
  readonly images?: readonly string[];
10
12
  readonly model?: string;
@@ -12,6 +14,7 @@ export interface RootLaunchOptions {
12
14
  readonly prompt?: string;
13
15
  readonly provider?: string;
14
16
  readonly resume?: string;
17
+ readonly sessionId?: string;
15
18
  }
16
19
  export interface SessionRuntime {
17
20
  readonly initialAttachments: readonly DraftAttachment[];
@@ -1,6 +1,9 @@
1
+ import { basename } from 'node:path';
2
+ import { AuthorizationDeclinedError } from '@deepseek-ai/dsh-authorization';
3
+ import { parseCredentialKey } from '@deepseek-ai/dsh-credentials';
1
4
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
5
  import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
3
- import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, statusOverlay, } from './catalogs.js';
6
+ import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, skillOverlay, statusOverlay, } from './catalogs.js';
4
7
  import { diffOverlay } from './diff-view.js';
5
8
  import { eventsFromRecords } from './history-records.js';
6
9
  import { admitDraftImages, encodeDraftImages, readDraftImages } from './image-input.js';
@@ -12,6 +15,9 @@ import { markdownTranscript } from './session-export.js';
12
15
  import { isSessionId, sessionMatches, sessionNotFound } from './session-list.js';
13
16
  import { memoryOverlay } from './memory.js';
14
17
  import { foldCells, pendingShellCells } from './transcript.js';
18
+ function starterAgents(cwd) {
19
+ 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`;
20
+ }
15
21
  function detail(error) {
16
22
  return error instanceof Error ? error.message : String(error);
17
23
  }
@@ -91,11 +97,13 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
91
97
  let started = false;
92
98
  let shutdown;
93
99
  let decisionSequence = 0;
94
- const initialId = options.resume === undefined
100
+ let initialId = options.resume === undefined
95
101
  ? options.continue
96
102
  ? continuation((await ctx.sessionController.list({}, lifetime.signal)).items, options.cwd)
97
- : (await ctx.sessionController.create({ cwd: options.cwd })).sessionId
103
+ : (await ctx.sessionController.create({ cwd: options.cwd, ...(options.agentPreset === undefined ? {} : { agentPreset: options.agentPreset }), ...(options.sessionId === undefined ? {} : { sessionId: SessionId(options.sessionId) }) })).sessionId
98
104
  : SessionId(options.resume);
105
+ if (options.forkSession)
106
+ initialId = (await ctx.sessionController.fork({ sessionId: initialId })).sessionId;
99
107
  const prepare = async (sessionId, name) => {
100
108
  const abort = new AbortController();
101
109
  try {
@@ -191,6 +199,15 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
191
199
  signal?.removeEventListener('abort', abortDecision);
192
200
  });
193
201
  };
202
+ const authorizationPrompt = (bound, prompt, detail) => decide({
203
+ answers: [], cursor: 0, custom: '', id: `authorization:${String(++decisionSequence)}`, index: 0, kind: 'question', owner: { id: bound.root.id, ...(bound.root.title === undefined ? {} : { label: bound.root.title }) }, selected: [],
204
+ questions: [{ allowCustom: prompt.kind !== 'select', ...(detail === undefined ? {} : { detail }), header: 'Login', id: 'authorization', multiSelect: false, options: prompt.kind === 'select' ? prompt.options : [], question: prompt.message, secret: prompt.kind === 'secret' }],
205
+ }, prompt.signal).then(result => prompt.kind === 'select' ? prompt.options[prompt.options.findIndex(option => option.label === result.answers[0]?.selected[0])]?.id ?? ''
206
+ : result.answers[0]?.custom ?? '').catch(error => {
207
+ if (prompt.signal?.aborted !== true && error.code === 'ASK_ABORTED')
208
+ throw new AuthorizationDeclinedError();
209
+ throw error;
210
+ });
194
211
  const transcriptCells = (bound) => [
195
212
  ...foldCells(bound.events, bound.presenter, { truncatedStart: bound.hasMore }),
196
213
  ...pendingShellCells(bound.agent.inbox.nextStep),
@@ -464,7 +481,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
464
481
  if (nativePermission === undefined)
465
482
  throw new Error('dashi: DSH supplied no /permission command');
466
483
  const ensureCurrent = (invocation) => current().agent === invocation.agent ? undefined : { kind: 'error', text: 'this root is no longer current' };
467
- return [
484
+ const definitions = [
468
485
  {
469
486
  name: 'help', description: 'Show keyboard and command help',
470
487
  handler: (invocation) => {
@@ -514,6 +531,20 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
514
531
  return { kind: 'success' };
515
532
  },
516
533
  },
534
+ {
535
+ name: 'skills', description: 'List skills resolved for this session', input: { hint: '[TEXT]' },
536
+ handler: async (invocation) => {
537
+ const stale = ensureCurrent(invocation);
538
+ if (stale !== undefined)
539
+ return stale;
540
+ const overlay = await skillOverlay(ctx, bound.agent, invocation.rawInput, invocation.signal);
541
+ const replaced = ensureCurrent(invocation);
542
+ if (replaced !== undefined)
543
+ return replaced;
544
+ dispatch({ type: 'open-overlay', overlay });
545
+ return { kind: 'success' };
546
+ },
547
+ },
517
548
  {
518
549
  name: 'config', description: 'Show or update native DSH settings', input: { hint: '[NAMESPACE KEY=VALUE]' },
519
550
  handler: async (invocation) => {
@@ -535,6 +566,43 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
535
566
  return { kind: 'success', text: `updated ${match[1]}.${match[2]}` };
536
567
  },
537
568
  },
569
+ { name: 'login', description: 'List or start provider sign-in', input: { hint: '[KEY [METHOD]]' },
570
+ handler: async (invocation) => {
571
+ const stale = ensureCurrent(invocation);
572
+ if (stale !== undefined)
573
+ return stale;
574
+ const values = invocation.rawInput.trim().split(/\s+/u).filter(Boolean);
575
+ if (values.length === 0)
576
+ return { kind: 'success', text: ctx.authorization.list().map(entry => `${String(entry.key)} · ${entry.label} · ${entry.methods.map(method => `${method.id}: ${method.label}`).join(', ')}${entry.inFlight ? ' · in progress' : ''}`).join('\n') || 'no authorization flows' };
577
+ if (values.length > 2)
578
+ return { kind: 'error', text: 'usage: /login [KEY [METHOD]]' };
579
+ const entry = ctx.authorization.describe(parseCredentialKey(values[0]));
580
+ if (entry === undefined)
581
+ return { kind: 'error', text: `no authorization flow for ${values[0]}` };
582
+ const notices = [];
583
+ const outcome = await ctx.authorization.begin({ key: entry.key, ...(values[1] === undefined ? {} : { method: values[1] }), signal: invocation.signal,
584
+ interaction: { notify: notice => {
585
+ notices.push(notice.message, ...(notice.url === undefined ? [] : [notice.url]), ...(notice.code === undefined ? [] : [`Code: ${notice.code}`]));
586
+ dispatch({ type: 'open-overlay', overlay: { kind: 'info', title: 'Login', lines: notices } });
587
+ }, prompt: prompt => authorizationPrompt(bound, prompt, notices.join('\n')) },
588
+ }).finally(() => { dispatch({ type: 'overlay-close' }); });
589
+ return { kind: 'success', text: outcome.status === 'authorized' ? `signed in to ${entry.label}` : 'login cancelled' };
590
+ },
591
+ },
592
+ { name: 'logout', description: 'List or forget provider credentials', input: { hint: '[KEY]' },
593
+ handler: async (invocation) => {
594
+ const stale = ensureCurrent(invocation);
595
+ if (stale !== undefined)
596
+ return stale;
597
+ const raw = invocation.rawInput.trim();
598
+ if (raw === '')
599
+ return { kind: 'success', text: (await ctx.credentials.listRecords()).map(record => `${String(record.key)} · ${record.kind}`).join('\n') || 'no stored credentials' };
600
+ if (/\s/u.test(raw))
601
+ return { kind: 'error', text: 'usage: /logout [KEY]' };
602
+ await ctx.credentials.deleteRecord(parseCredentialKey(raw));
603
+ return { kind: 'success', text: `signed out ${raw}` };
604
+ },
605
+ },
538
606
  {
539
607
  name: 'diff', description: 'Show working tree or last-turn file changes', input: { hint: '[turn]' }, handler: async (invocation) => {
540
608
  const invalid = ensureCurrent(invocation);
@@ -717,15 +785,17 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
717
785
  },
718
786
  },
719
787
  {
720
- name: 'copy', description: 'Copy the latest completed assistant response',
788
+ name: 'copy', description: 'Copy an assistant response or code block', input: { hint: '[N|code]' },
721
789
  handler: (invocation) => {
722
790
  const stale = ensureCurrent(invocation);
723
791
  if (stale !== undefined)
724
792
  return stale;
725
- const invalid = usage(invocation.rawInput, '/copy');
726
- if (invalid !== undefined)
727
- return invalid;
728
- dispatch({ type: 'copy-latest' });
793
+ const raw = invocation.rawInput.trim();
794
+ const selection = raw === '' ? 1 : raw === 'code' ? 'code' : Number(raw);
795
+ if (raw !== '' && selection !== 'code' && (!/^[1-9]\d*$/u.test(raw) || !Number.isSafeInteger(selection))) {
796
+ return { kind: 'error', text: 'usage: /copy [N|code]' };
797
+ }
798
+ dispatch({ selection, type: 'copy-assistant' });
729
799
  return { kind: 'success' };
730
800
  },
731
801
  },
@@ -758,6 +828,32 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
758
828
  return { kind: 'success' };
759
829
  },
760
830
  },
831
+ {
832
+ name: 'init', description: 'Create a starter AGENTS.md',
833
+ handler: async (invocation) => {
834
+ const stale = ensureCurrent(invocation);
835
+ if (stale !== undefined)
836
+ return stale;
837
+ const invalid = usage(invocation.rawInput, '/init');
838
+ if (invalid !== undefined)
839
+ return invalid;
840
+ const target = await ctx.fs.resolve('AGENTS.md', { cwd: bound.root.cwd, signal: lifetime.signal });
841
+ await ctx.fs.writeText(target, starterAgents(bound.root.cwd), { kind: 'createIfAbsent' }, lifetime.signal, ctx.sandboxPolicy.resolve({ session: bound.agent.session }));
842
+ return { kind: 'success', text: `created ${ctx.fs.processPath(target)}` };
843
+ },
844
+ },
845
+ {
846
+ name: 'effort', description: 'Select the reasoning effort', input: { hint: 'LEVEL' },
847
+ handler: async (invocation) => {
848
+ const invalid = ensureCurrent(invocation);
849
+ const effort = invocation.rawInput.trim();
850
+ if (invalid !== undefined || effort === '')
851
+ return invalid ?? { kind: 'error', text: 'usage: /effort LEVEL' };
852
+ const selected = currentModel(ctx, bound.agent) ?? (await ctx.sessionController.modelCatalog()).default;
853
+ await activate({ kind: 'model', effort, model: selected.model, provider: selected.provider });
854
+ return { kind: 'success' };
855
+ },
856
+ },
761
857
  {
762
858
  ...nativePermission,
763
859
  handler: async (invocation) => {
@@ -798,6 +894,8 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
798
894
  },
799
895
  },
800
896
  ];
897
+ return [...definitions, ...[['quit', 'exit'], ['reset', 'clear'], ['continue', 'resume'], ['branch', 'fork']]
898
+ .map(([name, target]) => ({ ...definitions.find(definition => definition.name === target), name }))];
801
899
  };
802
900
  async function install(bound) {
803
901
  const id = bound.root.id;
package/lib/state.d.ts CHANGED
@@ -87,6 +87,7 @@ export interface QuestionOption {
87
87
  readonly label: string;
88
88
  }
89
89
  export interface QuestionItem {
90
+ readonly allowCustom?: boolean;
90
91
  readonly detail?: string;
91
92
  readonly header?: string;
92
93
  readonly id: string;
@@ -97,6 +98,7 @@ export interface QuestionItem {
97
98
  readonly multiSelect: boolean;
98
99
  readonly options: readonly QuestionOption[];
99
100
  readonly question: string;
101
+ readonly secret?: boolean;
100
102
  }
101
103
  export interface QuestionAnswer {
102
104
  readonly custom?: string;
@@ -129,6 +131,9 @@ export type OverlayValue = {
129
131
  readonly path: string;
130
132
  readonly source: string;
131
133
  readonly text: string;
134
+ } | {
135
+ readonly kind: 'copy';
136
+ readonly text: string;
132
137
  } | {
133
138
  readonly kind: 'insert';
134
139
  readonly text: string;
@@ -182,6 +187,8 @@ export type OverlayValue = {
182
187
  };
183
188
  export type ActivatableOverlayValue = Exclude<OverlayValue, {
184
189
  kind: 'attach';
190
+ } | {
191
+ kind: 'copy';
185
192
  } | {
186
193
  kind: 'insert';
187
194
  }>;
@@ -200,7 +207,7 @@ export type Overlay = {
200
207
  readonly kind: 'list';
201
208
  readonly notice?: string;
202
209
  readonly options: readonly OverlayOption[];
203
- readonly purpose: 'agents' | 'completion' | 'memory' | 'model' | 'permission' | 'resume' | 'rewind' | 'rewind-action' | 'search';
210
+ readonly purpose: 'agents' | 'completion' | 'copy' | 'memory' | 'model' | 'permission' | 'resume' | 'rewind' | 'rewind-action' | 'search' | 'skills';
204
211
  readonly title: string;
205
212
  } | {
206
213
  readonly cells?: readonly TerminalCell[];
@@ -428,7 +435,8 @@ export type UiAction = {
428
435
  readonly hasMore?: boolean;
429
436
  readonly rootId?: string;
430
437
  } | {
431
- readonly type: 'copy-latest';
438
+ readonly selection: number | 'code';
439
+ readonly type: 'copy-assistant';
432
440
  };
433
441
  export type UiEffect = {
434
442
  readonly type: 'activate-overlay';
package/lib/state.js CHANGED
@@ -25,7 +25,7 @@ function moveCursor(decision, cursor) {
25
25
  const question = decision.questions[decision.index];
26
26
  if (question === undefined)
27
27
  return decision;
28
- return { ...decision, cursor: Math.max(0, Math.min(question.options.length, cursor)) };
28
+ return { ...decision, cursor: Math.max(0, Math.min(question.options.length - (question.allowCustom === false ? 1 : 0), cursor)) };
29
29
  }
30
30
  function help() {
31
31
  return {
@@ -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 /resume [--all] [NAME|UUID] /fork /rewind /rename /model /permission /agents /queue',
45
- '/status /context /memory /config /diff /plugins /plugin /tasks /history /export /copy /exit',
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 /history /export /copy /exit /quit',
46
46
  ...FLAG_HELP,
47
47
  ],
48
48
  };
@@ -86,13 +86,24 @@ function refreshSearch(search, cells) {
86
86
  const found = matches(cells, search.query);
87
87
  return { ...search, cursor: Math.min(search.cursor, Math.max(0, found.length - 1)), matches: found };
88
88
  }
89
- function latestCompletedAssistant(cells) {
89
+ function completedAssistant(cells, nth) {
90
+ let seen = 0;
90
91
  for (let index = cells.length - 1; index >= 0; index--) {
91
92
  const cell = cells[index];
92
- if (cell?.kind === 'assistant' && cell.pending !== true)
93
+ if (cell?.kind === 'assistant' && cell.pending !== true && ++seen === nth)
93
94
  return cell;
94
95
  }
95
96
  }
97
+ function codeBlockOptions(text) {
98
+ const fence = /(?:^|\r?\n)(`{3,}|~{3,})([^\r\n]*)\r?\n([\s\S]*?)\r?\n\1[ \t]*(?=\r?\n|$)/gu;
99
+ return [...text.matchAll(fence)].map(match => {
100
+ const code = match[3] ?? '';
101
+ return {
102
+ label: `${match[2]?.trim() || 'text'} · ${code.split(/\r?\n/u)[0] ?? ''}`,
103
+ value: { kind: 'copy', text: code },
104
+ };
105
+ });
106
+ }
96
107
  export function reduce(state, action) {
97
108
  const head = state.decisions[0];
98
109
  switch (action.type) {
@@ -218,7 +229,7 @@ export function reduce(state, action) {
218
229
  const question = head.questions[head.index];
219
230
  if (question === undefined)
220
231
  return [state, []];
221
- const custom = head.cursor === question.options.length ? head.custom.trim() : undefined;
232
+ const custom = question.allowCustom !== false && head.cursor === question.options.length ? question.secret === true ? head.custom : head.custom.trim() : undefined;
222
233
  if (custom !== undefined && custom === '')
223
234
  return [state, []];
224
235
  const option = question.options[head.cursor];
@@ -410,6 +421,10 @@ export function reduce(state, action) {
410
421
  type: 'attach', path: option.value.path, source: option.value.source, text: option.value.text,
411
422
  }, ...redraw()]];
412
423
  }
424
+ if (option.value.kind === 'copy') {
425
+ const { overlay: _overlay, ...withoutOverlay } = state;
426
+ return [withoutOverlay, [{ type: 'copy', text: option.value.text }, ...redraw()]];
427
+ }
413
428
  if (option.value.kind === 'permission' && option.danger === true) {
414
429
  return [{ ...state, overlay: {
415
430
  acceptLabel: 'Enable',
@@ -636,9 +651,15 @@ export function reduce(state, action) {
636
651
  }),
637
652
  }, redraw()];
638
653
  }
639
- case 'copy-latest': {
640
- const cell = latestCompletedAssistant(state.cells);
641
- return cell === undefined ? [state, []] : [state, [{ type: 'copy', text: cell.text }]];
654
+ case 'copy-assistant': {
655
+ const cell = completedAssistant(state.cells, action.selection === 'code' ? 1 : action.selection);
656
+ if (cell === undefined)
657
+ return [state, []];
658
+ if (action.selection !== 'code')
659
+ return [state, [{ type: 'copy', text: cell.text }]];
660
+ return [{ ...state, overlay: {
661
+ cursor: 0, kind: 'list', options: codeBlockOptions(cell.text), purpose: 'copy', title: 'Copy code block',
662
+ } }, redraw()];
642
663
  }
643
664
  }
644
665
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antst/dashi",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0-alpha.9",
4
4
  "description": "A terminal UI for DeepSeek Harness",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -43,8 +43,10 @@
43
43
  "@deepseek-ai/dsh-agent-presets": "0.1.2-rc.1",
44
44
  "@deepseek-ai/dsh-api-session-controller": "0.1.2-rc.1",
45
45
  "@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
46
+ "@deepseek-ai/dsh-authorization": "0.1.2-rc.1",
46
47
  "@deepseek-ai/dsh-cmdline": "0.1.2-rc.1",
47
48
  "@deepseek-ai/dsh-commands": "0.1.2-rc.1",
49
+ "@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
48
50
  "@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
49
51
  "@deepseek-ai/dsh-file-reference": "0.1.2-rc.1",
50
52
  "@deepseek-ai/dsh-fs": "0.1.2-rc.1",