deepseek-cli-mode 1.0.2 → 1.2.0
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/CHANGELOG.md +23 -0
- package/README.md +36 -0
- package/bin/deepseek.js +9 -6
- package/package.json +1 -1
- package/runner/line-editor.js +354 -0
- package/runner/runner.js +204 -55
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0 — live slash-command menu
|
|
4
|
+
|
|
5
|
+
- Typing `/` at the prompt now opens a live command menu (like Claude Code): matching commands and descriptions,
|
|
6
|
+
`↑`/`↓` to move, `Tab` to complete, `Enter` to run the highlighted command.
|
|
7
|
+
- New raw-mode line editor (`runner/line-editor.js`) for TTY sessions: cursor movement, history, bracketed paste,
|
|
8
|
+
Ctrl+A/E, and a menu that filters as you type.
|
|
9
|
+
- Piped/non-TTY input keeps the existing readline path; one-shot mode is unchanged.
|
|
10
|
+
- Security: the editor strips C0/C1 control characters from inserted text and renders command names/descriptions
|
|
11
|
+
through the sanitizer; it never executes anything itself.
|
|
12
|
+
- The launcher now copies every runner module into the profile (previously only `runner.js`).
|
|
13
|
+
|
|
14
|
+
## 1.1.0 — Claude-Code-style terminal UI
|
|
15
|
+
|
|
16
|
+
- New welcome panel: rounded box with version, model, cwd, and first-run hints.
|
|
17
|
+
- Prompt is now `> `; each turn ends with a dim status line (`model · tokens · seconds`).
|
|
18
|
+
- Tool activity renders as `⏺ Tool(args)` followed by a `⎿ result` summary line.
|
|
19
|
+
- Animated `✻ Thinking… (Ns · ctrl+c to interrupt)` spinner while the model works.
|
|
20
|
+
- Permission prompts use the numbered `Do you want to allow …? 1. Yes / 2. No` dialog.
|
|
21
|
+
- Todo list uses `☒`/`☐` checkboxes and prints only changed items after the first render.
|
|
22
|
+
- New `/status` command (session, model, cwd, mode, token pressure); `/session` kept.
|
|
23
|
+
- Security: all new rendering paths pass model/repository text through the existing
|
|
24
|
+
control-character sanitizer; no new process, network, or filesystem surfaces were added.
|
|
25
|
+
|
|
3
26
|
## 1.0.2 — runtime discovery fixes
|
|
4
27
|
|
|
5
28
|
- `resolveDshBin()` now also looks for `@deepseek-ai/dsh` next to this package (same `node_modules`) and in the
|
package/README.md
CHANGED
|
@@ -12,6 +12,31 @@ interactive **CLI** that boots the *same* Harness runtime (`dsh-base` + `dsh-hea
|
|
|
12
12
|
session persistence, approvals) and keeps **one live Agent for the whole session**, so multi-turn context, tool state,
|
|
13
13
|
and the model's request prefix stay warm between messages.
|
|
14
14
|
|
|
15
|
+
The terminal UI follows the Claude Code layout:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
╭──────────────────────────────────────────────────────╮
|
|
19
|
+
│ ✻ DeepSeek CLI 1.1.0 │
|
|
20
|
+
│ │
|
|
21
|
+
│ /help for help · /status for your current setup │
|
|
22
|
+
│ model: deepseek-official/deepseek-chat │
|
|
23
|
+
│ cwd: C:\work\my-project │
|
|
24
|
+
╰──────────────────────────────────────────────────────╯
|
|
25
|
+
|
|
26
|
+
> explain what changed and run the tests
|
|
27
|
+
|
|
28
|
+
⏺ glob(**/*.ts)
|
|
29
|
+
⎿ src/app.ts src/auth.ts tests/auth.spec.ts
|
|
30
|
+
✻ Thinking… (2s · ctrl+c to interrupt)
|
|
31
|
+
|
|
32
|
+
⏺ pwsh(npm test)
|
|
33
|
+
⎿ Tests: 42 passed, 42 total
|
|
34
|
+
|
|
35
|
+
The auth change is safe; all tests pass.
|
|
36
|
+
|
|
37
|
+
✻ deepseek-official/deepseek-chat · 8.4k tokens · 12.1s
|
|
38
|
+
```
|
|
39
|
+
|
|
15
40
|
```
|
|
16
41
|
❯ explain what changed in this repo and run the tests
|
|
17
42
|
|
|
@@ -109,6 +134,16 @@ deepseek "review @src/app.ts and tell me what is risky"
|
|
|
109
134
|
|
|
110
135
|
### Inside a session
|
|
111
136
|
|
|
137
|
+
Typing `/` opens a live command menu, like Claude Code:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
> /se
|
|
141
|
+
❯ /session Print the current session id
|
|
142
|
+
/sessions List recent sessions in this directory
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`↑`/`↓` moves the selection, `Tab` completes it, `Enter` runs it. The menu filters as you type.
|
|
146
|
+
|
|
112
147
|
| Command | What it does |
|
|
113
148
|
|---|---|
|
|
114
149
|
| `/help` | command list, including the live Harness command registry |
|
|
@@ -125,6 +160,7 @@ deepseek "review @src/app.ts and tell me what is risky"
|
|
|
125
160
|
| `/export [file]` | write this run's transcript as markdown |
|
|
126
161
|
| `/thinking` | print the last turn's reasoning (collapsed by default) |
|
|
127
162
|
| `/session` | print the current session id (for `--resume`) |
|
|
163
|
+
| `/status` | session, model, cwd, mode, and token pressure |
|
|
128
164
|
| `/clear` | clear the screen |
|
|
129
165
|
| `/quit`, `/exit` | leave (Ctrl+C at an empty prompt also exits) |
|
|
130
166
|
|
package/bin/deepseek.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// The profile keeps ONE live Agent for the whole session, so tools, approvals,
|
|
10
10
|
// session persistence, and the model's request prefix stay warm between turns.
|
|
11
11
|
import { spawn, spawnSync } from "node:child_process";
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { dirname, delimiter, join, resolve } from "node:path";
|
|
@@ -198,11 +198,14 @@ function ensureProfile() {
|
|
|
198
198
|
if (!existsSync(target) || stampVersion !== VERSION) writeFileSync(target, content);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
// Keep
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
// Keep every runner file exactly in sync with this package.
|
|
202
|
+
const runnerSrcDir = join(root, "runner");
|
|
203
|
+
for (const entry of readdirSync(runnerSrcDir, { withFileTypes: true })) {
|
|
204
|
+
if (!entry.isFile() || !/\.(?:js|mjs|cjs)$/.test(entry.name)) continue;
|
|
205
|
+
const src = readFileSync(join(runnerSrcDir, entry.name), "utf8");
|
|
206
|
+
const dst = join(profileDir, entry.name);
|
|
207
|
+
if (!existsSync(dst) || readFileSync(dst, "utf8") !== src) writeFileSync(dst, src);
|
|
208
|
+
}
|
|
206
209
|
|
|
207
210
|
writeFileSync(stamp, VERSION + "\n");
|
|
208
211
|
return profileDir;
|
package/package.json
CHANGED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// Minimal terminal line editor with a live slash-command menu, in the style of
|
|
2
|
+
// Claude Code. No dependencies. Attaches to a TTY in raw mode and renders one
|
|
3
|
+
// input line plus an optional suggestion list below it.
|
|
4
|
+
//
|
|
5
|
+
// Security: every string this module prints as user content is stripped of
|
|
6
|
+
// C0/C1 control characters (except newline/tab handled by the caller). The
|
|
7
|
+
// editor never executes anything; it only returns the submitted line.
|
|
8
|
+
|
|
9
|
+
const CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
10
|
+
const ANSI = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
11
|
+
|
|
12
|
+
/** Visible width of a prompt that may contain ANSI escapes. */
|
|
13
|
+
function visibleWidth(text) {
|
|
14
|
+
return String(text).replace(ANSI, "").length;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Strip terminal control characters from untrusted text. */
|
|
18
|
+
function clean(text) {
|
|
19
|
+
return String(text).replace(CONTROL_CHARS, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class LineEditor {
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} options
|
|
25
|
+
* @param {NodeJS.ReadStream} options.input
|
|
26
|
+
* @param {NodeJS.WriteStream} options.output
|
|
27
|
+
* @param {() => number} [options.columns]
|
|
28
|
+
* @param {() => void} [options.onInterrupt] called for Ctrl+C while no question is pending
|
|
29
|
+
* @param {() => void} [options.onEnd] called when stdin ends
|
|
30
|
+
*/
|
|
31
|
+
constructor({ input, output, columns, onInterrupt, onEnd }) {
|
|
32
|
+
this.input = input;
|
|
33
|
+
this.output = output;
|
|
34
|
+
this.columns = columns ?? (() => this.output.columns || 80);
|
|
35
|
+
this.onInterrupt = onInterrupt ?? (() => {});
|
|
36
|
+
this.onEnd = onEnd ?? (() => {});
|
|
37
|
+
this.commands = [];
|
|
38
|
+
this.buffer = "";
|
|
39
|
+
this.cursor = 0;
|
|
40
|
+
this.history = [];
|
|
41
|
+
this.historyIndex = -1;
|
|
42
|
+
this.draft = "";
|
|
43
|
+
this.menu = [];
|
|
44
|
+
this.menuIndex = 0;
|
|
45
|
+
this.active = false;
|
|
46
|
+
this.menuEnabled = false;
|
|
47
|
+
this.prompt = "";
|
|
48
|
+
this.resolve = null;
|
|
49
|
+
this.renderedRows = 0;
|
|
50
|
+
this.paste = false;
|
|
51
|
+
this.escape = null;
|
|
52
|
+
this.started = false;
|
|
53
|
+
this._onData = (chunk) => this.handleData(chunk);
|
|
54
|
+
this._onEnd = () => {
|
|
55
|
+
this.onEnd();
|
|
56
|
+
this.settle(null, false);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
start() {
|
|
61
|
+
if (this.started) return;
|
|
62
|
+
this.started = true;
|
|
63
|
+
this.input.setRawMode?.(true);
|
|
64
|
+
this.input.setEncoding?.("utf8");
|
|
65
|
+
this.input.resume?.();
|
|
66
|
+
this.input.on("data", this._onData);
|
|
67
|
+
this.input.on("end", this._onEnd);
|
|
68
|
+
this.output.write("\x1b[?2004h");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
stop() {
|
|
72
|
+
if (!this.started) return;
|
|
73
|
+
this.started = false;
|
|
74
|
+
this.input.off?.("data", this._onData);
|
|
75
|
+
this.input.off?.("end", this._onEnd);
|
|
76
|
+
this.output.write("\x1b[?2004l");
|
|
77
|
+
this.input.setRawMode?.(false);
|
|
78
|
+
this.input.pause?.();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Replace the suggestion list (name + description objects). */
|
|
82
|
+
setCommands(commands) {
|
|
83
|
+
this.commands = Array.isArray(commands) ? commands : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Enable/disable input while a turn runs (Ctrl+C still interrupts). */
|
|
87
|
+
setActive(active) {
|
|
88
|
+
if (this.active === active) return;
|
|
89
|
+
this.active = active;
|
|
90
|
+
if (!active) this.clearRender();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Ask one line.
|
|
95
|
+
* @param {string} prompt prompt string (may contain ANSI)
|
|
96
|
+
* @param {boolean} menuEnabled show slash-command suggestions
|
|
97
|
+
* @returns {Promise<string|null>} line, or null on Ctrl+C / EOF
|
|
98
|
+
*/
|
|
99
|
+
ask(prompt, menuEnabled = false) {
|
|
100
|
+
this.prompt = prompt;
|
|
101
|
+
this.menuEnabled = menuEnabled;
|
|
102
|
+
this.buffer = "";
|
|
103
|
+
this.cursor = 0;
|
|
104
|
+
this.menu = [];
|
|
105
|
+
this.menuIndex = 0;
|
|
106
|
+
this.historyIndex = -1;
|
|
107
|
+
this.draft = "";
|
|
108
|
+
this.active = true;
|
|
109
|
+
this.renderedRows = 0;
|
|
110
|
+
this.render();
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
this.resolve = resolve;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- input --------------------------------------------------------------
|
|
117
|
+
handleData(chunk) {
|
|
118
|
+
for (const char of String(chunk)) {
|
|
119
|
+
if (this.escape !== null) {
|
|
120
|
+
this.escape += char;
|
|
121
|
+
if (/[A-Za-z~]$/.test(char)) {
|
|
122
|
+
const sequence = this.escape;
|
|
123
|
+
this.escape = null;
|
|
124
|
+
this.handleEscape(sequence);
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (char === "\x1b") {
|
|
129
|
+
this.escape = "\x1b";
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!this.active) {
|
|
133
|
+
if (char === "\x03") this.onInterrupt();
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
this.handleChar(char);
|
|
137
|
+
}
|
|
138
|
+
if (this.active) this.render();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
handleChar(char) {
|
|
142
|
+
if (this.paste) {
|
|
143
|
+
if (char === "\x1b") this.escape = "\x1b";
|
|
144
|
+
else this.insert(char === "\n" ? " " : char);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
switch (char) {
|
|
148
|
+
case "\r":
|
|
149
|
+
case "\n":
|
|
150
|
+
this.submit();
|
|
151
|
+
return;
|
|
152
|
+
case "\x03": // Ctrl+C
|
|
153
|
+
this.settle(null);
|
|
154
|
+
return;
|
|
155
|
+
case "\x04": // Ctrl+D
|
|
156
|
+
this.settle(null);
|
|
157
|
+
return;
|
|
158
|
+
case "\x7f":
|
|
159
|
+
case "\b":
|
|
160
|
+
this.backspace();
|
|
161
|
+
return;
|
|
162
|
+
case "\t":
|
|
163
|
+
this.complete();
|
|
164
|
+
return;
|
|
165
|
+
case "\x01": // Ctrl+A
|
|
166
|
+
this.cursor = 0;
|
|
167
|
+
return;
|
|
168
|
+
case "\x05": // Ctrl+E
|
|
169
|
+
this.cursor = this.buffer.length;
|
|
170
|
+
return;
|
|
171
|
+
default:
|
|
172
|
+
if (char >= " ") this.insert(char);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
handleEscape(sequence) {
|
|
177
|
+
if (sequence === "\x1b[200~") {
|
|
178
|
+
this.paste = true;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (sequence === "\x1b[201~") {
|
|
182
|
+
this.paste = false;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (this.paste) return;
|
|
186
|
+
switch (sequence) {
|
|
187
|
+
case "\x1b[A":
|
|
188
|
+
this.up();
|
|
189
|
+
break;
|
|
190
|
+
case "\x1b[B":
|
|
191
|
+
this.down();
|
|
192
|
+
break;
|
|
193
|
+
case "\x1b[C":
|
|
194
|
+
this.cursor = Math.min(this.cursor + 1, this.buffer.length);
|
|
195
|
+
break;
|
|
196
|
+
case "\x1b[D":
|
|
197
|
+
this.cursor = Math.max(this.cursor - 1, 0);
|
|
198
|
+
break;
|
|
199
|
+
case "\x1b[H":
|
|
200
|
+
case "\x1b[1~":
|
|
201
|
+
this.cursor = 0;
|
|
202
|
+
break;
|
|
203
|
+
case "\x1b[F":
|
|
204
|
+
case "\x1b[4~":
|
|
205
|
+
this.cursor = this.buffer.length;
|
|
206
|
+
break;
|
|
207
|
+
case "\x1b[3~":
|
|
208
|
+
this.deleteForward();
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
insert(text) {
|
|
216
|
+
const safe = clean(text);
|
|
217
|
+
this.buffer = this.buffer.slice(0, this.cursor) + safe + this.buffer.slice(this.cursor);
|
|
218
|
+
this.cursor += safe.length;
|
|
219
|
+
this.historyIndex = -1;
|
|
220
|
+
this.menuIndex = 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
backspace() {
|
|
224
|
+
if (this.cursor === 0) return;
|
|
225
|
+
this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
|
|
226
|
+
this.cursor -= 1;
|
|
227
|
+
this.historyIndex = -1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
deleteForward() {
|
|
231
|
+
if (this.cursor >= this.buffer.length) return;
|
|
232
|
+
this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
|
|
233
|
+
this.historyIndex = -1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
up() {
|
|
237
|
+
if (this.menu.length > 0) {
|
|
238
|
+
this.menuIndex = (this.menuIndex - 1 + this.menu.length) % this.menu.length;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (this.history.length === 0) return;
|
|
242
|
+
if (this.historyIndex === -1) {
|
|
243
|
+
this.draft = this.buffer;
|
|
244
|
+
this.historyIndex = this.history.length - 1;
|
|
245
|
+
} else if (this.historyIndex > 0) {
|
|
246
|
+
this.historyIndex -= 1;
|
|
247
|
+
}
|
|
248
|
+
this.buffer = this.history[this.historyIndex];
|
|
249
|
+
this.cursor = this.buffer.length;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
down() {
|
|
253
|
+
if (this.menu.length > 0) {
|
|
254
|
+
this.menuIndex = (this.menuIndex + 1) % this.menu.length;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (this.historyIndex === -1) return;
|
|
258
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
259
|
+
this.historyIndex += 1;
|
|
260
|
+
this.buffer = this.history[this.historyIndex];
|
|
261
|
+
} else {
|
|
262
|
+
this.historyIndex = -1;
|
|
263
|
+
this.buffer = this.draft;
|
|
264
|
+
}
|
|
265
|
+
this.cursor = this.buffer.length;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
complete() {
|
|
269
|
+
const item = this.menu[this.menuIndex];
|
|
270
|
+
if (item === undefined) return;
|
|
271
|
+
this.buffer = "/" + item.name;
|
|
272
|
+
this.cursor = this.buffer.length;
|
|
273
|
+
this.menu = [];
|
|
274
|
+
this.menuIndex = 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
submit() {
|
|
278
|
+
const item = this.menu[this.menuIndex];
|
|
279
|
+
if (item !== undefined && this.buffer.trim() !== "/" + item.name) {
|
|
280
|
+
this.buffer = "/" + item.name;
|
|
281
|
+
this.cursor = this.buffer.length;
|
|
282
|
+
}
|
|
283
|
+
const line = this.buffer;
|
|
284
|
+
if (line.trim() !== "" && this.history[this.history.length - 1] !== line) {
|
|
285
|
+
this.history.push(line);
|
|
286
|
+
if (this.history.length > 200) this.history.shift();
|
|
287
|
+
}
|
|
288
|
+
this.settle(line);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
settle(value, echo = true) {
|
|
292
|
+
if (this.resolve === null) return;
|
|
293
|
+
const resolve = this.resolve;
|
|
294
|
+
this.resolve = null;
|
|
295
|
+
this.active = false;
|
|
296
|
+
this.clearRender();
|
|
297
|
+
this.menu = [];
|
|
298
|
+
if (echo) this.output.write("\n");
|
|
299
|
+
resolve(value);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---- rendering ----------------------------------------------------------
|
|
303
|
+
computeMenu() {
|
|
304
|
+
if (!this.menuEnabled) return [];
|
|
305
|
+
if (!this.buffer.startsWith("/")) return [];
|
|
306
|
+
if (/\s/.test(this.buffer)) return [];
|
|
307
|
+
const query = this.buffer.slice(1).toLowerCase();
|
|
308
|
+
const items = this.commands.filter((command) => command.name.toLowerCase().startsWith(query));
|
|
309
|
+
return items.slice(0, 8);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
clearRender() {
|
|
313
|
+
if (this.renderedRows > 0) {
|
|
314
|
+
this.output.write(`\x1b[${this.renderedRows}B`);
|
|
315
|
+
this.output.write("\r\x1b[0J");
|
|
316
|
+
this.output.write(`\x1b[${this.renderedRows}A`);
|
|
317
|
+
}
|
|
318
|
+
this.output.write("\r\x1b[2K");
|
|
319
|
+
this.renderedRows = 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
render() {
|
|
323
|
+
if (!this.active) return;
|
|
324
|
+
this.clearRender();
|
|
325
|
+
const columns = Math.max(40, this.columns() || 80);
|
|
326
|
+
const prompt = this.prompt;
|
|
327
|
+
const promptWidth = visibleWidth(prompt);
|
|
328
|
+
const available = Math.max(8, columns - promptWidth - 1);
|
|
329
|
+
let offset = 0;
|
|
330
|
+
if (this.buffer.length > available) offset = Math.max(0, this.cursor - available + 1);
|
|
331
|
+
const visible = this.buffer.slice(offset, offset + available);
|
|
332
|
+
const column = promptWidth + (this.cursor - offset);
|
|
333
|
+
this.output.write(prompt + visible);
|
|
334
|
+
|
|
335
|
+
this.menu = this.computeMenu();
|
|
336
|
+
if (this.menuIndex >= this.menu.length) this.menuIndex = 0;
|
|
337
|
+
const rows = this.menu.length;
|
|
338
|
+
if (rows > 0) {
|
|
339
|
+
const nameWidth = Math.min(18, Math.max(...this.menu.map((item) => item.name.length)) + 1);
|
|
340
|
+
for (let index = 0; index < rows; index += 1) {
|
|
341
|
+
const item = this.menu[index];
|
|
342
|
+
const selected = index === this.menuIndex;
|
|
343
|
+
const marker = selected ? "❯" : " ";
|
|
344
|
+
const name = ("/" + clean(item.name)).padEnd(nameWidth + 1);
|
|
345
|
+
const description = clean(item.description ?? "").slice(0, Math.max(10, columns - nameWidth - 8));
|
|
346
|
+
const line = ` ${marker} ${selected ? `\x1b[36m${name}\x1b[39m` : name} ${description}`;
|
|
347
|
+
this.output.write("\n" + line);
|
|
348
|
+
}
|
|
349
|
+
this.output.write(`\x1b[${rows}A`);
|
|
350
|
+
}
|
|
351
|
+
this.renderedRows = rows;
|
|
352
|
+
this.output.write(`\x1b[${column + 1}G`);
|
|
353
|
+
}
|
|
354
|
+
}
|
package/runner/runner.js
CHANGED
|
@@ -23,6 +23,7 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
|
23
23
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
24
24
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
25
25
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
26
|
+
import { LineEditor } from "./line-editor.js";
|
|
26
27
|
|
|
27
28
|
/** Stable Cordis plugin name. */
|
|
28
29
|
export const name = "cli-runner";
|
|
@@ -210,6 +211,9 @@ function createRenderer({ write, capture }) {
|
|
|
210
211
|
get lastTodos() {
|
|
211
212
|
return lastTodos;
|
|
212
213
|
},
|
|
214
|
+
get turnUsage() {
|
|
215
|
+
return { ...turnUsage };
|
|
216
|
+
},
|
|
213
217
|
resetTurn() {
|
|
214
218
|
streamedThisTurn = false;
|
|
215
219
|
reasoning = "";
|
|
@@ -246,15 +250,24 @@ function createRenderer({ write, capture }) {
|
|
|
246
250
|
case "tool/call": {
|
|
247
251
|
if (data.name === "todo_write") break; // the checklist below is the presentation
|
|
248
252
|
endLine();
|
|
249
|
-
const icon = TOOL_ICONS[data.name] ?? "⏺";
|
|
250
253
|
const preview = previewToolCall(data.name, data.arguments);
|
|
251
|
-
write(
|
|
254
|
+
write(
|
|
255
|
+
paint(ansi.cyan, "⏺ ") +
|
|
256
|
+
paint(ansi.bold, sanitize(data.name)) +
|
|
257
|
+
paint(ansi.dim, `(${preview})`) +
|
|
258
|
+
"\n"
|
|
259
|
+
);
|
|
252
260
|
break;
|
|
253
261
|
}
|
|
254
262
|
case "tool/result": {
|
|
255
|
-
|
|
256
|
-
|
|
263
|
+
let summary;
|
|
264
|
+
if (data.error) summary = `Error: ${sanitize(data.error.name)}${data.error.code ? ` (${sanitize(data.error.code)})` : ""}`;
|
|
265
|
+
else {
|
|
266
|
+
const content = data.message?.content ?? [];
|
|
267
|
+
const text = content.filter((b) => b.type === "text").map((b) => b.text).join(" ").replace(/\s+/g, " ").trim();
|
|
268
|
+
summary = text === "" ? "Done" : truncate(text, 76);
|
|
257
269
|
}
|
|
270
|
+
write(paint(ansi.dim, ` ⎿ ${sanitize(summary)}`) + "\n");
|
|
258
271
|
break;
|
|
259
272
|
}
|
|
260
273
|
case "assistant/message": {
|
|
@@ -280,15 +293,15 @@ function createRenderer({ write, capture }) {
|
|
|
280
293
|
endLine();
|
|
281
294
|
flushText();
|
|
282
295
|
const done = todos.filter((todo) => todo.status === "completed").length;
|
|
283
|
-
write(paint(ansi.dim, `
|
|
296
|
+
write(paint(ansi.dim, ` ☒ todos ${done}/${todos.length}`) + "\n");
|
|
284
297
|
// Print the whole list once; later writes show only the items that moved.
|
|
285
298
|
const shown = firstRender ? todos : todos.filter((todo) => previous.get(todo.content) !== todo.status);
|
|
286
299
|
for (const todo of shown) {
|
|
287
300
|
const mark =
|
|
288
301
|
todo.status === "completed"
|
|
289
|
-
? paint(ansi.green, "
|
|
302
|
+
? paint(ansi.green, "☒")
|
|
290
303
|
: todo.status === "in_progress"
|
|
291
|
-
? paint(ansi.yellow, "
|
|
304
|
+
? paint(ansi.yellow, "☐")
|
|
292
305
|
: paint(ansi.dim, "☐");
|
|
293
306
|
const content = todo.status === "completed" ? paint(ansi.dim, sanitize(todo.content)) : sanitize(todo.content);
|
|
294
307
|
write(` ${mark} ${content}\n`);
|
|
@@ -442,32 +455,78 @@ function printSessionList(sessions, write = out) {
|
|
|
442
455
|
write(paint(ansi.dim, " no sessions in this directory yet\n"));
|
|
443
456
|
return;
|
|
444
457
|
}
|
|
445
|
-
write(` Recent sessions in ${process.cwd()}:\n`);
|
|
458
|
+
write(` Recent sessions in ${sanitize(process.cwd())}:\n`);
|
|
446
459
|
sessions.slice(0, 10).forEach((session, index) => {
|
|
447
460
|
const running = session.openTurn ? paint(ansi.yellow, " ▶ running") : "";
|
|
448
|
-
write(` ${paint(ansi.cyan, String(index + 1))}) ${paint(ansi.dim, session.id)} (${paint(ansi.dim, timeAgo(session.mtime))})${running}\n`);
|
|
461
|
+
write(` ${paint(ansi.cyan, String(index + 1))}) ${paint(ansi.dim, sanitize(session.id))} (${paint(ansi.dim, timeAgo(session.mtime))})${running}\n`);
|
|
449
462
|
});
|
|
450
463
|
}
|
|
451
464
|
|
|
452
465
|
// ---------------------------------------------------------------------------
|
|
453
|
-
// Help /
|
|
466
|
+
// Help / welcome
|
|
454
467
|
// ---------------------------------------------------------------------------
|
|
468
|
+
/** Draw a rounded box, truncating content to the terminal width. */
|
|
469
|
+
function box(lines, minWidth = 56) {
|
|
470
|
+
const cols = Math.max(40, Math.min(process.stdout.columns || 88, 100));
|
|
471
|
+
const width = Math.min(cols - 2, Math.max(minWidth, ...lines.map((line) => line.length + 4)));
|
|
472
|
+
const inner = width - 4;
|
|
473
|
+
const outLines = [];
|
|
474
|
+
outLines.push(paint(ansi.dim, "╭" + "─".repeat(width - 2) + "╮"));
|
|
475
|
+
for (const line of lines) {
|
|
476
|
+
let text = line;
|
|
477
|
+
if (text.length > inner) text = text.slice(0, inner - 1) + "…";
|
|
478
|
+
outLines.push(paint(ansi.dim, "│ ") + text.padEnd(inner) + paint(ansi.dim, " │"));
|
|
479
|
+
}
|
|
480
|
+
outLines.push(paint(ansi.dim, "╰" + "─".repeat(width - 2) + "╯"));
|
|
481
|
+
for (const line of outLines) out(line + "\n");
|
|
482
|
+
}
|
|
483
|
+
|
|
455
484
|
function printBanner(model) {
|
|
456
|
-
const
|
|
457
|
-
" ooo",
|
|
458
|
-
' " """',
|
|
459
|
-
' " "',
|
|
460
|
-
' ___:____ " "|',
|
|
461
|
-
" ,' \\. \\//",
|
|
462
|
-
" | O \\___/ |",
|
|
463
|
-
"~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~",
|
|
464
|
-
];
|
|
485
|
+
const modelLine = sanitize(`${model.provider}/${model.model}${model.reasoningEffort ? ` · ${model.reasoningEffort}` : ""}`);
|
|
465
486
|
out("\n");
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
487
|
+
box([
|
|
488
|
+
`${paint(ansi.cyan, "✻")} ${paint(ansi.bold, "DeepSeek CLI")} ${paint(ansi.dim, VERSION)}`,
|
|
489
|
+
"",
|
|
490
|
+
paint(ansi.dim, " /help for help · /status for your current setup"),
|
|
491
|
+
paint(ansi.dim, ` model: ${modelLine}`),
|
|
492
|
+
paint(ansi.dim, ` cwd: ${sanitize(process.cwd())}`),
|
|
493
|
+
]);
|
|
494
|
+
out(paint(ansi.dim, " Type a task, or /help. Ctrl+C interrupts; twice exits.") + "\n\n");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Slash-command suggestions for the live menu (native Harness commands are
|
|
498
|
+
// merged in at runtime).
|
|
499
|
+
const BUILTIN_COMMANDS = [
|
|
500
|
+
{ name: "clear", description: "Clear the screen" },
|
|
501
|
+
{ name: "exit", description: "Leave the session" },
|
|
502
|
+
{ name: "export", description: "Write this run's transcript as markdown" },
|
|
503
|
+
{ name: "help", description: "Show commands" },
|
|
504
|
+
{ name: "image", description: "Attach an image to the next message" },
|
|
505
|
+
{ name: "jobs", description: "List this session's background jobs" },
|
|
506
|
+
{ name: "model", description: "Show or switch the model" },
|
|
507
|
+
{ name: "new", description: "Start a fresh session" },
|
|
508
|
+
{ name: "permissions", description: "Show or switch the permission preset" },
|
|
509
|
+
{ name: "quit", description: "Leave the session" },
|
|
510
|
+
{ name: "reasoning", description: "Show or set the reasoning effort" },
|
|
511
|
+
{ name: "resume", description: "Switch to another session" },
|
|
512
|
+
{ name: "session", description: "Print the current session id" },
|
|
513
|
+
{ name: "sessions", description: "List recent sessions in this directory" },
|
|
514
|
+
{ name: "skills", description: "List discovered skills" },
|
|
515
|
+
{ name: "status", description: "Session, model, cwd, token pressure" },
|
|
516
|
+
{ name: "thinking", description: "Show the last turn's reasoning" },
|
|
517
|
+
{ name: "todos", description: "Show the current todo list" },
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
/** Built-ins plus whatever commands the Harness profile registers. */
|
|
521
|
+
function commandMenu(agent, commands) {
|
|
522
|
+
const merged = new Map();
|
|
523
|
+
for (const command of BUILTIN_COMMANDS) merged.set(command.name, command);
|
|
524
|
+
try {
|
|
525
|
+
for (const native of commands?.list(agent) ?? []) {
|
|
526
|
+
if (!merged.has(native.name)) merged.set(native.name, { name: native.name, description: native.description });
|
|
527
|
+
}
|
|
528
|
+
} catch {}
|
|
529
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
471
530
|
}
|
|
472
531
|
|
|
473
532
|
function printHelp(write = out) {
|
|
@@ -498,6 +557,7 @@ Interactive commands:
|
|
|
498
557
|
/export [file] write this run's transcript as markdown
|
|
499
558
|
/thinking show the last turn's reasoning
|
|
500
559
|
/session print the current session id (for --resume)
|
|
560
|
+
/status session, model, cwd, token pressure
|
|
501
561
|
/clear clear the screen
|
|
502
562
|
/quit, /exit leave (also Ctrl+C at an empty prompt)
|
|
503
563
|
|
|
@@ -576,6 +636,7 @@ async function run(ctx, options, exit) {
|
|
|
576
636
|
const planMode = ctx.get("planMode");
|
|
577
637
|
const permissionPresets = ctx.get("permissionPresets");
|
|
578
638
|
const attachments = ctx.get("attachments");
|
|
639
|
+
const tokenMeter = ctx.get("tokenMeter");
|
|
579
640
|
|
|
580
641
|
if (options.list) {
|
|
581
642
|
printSessionList(listSessions());
|
|
@@ -606,17 +667,31 @@ async function run(ctx, options, exit) {
|
|
|
606
667
|
const write = (text) => process.stdout.write(text);
|
|
607
668
|
|
|
608
669
|
// ---- line input ---------------------------------------------------------
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
const
|
|
612
|
-
|
|
670
|
+
// TTY: a raw-mode line editor with a live slash-command menu (Claude style).
|
|
671
|
+
// Non-TTY: readline over the piped stream. One-shot: no input owner at all.
|
|
672
|
+
const useEditor = interactive && process.stdin.isTTY === true;
|
|
673
|
+
const editor = useEditor
|
|
674
|
+
? new LineEditor({
|
|
613
675
|
input: process.stdin,
|
|
614
676
|
output: process.stdout,
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
677
|
+
columns: () => process.stdout.columns || 80,
|
|
678
|
+
onInterrupt: () => handleInterrupt(),
|
|
679
|
+
onEnd: () => {
|
|
680
|
+
stdinClosed = true;
|
|
681
|
+
if (ready && !busy) shutdown(0);
|
|
682
|
+
},
|
|
618
683
|
})
|
|
619
684
|
: null;
|
|
685
|
+
const rlInterface =
|
|
686
|
+
interactive && !useEditor
|
|
687
|
+
? createInterface({
|
|
688
|
+
input: process.stdin,
|
|
689
|
+
output: process.stdout,
|
|
690
|
+
terminal: false,
|
|
691
|
+
historySize: 200,
|
|
692
|
+
removeHistoryDuplicates: true,
|
|
693
|
+
})
|
|
694
|
+
: null;
|
|
620
695
|
// Hold the input until the first agent is ready: buffered piped lines must
|
|
621
696
|
// not be consumed (and dropped) while the profile is still starting.
|
|
622
697
|
if (rlInterface) {
|
|
@@ -625,8 +700,14 @@ async function run(ctx, options, exit) {
|
|
|
625
700
|
paused = true;
|
|
626
701
|
} catch {}
|
|
627
702
|
}
|
|
703
|
+
editor?.start();
|
|
704
|
+
editor?.setActive(false);
|
|
628
705
|
|
|
629
706
|
function setPaused(value) {
|
|
707
|
+
if (editor) {
|
|
708
|
+
editor.setActive(!value);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
630
711
|
if (!rlInterface || paused === value) return;
|
|
631
712
|
paused = value;
|
|
632
713
|
try {
|
|
@@ -647,12 +728,16 @@ async function run(ctx, options, exit) {
|
|
|
647
728
|
|
|
648
729
|
/**
|
|
649
730
|
* Ask one line. Resolves null when the question is interrupted (Ctrl+C) or
|
|
650
|
-
* stdin closes.
|
|
651
|
-
* or typing while the model works), so no line is ever dropped.
|
|
731
|
+
* stdin closes. On a TTY the editor renders a live command menu.
|
|
652
732
|
* @param {string} prompt
|
|
733
|
+
* @param {boolean} [menu]
|
|
653
734
|
* @returns {Promise<string | null>}
|
|
654
735
|
*/
|
|
655
|
-
function ask(prompt) {
|
|
736
|
+
function ask(prompt, menu = false) {
|
|
737
|
+
if (editor) {
|
|
738
|
+
setPaused(false);
|
|
739
|
+
return editor.ask(prompt, menu);
|
|
740
|
+
}
|
|
656
741
|
return new Promise((resolve) => {
|
|
657
742
|
if (!rlInterface) {
|
|
658
743
|
resolve(null);
|
|
@@ -686,6 +771,7 @@ async function run(ctx, options, exit) {
|
|
|
686
771
|
|
|
687
772
|
function closeInput() {
|
|
688
773
|
try {
|
|
774
|
+
editor?.stop();
|
|
689
775
|
rlInterface?.close();
|
|
690
776
|
} catch {}
|
|
691
777
|
}
|
|
@@ -783,24 +869,63 @@ async function run(ctx, options, exit) {
|
|
|
783
869
|
// ---- agent lifecycle ----------------------------------------------------
|
|
784
870
|
const makeAgentOptions = () => ({ ...selectionRef.current });
|
|
785
871
|
|
|
872
|
+
// ---- Claude-style activity indicator ------------------------------------
|
|
873
|
+
let spinnerTimer = null;
|
|
874
|
+
let spinnerIndex = 0;
|
|
875
|
+
let spinnerActive = false;
|
|
876
|
+
const SPINNER = ["✻", "✳", "✶", "✳"];
|
|
877
|
+
|
|
878
|
+
function startSpinner(label = "Thinking") {
|
|
879
|
+
if (!tty || options.mode === "oneshot" || spinnerActive) return;
|
|
880
|
+
spinnerActive = true;
|
|
881
|
+
spinnerIndex = 0;
|
|
882
|
+
const started = Date.now();
|
|
883
|
+
out("\x1b[?25l");
|
|
884
|
+
const tick = () => {
|
|
885
|
+
const glyph = SPINNER[spinnerIndex++ % SPINNER.length];
|
|
886
|
+
const seconds = Math.floor((Date.now() - started) / 1000);
|
|
887
|
+
out(`\r\x1b[2K${paint(ansi.dim, `${glyph} ${label}… (${seconds}s · ctrl+c to interrupt)`)}`);
|
|
888
|
+
};
|
|
889
|
+
tick();
|
|
890
|
+
spinnerTimer = setInterval(tick, 120);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function stopSpinner() {
|
|
894
|
+
if (!spinnerActive) return;
|
|
895
|
+
spinnerActive = false;
|
|
896
|
+
if (spinnerTimer !== null) {
|
|
897
|
+
clearInterval(spinnerTimer);
|
|
898
|
+
spinnerTimer = null;
|
|
899
|
+
}
|
|
900
|
+
out("\r\x1b[2K\x1b[?25h");
|
|
901
|
+
}
|
|
902
|
+
|
|
786
903
|
const wire = (agent) => {
|
|
787
904
|
agent.ctx.on("session/event", (session, event) => {
|
|
788
|
-
if (session === agent.session)
|
|
905
|
+
if (session === agent.session) {
|
|
906
|
+
stopSpinner();
|
|
907
|
+
renderer.event(event);
|
|
908
|
+
}
|
|
789
909
|
});
|
|
790
910
|
agent.ctx.on("agent/assistant-stream", (payload) => {
|
|
791
|
-
if (payload.agent === agent)
|
|
911
|
+
if (payload.agent === agent) {
|
|
912
|
+
stopSpinner();
|
|
913
|
+
renderer.frame(payload.frame);
|
|
914
|
+
}
|
|
792
915
|
});
|
|
793
916
|
agent.ctx.on("user-questions/request", async (request) => answerQuestions(request));
|
|
794
917
|
agent.ctx.on("approval/request", async (request) => {
|
|
795
|
-
const toolName = request.toolName ?? "tool";
|
|
918
|
+
const toolName = sanitize(request.toolName ?? "tool");
|
|
796
919
|
if (autoYes) return "allowed-once";
|
|
797
|
-
|
|
798
|
-
out(paint(ansi.dim, paint(ansi.italic, ` ⚠ ${toolName} needs approval: ${request.reason}`)) + "\n");
|
|
799
|
-
}
|
|
920
|
+
stopSpinner();
|
|
800
921
|
if (!interactive || !process.stdin.isTTY) return "rejected";
|
|
801
|
-
|
|
802
|
-
if (
|
|
803
|
-
|
|
922
|
+
out("\n");
|
|
923
|
+
if (request.reason) out(paint(ansi.dim, ` ${sanitize(request.reason)}`) + "\n");
|
|
924
|
+
out(` ${paint(ansi.bold, "Do you want to allow")} ${paint(ansi.cyan, toolName)}${paint(ansi.bold, "?")}\n`);
|
|
925
|
+
out(` ${paint(ansi.cyan, "1")}. Yes\n ${paint(ansi.cyan, "2")}. No\n`);
|
|
926
|
+
const answer = (await ask(paint(ansi.green, " ❯ "))) ?? "";
|
|
927
|
+
const choice = answer.trim().toLowerCase();
|
|
928
|
+
return choice === "1" || choice === "y" || choice === "yes" ? "allowed-once" : "rejected";
|
|
804
929
|
});
|
|
805
930
|
};
|
|
806
931
|
|
|
@@ -836,17 +961,26 @@ async function run(ctx, options, exit) {
|
|
|
836
961
|
busy = true;
|
|
837
962
|
cancelSent = false;
|
|
838
963
|
renderer.resetTurn();
|
|
964
|
+
const startedAt = Date.now();
|
|
839
965
|
setPaused(true);
|
|
966
|
+
startSpinner("Thinking");
|
|
840
967
|
try {
|
|
841
968
|
agent.followup(createUserMessage({ content, source: { kind: "user" } }));
|
|
842
969
|
await agent.whenIdle();
|
|
843
970
|
await persistence.flush();
|
|
844
971
|
} finally {
|
|
972
|
+
stopSpinner();
|
|
845
973
|
busy = false;
|
|
846
974
|
setPaused(false);
|
|
847
975
|
if (renderer.lastAssistantText && renderer.lastAssistantText !== previousAnswer) {
|
|
848
976
|
transcript.push({ role: "assistant", text: renderer.lastAssistantText });
|
|
849
977
|
}
|
|
978
|
+
if (interactive && tty) {
|
|
979
|
+
const usage = renderer.turnUsage;
|
|
980
|
+
const seconds = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
981
|
+
const tokens = usage.input + usage.output;
|
|
982
|
+
out(paint(ansi.dim, ` ✻ ${sanitize(modelDesc())}${tokens > 0 ? ` · ${formatTokens(tokens)} tokens` : ""} · ${seconds}s`) + "\n\n");
|
|
983
|
+
}
|
|
850
984
|
}
|
|
851
985
|
};
|
|
852
986
|
|
|
@@ -1057,7 +1191,7 @@ async function run(ctx, options, exit) {
|
|
|
1057
1191
|
try {
|
|
1058
1192
|
agent = resumeTarget ? await startAgent(undefined, resumeTarget) : await startAgent(SessionId(`session-${randomUUID()}`), undefined);
|
|
1059
1193
|
} catch (error) {
|
|
1060
|
-
err(`deepseek: cannot start a session: ${
|
|
1194
|
+
err(`deepseek: cannot start a session: ${sanitize(describeError(error))}\n`);
|
|
1061
1195
|
await shutdown(1);
|
|
1062
1196
|
return;
|
|
1063
1197
|
}
|
|
@@ -1070,14 +1204,15 @@ async function run(ctx, options, exit) {
|
|
|
1070
1204
|
|
|
1071
1205
|
printBanner(selectionRef.current);
|
|
1072
1206
|
if (resumeTarget) {
|
|
1073
|
-
out(paint(ansi.green, `▶ resumed `) + paint(ansi.dim, resumeTarget) + "\n");
|
|
1207
|
+
out(paint(ansi.green, `▶ resumed `) + paint(ansi.dim, sanitize(resumeTarget)) + "\n");
|
|
1074
1208
|
out(paint(ansi.dim, " (history is in the session log; continue the conversation below)") + "\n\n");
|
|
1075
1209
|
} else {
|
|
1076
|
-
out(paint(ansi.dim, `session: ${agent.session.id} (resume with: deepseek --resume ${agent.session.id})`) + "\n\n");
|
|
1210
|
+
out(paint(ansi.dim, `session: ${sanitize(agent.session.id)} (resume with: deepseek --resume ${sanitize(agent.session.id)})`) + "\n\n");
|
|
1077
1211
|
}
|
|
1078
1212
|
|
|
1079
1213
|
while (!quitting) {
|
|
1080
|
-
|
|
1214
|
+
if (editor) editor.setCommands(commandMenu(agent, commands));
|
|
1215
|
+
const line = await ask(paint(ansi.bold, "> "), true);
|
|
1081
1216
|
if (line === null) break;
|
|
1082
1217
|
const text = line.trim();
|
|
1083
1218
|
if (text === "") continue;
|
|
@@ -1098,9 +1233,9 @@ async function run(ctx, options, exit) {
|
|
|
1098
1233
|
out(paint(ansi.dim, " ↻ new session\n"));
|
|
1099
1234
|
try {
|
|
1100
1235
|
agent = await startAgent(SessionId(`session-${randomUUID()}`), undefined);
|
|
1101
|
-
out(paint(ansi.dim, ` session: ${agent.session.id}\n`));
|
|
1236
|
+
out(paint(ansi.dim, ` session: ${sanitize(agent.session.id)}\n`));
|
|
1102
1237
|
} catch (error) {
|
|
1103
|
-
out(paint(ansi.red, ` ✘ ${
|
|
1238
|
+
out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
|
|
1104
1239
|
}
|
|
1105
1240
|
continue;
|
|
1106
1241
|
}
|
|
@@ -1116,13 +1251,27 @@ async function run(ctx, options, exit) {
|
|
|
1116
1251
|
out(paint(ansi.dim, ` ↻ resuming ${argument}\n`));
|
|
1117
1252
|
agent = await startAgent(undefined, argument);
|
|
1118
1253
|
} catch (error) {
|
|
1119
|
-
out(paint(ansi.red, ` ✘ ${
|
|
1254
|
+
out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
|
|
1120
1255
|
}
|
|
1121
1256
|
continue;
|
|
1122
1257
|
}
|
|
1123
1258
|
case "/session":
|
|
1124
|
-
out(paint(ansi.dim, ` ${agent.session.id}\n`));
|
|
1259
|
+
out(paint(ansi.dim, ` ${sanitize(agent.session.id)}\n`));
|
|
1125
1260
|
continue;
|
|
1261
|
+
case "/status": {
|
|
1262
|
+
out(paint(ansi.bold, " Status") + "\n");
|
|
1263
|
+
out(` session: ${sanitize(agent.session.id)}\n`);
|
|
1264
|
+
out(` model: ${sanitize(modelDesc())}\n`);
|
|
1265
|
+
out(` cwd: ${sanitize(process.cwd())}\n`);
|
|
1266
|
+
out(` mode: ${interactive ? "interactive" : "one-shot"}${autoYes ? " · auto-approve" : ""}\n`);
|
|
1267
|
+
try {
|
|
1268
|
+
if (tokenMeter !== undefined) {
|
|
1269
|
+
const measurement = tokenMeter.measure(agent.session);
|
|
1270
|
+
out(` tokens: ${measurement.totalTokens} (surface ${measurement.surfaceTokens})\n`);
|
|
1271
|
+
}
|
|
1272
|
+
} catch {}
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1126
1275
|
case "/model": {
|
|
1127
1276
|
if (argument === "") {
|
|
1128
1277
|
out(paint(ansi.dim, ` model: ${modelDesc()}\n`));
|
|
@@ -1134,7 +1283,7 @@ async function run(ctx, options, exit) {
|
|
|
1134
1283
|
selectionRef.current = next;
|
|
1135
1284
|
out(paint(ansi.green, ` ✓ model set: ${next.provider}/${next.model} (next message)\n`));
|
|
1136
1285
|
} catch (error) {
|
|
1137
|
-
out(paint(ansi.red, ` ✘ ${
|
|
1286
|
+
out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
|
|
1138
1287
|
}
|
|
1139
1288
|
continue;
|
|
1140
1289
|
}
|
|
@@ -1149,7 +1298,7 @@ async function run(ctx, options, exit) {
|
|
|
1149
1298
|
selectionRef.current = next;
|
|
1150
1299
|
out(paint(ansi.green, ` ✓ reasoning set: ${argument} (next message)\n`));
|
|
1151
1300
|
} catch (error) {
|
|
1152
|
-
out(paint(ansi.red, ` ✘ ${
|
|
1301
|
+
out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
|
|
1153
1302
|
}
|
|
1154
1303
|
continue;
|
|
1155
1304
|
}
|
|
@@ -1281,7 +1430,7 @@ async function run(ctx, options, exit) {
|
|
|
1281
1430
|
}
|
|
1282
1431
|
}
|
|
1283
1432
|
|
|
1284
|
-
out("\n" + paint(ansi.
|
|
1433
|
+
out("\n" + paint(ansi.bold, "> ") + sanitize(text) + "\n\n");
|
|
1285
1434
|
await runTurn(agent, text);
|
|
1286
1435
|
}
|
|
1287
1436
|
|
|
@@ -1310,7 +1459,7 @@ export function apply(ctx) {
|
|
|
1310
1459
|
}
|
|
1311
1460
|
run(ctx, options, exit).catch((error) => {
|
|
1312
1461
|
const detail = process.env.DSH_CLI_DEBUG === "1" && error instanceof Error ? `\n${error.stack}` : "";
|
|
1313
|
-
process.stderr.write(`deepseek: ${
|
|
1462
|
+
process.stderr.write(`deepseek: ${sanitize(describeError(error))}${detail}\n`);
|
|
1314
1463
|
try {
|
|
1315
1464
|
exit(1);
|
|
1316
1465
|
} catch {}
|