deepseek-cli-mode 1.2.0 → 1.4.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0 — reasoning effort levels in the model picker
4
+
5
+ - `/model` now asks for the reasoning effort after you pick a model, listing the adapter's real levels
6
+ (`low`, `medium`, `high`, `max`) plus `default`, with the current value preselected.
7
+ - `/reasoning` lists the same adapter-provided levels (falling back to `low`/`medium`/`high`/`max` when the
8
+ adapter exposes no metadata).
9
+ - The footer and banner show the active model **and** effort, e.g. `deepseek-flash · high`.
10
+ - Direct arguments still work: `/reasoning low`, `/model deepseek-v4-pro`.
11
+
12
+ ## 1.3.0 — pickers, Shift+Tab permission cycling, permanent mode footer
13
+
14
+ - `/model`, `/reasoning`, `/permissions`, and `/resume` now open an interactive picker (`↑`/`↓`, `Enter`,
15
+ `Ctrl+C` to cancel) instead of requiring an argument. `/model` lists the models discovered from the provider.
16
+ - **Shift+Tab** cycles the permission mode (read-only → workspace-write → danger-full-access) without losing the
17
+ typed line, with a transient hint.
18
+ - A permanent dim footer above the prompt shows the active permission mode, model, and the Shift+Tab hint:
19
+ `⏵⏵ workspace-write · deepseek-official/deepseek-chat · shift+tab to cycle`.
20
+ - Direct arguments still work (`/model <id>`, `/reasoning high`, `/permissions read-only`, `/resume <id>`).
21
+ - Security: picker labels/descriptions and the footer pass through the sanitizer; permission changes still require a
22
+ user keystroke (the model cannot trigger them).
23
+
3
24
  ## 1.2.0 — live slash-command menu
4
25
 
5
26
  - Typing `/` at the prompt now opens a live command menu (like Claude Code): matching commands and descriptions,
package/README.md CHANGED
@@ -144,6 +144,14 @@ Typing `/` opens a live command menu, like Claude Code:
144
144
 
145
145
  `↑`/`↓` moves the selection, `Tab` completes it, `Enter` runs it. The menu filters as you type.
146
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
+
147
155
  | Command | What it does |
148
156
  |---|---|
149
157
  | `/help` | command list, including the live Harness command registry |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-cli-mode",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Terminal (CLI) mode for DeepSeek Harness — an interactive, Claude Code-style coding agent for your shell.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -28,12 +28,13 @@ export class LineEditor {
28
28
  * @param {() => void} [options.onInterrupt] called for Ctrl+C while no question is pending
29
29
  * @param {() => void} [options.onEnd] called when stdin ends
30
30
  */
