deepseek-cli-mode 1.1.0 → 1.3.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 +18 -0
- package/bin/deepseek.js +9 -6
- package/package.json +1 -1
- package/runner/line-editor.js +462 -0
- package/runner/runner.js +219 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0 — pickers, Shift+Tab permission cycling, permanent mode footer
|
|
4
|
+
|
|
5
|
+
- `/model`, `/reasoning`, `/permissions`, and `/resume` now open an interactive picker (`↑`/`↓`, `Enter`,
|
|
6
|
+
`Ctrl+C` to cancel) instead of requiring an argument. `/model` lists the models discovered from the provider.
|
|
7
|
+
- **Shift+Tab** cycles the permission mode (read-only → workspace-write → danger-full-access) without losing the
|
|
8
|
+
typed line, with a transient hint.
|
|
9
|
+
- A permanent dim footer above the prompt shows the active permission mode, model, and the Shift+Tab hint:
|
|
10
|
+
`⏵⏵ workspace-write · deepseek-official/deepseek-chat · shift+tab to cycle`.
|
|
11
|
+
- Direct arguments still work (`/model <id>`, `/reasoning high`, `/permissions read-only`, `/resume <id>`).
|
|
12
|
+
- Security: picker labels/descriptions and the footer pass through the sanitizer; permission changes still require a
|
|
13
|
+
user keystroke (the model cannot trigger them).
|
|
14
|
+
|
|
15
|
+
## 1.2.0 — live slash-command menu
|
|
16
|
+
|
|
17
|
+
- Typing `/` at the prompt now opens a live command menu (like Claude Code): matching commands and descriptions,
|
|
18
|
+
`↑`/`↓` to move, `Tab` to complete, `Enter` to run the highlighted command.
|
|
19
|
+
- New raw-mode line editor (`runner/line-editor.js`) for TTY sessions: cursor movement, history, bracketed paste,
|
|
20
|
+
Ctrl+A/E, and a menu that filters as you type.
|
|
21
|
+
- Piped/non-TTY input keeps the existing readline path; one-shot mode is unchanged.
|
|
22
|
+
- Security: the editor strips C0/C1 control characters from inserted text and renders command names/descriptions
|
|
23
|
+
through the sanitizer; it never executes anything itself.
|
|
24
|
+
- The launcher now copies every runner module into the profile (previously only `runner.js`).
|
|
25
|
+
|
|
3
26
|
## 1.1.0 — Claude-Code-style terminal UI
|
|
4
27
|
|
|
5
28
|
- New welcome panel: rounded box with version, model, cwd, and first-run hints.
|
package/README.md
CHANGED
|
@@ -134,6 +134,24 @@ 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
|
+
|
|
147
|
+
`/model`, `/reasoning`, `/permissions`, and `/resume` open the same kind of picker when called without an argument.
|
|
148
|
+
**Shift+Tab** cycles the permission mode, and a dim footer above the prompt always shows the active one:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
⏵⏵ workspace-write · deepseek-official/deepseek-chat · shift+tab to cycle
|
|
152
|
+
>
|
|
153
|
+
```
|
|
154
|
+
|
|
137
155
|
| Command | What it does |
|
|
138
156
|
|---|---|
|
|
139
157
|
| `/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,462 @@
|
|
|
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, onShiftTab }) {
|
|
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.onShiftTab = onShiftTab ?? (() => undefined);
|
|
38
|
+
this.commands = [];
|
|
39
|
+
this.buffer = "";
|
|
40
|
+
this.cursor = 0;
|
|
41
|
+
this.history = [];
|
|
42
|
+
this.historyIndex = -1;
|
|
43
|
+
this.draft = "";
|
|
44
|
+
this.menu = [];
|
|
45
|
+
this.menuIndex = 0;
|
|
46
|
+
this.active = false;
|
|
47
|
+
this.menuEnabled = false;
|
|
48
|
+
this.prompt = "";
|
|
49
|
+
this.resolve = null;
|
|
50
|
+
this.renderedRows = 0;
|
|
51
|
+
this.paste = false;
|
|
52
|
+
this.escape = null;
|
|
53
|
+
this.started = false;
|
|
54
|
+
this.selectMode = false;
|
|
55
|
+
this.selectOptions = [];
|
|
56
|
+
this.selectIndex = 0;
|
|
57
|
+
this.hint = "";
|
|
58
|
+
this._onData = (chunk) => this.handleData(chunk);
|
|
59
|
+
this._onEnd = () => {
|
|
60
|
+
this.onEnd();
|
|
61
|
+
this.settle(null, false);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
start() {
|
|
66
|
+
if (this.started) return;
|
|
67
|
+
this.started = true;
|
|
68
|
+
this.input.setRawMode?.(true);
|
|
69
|
+
this.input.setEncoding?.("utf8");
|
|
70
|
+
this.input.resume?.();
|
|
71
|
+
this.input.on("data", this._onData);
|
|
72
|
+
this.input.on("end", this._onEnd);
|
|
73
|
+
this.output.write("\x1b[?2004h");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
stop() {
|
|
77
|
+
if (!this.started) return;
|
|
78
|
+
this.started = false;
|
|
79
|
+
this.input.off?.("data", this._onData);
|
|
80
|
+
this.input.off?.("end", this._onEnd);
|
|
81
|
+
this.output.write("\x1b[?2004l");
|
|
82
|
+
this.input.setRawMode?.(false);
|
|
83
|
+
this.input.pause?.();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Replace the suggestion list (name + description objects). */
|
|
87
|
+
setCommands(commands) {
|
|
88
|
+
this.commands = Array.isArray(commands) ? commands : [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Enable/disable input while a turn runs (Ctrl+C still interrupts). */
|
|
92
|
+
setActive(active) {
|
|
93
|
+
if (this.active === active) return;
|
|
94
|
+
this.active = active;
|
|
95
|
+
if (!active) this.clearRender();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Ask one line.
|
|
100
|
+
* @param {string} prompt prompt string (may contain ANSI)
|
|
101
|
+
* @param {boolean} menuEnabled show slash-command suggestions
|
|
102
|
+
* @returns {Promise<string|null>} line, or null on Ctrl+C / EOF
|
|
103
|
+
*/
|
|
104
|
+
ask(prompt, menuEnabled = false) {
|
|
105
|
+
this.prompt = prompt;
|
|
106
|
+
this.menuEnabled = menuEnabled;
|
|
107
|
+
this.buffer = "";
|
|
108
|
+
this.cursor = 0;
|
|
109
|
+
this.menu = [];
|
|
110
|
+
this.menuIndex = 0;
|
|
111
|
+
this.historyIndex = -1;
|
|
112
|
+
this.draft = "";
|
|
113
|
+
this.active = true;
|
|
114
|
+
this.renderedRows = 0;
|
|
115
|
+
this.render();
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
this.resolve = resolve;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Ask the user to pick one option from a list.
|
|
123
|
+
* @param {string} title prompt line
|
|
124
|
+
* @param {{label: string, description?: string, value: string}[]} options
|
|
125
|
+
* @param {number} [initial]
|
|
126
|
+
* @returns {Promise<string|null>}
|
|
127
|
+
*/
|
|
128
|
+
askSelect(title, options, initial = 0) {
|
|
129
|
+
this.selectMode = true;
|
|
130
|
+
this.selectOptions = Array.isArray(options) ? options : [];
|
|
131
|
+
this.selectIndex = Math.max(0, Math.min(initial, this.selectOptions.length - 1));
|
|
132
|
+
this.prompt = title;
|
|
133
|
+
this.menuEnabled = false;
|
|
134
|
+
this.hint = "";
|
|
135
|
+
this.buffer = "";
|
|
136
|
+
this.cursor = 0;
|
|
137
|
+
this.active = true;
|
|
138
|
+
this.renderedRows = 0;
|
|
139
|
+
this.output.write("\x1b[?25l");
|
|
140
|
+
this.render();
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
this.resolve = resolve;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
chooseSelect() {
|
|
147
|
+
const option = this.selectOptions[this.selectIndex];
|
|
148
|
+
this.selectMode = false;
|
|
149
|
+
this.selectOptions = [];
|
|
150
|
+
this.settle(option === undefined ? null : option.value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
cancelSelect() {
|
|
154
|
+
if (!this.selectMode) return;
|
|
155
|
+
this.selectMode = false;
|
|
156
|
+
this.selectOptions = [];
|
|
157
|
+
this.settle(null);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- input --------------------------------------------------------------
|
|
161
|
+
handleData(chunk) {
|
|
162
|
+
for (const char of String(chunk)) {
|
|
163
|
+
if (this.escape !== null) {
|
|
164
|
+
if (this.escape === "\x1b" && char !== "[" && char !== "O") {
|
|
165
|
+
// Lone Esc (not a CSI/SS3 sequence).
|
|
166
|
+
this.escape = null;
|
|
167
|
+
if (this.selectMode) this.cancelSelect();
|
|
168
|
+
else if (this.active) {
|
|
169
|
+
this.hint = "";
|
|
170
|
+
this.menu = [];
|
|
171
|
+
this.render();
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
this.escape += char;
|
|
176
|
+
if (/[A-Za-z~]$/.test(char)) {
|
|
177
|
+
const sequence = this.escape;
|
|
178
|
+
this.escape = null;
|
|
179
|
+
this.handleEscape(sequence);
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (char === "\x1b") {
|
|
184
|
+
this.escape = "\x1b";
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!this.active) {
|
|
188
|
+
if (char === "\x03") this.onInterrupt();
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
this.handleChar(char);
|
|
192
|
+
}
|
|
193
|
+
if (this.active) this.render();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
handleChar(char) {
|
|
197
|
+
if (this.selectMode) {
|
|
198
|
+
if (char === "\r" || char === "\n") this.chooseSelect();
|
|
199
|
+
else if (char === "\x03" || char === "\x04") this.cancelSelect();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (this.paste) {
|
|
203
|
+
if (char === "\x1b") this.escape = "\x1b";
|
|
204
|
+
else this.insert(char === "\n" ? " " : char);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.hint = "";
|
|
208
|
+
switch (char) {
|
|
209
|
+
case "\r":
|
|
210
|
+
case "\n":
|
|
211
|
+
this.submit();
|
|
212
|
+
return;
|
|
213
|
+
case "\x03": // Ctrl+C
|
|
214
|
+
this.settle(null);
|
|
215
|
+
return;
|
|
216
|
+
case "\x04": // Ctrl+D
|
|
217
|
+
this.settle(null);
|
|
218
|
+
return;
|
|
219
|
+
case "\x7f":
|
|
220
|
+
case "\b":
|
|
221
|
+
this.backspace();
|
|
222
|
+
return;
|
|
223
|
+
case "\t":
|
|
224
|
+
this.complete();
|
|
225
|
+
return;
|
|
226
|
+
case "\x01": // Ctrl+A
|
|
227
|
+
this.cursor = 0;
|
|
228
|
+
return;
|
|
229
|
+
case "\x05": // Ctrl+E
|
|
230
|
+
this.cursor = this.buffer.length;
|
|
231
|
+
return;
|
|
232
|
+
default:
|
|
233
|
+
if (char >= " ") this.insert(char);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
handleEscape(sequence) {
|
|
238
|
+
if (sequence === "\x1b[200~") {
|
|
239
|
+
this.paste = true;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (sequence === "\x1b[201~") {
|
|
243
|
+
this.paste = false;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (this.paste) return;
|
|
247
|
+
if (sequence === "\x1b[Z") {
|
|
248
|
+
// Shift+Tab: cycle a mode (permission preset) while keeping the line.
|
|
249
|
+
if (this.selectMode) return;
|
|
250
|
+
this.hint = this.onShiftTab?.() ?? "";
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (this.selectMode) {
|
|
254
|
+
if (sequence === "\x1b[A") {
|
|
255
|
+
this.selectIndex = (this.selectIndex - 1 + this.selectOptions.length) % Math.max(1, this.selectOptions.length);
|
|
256
|
+
} else if (sequence === "\x1b[B") {
|
|
257
|
+
this.selectIndex = (this.selectIndex + 1) % Math.max(1, this.selectOptions.length);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
switch (sequence) {
|
|
262
|
+
case "\x1b[A":
|
|
263
|
+
this.up();
|
|
264
|
+
break;
|
|
265
|
+
case "\x1b[B":
|
|
266
|
+
this.down();
|
|
267
|
+
break;
|
|
268
|
+
case "\x1b[C":
|
|
269
|
+
this.cursor = Math.min(this.cursor + 1, this.buffer.length);
|
|
270
|
+
break;
|
|
271
|
+
case "\x1b[D":
|
|
272
|
+
this.cursor = Math.max(this.cursor - 1, 0);
|
|
273
|
+
break;
|
|
274
|
+
case "\x1b[H":
|
|
275
|
+
case "\x1b[1~":
|
|
276
|
+
this.cursor = 0;
|
|
277
|
+
break;
|
|
278
|
+
case "\x1b[F":
|
|
279
|
+
case "\x1b[4~":
|
|
280
|
+
this.cursor = this.buffer.length;
|
|
281
|
+
break;
|
|
282
|
+
case "\x1b[3~":
|
|
283
|
+
this.deleteForward();
|
|
284
|
+
break;
|
|
285
|
+
default:
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
insert(text) {
|
|
291
|
+
const safe = clean(text);
|
|
292
|
+
this.buffer = this.buffer.slice(0, this.cursor) + safe + this.buffer.slice(this.cursor);
|
|
293
|
+
this.cursor += safe.length;
|
|
294
|
+
this.historyIndex = -1;
|
|
295
|
+
this.menuIndex = 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
backspace() {
|
|
299
|
+
if (this.cursor === 0) return;
|
|
300
|
+
this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
|
|
301
|
+
this.cursor -= 1;
|
|
302
|
+
this.historyIndex = -1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
deleteForward() {
|
|
306
|
+
if (this.cursor >= this.buffer.length) return;
|
|
307
|
+
this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
|
|
308
|
+
this.historyIndex = -1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
up() {
|
|
312
|
+
if (this.menu.length > 0) {
|
|
313
|
+
this.menuIndex = (this.menuIndex - 1 + this.menu.length) % this.menu.length;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (this.history.length === 0) return;
|
|
317
|
+
if (this.historyIndex === -1) {
|
|
318
|
+
this.draft = this.buffer;
|
|
319
|
+
this.historyIndex = this.history.length - 1;
|
|
320
|
+
} else if (this.historyIndex > 0) {
|
|
321
|
+
this.historyIndex -= 1;
|
|
322
|
+
}
|
|
323
|
+
this.buffer = this.history[this.historyIndex];
|
|
324
|
+
this.cursor = this.buffer.length;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
down() {
|
|
328
|
+
if (this.menu.length > 0) {
|
|
329
|
+
this.menuIndex = (this.menuIndex + 1) % this.menu.length;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (this.historyIndex === -1) return;
|
|
333
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
334
|
+
this.historyIndex += 1;
|
|
335
|
+
this.buffer = this.history[this.historyIndex];
|
|
336
|
+
} else {
|
|
337
|
+
this.historyIndex = -1;
|
|
338
|
+
this.buffer = this.draft;
|
|
339
|
+
}
|
|
340
|
+
this.cursor = this.buffer.length;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
complete() {
|
|
344
|
+
const item = this.menu[this.menuIndex];
|
|
345
|
+
if (item === undefined) return;
|
|
346
|
+
this.buffer = "/" + item.name;
|
|
347
|
+
this.cursor = this.buffer.length;
|
|
348
|
+
this.menu = [];
|
|
349
|
+
this.menuIndex = 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
submit() {
|
|
353
|
+
const item = this.menu[this.menuIndex];
|
|
354
|
+
if (item !== undefined && this.buffer.trim() !== "/" + item.name) {
|
|
355
|
+
this.buffer = "/" + item.name;
|
|
356
|
+
this.cursor = this.buffer.length;
|
|
357
|
+
}
|
|
358
|
+
const line = this.buffer;
|
|
359
|
+
if (line.trim() !== "" && this.history[this.history.length - 1] !== line) {
|
|
360
|
+
this.history.push(line);
|
|
361
|
+
if (this.history.length > 200) this.history.shift();
|
|
362
|
+
}
|
|
363
|
+
this.settle(line);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
settle(value, echo = true) {
|
|
367
|
+
if (this.resolve === null) return;
|
|
368
|
+
const resolve = this.resolve;
|
|
369
|
+
this.resolve = null;
|
|
370
|
+
this.active = false;
|
|
371
|
+
this.selectMode = false;
|
|
372
|
+
this.selectOptions = [];
|
|
373
|
+
this.hint = "";
|
|
374
|
+
this.clearRender();
|
|
375
|
+
this.menu = [];
|
|
376
|
+
this.output.write("\x1b[?25h");
|
|
377
|
+
if (echo) this.output.write("\n");
|
|
378
|
+
resolve(value);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---- rendering ----------------------------------------------------------
|
|
382
|
+
computeMenu() {
|
|
383
|
+
if (!this.menuEnabled) return [];
|
|
384
|
+
if (!this.buffer.startsWith("/")) return [];
|
|
385
|
+
if (/\s/.test(this.buffer)) return [];
|
|
386
|
+
const query = this.buffer.slice(1).toLowerCase();
|
|
387
|
+
const items = this.commands.filter((command) => command.name.toLowerCase().startsWith(query));
|
|
388
|
+
return items.slice(0, 8);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
clearRender() {
|
|
392
|
+
if (this.renderedRows > 0) {
|
|
393
|
+
this.output.write(`\x1b[${this.renderedRows}B`);
|
|
394
|
+
this.output.write("\r\x1b[0J");
|
|
395
|
+
this.output.write(`\x1b[${this.renderedRows}A`);
|
|
396
|
+
}
|
|
397
|
+
this.output.write("\r\x1b[2K");
|
|
398
|
+
this.renderedRows = 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
render() {
|
|
402
|
+
if (!this.active) return;
|
|
403
|
+
if (this.selectMode) {
|
|
404
|
+
this.renderSelect();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
this.clearRender();
|
|
408
|
+
const columns = Math.max(40, this.columns() || 80);
|
|
409
|
+
const prompt = this.prompt;
|
|
410
|
+
const promptWidth = visibleWidth(prompt);
|
|
411
|
+
const available = Math.max(8, columns - promptWidth - 1);
|
|
412
|
+
let offset = 0;
|
|
413
|
+
if (this.buffer.length > available) offset = Math.max(0, this.cursor - available + 1);
|
|
414
|
+
const visible = this.buffer.slice(offset, offset + available);
|
|
415
|
+
const column = promptWidth + (this.cursor - offset);
|
|
416
|
+
this.output.write(prompt + visible);
|
|
417
|
+
|
|
418
|
+
this.menu = this.computeMenu();
|
|
419
|
+
if (this.menuIndex >= this.menu.length) this.menuIndex = 0;
|
|
420
|
+
let rows = 0;
|
|
421
|
+
if (this.menu.length > 0) {
|
|
422
|
+
const nameWidth = Math.min(18, Math.max(...this.menu.map((item) => item.name.length)) + 1);
|
|
423
|
+
for (let index = 0; index < this.menu.length; index += 1) {
|
|
424
|
+
const item = this.menu[index];
|
|
425
|
+
const selected = index === this.menuIndex;
|
|
426
|
+
const marker = selected ? "❯" : " ";
|
|
427
|
+
const name = ("/" + clean(item.name)).padEnd(nameWidth + 1);
|
|
428
|
+
const description = clean(item.description ?? "").slice(0, Math.max(10, columns - nameWidth - 8));
|
|
429
|
+
const line = ` ${marker} ${selected ? `\x1b[36m${name}\x1b[39m` : name} ${description}`;
|
|
430
|
+
this.output.write("\n" + line);
|
|
431
|
+
}
|
|
432
|
+
rows = this.menu.length;
|
|
433
|
+
}
|
|
434
|
+
if (this.hint !== "") {
|
|
435
|
+
this.output.write(`\n \x1b[2m${clean(this.hint)}\x1b[22m`);
|
|
436
|
+
rows += 1;
|
|
437
|
+
}
|
|
438
|
+
if (rows > 0) this.output.write(`\x1b[${rows}A`);
|
|
439
|
+
this.renderedRows = rows;
|
|
440
|
+
this.output.write(`\x1b[${column + 1}G`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
renderSelect() {
|
|
444
|
+
this.clearRender();
|
|
445
|
+
const columns = Math.max(40, this.columns() || 80);
|
|
446
|
+
this.output.write(this.prompt);
|
|
447
|
+
const options = this.selectOptions;
|
|
448
|
+
const rows = options.length;
|
|
449
|
+
for (let index = 0; index < rows; index += 1) {
|
|
450
|
+
const option = options[index];
|
|
451
|
+
const selected = index === this.selectIndex;
|
|
452
|
+
const marker = selected ? "❯" : " ";
|
|
453
|
+
const label = clean(option.label ?? option.value ?? "");
|
|
454
|
+
const description = option.description ? ` \x1b[2m${clean(option.description)}\x1b[22m` : "";
|
|
455
|
+
const line = ` ${marker} ${selected ? `\x1b[36m${label}\x1b[39m` : label}${description}`;
|
|
456
|
+
this.output.write("\n" + line.slice(0, columns + 16));
|
|
457
|
+
}
|
|
458
|
+
if (rows > 0) this.output.write(`\x1b[${rows}A`);
|
|
459
|
+
this.renderedRows = rows;
|
|
460
|
+
this.output.write(`\x1b[${visibleWidth(this.prompt) + 1}G`);
|
|
461
|
+
}
|
|
462
|
+
}
|
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
|
|
|
@@ -511,9 +547,9 @@ Interactive commands:
|
|
|
511
547
|
/new start a fresh session
|
|
512
548
|
/sessions list recent sessions in this directory
|
|
513
549
|
/resume <id> switch to another session
|
|
514
|
-
/model [id]
|
|
515
|
-
/reasoning [off|high|max]
|
|
516
|
-
/permissions [p]
|
|
550
|
+
/model [id] pick a model, or pass an id directly
|
|
551
|
+
/reasoning [off|high|max] pick a reasoning effort
|
|
552
|
+
/permissions [p] pick a permission mode (native: /permission; Shift+Tab cycles)
|
|
517
553
|
/skills list the skills discovered in this workspace
|
|
518
554
|
/jobs list this session's background jobs
|
|
519
555
|
/todos show the current todo list
|
|
@@ -528,6 +564,12 @@ Interactive commands:
|
|
|
528
564
|
Harness commands (shared with the Web and Desktop apps):
|
|
529
565
|
/compact, /goal, /plan, and any command a plugin registers.
|
|
530
566
|
Type /help inside a session to see the live list.
|
|
567
|
+
|
|
568
|
+
Keyboard:
|
|
569
|
+
Shift+Tab cycle the permission mode (workspace-write, read-only, …)
|
|
570
|
+
Ctrl+C interrupt a running turn; twice exits
|
|
571
|
+
↑/↓ history (or move in the / command menu)
|
|
572
|
+
Tab complete the highlighted / command
|
|
531
573
|
`);
|
|
532
574
|
}
|
|
533
575
|
|
|
@@ -601,6 +643,7 @@ async function run(ctx, options, exit) {
|
|
|
601
643
|
const permissionPresets = ctx.get("permissionPresets");
|
|
602
644
|
const attachments = ctx.get("attachments");
|
|
603
645
|
const tokenMeter = ctx.get("tokenMeter");
|
|
646
|
+
const llm = ctx.get("llm");
|
|
604
647
|
|
|
605
648
|
if (options.list) {
|
|
606
649
|
printSessionList(listSessions());
|
|
@@ -616,6 +659,7 @@ async function run(ctx, options, exit) {
|
|
|
616
659
|
const autoYes = options.yes || process.env.DSH_CLI_YES === "1";
|
|
617
660
|
|
|
618
661
|
let handle = null;
|
|
662
|
+
let activeAgent = null;
|
|
619
663
|
let busy = false;
|
|
620
664
|
let quitting = false;
|
|
621
665
|
let cancelSent = false;
|
|
@@ -631,17 +675,32 @@ async function run(ctx, options, exit) {
|
|
|
631
675
|
const write = (text) => process.stdout.write(text);
|
|
632
676
|
|
|
633
677
|
// ---- line input ---------------------------------------------------------
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
const
|
|
637
|
-
|
|
678
|
+
// TTY: a raw-mode line editor with a live slash-command menu (Claude style).
|
|
679
|
+
// Non-TTY: readline over the piped stream. One-shot: no input owner at all.
|
|
680
|
+
const useEditor = interactive && process.stdin.isTTY === true;
|
|
681
|
+
const editor = useEditor
|
|
682
|
+
? new LineEditor({
|
|
638
683
|
input: process.stdin,
|
|
639
684
|
output: process.stdout,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
685
|
+
columns: () => process.stdout.columns || 80,
|
|
686
|
+
onInterrupt: () => handleInterrupt(),
|
|
687
|
+
onShiftTab: () => cyclePermission(),
|
|
688
|
+
onEnd: () => {
|
|
689
|
+
stdinClosed = true;
|
|
690
|
+
if (ready && !busy) shutdown(0);
|
|
691
|
+
},
|
|
643
692
|
})
|
|
644
693
|
: null;
|
|
694
|
+
const rlInterface =
|
|
695
|
+
interactive && !useEditor
|
|
696
|
+
? createInterface({
|
|
697
|
+
input: process.stdin,
|
|
698
|
+
output: process.stdout,
|
|
699
|
+
terminal: false,
|
|
700
|
+
historySize: 200,
|
|
701
|
+
removeHistoryDuplicates: true,
|
|
702
|
+
})
|
|
703
|
+
: null;
|
|
645
704
|
// Hold the input until the first agent is ready: buffered piped lines must
|
|
646
705
|
// not be consumed (and dropped) while the profile is still starting.
|
|
647
706
|
if (rlInterface) {
|
|
@@ -650,8 +709,14 @@ async function run(ctx, options, exit) {
|
|
|
650
709
|
paused = true;
|
|
651
710
|
} catch {}
|
|
652
711
|
}
|
|
712
|
+
editor?.start();
|
|
713
|
+
editor?.setActive(false);
|
|
653
714
|
|
|
654
715
|
function setPaused(value) {
|
|
716
|
+
if (editor) {
|
|
717
|
+
editor.setActive(!value);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
655
720
|
if (!rlInterface || paused === value) return;
|
|
656
721
|
paused = value;
|
|
657
722
|
try {
|
|
@@ -672,12 +737,16 @@ async function run(ctx, options, exit) {
|
|
|
672
737
|
|
|
673
738
|
/**
|
|
674
739
|
* 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.
|
|
740
|
+
* stdin closes. On a TTY the editor renders a live command menu.
|
|
677
741
|
* @param {string} prompt
|
|
742
|
+
* @param {boolean} [menu]
|
|
678
743
|
* @returns {Promise<string | null>}
|
|
679
744
|
*/
|
|
680
|
-
function ask(prompt) {
|
|
745
|
+
function ask(prompt, menu = false) {
|
|
746
|
+
if (editor) {
|
|
747
|
+
setPaused(false);
|
|
748
|
+
return editor.ask(prompt, menu);
|
|
749
|
+
}
|
|
681
750
|
return new Promise((resolve) => {
|
|
682
751
|
if (!rlInterface) {
|
|
683
752
|
resolve(null);
|
|
@@ -711,6 +780,7 @@ async function run(ctx, options, exit) {
|
|
|
711
780
|
|
|
712
781
|
function closeInput() {
|
|
713
782
|
try {
|
|
783
|
+
editor?.stop();
|
|
714
784
|
rlInterface?.close();
|
|
715
785
|
} catch {}
|
|
716
786
|
}
|
|
@@ -888,6 +958,7 @@ async function run(ctx, options, exit) {
|
|
|
888
958
|
setup,
|
|
889
959
|
});
|
|
890
960
|
handle = created;
|
|
961
|
+
activeAgent = created.agent;
|
|
891
962
|
wire(created.agent);
|
|
892
963
|
await created.agent.whenIdle();
|
|
893
964
|
return created.agent;
|
|
@@ -935,6 +1006,73 @@ async function run(ctx, options, exit) {
|
|
|
935
1006
|
return `${current.provider}/${current.model}${current.reasoningEffort ? ` · ${current.reasoningEffort}` : ""}`;
|
|
936
1007
|
};
|
|
937
1008
|
|
|
1009
|
+
// ---- permission state, pickers, status bar ------------------------------
|
|
1010
|
+
function currentPermission() {
|
|
1011
|
+
try {
|
|
1012
|
+
if (permissionPresets !== undefined && activeAgent !== null) return permissionPresets.current(activeAgent.session);
|
|
1013
|
+
} catch {}
|
|
1014
|
+
return "workspace-write";
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Shift+Tab: cycle the sandbox/approval preset and return a hint label. */
|
|
1018
|
+
function cyclePermission() {
|
|
1019
|
+
try {
|
|
1020
|
+
if (permissionPresets === undefined || activeAgent === null) return "";
|
|
1021
|
+
const options = permissionPresets.catalog().options.map((option) => option.value);
|
|
1022
|
+
if (options.length === 0) return "";
|
|
1023
|
+
const current = currentPermission();
|
|
1024
|
+
const next = options[(Math.max(0, options.indexOf(current)) + 1) % options.length];
|
|
1025
|
+
permissionPresets.set(activeAgent.session, next);
|
|
1026
|
+
return `⏵⏵ ${next} (shift+tab to cycle)`;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return "";
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/** Permanent one-line footer above the prompt. */
|
|
1033
|
+
function printStatusBar() {
|
|
1034
|
+
if (!tty || !interactive) return;
|
|
1035
|
+
out(paint(ansi.dim, ` ⏵⏵ ${sanitize(currentPermission())} · ${sanitize(modelDesc())} · shift+tab to cycle`) + "\n");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** One list picker shared by /model, /reasoning, /permissions, /resume. */
|
|
1039
|
+
async function selectOption(title, options, initial = 0) {
|
|
1040
|
+
if (options.length === 0) return null;
|
|
1041
|
+
if (editor) {
|
|
1042
|
+
printStatusBar();
|
|
1043
|
+
return editor.askSelect(paint(ansi.bold, ` ${title}`), options, initial);
|
|
1044
|
+
}
|
|
1045
|
+
out(`\n ${sanitize(title)}\n`);
|
|
1046
|
+
options.forEach((option, index) => {
|
|
1047
|
+
out(` ${index + 1}) ${sanitize(option.label)}${option.description ? paint(ansi.dim, ` — ${sanitize(option.description)}`) : ""}\n`);
|
|
1048
|
+
});
|
|
1049
|
+
const answer = await ask(paint(ansi.green, " choose: "));
|
|
1050
|
+
const index = Number.parseInt(answer ?? "", 10) - 1;
|
|
1051
|
+
return options[index]?.value ?? null;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function applyModelSelection(modelId) {
|
|
1055
|
+
const next = { ...selectionRef.current, model: modelId };
|
|
1056
|
+
try {
|
|
1057
|
+
await defaultModel.saveSelection(next);
|
|
1058
|
+
selectionRef.current = next;
|
|
1059
|
+
out(paint(ansi.green, ` ✓ model set: ${sanitize(next.provider)}/${sanitize(next.model)} (next message)\n`));
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function applyReasoningSelection(effort) {
|
|
1066
|
+
const next = { ...selectionRef.current, reasoningEffort: effort === "default" ? undefined : effort };
|
|
1067
|
+
try {
|
|
1068
|
+
await defaultModel.saveSelection(next);
|
|
1069
|
+
selectionRef.current = next;
|
|
1070
|
+
out(paint(ansi.green, ` ✓ reasoning set: ${sanitize(effort)} (next message)\n`));
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
938
1076
|
// ---- parity helpers -----------------------------------------------------
|
|
939
1077
|
const transcript = [];
|
|
940
1078
|
const pendingImages = [];
|
|
@@ -1150,7 +1288,9 @@ async function run(ctx, options, exit) {
|
|
|
1150
1288
|
}
|
|
1151
1289
|
|
|
1152
1290
|
while (!quitting) {
|
|
1153
|
-
|
|
1291
|
+
printStatusBar();
|
|
1292
|
+
if (editor) editor.setCommands(commandMenu(agent, commands));
|
|
1293
|
+
const line = await ask(paint(ansi.bold, "> "), true);
|
|
1154
1294
|
if (line === null) break;
|
|
1155
1295
|
const text = line.trim();
|
|
1156
1296
|
if (text === "") continue;
|
|
@@ -1181,13 +1321,27 @@ async function run(ctx, options, exit) {
|
|
|
1181
1321
|
printSessionList(listSessions(), write);
|
|
1182
1322
|
continue;
|
|
1183
1323
|
case "/resume": {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1324
|
+
let target = argument;
|
|
1325
|
+
if (target === "") {
|
|
1326
|
+
const sessions = listSessions();
|
|
1327
|
+
if (sessions.length === 0) {
|
|
1328
|
+
out(paint(ansi.yellow, " no sessions in this directory yet\n"));
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
target = await selectOption(
|
|
1332
|
+
"Resume session",
|
|
1333
|
+
sessions.slice(0, 10).map((session) => ({
|
|
1334
|
+
label: session.id,
|
|
1335
|
+
description: timeAgo(session.mtime) + (session.openTurn ? " · running" : ""),
|
|
1336
|
+
value: session.id,
|
|
1337
|
+
})),
|
|
1338
|
+
0
|
|
1339
|
+
);
|
|
1340
|
+
if (target === null) continue;
|
|
1187
1341
|
}
|
|
1188
1342
|
try {
|
|
1189
|
-
out(paint(ansi.dim, ` ↻ resuming ${
|
|
1190
|
-
agent = await startAgent(undefined,
|
|
1343
|
+
out(paint(ansi.dim, ` ↻ resuming ${sanitize(target)}\n`));
|
|
1344
|
+
agent = await startAgent(undefined, target);
|
|
1191
1345
|
} catch (error) {
|
|
1192
1346
|
out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
|
|
1193
1347
|
}
|
|
@@ -1211,33 +1365,45 @@ async function run(ctx, options, exit) {
|
|
|
1211
1365
|
continue;
|
|
1212
1366
|
}
|
|
1213
1367
|
case "/model": {
|
|
1214
|
-
if (argument
|
|
1215
|
-
|
|
1368
|
+
if (argument !== "") {
|
|
1369
|
+
await applyModelSelection(argument);
|
|
1216
1370
|
continue;
|
|
1217
1371
|
}
|
|
1218
|
-
|
|
1372
|
+
let models = [];
|
|
1219
1373
|
try {
|
|
1220
|
-
await
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
out(paint(ansi.
|
|
1374
|
+
models = llm === undefined ? [] : await llm.listModels(selectionRef.current.provider);
|
|
1375
|
+
} catch {}
|
|
1376
|
+
if (models.length === 0) {
|
|
1377
|
+
out(paint(ansi.dim, ` model: ${sanitize(modelDesc())}`) + "\n");
|
|
1378
|
+
out(paint(ansi.dim, " no model list is discoverable here — use /model <id>\n"));
|
|
1379
|
+
continue;
|
|
1225
1380
|
}
|
|
1381
|
+
const currentIndex = Math.max(0, models.findIndex((model) => model.id === selectionRef.current.model));
|
|
1382
|
+
const chosen = await selectOption(
|
|
1383
|
+
"Select model",
|
|
1384
|
+
models.map((model) => ({ label: model.name || model.id, description: model.id, value: model.id })),
|
|
1385
|
+
currentIndex
|
|
1386
|
+
);
|
|
1387
|
+
if (chosen !== null) await applyModelSelection(chosen);
|
|
1226
1388
|
continue;
|
|
1227
1389
|
}
|
|
1228
1390
|
case "/reasoning": {
|
|
1229
|
-
if (argument
|
|
1230
|
-
|
|
1391
|
+
if (argument !== "") {
|
|
1392
|
+
await applyReasoningSelection(argument);
|
|
1231
1393
|
continue;
|
|
1232
1394
|
}
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1395
|
+
const efforts = ["off", "high", "max", "default"];
|
|
1396
|
+
const current = selectionRef.current.reasoningEffort ?? "default";
|
|
1397
|
+
const chosen = await selectOption(
|
|
1398
|
+
"Select reasoning effort",
|
|
1399
|
+
efforts.map((effort) => ({
|
|
1400
|
+
label: effort,
|
|
1401
|
+
description: effort === "default" ? "provider default" : `effort: ${effort}`,
|
|
1402
|
+
value: effort,
|
|
1403
|
+
})),
|
|
1404
|
+
Math.max(0, efforts.indexOf(current))
|
|
1405
|
+
);
|
|
1406
|
+
if (chosen !== null) await applyReasoningSelection(chosen);
|
|
1241
1407
|
continue;
|
|
1242
1408
|
}
|
|
1243
1409
|
case "/thinking": {
|
|
@@ -1319,19 +1485,25 @@ async function run(ctx, options, exit) {
|
|
|
1319
1485
|
continue;
|
|
1320
1486
|
}
|
|
1321
1487
|
try {
|
|
1322
|
-
|
|
1323
|
-
if (argument === "") {
|
|
1324
|
-
out(paint(ansi.bold, "Permission presets:") + "\n");
|
|
1325
|
-
for (const option of catalog.options) {
|
|
1326
|
-
out(` ${paint(ansi.cyan, option.value.padEnd(18))} ${option.name}${option.description ? paint(ansi.dim, ` — ${option.description}`) : ""}\n`);
|
|
1327
|
-
}
|
|
1328
|
-
out(paint(ansi.dim, ` default: ${catalog.defaultPreset}`) + "\n");
|
|
1329
|
-
} else {
|
|
1488
|
+
if (argument !== "") {
|
|
1330
1489
|
permissionPresets.set(agent.session, argument);
|
|
1331
|
-
out(paint(ansi.green, ` ✓
|
|
1490
|
+
out(paint(ansi.green, ` ✓ permission mode: ${sanitize(argument)}\n`));
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const catalog = permissionPresets.catalog();
|
|
1494
|
+
const options = catalog.options.map((option) => ({
|
|
1495
|
+
label: option.value === catalog.defaultPreset ? `${option.value} (default)` : option.value,
|
|
1496
|
+
description: option.description || option.name,
|
|
1497
|
+
value: option.value,
|
|
1498
|
+
}));
|
|
1499
|
+
const initial = Math.max(0, options.findIndex((option) => option.value === currentPermission()));
|
|
1500
|
+
const chosen = await selectOption("Permission mode", options, initial);
|
|
1501
|
+
if (chosen !== null) {
|
|
1502
|
+
permissionPresets.set(agent.session, chosen);
|
|
1503
|
+
out(paint(ansi.green, ` ✓ permission mode: ${sanitize(chosen)}\n`));
|
|
1332
1504
|
}
|
|
1333
1505
|
} catch (error) {
|
|
1334
|
-
out(paint(ansi.red, ` ✘ ${describeError(error)}\n`));
|
|
1506
|
+
out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
|
|
1335
1507
|
}
|
|
1336
1508
|
continue;
|
|
1337
1509
|
}
|