@antst/dashi 0.1.0-alpha.10 → 0.1.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/application.d.ts +2 -0
- package/lib/application.js +2 -2
- package/lib/catalogs.d.ts +3 -0
- package/lib/catalogs.js +38 -0
- package/lib/help.d.ts +1 -1
- package/lib/help.js +1 -1
- package/lib/index.js +29 -1
- package/lib/input.d.ts +1 -1
- package/lib/input.js +5 -0
- package/lib/renderer.d.ts +1 -0
- package/lib/renderer.js +68 -13
- package/lib/session-runtime.d.ts +3 -0
- package/lib/session-runtime.js +179 -19
- package/lib/shell-command.d.ts +1 -1
- package/lib/shell-command.js +3 -2
- package/lib/state.d.ts +7 -1
- package/lib/state.js +13 -6
- package/package.json +4 -1
package/lib/application.d.ts
CHANGED
|
@@ -20,12 +20,14 @@ export interface TerminalShellOptions {
|
|
|
20
20
|
readonly initialHasMore?: boolean;
|
|
21
21
|
readonly initialPrompts?: ViewState['prompts'];
|
|
22
22
|
readonly initialRoot?: ViewState['root'];
|
|
23
|
+
readonly initialToolMode?: ViewState['toolMode'];
|
|
23
24
|
readonly interrupt?: () => void;
|
|
24
25
|
readonly inline: boolean;
|
|
25
26
|
readonly loadHistory?: (rootId: string) => Promise<void>;
|
|
26
27
|
readonly onReleased?: () => void;
|
|
27
28
|
readonly pasteImage?: () => Promise<DraftAttachment>;
|
|
28
29
|
readonly search?: (query: string, rootId: string) => Promise<void>;
|
|
30
|
+
readonly statusLine?: () => string | undefined;
|
|
29
31
|
readonly submit?: (text: string, mode: 'next-turn' | 'steer', attachments: readonly DraftAttachment[]) => Promise<void> | void;
|
|
30
32
|
readonly suspend?: () => Promise<void>;
|
|
31
33
|
readonly writeError?: (message: string) => void;
|
package/lib/application.js
CHANGED
|
@@ -16,7 +16,7 @@ function suspendProcess() {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
export function createTerminalShell(options) {
|
|
19
|
-
let state = initialViewState(options.cwd, options.inline, options.initialRoot, options.initialCells, options.initialPrompts, options.initialHasMore, options.initialAttachments);
|
|
19
|
+
let state = initialViewState(options.cwd, options.inline, options.initialRoot, options.initialCells, options.initialPrompts, options.initialHasMore, options.initialAttachments, options.initialToolMode);
|
|
20
20
|
let accepting = true;
|
|
21
21
|
let failed = false;
|
|
22
22
|
let queue = Promise.resolve();
|
|
@@ -26,7 +26,7 @@ export function createTerminalShell(options) {
|
|
|
26
26
|
readState: () => state,
|
|
27
27
|
};
|
|
28
28
|
const renderer = options.createView?.(bindings) ?? createRenderer({
|
|
29
|
-
...bindings, ...(options.accessible === undefined ? {} : { accessible: options.accessible }), inline: options.inline,
|
|
29
|
+
...bindings, ...(options.accessible === undefined ? {} : { accessible: options.accessible }), ...(options.statusLine === undefined ? {} : { statusLine: options.statusLine }), inline: options.inline,
|
|
30
30
|
});
|
|
31
31
|
const guard = createTerminalGuard(renderer);
|
|
32
32
|
const writeError = options.writeError ?? (message => { process.stderr.write(message); });
|
package/lib/catalogs.d.ts
CHANGED
|
@@ -14,10 +14,13 @@ export declare function sessionOverlay(items: readonly SessionSummary[], current
|
|
|
14
14
|
export declare function modelOverlay(catalog: ModelCatalog, selected?: ModelSelection): Overlay;
|
|
15
15
|
/** Build the preset picker from the native preset service. */
|
|
16
16
|
export declare function permissionOverlay(ctx: Context, agent: Agent): Overlay;
|
|
17
|
+
export declare function sessionStatusLine(ctx: Context, agent: Agent, root: RootView, branch?: string): string;
|
|
17
18
|
/** Read DSH-owned root facts for /status in one projection cut. */
|
|
18
19
|
export declare function statusOverlay(ctx: Context, agent: Agent, root: RootView): Overlay;
|
|
19
20
|
/** Show DSH's heuristic next-request composition without treating it as an exact total. */
|
|
20
21
|
export declare function contextOverlay(ctx: Context, agent: Agent): Overlay;
|
|
22
|
+
/** Read DSH's whole-log token and timing projections without folding session history. */
|
|
23
|
+
export declare function usageOverlay(ctx: Context, agent: Agent): Overlay;
|
|
21
24
|
/** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
|
|
22
25
|
export declare function agentPresetOverlay(presets: readonly AgentPreset[], current: string, blank: boolean): Overlay;
|
|
23
26
|
/** Build bounded same-cwd prompt-search rows from DSH query hits. */
|
package/lib/catalogs.js
CHANGED
|
@@ -153,6 +153,23 @@ export function permissionOverlay(ctx, agent) {
|
|
|
153
153
|
function number(value) {
|
|
154
154
|
return typeof value === 'number' ? value.toLocaleString('en-US') : 'unavailable';
|
|
155
155
|
}
|
|
156
|
+
function milliseconds(value) {
|
|
157
|
+
return typeof value === 'number' ? `${number(value)} ms` : 'unavailable';
|
|
158
|
+
}
|
|
159
|
+
export function sessionStatusLine(ctx, agent, root, branch) {
|
|
160
|
+
const { modelSelection, permissions, tokenUsage, contextPressure } = ctx.sessionProjections
|
|
161
|
+
.snapshot(agent.session, ['modelSelection', 'permissions', 'tokenUsage', 'contextPressure']).values;
|
|
162
|
+
const model = modelSelection?.next ?? modelSelection?.lastUsed;
|
|
163
|
+
const input = tokenUsage === undefined ? 0 : tokenUsage.uncachedInputTokens + tokenUsage.cacheReadTokens + tokenUsage.cacheWriteTokens;
|
|
164
|
+
const used = contextPressure?.projectedTokens ?? contextPressure?.pressureTokens;
|
|
165
|
+
return [
|
|
166
|
+
`model ${model == null ? `${root.provider === undefined ? '' : `${root.provider}/`}${root.model}` : `${model.provider}/${model.model}`}`,
|
|
167
|
+
`permission ${permissions?.currentValue ?? root.permission ?? 'unknown'}`,
|
|
168
|
+
...(used === undefined || contextPressure?.contextWindow === undefined ? [] : [`context ${number(used)}/${number(contextPressure.contextWindow)}`]),
|
|
169
|
+
...(input === 0 ? [] : [`cache ${String(Math.min(tokenUsage?.cacheReadTokens === input ? 100 : 99, Math.round((tokenUsage?.cacheReadTokens ?? 0) / input * 100)))}%`]),
|
|
170
|
+
...(branch === undefined ? [] : [`branch ${branch}`]),
|
|
171
|
+
].join(' · ');
|
|
172
|
+
}
|
|
156
173
|
/** Read DSH-owned root facts for /status in one projection cut. */
|
|
157
174
|
export function statusOverlay(ctx, agent, root) {
|
|
158
175
|
const values = ctx.sessionProjections.snapshot(agent.session, [
|
|
@@ -196,6 +213,27 @@ export function contextOverlay(ctx, agent) {
|
|
|
196
213
|
title: 'Context',
|
|
197
214
|
};
|
|
198
215
|
}
|
|
216
|
+
/** Read DSH's whole-log token and timing projections without folding session history. */
|
|
217
|
+
export function usageOverlay(ctx, agent) {
|
|
218
|
+
const values = ctx.sessionProjections.snapshot(agent.session, ['tokenUsage', 'sessionStats']).values;
|
|
219
|
+
const tokens = values.tokenUsage;
|
|
220
|
+
const stats = values.sessionStats;
|
|
221
|
+
const measured = stats === undefined ? undefined : stats.llmMs + stats.toolMs;
|
|
222
|
+
return {
|
|
223
|
+
kind: 'info',
|
|
224
|
+
lines: [
|
|
225
|
+
`Input tokens: ${number(tokens?.uncachedInputTokens)}`,
|
|
226
|
+
`Output tokens: ${number(tokens?.outputTokens)}`,
|
|
227
|
+
`Cache read: ${number(tokens?.cacheReadTokens)}`,
|
|
228
|
+
`Cache write: ${number(tokens?.cacheWriteTokens)}`,
|
|
229
|
+
`Turns: ${number(stats?.turns)} · Steps: ${number(stats?.steps)}`,
|
|
230
|
+
`Model time: ${milliseconds(stats?.llmMs)}`,
|
|
231
|
+
`Tool time: ${milliseconds(stats?.toolMs)}`,
|
|
232
|
+
`Measured wall time: ${milliseconds(measured)}`,
|
|
233
|
+
],
|
|
234
|
+
title: 'Usage',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
199
237
|
/** Build the native agent-preset picker; broken presets are visible elsewhere, not selectable here. */
|
|
200
238
|
export function agentPresetOverlay(presets, current, blank) {
|
|
201
239
|
const options = presets.filter(preset => preset.broken === undefined).map(preset => ({
|
package/lib/help.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const VERSION_LINE: string;
|
|
2
|
-
export declare const FLAG_HELP: readonly ["Launch flags: --inline | --fullscreen · --accessible · -r, --resume [NAME|UUID] · -c, --continue · --fork-session · --all", " -n, --name TITLE · --agent PRESET · --session-id UUID · --model ID [--provider ID] · --effort LEVEL", " --permission PRESET · --tools NAMES · --disallowedTools NAMES · --yolo | --dangerously-skip-permissions · --image PATH · -h, --help"];
|
|
2
|
+
export declare const FLAG_HELP: readonly ["Launch flags: --inline | --fullscreen · --accessible · --verbose · -r, --resume [NAME|UUID] · -c, --continue · --fork-session · --all", " -n, --name TITLE · --agent PRESET · --session-id UUID · --model ID [--provider ID] · --effort LEVEL", " --permission PRESET · --tools NAMES · --disallowedTools NAMES · --yolo | --dangerously-skip-permissions · --image PATH", " --system-prompt TEXT | --system-prompt-file PATH · --append-system-prompt TEXT | --append-system-prompt-file PATH · -h, --help"];
|
package/lib/help.js
CHANGED
|
@@ -2,5 +2,5 @@ import dashi from '../package.json' with { type: 'json' };
|
|
|
2
2
|
import dsh from '@deepseek-ai/dsh-cmdline/package.json' with { type: 'json' };
|
|
3
3
|
export const VERSION_LINE = `dashi ${dashi.version} on DSH ${dsh.version}`;
|
|
4
4
|
export const FLAG_HELP = [
|
|
5
|
-
'Launch flags: --inline | --fullscreen · --accessible · -r, --resume [NAME|UUID] · -c, --continue · --fork-session · --all', ' -n, --name TITLE · --agent PRESET · --session-id UUID · --model ID [--provider ID] · --effort LEVEL', ' --permission PRESET · --tools NAMES · --disallowedTools NAMES · --yolo | --dangerously-skip-permissions · --image PATH · -h, --help'
|
|
5
|
+
'Launch flags: --inline | --fullscreen · --accessible · --verbose · -r, --resume [NAME|UUID] · -c, --continue · --fork-session · --all', ' -n, --name TITLE · --agent PRESET · --session-id UUID · --model ID [--provider ID] · --effort LEVEL', ' --permission PRESET · --tools NAMES · --disallowedTools NAMES · --yolo | --dangerously-skip-permissions · --image PATH', ' --system-prompt TEXT | --system-prompt-file PATH · --append-system-prompt TEXT | --append-system-prompt-file PATH · -h, --help'
|
|
6
6
|
];
|
package/lib/index.js
CHANGED
|
@@ -13,11 +13,12 @@ import { TuiRoot } from './tui-root.js';
|
|
|
13
13
|
export const name = 'dashi';
|
|
14
14
|
export const inject = [
|
|
15
15
|
'agentPresets', 'agents', 'attachments', 'authorization', 'cmdlineArgs', 'commands', 'credentials', 'fs', 'jobs', 'loader', 'permissionPresets', 'subagents',
|
|
16
|
-
'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'tools',
|
|
16
|
+
'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'systemPrompt', 'tools',
|
|
17
17
|
];
|
|
18
18
|
const validatedVersions = JSON.parse(readFileSync(new URL('../validated-dsh-versions.json', import.meta.url), 'utf8'));
|
|
19
19
|
function parseArgs(args) {
|
|
20
20
|
let accessible = false;
|
|
21
|
+
let argumentError;
|
|
21
22
|
let agentPreset, sessionId;
|
|
22
23
|
let forkSession = false;
|
|
23
24
|
let inline = true;
|
|
@@ -31,7 +32,9 @@ function parseArgs(args) {
|
|
|
31
32
|
let resume;
|
|
32
33
|
let resumeAll = false;
|
|
33
34
|
let resumePicker = false;
|
|
35
|
+
let systemPrompt, appendSystemPrompt;
|
|
34
36
|
let useContinue = false;
|
|
37
|
+
let verbose = false;
|
|
35
38
|
const images = [];
|
|
36
39
|
const prompt = [];
|
|
37
40
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -43,6 +46,7 @@ function parseArgs(args) {
|
|
|
43
46
|
else if (argument === '--fullscreen')
|
|
44
47
|
inline = false;
|
|
45
48
|
else if (['--name', '-n', '--agent', '--image', '--effort', '--model', '--permission', '--provider', '--tools', '--disallowedTools',
|
|
49
|
+
'--system-prompt', '--system-prompt-file', '--append-system-prompt', '--append-system-prompt-file',
|
|
46
50
|
'--session-id'].includes(argument ?? '')) {
|
|
47
51
|
const value = args[++index];
|
|
48
52
|
if (value === undefined || value === '')
|
|
@@ -70,6 +74,21 @@ function parseArgs(args) {
|
|
|
70
74
|
else
|
|
71
75
|
disallowedTools = names;
|
|
72
76
|
}
|
|
77
|
+
else if (argument?.includes('system-prompt') === true) {
|
|
78
|
+
let text = value;
|
|
79
|
+
if (argument.endsWith('-file')) {
|
|
80
|
+
try {
|
|
81
|
+
text = readFileSync(value, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
argumentError = error instanceof Error ? error.message : String(error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (argument.startsWith('--append'))
|
|
88
|
+
appendSystemPrompt = text;
|
|
89
|
+
else
|
|
90
|
+
systemPrompt = text;
|
|
91
|
+
}
|
|
73
92
|
else
|
|
74
93
|
provider = value;
|
|
75
94
|
}
|
|
@@ -88,6 +107,8 @@ function parseArgs(args) {
|
|
|
88
107
|
useContinue = true;
|
|
89
108
|
else if (argument === '--fork-session')
|
|
90
109
|
forkSession = true;
|
|
110
|
+
else if (argument === '--verbose')
|
|
111
|
+
verbose = true;
|
|
91
112
|
else if (argument === '--yolo' || argument === '--dangerously-skip-permissions') {
|
|
92
113
|
permission = 'danger-full-access';
|
|
93
114
|
}
|
|
@@ -106,6 +127,8 @@ function parseArgs(args) {
|
|
|
106
127
|
return undefined;
|
|
107
128
|
return {
|
|
108
129
|
accessible,
|
|
130
|
+
...(argumentError === undefined ? {} : { argumentError }),
|
|
131
|
+
...(appendSystemPrompt === undefined ? {} : { appendSystemPrompt }),
|
|
109
132
|
continue: useContinue,
|
|
110
133
|
...(agentPreset === undefined ? {} : { agentPreset }), forkSession, inline,
|
|
111
134
|
resumeAll,
|
|
@@ -120,6 +143,7 @@ function parseArgs(args) {
|
|
|
120
143
|
...(tools === undefined ? {} : { tools }),
|
|
121
144
|
...(disallowedTools === undefined ? {} : { disallowedTools }),
|
|
122
145
|
...(resume === undefined ? {} : { resume }), ...(sessionId === undefined ? {} : { sessionId }),
|
|
146
|
+
...(systemPrompt === undefined ? {} : { systemPrompt }), verbose,
|
|
123
147
|
};
|
|
124
148
|
}
|
|
125
149
|
function packageVersion(path) {
|
|
@@ -213,6 +237,8 @@ export function apply(ctx) {
|
|
|
213
237
|
const announce = ready.onReady(() => {
|
|
214
238
|
warnDshVersion();
|
|
215
239
|
boot = (async () => {
|
|
240
|
+
if (parsed.argumentError !== undefined)
|
|
241
|
+
throw new LaunchArgumentError(parsed.argumentError);
|
|
216
242
|
const cwd = process.cwd();
|
|
217
243
|
const makeRuntime = (options) => createSessionRuntime(ctx, options, tuiRoot, action => { shell?.dispatch(action); }, {
|
|
218
244
|
cancel: id => { shell?.cancelDecision(id); },
|
|
@@ -266,12 +292,14 @@ export function apply(ctx) {
|
|
|
266
292
|
initialAttachments: initial.initialAttachments, initialCells: initial.initialCells, initialHasMore: initial.initialHasMore,
|
|
267
293
|
initialPrompts: initial.initialPrompts, initialRoot: initial.root,
|
|
268
294
|
}),
|
|
295
|
+
...(parsed.verbose ? { initialToolMode: 'expanded' } : {}),
|
|
269
296
|
inline: parsed.inline || parsed.accessible,
|
|
270
297
|
interrupt: () => { runtime?.interrupt(); },
|
|
271
298
|
loadHistory: rootId => runtime?.loadHistory(rootId) ?? Promise.resolve(),
|
|
272
299
|
onReleased: () => { process.stdout.write(`${runtime?.summary ?? ''}\n`); },
|
|
273
300
|
pasteImage: readClipboardImage,
|
|
274
301
|
search: (query, rootId) => runtime?.search(query, rootId) ?? Promise.resolve(),
|
|
302
|
+
statusLine: () => runtime?.statusLine,
|
|
275
303
|
submit: (text, mode, attachments) => runtime?.submit(text, mode, attachments),
|
|
276
304
|
});
|
|
277
305
|
shell.start();
|
package/lib/input.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TUI } from '@earendil-works/pi-tui';
|
|
2
|
-
import type
|
|
2
|
+
import { type ComposerCursor, type UiAction, type ViewState } from './state.js';
|
|
3
3
|
export interface InputBindings {
|
|
4
4
|
readonly dispatch: (action: UiAction) => void;
|
|
5
5
|
readonly insertNewline?: () => void;
|
package/lib/input.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isKeyRelease, matchesKey } from '@earendil-works/pi-tui';
|
|
2
|
+
import { PAGE_CELLS } from './state.js';
|
|
2
3
|
export function caretOffset(text, cursor) {
|
|
3
4
|
const lines = text.split('\n');
|
|
4
5
|
let offset = 0;
|
|
@@ -98,6 +99,10 @@ export function installInput(tui, bindings) {
|
|
|
98
99
|
bindings.dispatch({ type: 'search-open', scope: 'history' });
|
|
99
100
|
else if (overlay.kind === 'history' && matchesKey(data, 'y'))
|
|
100
101
|
bindings.dispatch({ type: 'history-copy' });
|
|
102
|
+
else if (overlay.kind === 'info' && matchesKey(data, 'pageUp'))
|
|
103
|
+
bindings.dispatch({ type: 'overlay-move', offset: -PAGE_CELLS });
|
|
104
|
+
else if (overlay.kind === 'info' && matchesKey(data, 'pageDown'))
|
|
105
|
+
bindings.dispatch({ type: 'overlay-move', offset: PAGE_CELLS });
|
|
101
106
|
else if (matchesKey(data, 'up'))
|
|
102
107
|
bindings.dispatch({ type: 'overlay-move', offset: -1 });
|
|
103
108
|
else if (matchesKey(data, 'down'))
|
package/lib/renderer.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface Renderer extends TerminalSession {
|
|
|
13
13
|
export interface RendererOptions extends InputBindings {
|
|
14
14
|
readonly accessible?: boolean;
|
|
15
15
|
readonly inline: boolean;
|
|
16
|
+
readonly statusLine?: () => string | undefined;
|
|
16
17
|
readonly terminal?: Terminal;
|
|
17
18
|
readonly terminalType?: string;
|
|
18
19
|
}
|
package/lib/renderer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Editor, TuiAltScreen, TuiMainScreen, stripTerminalSequences, truncateToWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
|
|
1
|
+
import { Editor, Markdown, TuiAltScreen, TuiMainScreen, stripTerminalSequences, truncateToWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
|
|
2
2
|
import { caretOffset, installInput } from './input.js';
|
|
3
3
|
import { hasCompletionTrigger } from './completion-trigger.js';
|
|
4
4
|
import { createProcessTerminal } from './terminal-guard.js';
|
|
@@ -15,6 +15,22 @@ const THEME_GENERATION = 1;
|
|
|
15
15
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
16
16
|
const SPINNER_INTERVAL_MS = 80;
|
|
17
17
|
const MASK_TOKEN = new RegExp(String.raw `\u001B(?:\[[0-?]*[ -/]*[@-~]|[_P^X][^\u0007]*(?:\u0007|\u001B\\))|[^\s]`, 'gu');
|
|
18
|
+
const markdownTheme = {
|
|
19
|
+
heading: text => `\u001B[36m${text}${RESET}`,
|
|
20
|
+
link: text => `\u001B[36m${text}${RESET}`,
|
|
21
|
+
linkUrl: text => `${DIM}${text}${RESET}`,
|
|
22
|
+
code: text => `${YELLOW}${text}${RESET}`,
|
|
23
|
+
codeBlock: text => `${GREEN}${text}${RESET}`,
|
|
24
|
+
codeBlockBorder: text => `${DIM}${text}${RESET}`,
|
|
25
|
+
quote: text => `${DIM}${text}${RESET}`,
|
|
26
|
+
quoteBorder: text => `${DIM}${text}${RESET}`,
|
|
27
|
+
hr: text => `${DIM}${text}${RESET}`,
|
|
28
|
+
listBullet: text => `\u001B[36m${text}${RESET}`,
|
|
29
|
+
bold: text => `\u001B[1m${text}\u001B[22m`,
|
|
30
|
+
italic: text => `\u001B[3m${text}\u001B[23m`,
|
|
31
|
+
strikethrough: text => `\u001B[9m${text}\u001B[29m`,
|
|
32
|
+
underline: text => `\u001B[4m${text}\u001B[24m`,
|
|
33
|
+
};
|
|
18
34
|
export function supportsOsc52(terminalType) {
|
|
19
35
|
return terminalType?.toLocaleLowerCase() !== 'dumb';
|
|
20
36
|
}
|
|
@@ -138,6 +154,22 @@ function plain(text, preserveNewlines = false) {
|
|
|
138
154
|
function line(text, width) {
|
|
139
155
|
return truncateToWidth(text, Math.max(1, width), '…');
|
|
140
156
|
}
|
|
157
|
+
function windowRows(values, size, position, follow) {
|
|
158
|
+
if (values.length <= size)
|
|
159
|
+
return [...values];
|
|
160
|
+
if (size < 3)
|
|
161
|
+
return [values[Math.max(0, Math.min(values.length - 1, position))] ?? ''];
|
|
162
|
+
const start = Math.max(0, Math.min(values.length - Math.max(1, size - 1), follow ? position - Math.floor((size - 2) / 2) : position));
|
|
163
|
+
const above = start > 0;
|
|
164
|
+
let end = Math.min(values.length, start + size - Number(above));
|
|
165
|
+
if (end < values.length)
|
|
166
|
+
end--;
|
|
167
|
+
return [
|
|
168
|
+
...(above ? [`${DIM}↑ ${String(start)} more${RESET}`] : []),
|
|
169
|
+
...values.slice(start, end),
|
|
170
|
+
...(end < values.length ? [`${DIM}↓ ${String(values.length - end)} more${RESET}`] : []),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
141
173
|
function theme() {
|
|
142
174
|
const identity = (text) => text;
|
|
143
175
|
return {
|
|
@@ -160,7 +192,9 @@ export function createRenderer(options) {
|
|
|
160
192
|
const osc52 = supportsOsc52(terminalType);
|
|
161
193
|
const tui = options.inline
|
|
162
194
|
? new TuiMainScreen(terminal, true)
|
|
163
|
-
: new
|
|
195
|
+
: new class extends TuiAltScreen {
|
|
196
|
+
isOverlayFocused() { return options.readState().overlay !== undefined || super.isOverlayFocused(); }
|
|
197
|
+
}(terminal, true, undefined, { mouse: true });
|
|
164
198
|
let editor = new Editor(tui, theme(), { paddingX: 1 });
|
|
165
199
|
editor.disableSubmit = true;
|
|
166
200
|
editor.onChange = text => {
|
|
@@ -184,7 +218,11 @@ export function createRenderer(options) {
|
|
|
184
218
|
const cached = cell.pending === true ? undefined : lineCache.get(key);
|
|
185
219
|
if (cached !== undefined)
|
|
186
220
|
return cached;
|
|
187
|
-
const
|
|
221
|
+
const cellWidth = Math.max(1, width - 2);
|
|
222
|
+
const lines = !accessible && cell.kind === 'assistant' && cell.pending !== true
|
|
223
|
+
? [`${BOLD_CYAN}Assistant${RESET}`, ...new Markdown(plain(cell.text, true), 0, 0, markdownTheme).render(cellWidth).map(value => value.trimEnd())]
|
|
224
|
+
: wrapTextWithAnsi(cellText(cell, toolMode, accessible, now), cellWidth);
|
|
225
|
+
const rendered = lines.map(value => ` ${value}`);
|
|
188
226
|
if (cell.pending !== true) {
|
|
189
227
|
lineCache.set(key, rendered);
|
|
190
228
|
if (lineCache.size > 512)
|
|
@@ -275,19 +313,19 @@ export function createRenderer(options) {
|
|
|
275
313
|
result.push(`${DIM}↑↓ select · Enter details · Esc close${RESET}`);
|
|
276
314
|
return result.slice(0, Math.max(2, rows - 3));
|
|
277
315
|
};
|
|
316
|
+
const infoRows = (overlay, state, width) => overlay.lines.flatMap(value => wrapTextWithAnsi(plain(value, true), width)).concat(overlay.cells?.flatMap(cell => wrapTextWithAnsi(cellText(cell, state.toolMode, accessible), width)) ?? []);
|
|
278
317
|
const overlayPanel = (overlay, state, width, rows) => {
|
|
279
318
|
if (overlay.kind === 'history')
|
|
280
319
|
return historyPanel(overlay, state, width, rows);
|
|
281
320
|
if (overlay.kind === 'details')
|
|
282
321
|
return detailsPanel(overlay, state, width, rows);
|
|
283
322
|
if (overlay.kind === 'info') {
|
|
284
|
-
const
|
|
323
|
+
const body = infoRows(overlay, state, width);
|
|
285
324
|
return [
|
|
286
325
|
`${BOLD_CYAN}${announcement(plain(overlay.title), overlay.lines.length + (overlay.cells?.length ?? 0))}${RESET}`,
|
|
287
|
-
...
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
].slice(0, Math.max(2, rows - 3));
|
|
326
|
+
...windowRows(body, Math.max(1, rows - 5), overlay.scrollOffset ?? 0, false),
|
|
327
|
+
`${DIM}↑↓ scroll · PageUp/PageDown · Enter or Esc close${RESET}`,
|
|
328
|
+
];
|
|
291
329
|
}
|
|
292
330
|
if (overlay.kind === 'confirm') {
|
|
293
331
|
const choices = [overlay.acceptLabel, 'Cancel'];
|
|
@@ -303,18 +341,23 @@ export function createRenderer(options) {
|
|
|
303
341
|
line(`${BOLD_CYAN}${announcement(plain(overlay.title), overlay.options.length, selected === undefined ? undefined : plain(selected.label), overlay.cursor)}${RESET}`, width),
|
|
304
342
|
...(overlay.notice === undefined ? [] : [`${DIM}${plain(overlay.notice)}${RESET}`]),
|
|
305
343
|
];
|
|
344
|
+
const body = [];
|
|
345
|
+
let selectedRow = 0;
|
|
306
346
|
let group;
|
|
307
347
|
for (const [index, option] of overlay.options.entries()) {
|
|
308
348
|
if (option.group !== undefined && option.group !== group) {
|
|
309
349
|
group = option.group;
|
|
310
|
-
|
|
350
|
+
body.push(`${DIM}${plain(group)}${RESET}`);
|
|
311
351
|
}
|
|
312
352
|
const detail = option.detail === undefined ? '' : ` — ${plain(option.detail)}`;
|
|
313
353
|
const active = option.active === true ? accessible ? ' [active]' : ' ●' : '';
|
|
314
354
|
const danger = option.danger === true ? ` ${YELLOW}${accessible ? '[danger]' : '!'}${RESET}` : '';
|
|
315
|
-
|
|
355
|
+
if (index === overlay.cursor)
|
|
356
|
+
selectedRow = body.length;
|
|
357
|
+
body.push(line(`${index === overlay.cursor ? BOLD_CYAN : DIM}${index === overlay.cursor ? accessible ? '>' : '›' : ' '} ${String(index + 1)} ${plain(option.label)}${active}${detail}${RESET}${danger}`, width));
|
|
316
358
|
}
|
|
317
|
-
|
|
359
|
+
const limit = Math.max(2, rows - (overlay.purpose === 'completion' ? 6 : 3));
|
|
360
|
+
return [...result, ...windowRows(body, Math.max(1, limit - result.length), selectedRow, true)];
|
|
318
361
|
};
|
|
319
362
|
const document = {
|
|
320
363
|
invalidate() { editor.invalidate(); },
|
|
@@ -368,6 +411,11 @@ export function createRenderer(options) {
|
|
|
368
411
|
const attachmentRows = state.attachments.map((attachment, index) => line(`${YELLOW}[image ${String(index + 1)}]${RESET} ${plain(attachment.name)} · ${attachment.mediaType} · ${String(attachment.bytes)} B · ${plain(attachment.path)}`, width));
|
|
369
412
|
const activityRows = decision !== undefined || state.search !== undefined || state.overlay !== undefined
|
|
370
413
|
? [] : activities(state).map(item => line(`${DIM}${activityText(item, now)}${RESET}`, width));
|
|
414
|
+
const persistentStatus = accessible || state.exitArmed ? undefined : options.statusLine?.();
|
|
415
|
+
const composer = () => [
|
|
416
|
+
...(persistentStatus === undefined ? [] : [line(`${DIM}${plain(persistentStatus)}${RESET}`, width)]),
|
|
417
|
+
...editor.render(width),
|
|
418
|
+
];
|
|
371
419
|
const control = decision !== undefined
|
|
372
420
|
? decisionPanel(decision, width, rows)
|
|
373
421
|
: state.search !== undefined
|
|
@@ -379,11 +427,11 @@ export function createRenderer(options) {
|
|
|
379
427
|
`${DIM}${state.search.matches.length === 0 ? 'no matches' : `${String(state.search.cursor + 1)}/${String(state.search.matches.length)}`} · Enter next · Shift+Enter previous${RESET}`,
|
|
380
428
|
]
|
|
381
429
|
: state.overlay === undefined
|
|
382
|
-
? [...activityRows, ...attachmentRows, ...
|
|
430
|
+
? [...activityRows, ...attachmentRows, ...composer()]
|
|
383
431
|
: [
|
|
384
432
|
...overlayPanel(state.overlay, state, width, rows),
|
|
385
433
|
...(state.overlay.kind === 'list' && state.overlay.purpose === 'completion'
|
|
386
|
-
? [...attachmentRows, ...
|
|
434
|
+
? [...attachmentRows, ...composer()]
|
|
387
435
|
: []),
|
|
388
436
|
];
|
|
389
437
|
const fixedRows = header.length + control.length + 2;
|
|
@@ -405,6 +453,13 @@ export function createRenderer(options) {
|
|
|
405
453
|
tui.setFocus(editor);
|
|
406
454
|
installInput(tui, {
|
|
407
455
|
...options,
|
|
456
|
+
dispatch: action => {
|
|
457
|
+
const overlay = options.readState().overlay;
|
|
458
|
+
const size = Math.max(1, terminal.rows - 5);
|
|
459
|
+
options.dispatch(action.type === 'overlay-move' && overlay?.kind === 'info'
|
|
460
|
+
? { ...action, limit: Math.max(0, infoRows(overlay, options.readState(), terminal.columns).length - Math.max(1, size - 1)) }
|
|
461
|
+
: action);
|
|
462
|
+
},
|
|
408
463
|
insertNewline: () => { editor.insertTextAtCursor('\n'); },
|
|
409
464
|
readComposerCursor: () => editor.getCursor(),
|
|
410
465
|
});
|
package/lib/session-runtime.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { TuiRoot } from './tui-root.js';
|
|
|
4
4
|
export interface RootLaunchOptions {
|
|
5
5
|
readonly agentPreset?: string;
|
|
6
6
|
readonly continue: boolean;
|
|
7
|
+
readonly appendSystemPrompt?: string;
|
|
7
8
|
readonly cwd: string;
|
|
8
9
|
readonly effort?: string;
|
|
9
10
|
readonly forkSession: boolean;
|
|
@@ -17,6 +18,7 @@ export interface RootLaunchOptions {
|
|
|
17
18
|
readonly disallowedTools?: readonly string[];
|
|
18
19
|
readonly resume?: string;
|
|
19
20
|
readonly sessionId?: string;
|
|
21
|
+
readonly systemPrompt?: string;
|
|
20
22
|
}
|
|
21
23
|
export declare class LaunchArgumentError extends Error {
|
|
22
24
|
}
|
|
@@ -26,6 +28,7 @@ export interface SessionRuntime {
|
|
|
26
28
|
readonly initialHasMore: boolean;
|
|
27
29
|
readonly initialPrompts: readonly string[];
|
|
28
30
|
readonly root: RootView;
|
|
31
|
+
readonly statusLine: string;
|
|
29
32
|
readonly summary: string;
|
|
30
33
|
activate(value: ActivatableOverlayValue): Promise<void>;
|
|
31
34
|
attach(path: string): Promise<DraftAttachment | undefined>;
|
package/lib/session-runtime.js
CHANGED
|
@@ -2,9 +2,9 @@ import { basename } from 'node:path';
|
|
|
2
2
|
import { AuthorizationDeclinedError } from '@deepseek-ai/dsh-authorization';
|
|
3
3
|
import { parseCredentialKey } from '@deepseek-ai/dsh-credentials';
|
|
4
4
|
import { JobId } from '@deepseek-ai/dsh-jobs';
|
|
5
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
5
|
+
import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm';
|
|
6
6
|
import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
|
|
7
|
-
import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionOverlay, skillOverlay, statusOverlay, } from './catalogs.js';
|
|
7
|
+
import { agentPresetOverlay, completionOptions, contextOverlay, currentModel, modelOverlay, permissionOverlay, searchOverlay, sessionStatusLine, sessionOverlay, skillOverlay, statusOverlay, usageOverlay, } from './catalogs.js';
|
|
8
8
|
import { diffOverlay } from './diff-view.js';
|
|
9
9
|
import { eventsFromRecords } from './history-records.js';
|
|
10
10
|
import { admitDraftImages, encodeDraftImages, readDraftImages } from './image-input.js';
|
|
@@ -21,6 +21,7 @@ export class LaunchArgumentError extends Error {
|
|
|
21
21
|
function starterAgents(cwd) {
|
|
22
22
|
return `# ${basename(cwd)}\n\n## Working agreement\n\n- Add project-specific architecture guidance.\n- Add required build, test, and lint commands.\n- Add repository conventions and constraints.\n`;
|
|
23
23
|
}
|
|
24
|
+
const RECAP_PROMPT = 'Summarize this conversation so far in ten lines: goal, decisions, open items.';
|
|
24
25
|
function detail(error) {
|
|
25
26
|
return error instanceof Error ? error.message : String(error);
|
|
26
27
|
}
|
|
@@ -79,9 +80,31 @@ async function firstControl(iterator) {
|
|
|
79
80
|
}
|
|
80
81
|
return first.value;
|
|
81
82
|
}
|
|
83
|
+
async function gitBranch(ctx, agent, signal) {
|
|
84
|
+
try {
|
|
85
|
+
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
86
|
+
command: 'git branch --show-current', sandboxPolicy: ctx.sandboxPolicy.resolve({ mode: 'read-only' }),
|
|
87
|
+
signal, stdoutMaxBytes: 512, timeoutMs: 2_000, workdir: agent.session.header.cwd,
|
|
88
|
+
}));
|
|
89
|
+
return result.exitCode === 0 && result.stdout.text.trim() !== '' ? result.stdout.text.trim() : undefined;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
82
95
|
function usage(raw, expected) {
|
|
83
96
|
return raw.trim() === '' ? undefined : { kind: 'error', text: `usage: ${expected}` };
|
|
84
97
|
}
|
|
98
|
+
function loopInterval(seconds) {
|
|
99
|
+
if (seconds % 3_600 === 0)
|
|
100
|
+
return `${String(seconds / 3_600)}h`;
|
|
101
|
+
return seconds % 60 === 0 ? `${String(seconds / 60)}m` : `${String(seconds)}s`;
|
|
102
|
+
}
|
|
103
|
+
function loopLine(schedule) {
|
|
104
|
+
const rule = schedule.kind === 'every' ? `every ${loopInterval(schedule.everySeconds)}`
|
|
105
|
+
: schedule.kind === 'after' ? `after ${String(schedule.afterSeconds)}s` : `at ${schedule.scheduledAt}`;
|
|
106
|
+
return `${String(schedule.id)} · ${rule} · ${schedule.prompt}`;
|
|
107
|
+
}
|
|
85
108
|
function configValue(raw) {
|
|
86
109
|
try {
|
|
87
110
|
return JSON.parse(raw);
|
|
@@ -114,18 +137,26 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
114
137
|
if ('error' in resolved)
|
|
115
138
|
throw resolved.error;
|
|
116
139
|
const agent = resolved.agent;
|
|
117
|
-
|
|
140
|
+
const disposers = [];
|
|
118
141
|
if (options.tools !== undefined || options.disallowedTools !== undefined) {
|
|
119
142
|
try {
|
|
120
|
-
|
|
143
|
+
disposers.push(agent.ctx.tools.restrict({
|
|
121
144
|
...(options.tools === undefined ? {} : { allow: options.tools }),
|
|
122
145
|
...(options.disallowedTools === undefined ? {} : { deny: options.disallowedTools }),
|
|
123
|
-
});
|
|
146
|
+
}));
|
|
124
147
|
}
|
|
125
148
|
catch (error) {
|
|
126
149
|
throw new LaunchArgumentError(detail(error));
|
|
127
150
|
}
|
|
128
151
|
}
|
|
152
|
+
if (options.systemPrompt !== undefined)
|
|
153
|
+
disposers.push(agent.ctx.systemPrompt.section({
|
|
154
|
+
complete: true, name: 'dashi:system-prompt', order: Number.MAX_SAFE_INTEGER, text: options.systemPrompt,
|
|
155
|
+
}));
|
|
156
|
+
if (options.appendSystemPrompt !== undefined)
|
|
157
|
+
disposers.push(agent.ctx.systemPrompt.section({
|
|
158
|
+
name: 'dashi:append-system-prompt', order: Number.MAX_SAFE_INTEGER, text: options.appendSystemPrompt,
|
|
159
|
+
}));
|
|
129
160
|
let title;
|
|
130
161
|
if (name !== undefined)
|
|
131
162
|
title = (await ctx.sessionController.rename({ sessionId, title: name })).title;
|
|
@@ -134,8 +165,9 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
134
165
|
address: { kind: 'session', sessionId }, maxMessages: 50,
|
|
135
166
|
}, abort.signal)[Symbol.asyncIterator]();
|
|
136
167
|
const controlIterator = ctx.sessionController.control(abort.signal)[Symbol.asyncIterator]();
|
|
137
|
-
const [opening, controlOpening, subagents] = await Promise.all([
|
|
168
|
+
const [opening, controlOpening, subagents, branch] = await Promise.all([
|
|
138
169
|
firstSnapshot(iterator), firstControl(controlIterator), subagentViews(ctx, sessionId, abort.signal),
|
|
170
|
+
gitBranch(ctx, agent, abort.signal),
|
|
139
171
|
]);
|
|
140
172
|
title ??= projectionString(opening.projections, 'title');
|
|
141
173
|
const selected = projectionModel(opening.projections);
|
|
@@ -147,9 +179,10 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
147
179
|
return {
|
|
148
180
|
abort,
|
|
149
181
|
agent,
|
|
182
|
+
branch,
|
|
150
183
|
controlIterator,
|
|
151
184
|
cursor: opening.cursor,
|
|
152
|
-
disposers
|
|
185
|
+
disposers,
|
|
153
186
|
events: eventsFromRecords(opening.records),
|
|
154
187
|
hasMore: opening.hasMore,
|
|
155
188
|
iterator,
|
|
@@ -256,12 +289,20 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
256
289
|
});
|
|
257
290
|
}, 34);
|
|
258
291
|
};
|
|
292
|
+
const refreshBranch = async (bound) => {
|
|
293
|
+
const branch = await gitBranch(ctx, bound.agent, bound.abort.signal);
|
|
294
|
+
if (bound.abort.signal.aborted || current() !== bound || bound.branch === branch)
|
|
295
|
+
return;
|
|
296
|
+
bound.branch = branch;
|
|
297
|
+
dispatch({ type: 'redraw' });
|
|
298
|
+
};
|
|
259
299
|
const consume = (bound, frame) => {
|
|
260
300
|
const [event] = eventsFromRecords([frame]);
|
|
261
301
|
if (event === undefined)
|
|
262
302
|
return;
|
|
263
303
|
bound.events.push(event);
|
|
264
304
|
if (event.type === 'turn/end') {
|
|
305
|
+
void refreshBranch(bound);
|
|
265
306
|
const startedAt = turnStartTime(bound.events, event.data.turn);
|
|
266
307
|
if (startedAt !== undefined) {
|
|
267
308
|
dispatch({
|
|
@@ -511,6 +552,79 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
511
552
|
const result = ctx.jobs.kill(JobId(match[1]), bound.agent, 'stopped by /tasks');
|
|
512
553
|
return { kind: 'success', text: result === 'requested' ? `stopping ${match[1]}` : `${match[1]} already finished` };
|
|
513
554
|
};
|
|
555
|
+
const loop = async (invocation) => {
|
|
556
|
+
const stale = ensureCurrent(invocation);
|
|
557
|
+
if (stale !== undefined)
|
|
558
|
+
return stale;
|
|
559
|
+
const raw = invocation.rawInput.trim();
|
|
560
|
+
const stop = /^stop\s+(\S+)$/u.exec(raw);
|
|
561
|
+
const create = /^(\d+)([mh])\s+(.+)$/su.exec(raw);
|
|
562
|
+
if (raw !== '' && stop === null && create === null) {
|
|
563
|
+
return { kind: 'error', text: 'usage: /loop [INTERVAL TEXT | stop ID]' };
|
|
564
|
+
}
|
|
565
|
+
const name = raw === '' ? 'schedule_list' : stop === null ? 'schedule_create' : 'schedule_delete';
|
|
566
|
+
const args = raw === '' ? {} : stop === null ? {
|
|
567
|
+
every_seconds: Number(create[1]) * (create[2] === 'h' ? 3_600 : 60), prompt: create[3],
|
|
568
|
+
} : { id: stop[1] };
|
|
569
|
+
const result = await ctx.agents.withInitiator(bound.agent, () => ctx.tools.execute({
|
|
570
|
+
agent: bound.agent, arguments: args, callId: ToolCallId(`${String(invocation.commandId)}:loop`),
|
|
571
|
+
name, signal: invocation.signal,
|
|
572
|
+
}));
|
|
573
|
+
if (result.isError)
|
|
574
|
+
return { kind: 'error', text: result.error.message };
|
|
575
|
+
const value = result.value;
|
|
576
|
+
if (!Array.isArray(value) && 'message' in value)
|
|
577
|
+
return { kind: 'error', text: value.message };
|
|
578
|
+
if (Array.isArray(value))
|
|
579
|
+
return { kind: 'success', text: value.length === 0 ? 'no active schedules' : value.map(loopLine).join('\n') };
|
|
580
|
+
if ('deleted' in value)
|
|
581
|
+
return value.deleted ? { kind: 'success', text: `stopped ${String(value.id)}` }
|
|
582
|
+
: { kind: 'error', text: `schedule ${String(value.id)} not found` };
|
|
583
|
+
return { kind: 'success', text: `scheduled ${loopLine(value)}` };
|
|
584
|
+
};
|
|
585
|
+
const askAside = async (invocation, prompt = invocation.rawInput.trim()) => {
|
|
586
|
+
const stale = ensureCurrent(invocation);
|
|
587
|
+
if (stale !== undefined)
|
|
588
|
+
return stale;
|
|
589
|
+
if (prompt === '')
|
|
590
|
+
return { kind: 'error', text: 'usage: /btw TEXT' };
|
|
591
|
+
const forked = await ctx.sessionController.fork({ sessionId: bound.agent.id });
|
|
592
|
+
const resolved = await ctx.sessionController.resolveAgent(forked.sessionId);
|
|
593
|
+
if ('error' in resolved)
|
|
594
|
+
throw resolved.error;
|
|
595
|
+
const child = resolved.agent;
|
|
596
|
+
const turn = inheritedTurn(child.session.snapshotEvents(), child.session.inheritedEventCount);
|
|
597
|
+
if (turn === undefined)
|
|
598
|
+
throw new Error('the fork contains no completed turn');
|
|
599
|
+
const iterator = ctx.sessionController.follow({
|
|
600
|
+
address: { kind: 'session', sessionId: child.id }, maxMessages: 50,
|
|
601
|
+
}, invocation.signal)[Symbol.asyncIterator]();
|
|
602
|
+
await firstSnapshot(iterator);
|
|
603
|
+
await ctx.sessionController.rename({
|
|
604
|
+
sessionId: child.id, title: `btw · ${prompt.split(/\s+/u).slice(0, 6).join(' ')}`,
|
|
605
|
+
});
|
|
606
|
+
const from = child.session.seq;
|
|
607
|
+
child.steer(createUserMessage({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } }));
|
|
608
|
+
try {
|
|
609
|
+
for (;;) {
|
|
610
|
+
const next = await iterator.next();
|
|
611
|
+
if (next.done === true)
|
|
612
|
+
throw new Error('DSH session follow ended before the side turn completed');
|
|
613
|
+
if (next.value.type !== 'snapshot' && eventsFromRecords([next.value])[0]?.type === 'turn/end')
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
await iterator.return?.();
|
|
619
|
+
}
|
|
620
|
+
await ctx.sessions.flush(child.session);
|
|
621
|
+
const cells = foldCells(child.session.snapshotEvents(from), createToolPresenter(tool => ctx.tools.get(tool, child)))
|
|
622
|
+
.filter(cell => cell.kind !== 'user');
|
|
623
|
+
dispatch({ type: 'open-overlay', overlay: {
|
|
624
|
+
cells, kind: 'info', lines: [], title: `Btw · turn ${String(turn)}`,
|
|
625
|
+
} });
|
|
626
|
+
return { kind: 'success' };
|
|
627
|
+
};
|
|
514
628
|
const definitions = [
|
|
515
629
|
{
|
|
516
630
|
name: 'help', description: 'Show keyboard and command help',
|
|
@@ -551,6 +665,16 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
551
665
|
return { kind: 'success' };
|
|
552
666
|
},
|
|
553
667
|
},
|
|
668
|
+
{
|
|
669
|
+
name: 'usage', description: 'Show session token and timing totals',
|
|
670
|
+
handler: (invocation) => {
|
|
671
|
+
const invalid = ensureCurrent(invocation) ?? usage(invocation.rawInput, '/usage');
|
|
672
|
+
if (invalid !== undefined)
|
|
673
|
+
return invalid;
|
|
674
|
+
dispatch({ type: 'open-overlay', overlay: usageOverlay(ctx, bound.agent) });
|
|
675
|
+
return { kind: 'success' };
|
|
676
|
+
},
|
|
677
|
+
},
|
|
554
678
|
{
|
|
555
679
|
name: 'memory', description: 'Open an instruction file loaded by DSH',
|
|
556
680
|
handler: (invocation) => {
|
|
@@ -671,7 +795,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
671
795
|
if (invocation.rawInput.trim() === '')
|
|
672
796
|
return { kind: 'error', text: 'usage: /plugin ARGS' };
|
|
673
797
|
const command = `dsh plugin --profile ${quoteShellWord(decodeURIComponent(new URL(ctx.baseUrl).pathname).split('/').at(-2))}${invocation.rawInput}`;
|
|
674
|
-
const [message, result] = await bound.agent.runMaintenance(signal => runHumanShell(ctx, bound.agent, command, signal, 'changes load on the next launch; exit and run dashi again'));
|
|
798
|
+
const [message, result] = await bound.agent.runMaintenance(signal => runHumanShell(ctx, bound.agent, command, signal, 'changes load on the next launch; exit and run dashi again', false));
|
|
675
799
|
bound.agent.inject(message);
|
|
676
800
|
await ctx.sessions.flush(bound.agent.session);
|
|
677
801
|
publish(bound);
|
|
@@ -679,29 +803,35 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
679
803
|
},
|
|
680
804
|
},
|
|
681
805
|
{
|
|
682
|
-
name: 'new', description: 'Start a new session', input: { hint: '[--name TITLE]' },
|
|
806
|
+
name: 'new', description: 'Start a new session', input: { hint: '[NAME | --name TITLE]' },
|
|
683
807
|
handler: async (invocation) => {
|
|
684
808
|
const stale = ensureCurrent(invocation);
|
|
685
809
|
if (stale !== undefined)
|
|
686
810
|
return stale;
|
|
687
811
|
const raw = invocation.rawInput.trim();
|
|
688
812
|
const match = /^--name\s+(.+)$/su.exec(raw);
|
|
689
|
-
if (raw
|
|
690
|
-
return { kind: 'error', text: 'usage: /new [--name TITLE]' };
|
|
813
|
+
if (raw.startsWith('--') && match === null)
|
|
814
|
+
return { kind: 'error', text: 'usage: /new [NAME | --name TITLE]' };
|
|
691
815
|
const title = match?.[1]?.trim();
|
|
816
|
+
if (raw !== '' && match === null) {
|
|
817
|
+
await ctx.sessionController.rename({ sessionId: bound.agent.id, title: raw });
|
|
818
|
+
await ctx.sessions.flush(bound.agent.session);
|
|
819
|
+
}
|
|
692
820
|
await runRootOperation({ kind: 'new', ...(title === undefined ? {} : { title }) });
|
|
693
821
|
return { kind: 'success' };
|
|
694
822
|
},
|
|
695
823
|
},
|
|
696
824
|
{
|
|
697
|
-
name: 'clear', description: 'Start a new session',
|
|
825
|
+
name: 'clear', description: 'Start a new session', input: { hint: '[NAME]' },
|
|
698
826
|
handler: async (invocation) => {
|
|
699
827
|
const stale = ensureCurrent(invocation);
|
|
700
828
|
if (stale !== undefined)
|
|
701
829
|
return stale;
|
|
702
|
-
const
|
|
703
|
-
if (
|
|
704
|
-
|
|
830
|
+
const title = invocation.rawInput.trim();
|
|
831
|
+
if (title !== '') {
|
|
832
|
+
await ctx.sessionController.rename({ sessionId: bound.agent.id, title });
|
|
833
|
+
await ctx.sessions.flush(bound.agent.session);
|
|
834
|
+
}
|
|
705
835
|
await runRootOperation({ kind: 'new' });
|
|
706
836
|
return { kind: 'success' };
|
|
707
837
|
},
|
|
@@ -787,17 +917,46 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
787
917
|
},
|
|
788
918
|
},
|
|
789
919
|
{
|
|
790
|
-
name: '
|
|
920
|
+
name: 'loop', description: 'List, schedule, or stop a fixed-rate reminder',
|
|
921
|
+
input: { hint: '[INTERVAL TEXT | stop ID]' }, handler: loop,
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
name: 'btw', description: 'Ask a side question in a fork', input: { hint: 'TEXT' }, recordInput: false,
|
|
925
|
+
handler: invocation => askAside(invocation),
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
name: 'recap', description: 'Summarize the conversation in a fork', recordInput: false,
|
|
929
|
+
handler: invocation => usage(invocation.rawInput, '/recap') ?? askAside(invocation, RECAP_PROMPT),
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
name: 'agents', description: 'Choose or author a native DSH agent preset',
|
|
933
|
+
input: { hint: '[new NAME | copy SRC DEST | delete NAME]' },
|
|
791
934
|
handler: async (invocation) => {
|
|
792
935
|
const stale = ensureCurrent(invocation);
|
|
793
936
|
if (stale !== undefined)
|
|
794
937
|
return stale;
|
|
795
|
-
const
|
|
796
|
-
if (invalid !== undefined)
|
|
797
|
-
return invalid;
|
|
938
|
+
const raw = invocation.rawInput.trim();
|
|
798
939
|
const blank = !bound.agent.session.snapshotEvents().some(event => event.type === 'turn/start');
|
|
799
940
|
const selected = ctx.sessionProjections.stateOf(bound.agent.session, 'agentPreset')
|
|
800
941
|
?? ctx.agentPresets.defaultId;
|
|
942
|
+
const create = /^new\s+(\S+)$/u.exec(raw);
|
|
943
|
+
const copy = /^copy\s+(\S+)\s+(\S+)$/u.exec(raw);
|
|
944
|
+
const remove = /^delete\s+(\S+)$/u.exec(raw);
|
|
945
|
+
if (create?.[1] !== undefined) {
|
|
946
|
+
await ctx.agentPresets.copy(selected, create[1]);
|
|
947
|
+
dispatch({ type: 'open-file', path: (await ctx.agentPresets.resolve(create[1])).path });
|
|
948
|
+
return { kind: 'success', text: `created ${create[1]} from ${selected}` };
|
|
949
|
+
}
|
|
950
|
+
if (copy?.[1] !== undefined && copy[2] !== undefined) {
|
|
951
|
+
await ctx.agentPresets.copy(copy[1], copy[2]);
|
|
952
|
+
return { kind: 'success', text: `copied ${copy[1]} to ${copy[2]}` };
|
|
953
|
+
}
|
|
954
|
+
if (remove?.[1] !== undefined) {
|
|
955
|
+
await ctx.agentPresets.remove(remove[1]);
|
|
956
|
+
return { kind: 'success', text: `deleted ${remove[1]}` };
|
|
957
|
+
}
|
|
958
|
+
if (raw !== '')
|
|
959
|
+
return { kind: 'error', text: 'usage: /agents [new NAME | copy SRC DEST | delete NAME]' };
|
|
801
960
|
dispatch({
|
|
802
961
|
type: 'open-overlay',
|
|
803
962
|
overlay: agentPresetOverlay(await ctx.agentPresets.list(), selected, blank),
|
|
@@ -1120,6 +1279,7 @@ export async function createSessionRuntime(ctx, options, tuiRoot, dispatch, deci
|
|
|
1120
1279
|
get initialHasMore() { return current().hasMore; },
|
|
1121
1280
|
get initialPrompts() { return humanPrompts(current().events); },
|
|
1122
1281
|
get root() { return current().root; },
|
|
1282
|
+
get statusLine() { const bound = current(); return sessionStatusLine(ctx, bound.agent, bound.root, bound.branch); },
|
|
1123
1283
|
get summary() { return `Resume with: dsh --profile dashi --resume ${current().root.id}`; },
|
|
1124
1284
|
activate,
|
|
1125
1285
|
async attach(path) {
|
package/lib/shell-command.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export declare const HUMAN_SHELL_STREAM_BYTES: number;
|
|
|
7
7
|
export declare function shellResultText(command: string, result: ShellRunResult): string;
|
|
8
8
|
export declare function quoteShellWord(value: string): string;
|
|
9
9
|
/** Run an explicit human command through DSH and identify its next-step context. */
|
|
10
|
-
export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal, success?: string): Promise<readonly [UserMessage, ShellRunResult]>;
|
|
10
|
+
export declare function runHumanShell(ctx: Context, agent: Agent, command: string, signal: AbortSignal, success?: string, useSessionSandbox?: boolean): Promise<readonly [UserMessage, ShellRunResult]>;
|
package/lib/shell-command.js
CHANGED
|
@@ -45,11 +45,12 @@ export function shellResultText(command, result) {
|
|
|
45
45
|
}
|
|
46
46
|
export function quoteShellWord(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
|
|
47
47
|
/** Run an explicit human command through DSH and identify its next-step context. */
|
|
48
|
-
export async function runHumanShell(ctx, agent, command, signal, success) {
|
|
48
|
+
export async function runHumanShell(ctx, agent, command, signal, success, useSessionSandbox = true) {
|
|
49
49
|
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
50
50
|
command,
|
|
51
51
|
env: { DSH_HOME: resolveDshHome() },
|
|
52
|
-
sandboxPolicy: ctx.sandboxPolicy.resolve(
|
|
52
|
+
sandboxPolicy: ctx.sandboxPolicy.resolve(useSessionSandbox
|
|
53
|
+
? { session: agent.session } : { mode: 'danger-full-access' }),
|
|
53
54
|
signal,
|
|
54
55
|
stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES,
|
|
55
56
|
timeoutMs: HUMAN_SHELL_TIMEOUT_MS,
|
package/lib/state.d.ts
CHANGED
|
@@ -216,6 +216,7 @@ export type Overlay = {
|
|
|
216
216
|
readonly cells?: readonly TerminalCell[];
|
|
217
217
|
readonly kind: 'info';
|
|
218
218
|
readonly lines: readonly string[];
|
|
219
|
+
readonly scrollOffset?: number;
|
|
219
220
|
readonly title: string;
|
|
220
221
|
} | {
|
|
221
222
|
readonly cursor: number;
|
|
@@ -333,6 +334,9 @@ export type UiAction = {
|
|
|
333
334
|
readonly type: 'open-history';
|
|
334
335
|
} | {
|
|
335
336
|
readonly type: 'open-details';
|
|
337
|
+
} | {
|
|
338
|
+
readonly path: string;
|
|
339
|
+
readonly type: 'open-file';
|
|
336
340
|
} | {
|
|
337
341
|
readonly type: 'completion-ready';
|
|
338
342
|
readonly options: readonly OverlayOption[];
|
|
@@ -343,6 +347,7 @@ export type UiAction = {
|
|
|
343
347
|
} | {
|
|
344
348
|
readonly type: 'overlay-close';
|
|
345
349
|
} | {
|
|
350
|
+
readonly limit?: number;
|
|
346
351
|
readonly type: 'overlay-move';
|
|
347
352
|
readonly offset: number;
|
|
348
353
|
} | {
|
|
@@ -503,6 +508,7 @@ export type UiEffect = {
|
|
|
503
508
|
} | {
|
|
504
509
|
readonly type: 'suspend';
|
|
505
510
|
};
|
|
506
|
-
export declare function initialViewState(cwd: string, inline: boolean, root?: RootView, cells?: readonly TerminalCell[], prompts?: readonly string[], historyHasMore?: boolean, attachments?: readonly DraftAttachment[]): ViewState;
|
|
511
|
+
export declare function initialViewState(cwd: string, inline: boolean, root?: RootView, cells?: readonly TerminalCell[], prompts?: readonly string[], historyHasMore?: boolean, attachments?: readonly DraftAttachment[], toolMode?: ToolMode): ViewState;
|
|
507
512
|
export declare const TURN_BELL_THRESHOLD_MS = 10000;
|
|
513
|
+
export declare const PAGE_CELLS = 8;
|
|
508
514
|
export declare function reduce(state: ViewState, action: UiAction): readonly [ViewState, readonly UiEffect[]];
|
package/lib/state.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { FLAG_HELP, VERSION_LINE } from './help.js';
|
|
2
|
-
export function initialViewState(cwd, inline, root, cells = [], prompts = [], historyHasMore = false, attachments = []) {
|
|
2
|
+
export function initialViewState(cwd, inline, root, cells = [], prompts = [], historyHasMore = false, attachments = [], toolMode = 'collapsed') {
|
|
3
3
|
return {
|
|
4
4
|
attachments, cells, composer: '', cwd, decisions: [], exitArmed: false, historyHasMore, inline,
|
|
5
5
|
newOutput: 0, prompts, rewindArmed: false, scrollOffset: 0,
|
|
6
|
-
sendMode: 'steer', toolMode
|
|
6
|
+
sendMode: 'steer', toolMode,
|
|
7
7
|
...(root === undefined ? {} : { root }),
|
|
8
8
|
};
|
|
9
9
|
}
|
|
@@ -41,8 +41,8 @@ function help() {
|
|
|
41
41
|
'Ctrl+B activity details for jobs and subagents',
|
|
42
42
|
'@ paths require DSH file-reference · Ctrl+V pastes an image · Backspace removes it',
|
|
43
43
|
'Ctrl+S stash · Ctrl+G editor · F1 help · Ctrl+L redraw · Ctrl+Z suspend',
|
|
44
|
-
'/new /clear /reset /resume /continue [--all] [NAME|UUID] /fork /branch /rewind /rename /model /effort /permission /agents /queue /status /context',
|
|
45
|
-
'/init /memory /skills /config /login /logout /diff /plugins /plugin /tasks /bashes /subtask /history /export /copy /exit /quit',
|
|
44
|
+
'/new /clear /reset /resume /continue [--all] [NAME|UUID] /fork /branch /rewind /rename /model /effort /permission /agents /queue /status /usage /context',
|
|
45
|
+
'/init /memory /skills /config /login /logout /diff /plugins /plugin /tasks /bashes /subtask /loop /btw /recap /history /export /copy /exit /quit',
|
|
46
46
|
...FLAG_HELP,
|
|
47
47
|
],
|
|
48
48
|
};
|
|
@@ -55,7 +55,7 @@ function moveOverlay(overlay, cursor, detailsCount = 0) {
|
|
|
55
55
|
: Math.max(0, overlay.options.length - 1);
|
|
56
56
|
return { ...overlay, cursor: Math.max(0, Math.min(limit, cursor)) };
|
|
57
57
|
}
|
|
58
|
-
const PAGE_CELLS = 8;
|
|
58
|
+
export const PAGE_CELLS = 8;
|
|
59
59
|
function matches(cells, query) {
|
|
60
60
|
const needle = query.trim().toLocaleLowerCase();
|
|
61
61
|
if (needle === '')
|
|
@@ -366,6 +366,7 @@ export function reduce(state, action) {
|
|
|
366
366
|
return head === undefined
|
|
367
367
|
? [{ ...state, overlay: action.overlay, exitArmed: false, rewindArmed: false }, redraw()]
|
|
368
368
|
: [state, []];
|
|
369
|
+
case 'open-file': return [state, [{ type: 'activate-overlay', value: { kind: 'open-file', path: action.path } }]];
|
|
369
370
|
case 'overlay-close': {
|
|
370
371
|
if (state.overlay === undefined)
|
|
371
372
|
return [state, []];
|
|
@@ -375,8 +376,14 @@ export function reduce(state, action) {
|
|
|
375
376
|
case 'overlay-move': {
|
|
376
377
|
if (state.overlay === undefined)
|
|
377
378
|
return [state, []];
|
|
379
|
+
if (state.overlay.kind === 'info') {
|
|
380
|
+
const current = state.overlay.scrollOffset ?? 0;
|
|
381
|
+
const scrollOffset = Math.max(0, Math.min(action.limit ?? current, current + action.offset));
|
|
382
|
+
return scrollOffset === current ? [state, []]
|
|
383
|
+
: [{ ...state, overlay: { ...state.overlay, scrollOffset } }, redraw()];
|
|
384
|
+
}
|
|
378
385
|
const count = (state.root?.jobs?.length ?? 0) + (state.root?.subagents?.length ?? 0);
|
|
379
|
-
const overlay = moveOverlay(state.overlay, state.overlay.
|
|
386
|
+
const overlay = moveOverlay(state.overlay, state.overlay.cursor + action.offset, count);
|
|
380
387
|
return overlay === state.overlay ? [state, []] : [{ ...state, overlay }, redraw()];
|
|
381
388
|
}
|
|
382
389
|
case 'overlay-number': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antst/dashi",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
4
|
"description": "A terminal UI for DeepSeek Harness",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -57,13 +57,16 @@
|
|
|
57
57
|
"@deepseek-ai/dsh-permission-presets": "0.1.2-rc.1",
|
|
58
58
|
"@deepseek-ai/dsh-plan-mode": "0.1.2-rc.1",
|
|
59
59
|
"@deepseek-ai/dsh-sandbox-policy": "0.1.2-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-schedule": "0.1.2-rc.1",
|
|
60
61
|
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
|
|
61
62
|
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
|
|
62
63
|
"@deepseek-ai/dsh-session-query": "0.1.2-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-session-stats": "0.1.2-rc.1",
|
|
63
65
|
"@deepseek-ai/dsh-settings": "0.1.2-rc.1",
|
|
64
66
|
"@deepseek-ai/dsh-shell": "0.1.2-rc.1",
|
|
65
67
|
"@deepseek-ai/dsh-skill": "0.1.2-rc.1",
|
|
66
68
|
"@deepseek-ai/dsh-subagent": "0.1.2-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
|
|
67
70
|
"@deepseek-ai/dsh-token-meter": "0.1.2-rc.1",
|
|
68
71
|
"@deepseek-ai/dsh-tool-todo": "0.1.2-rc.1",
|
|
69
72
|
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
|