31
- constructor({ input, output, columns, onInterrupt, onEnd }) {
31
+ constructor({ input, output, columns, onInterrupt, onEnd, onShiftTab }) {
32
32
  this.input = input;
33
33
  this.output = output;
34
34
  this.columns = columns ?? (() => this.output.columns || 80);
35
35
  this.onInterrupt = onInterrupt ?? (() => {});
36
36
  this.onEnd = onEnd ?? (() => {});
37
+ this.onShiftTab = onShiftTab ?? (() => undefined);
37
38
  this.commands = [];
38
39
  this.buffer = "";
39
40
  this.cursor = 0;
@@ -50,6 +51,10 @@ export class LineEditor {
50
51
  this.paste = false;
51
52
  this.escape = null;
52
53
  this.started = false;
54
+ this.selectMode = false;
55
+ this.selectOptions = [];
56
+ this.selectIndex = 0;
57
+ this.hint = "";
53
58
  this._onData = (chunk) => this.handleData(chunk);
54
59
  this._onEnd = () => {
55
60
  this.onEnd();
@@ -113,10 +118,60 @@ export class LineEditor {
113
118
  });
114
119
  }
115
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
+
116
160
  // ---- input --------------------------------------------------------------
117
161
  handleData(chunk) {
118
162
  for (const char of String(chunk)) {
119
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
+ }
120
175
  this.escape += char;
121
176
  if (/[A-Za-z~]$/.test(char)) {
122
177
  const sequence = this.escape;
@@ -139,11 +194,17 @@ export class LineEditor {
139
194
  }
140
195
 
141
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
+ }
142
202
  if (this.paste) {
143
203
  if (char === "\x1b") this.escape = "\x1b";
144
204
  else this.insert(char === "\n" ? " " : char);
145
205
  return;
146
206
  }
207
+ this.hint = "";
147
208
  switch (char) {
148
209
  case "\r":
149
210
  case "\n":
@@ -183,6 +244,20 @@ export class LineEditor {
183
244
  return;
184
245
  }
185
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
+ }
186
261
  switch (sequence) {
187
262
  case "\x1b[A":
188
263
  this.up();
@@ -293,8 +368,12 @@ export class LineEditor {
293
368
  const resolve = this.resolve;
294
369
  this.resolve = null;
295
370
  this.active = false;
371
+ this.selectMode = false;
372
+ this.selectOptions = [];
373
+ this.hint = "";
296
374
  this.clearRender();
297
375
  this.menu = [];
376
+ this.output.write("\x1b[?25h");
298
377
  if (echo) this.output.write("\n");
299
378
  resolve(value);
300
379
  }
@@ -321,6 +400,10 @@ export class LineEditor {
321
400
 
322
401
  render() {
323
402
  if (!this.active) return;
403
+ if (this.selectMode) {
404
+ this.renderSelect();
405
+ return;
406
+ }
324
407
  this.clearRender();
325
408
  const columns = Math.max(40, this.columns() || 80);
326
409
  const prompt = this.prompt;
@@ -334,10 +417,10 @@ export class LineEditor {
334
417
 
335
418
  this.menu = this.computeMenu();
336
419
  if (this.menuIndex >= this.menu.length) this.menuIndex = 0;
337
- const rows = this.menu.length;
338
- if (rows > 0) {
420
+ let rows = 0;
421
+ if (this.menu.length > 0) {
339
422
  const nameWidth = Math.min(18, Math.max(...this.menu.map((item) => item.name.length)) + 1);
340
- for (let index = 0; index < rows; index += 1) {
423
+ for (let index = 0; index < this.menu.length; index += 1) {
341
424
  const item = this.menu[index];
342
425
  const selected = index === this.menuIndex;
343
426
  const marker = selected ? "❯" : " ";
@@ -346,9 +429,34 @@ export class LineEditor {
346
429
  const line = ` ${marker} ${selected ? `\x1b[36m${name}\x1b[39m` : name} ${description}`;
347
430
  this.output.write("\n" + line);
348
431
  }
349
- this.output.write(`\x1b[${rows}A`);
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;
350
437
  }
438
+ if (rows > 0) this.output.write(`\x1b[${rows}A`);
351
439
  this.renderedRows = rows;
352
440
  this.output.write(`\x1b[${column + 1}G`);
353
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
+ }
354
462
  }
package/runner/runner.js CHANGED
@@ -547,9 +547,9 @@ Interactive commands:
547
547
  /new start a fresh session
548
548
  /sessions list recent sessions in this directory
549
549
  /resume <id> switch to another session
550
- /model [id] show or switch the model (e.g. /model deepseek-v4-pro)
551
- /reasoning [off|high|max] show or set the reasoning effort
552
- /permissions [p] show or switch the permission preset (native: /permission)
550
+ /model [id] pick a model and its reasoning effort (low/medium/high/max)
551
+ /reasoning [off|high|max] pick a reasoning effort
552
+ /permissions [p] pick a permission mode (native: /permission; Shift+Tab cycles)
553
553
  /skills list the skills discovered in this workspace
554
554
  /jobs list this session's background jobs
555
555
  /todos show the current todo list
@@ -564,6 +564,12 @@ Interactive commands:
564
564
  Harness commands (shared with the Web and Desktop apps):
565
565
  /compact, /goal, /plan, and any command a plugin registers.
566
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
567
573
  `);
568
574
  }
569
575
 
@@ -637,6 +643,7 @@ async function run(ctx, options, exit) {
637
643
  const permissionPresets = ctx.get("permissionPresets");
638
644
  const attachments = ctx.get("attachments");
639
645
  const tokenMeter = ctx.get("tokenMeter");
646
+ const llm = ctx.get("llm");
640
647
 
641
648
  if (options.list) {
642
649
  printSessionList(listSessions());
@@ -652,6 +659,7 @@ async function run(ctx, options, exit) {
652
659
  const autoYes = options.yes || process.env.DSH_CLI_YES === "1";
653
660
 
654
661
  let handle = null;
662
+ let activeAgent = null;
655
663
  let busy = false;
656
664
  let quitting = false;
657
665
  let cancelSent = false;
@@ -676,6 +684,7 @@ async function run(ctx, options, exit) {
676
684
  output: process.stdout,
677
685
  columns: () => process.stdout.columns || 80,
678
686
  onInterrupt: () => handleInterrupt(),
687
+ onShiftTab: () => cyclePermission(),
679
688
  onEnd: () => {
680
689
  stdinClosed = true;
681
690
  if (ready && !busy) shutdown(0);
@@ -949,6 +958,7 @@ async function run(ctx, options, exit) {
949
958
  setup,
950
959
  });
951
960
  handle = created;
961
+ activeAgent = created.agent;
952
962
  wire(created.agent);
953
963
  await created.agent.whenIdle();
954
964
  return created.agent;
@@ -996,6 +1006,100 @@ async function run(ctx, options, exit) {
996
1006
  return `${current.provider}/${current.model}${current.reasoningEffort ? ` · ${current.reasoningEffort}` : ""}`;
997
1007
  };
998
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
+ /** Adapter-owned reasoning levels for the selected model, when exposed. */
1055
+ async function reasoningInfo() {
1056
+ try {
1057
+ if (llm === undefined) return null;
1058
+ const resolved = await llm.resolveModel(selectionRef.current.provider, selectionRef.current.model);
1059
+ return resolved.reasoning ?? null;
1060
+ } catch {
1061
+ return null;
1062
+ }
1063
+ }
1064
+
1065
+ async function applyModelSelection(modelId, pickEffort = true) {
1066
+ const next = { ...selectionRef.current, model: modelId };
1067
+ try {
1068
+ await defaultModel.saveSelection(next);
1069
+ selectionRef.current = next;
1070
+ out(paint(ansi.green, ` ✓ model set: ${sanitize(next.provider)}/${sanitize(next.model)} (next message)\n`));
1071
+ } catch (error) {
1072
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1073
+ return;
1074
+ }
1075
+ if (!pickEffort) return;
1076
+ const info = await reasoningInfo();
1077
+ const effortOptions =
1078
+ info !== null && info.efforts.length > 0
1079
+ ? info.efforts.map((effort) => ({ label: effort.name, description: effort.id, value: effort.id }))
1080
+ : ["low", "medium", "high", "max"].map((effort) => ({ label: effort, description: `effort: ${effort}`, value: effort }));
1081
+ const options = [...effortOptions, { label: "default", description: "provider default", value: "default" }];
1082
+ const defaultEffort = info !== null && info.defaultEffort !== undefined ? info.defaultEffort : "default";
1083
+ // Offer the effort choice for the newly selected model, keeping the current
1084
+ // value when it is still valid.
1085
+ const currentEffort = selectionRef.current.reasoningEffort ?? "default";
1086
+ const preferred = options.some((option) => option.value === currentEffort) ? currentEffort : defaultEffort;
1087
+ const initial = Math.max(0, options.findIndex((option) => option.value === preferred));
1088
+ const chosen = await selectOption(`Reasoning effort for ${modelId}`, options, initial);
1089
+ if (chosen !== null) await applyReasoningSelection(chosen);
1090
+ }
1091
+
1092
+ async function applyReasoningSelection(effort) {
1093
+ const next = { ...selectionRef.current, reasoningEffort: effort === "default" ? undefined : effort };
1094
+ try {
1095
+ await defaultModel.saveSelection(next);
1096
+ selectionRef.current = next;
1097
+ out(paint(ansi.green, ` ✓ reasoning set: ${sanitize(effort)} (next message)\n`));
1098
+ } catch (error) {
1099
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1100
+ }
1101
+ }
1102
+
999
1103
  // ---- parity helpers -----------------------------------------------------
1000
1104
  const transcript = [];
1001
1105
  const pendingImages = [];
@@ -1211,6 +1315,7 @@ async function run(ctx, options, exit) {
1211
1315
  }
1212
1316
 
1213
1317
  while (!quitting) {
1318
+ printStatusBar();
1214
1319
  if (editor) editor.setCommands(commandMenu(agent, commands));
1215
1320
  const line = await ask(paint(ansi.bold, "> "), true);
1216
1321
  if (line === null) break;
@@ -1243,13 +1348,27 @@ async function run(ctx, options, exit) {
1243
1348
  printSessionList(listSessions(), write);
1244
1349
  continue;
1245
1350
  case "/resume": {
1246
- if (argument === "") {
1247
- out(paint(ansi.yellow, " usage: /resume <session-id> (see /sessions)\n"));
1248
- continue;
1351
+ let target = argument;
1352
+ if (target === "") {
1353
+ const sessions = listSessions();
1354
+ if (sessions.length === 0) {
1355
+ out(paint(ansi.yellow, " no sessions in this directory yet\n"));
1356
+ continue;
1357
+ }
1358
+ target = await selectOption(
1359
+ "Resume session",
1360
+ sessions.slice(0, 10).map((session) => ({
1361
+ label: session.id,
1362
+ description: timeAgo(session.mtime) + (session.openTurn ? " · running" : ""),
1363
+ value: session.id,
1364
+ })),
1365
+ 0
1366
+ );
1367
+ if (target === null) continue;
1249
1368
  }
1250
1369
  try {
1251
- out(paint(ansi.dim, ` ↻ resuming ${argument}\n`));
1252
- agent = await startAgent(undefined, argument);
1370
+ out(paint(ansi.dim, ` ↻ resuming ${sanitize(target)}\n`));
1371
+ agent = await startAgent(undefined, target);
1253
1372
  } catch (error) {
1254
1373
  out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1255
1374
  }
@@ -1273,33 +1392,50 @@ async function run(ctx, options, exit) {
1273
1392
  continue;
1274
1393
  }
1275
1394
  case "/model": {
1276
- if (argument === "") {
1277
- out(paint(ansi.dim, ` model: ${modelDesc()}\n`));
1395
+ if (argument !== "") {
1396
+ await applyModelSelection(argument);
1278
1397
  continue;
1279
1398
  }
1280
- const next = { ...selectionRef.current, model: argument };
1399
+ let models = [];
1281
1400
  try {
1282
- await defaultModel.saveSelection(next);
1283
- selectionRef.current = next;
1284
- out(paint(ansi.green, ` ✓ model set: ${next.provider}/${next.model} (next message)\n`));
1285
- } catch (error) {
1286
- out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1401
+ models = llm === undefined ? [] : await llm.listModels(selectionRef.current.provider);
1402
+ } catch {}
1403
+ if (models.length === 0) {
1404
+ out(paint(ansi.dim, ` model: ${sanitize(modelDesc())}`) + "\n");
1405
+ out(paint(ansi.dim, " no model list is discoverable here — use /model <id>\n"));
1406
+ continue;
1287
1407
  }
1408
+ const currentIndex = Math.max(0, models.findIndex((model) => model.id === selectionRef.current.model));
1409
+ const chosen = await selectOption(
1410
+ "Select model",
1411
+ models.map((model) => ({ label: model.name || model.id, description: model.id, value: model.id })),
1412
+ currentIndex
1413
+ );
1414
+ if (chosen !== null) await applyModelSelection(chosen);
1288
1415
  continue;
1289
1416
  }
1290
1417
  case "/reasoning": {
1291
- if (argument === "") {
1292
- out(paint(ansi.dim, ` reasoning: ${selectionRef.current.reasoningEffort ?? "default"}\n`));
1418
+ if (argument !== "") {
1419
+ await applyReasoningSelection(argument);
1293
1420
  continue;
1294
1421
  }
1295
- const next = { ...selectionRef.current, reasoningEffort: argument === "default" ? undefined : argument };
1296
- try {
1297
- await defaultModel.saveSelection(next);
1298
- selectionRef.current = next;
1299
- out(paint(ansi.green, ` ✓ reasoning set: ${argument} (next message)\n`));
1300
- } catch (error) {
1301
- out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1302
- }
1422
+ const info = await reasoningInfo();
1423
+ const efforts =
1424
+ info !== null && info.efforts.length > 0
1425
+ ? info.efforts.map((effort) => ({
1426
+ label: effort.name,
1427
+ description: effort.description ?? effort.id,
1428
+ value: effort.id,
1429
+ }))
1430
+ : ["low", "medium", "high", "max"].map((effort) => ({
1431
+ label: effort,
1432
+ description: `effort: ${effort}`,
1433
+ value: effort,
1434
+ }));
1435
+ efforts.push({ label: "default", description: "provider default", value: "default" });
1436
+ const current = selectionRef.current.reasoningEffort ?? "default";
1437
+ const chosen = await selectOption("Select reasoning effort", efforts, Math.max(0, efforts.findIndex((option) => option.value === current)));
1438
+ if (chosen !== null) await applyReasoningSelection(chosen);
1303
1439
  continue;
1304
1440
  }
1305
1441
  case "/thinking": {
@@ -1381,19 +1517,25 @@ async function run(ctx, options, exit) {
1381
1517
  continue;
1382
1518
  }
1383
1519
  try {
1384
- const catalog = permissionPresets.catalog();
1385
- if (argument === "") {
1386
- out(paint(ansi.bold, "Permission presets:") + "\n");
1387
- for (const option of catalog.options) {
1388
- out(` ${paint(ansi.cyan, option.value.padEnd(18))} ${option.name}${option.description ? paint(ansi.dim, ` — ${option.description}`) : ""}\n`);
1389
- }
1390
- out(paint(ansi.dim, ` default: ${catalog.defaultPreset}`) + "\n");
1391
- } else {
1520
+ if (argument !== "") {
1392
1521
  permissionPresets.set(agent.session, argument);
1393
- out(paint(ansi.green, ` ✓ permissions: ${argument} (applies to the next tool call)\n`));
1522
+ out(paint(ansi.green, ` ✓ permission mode: ${sanitize(argument)}\n`));
1523
+ continue;
1524
+ }
1525
+ const catalog = permissionPresets.catalog();
1526
+ const options = catalog.options.map((option) => ({
1527
+ label: option.value === catalog.defaultPreset ? `${option.value} (default)` : option.value,
1528
+ description: option.description || option.name,
1529
+ value: option.value,
1530
+ }));
1531
+ const initial = Math.max(0, options.findIndex((option) => option.value === currentPermission()));
1532
+ const chosen = await selectOption("Permission mode", options, initial);
1533
+ if (chosen !== null) {
1534
+ permissionPresets.set(agent.session, chosen);
1535
+ out(paint(ansi.green, ` ✓ permission mode: ${sanitize(chosen)}\n`));
1394
1536
  }
1395
1537
  } catch (error) {
1396
- out(paint(ansi.red, ` ✘ ${describeError(error)}\n`));
1538
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1397
1539
  }
1398
1540
  continue;
1399
1541
  }