@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.
- package/lib/application.js +5 -0
- package/lib/catalogs.d.ts +2 -0
- package/lib/catalogs.js +17 -0
- package/lib/diff-view.js +2 -1
- package/lib/help.d.ts +1 -1
- package/lib/help.js +1 -1
- package/lib/index.js +18 -7
- package/lib/renderer.d.ts +1 -0
- package/lib/renderer.js +19 -5
- package/lib/session-runtime.d.ts +3 -0
- package/lib/session-runtime.js +107 -9
- package/lib/state.d.ts +10 -2
- package/lib/state.js +30 -9
- package/package.json +3 -1
package/lib/application.js
CHANGED
|
@@ -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:
|
|
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 · --
|
|
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 · --
|
|
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 (
|
|
42
|
-
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|
|
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); },
|
package/lib/session-runtime.d.ts
CHANGED
|
@@ -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[];
|
package/lib/session-runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
|
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
|
-
'/
|
|
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
|
|
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-
|
|
640
|
-
const cell =
|
|
641
|
-
|
|
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.
|
|
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",
|