deepseek-cli-mode 1.1.0 → 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 +11 -0
- package/README.md +10 -0
- package/bin/deepseek.js +9 -6
- package/package.json +1 -1
- package/runner/line-editor.js +354 -0
- package/runner/runner.js +73 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
|
|
3
14
|
## 1.1.0 — Claude-Code-style terminal UI
|
|
4
15
|
|
|
5
16
|
- New welcome panel: rounded box with version, model, cwd, and first-run hints.
|
package/README.md
CHANGED
|
@@ -134,6 +134,16 @@ deepseek "review @src/app.ts and tell me what is risky"
|
|
|
134
134
|
|
|
135
135
|
### Inside a session
|
|
136
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
|
+
|
|
137
147
|
| Command | What it does |
|
|
138
148
|
|---|---|
|
|
139
149
|
| `/help` | command list, including the live Harness command registry |
|
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";
|
|
@@ -493,6 +494,41 @@ function printBanner(model) {
|
|
|
493
494
|
out(paint(ansi.dim, " Type a task, or /help. Ctrl+C interrupts; twice exits.") + "\n\n");
|
|
494
495
|
}
|
|
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));
|
|
530
|
+
}
|
|
531
|
+
|
|
496
532
|
function printHelp(write = out) {
|
|
497
533
|
write(`deepseek ${VERSION} — DeepSeek Harness in your terminal.
|
|
498
534
|
|
|
@@ -631,17 +667,31 @@ async function run(ctx, options, exit) {
|
|
|
631
667
|
const write = (text) => process.stdout.write(text);
|
|
632
668
|
|
|
633
669
|
// ---- line input ---------------------------------------------------------
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
const
|
|
637
|
-
|
|
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({
|
|
638
675
|
input: process.stdin,
|
|
639
676
|
output: process.stdout,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
677
|
+
columns: () => process.stdout.columns || 80,
|
|
678
|
+
onInterrupt: () => handleInterrupt(),
|
|
679
|
+
onEnd: () => {
|
|
680
|
+
stdinClosed = true;
|
|
681
|
+
if (ready && !busy) shutdown(0);
|
|
682
|
+
},
|
|
643
683
|
})
|
|
644
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;
|
|
645
695
|
// Hold the input until the first agent is ready: buffered piped lines must
|
|
646
696
|
// not be consumed (and dropped) while the profile is still starting.
|
|
647
697
|
if (rlInterface) {
|
|
@@ -650,8 +700,14 @@ async function run(ctx, options, exit) {
|
|
|
650
700
|
paused = true;
|
|
651
701
|
} catch {}
|
|
652
702
|
}
|
|
703
|
+
editor?.start();
|
|
704
|
+
editor?.setActive(false);
|
|
653
705
|
|
|
654
706
|
function setPaused(value) {
|
|
707
|
+
if (editor) {
|
|
708
|
+
editor.setActive(!value);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
655
711
|
if (!rlInterface || paused === value) return;
|
|
656
712
|
paused = value;
|
|
657
713
|
try {
|
|
@@ -672,12 +728,16 @@ async function run(ctx, options, exit) {
|
|
|
672
728
|
|
|
673
729
|
/**
|
|
674
730
|
* Ask one line. Resolves null when the question is interrupted (Ctrl+C) or
|
|
675
|
-
* stdin closes.
|
|
676
|
-
* 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.
|
|
677
732
|
* @param {string} prompt
|
|
733
|
+
* @param {boolean} [menu]
|
|
678
734
|
* @returns {Promise<string | null>}
|
|
679
735
|
*/
|
|
680
|
-
function ask(prompt) {
|
|
736
|
+
function ask(prompt, menu = false) {
|
|
737
|
+
if (editor) {
|
|
738
|
+
setPaused(false);
|
|
739
|
+
return editor.ask(prompt, menu);
|
|
740
|
+
}
|
|
681
741
|
return new Promise((resolve) => {
|
|
682
742
|
if (!rlInterface) {
|
|
683
743
|
resolve(null);
|
|
@@ -711,6 +771,7 @@ async function run(ctx, options, exit) {
|
|
|
711
771
|
|
|
712
772
|
function closeInput() {
|
|
713
773
|
try {
|
|
774
|
+
editor?.stop();
|
|
714
775
|
rlInterface?.close();
|
|
715
776
|
} catch {}
|
|
716
777
|
}
|
|
@@ -1150,7 +1211,8 @@ async function run(ctx, options, exit) {
|
|
|
1150
1211
|
}
|
|
1151
1212
|
|
|
1152
1213
|
while (!quitting) {
|
|
1153
|
-
|
|
1214
|
+
if (editor) editor.setCommands(commandMenu(agent, commands));
|
|
1215
|
+
const line = await ask(paint(ansi.bold, "> "), true);
|
|
1154
1216
|
if (line === null) break;
|
|
1155
1217
|
const text = line.trim();
|
|
1156
1218
|
if (text === "") continue;
|