deepseek-cli-mode 1.2.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 CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 1.2.0 — live slash-command menu
4
16
 
5
17
  - 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.3.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, 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)
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,73 @@ 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
+ 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
+
999
1076
  // ---- parity helpers -----------------------------------------------------
1000
1077
  const transcript = [];
1001
1078
  const pendingImages = [];
@@ -1211,6 +1288,7 @@ async function run(ctx, options, exit) {
1211
1288
  }
1212
1289
 
1213
1290
  while (!quitting) {
1291
+ printStatusBar();
1214
1292
  if (editor) editor.setCommands(commandMenu(agent, commands));
1215
1293
  const line = await ask(paint(ansi.bold, "> "), true);
1216
1294
  if (line === null) break;
@@ -1243,13 +1321,27 @@ async function run(ctx, options, exit) {
1243
1321
  printSessionList(listSessions(), write);
1244
1322
  continue;
1245
1323
  case "/resume": {
1246
- if (argument === "") {
1247
- out(paint(ansi.yellow, " usage: /resume <session-id> (see /sessions)\n"));
1248
- continue;
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;
1249
1341
  }
1250
1342
  try {
1251
- out(paint(ansi.dim, ` ↻ resuming ${argument}\n`));
1252
- agent = await startAgent(undefined, argument);
1343
+ out(paint(ansi.dim, ` ↻ resuming ${sanitize(target)}\n`));
1344
+ agent = await startAgent(undefined, target);
1253
1345
  } catch (error) {
1254
1346
  out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1255
1347
  }
@@ -1273,33 +1365,45 @@ async function run(ctx, options, exit) {
1273
1365
  continue;
1274
1366
  }
1275
1367
  case "/model": {
1276
- if (argument === "") {
1277
- out(paint(ansi.dim, ` model: ${modelDesc()}\n`));
1368
+ if (argument !== "") {
1369
+ await applyModelSelection(argument);
1278
1370
  continue;
1279
1371
  }
1280
- const next = { ...selectionRef.current, model: argument };
1372
+ let models = [];
1281
1373
  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`));
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;
1287
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);
1288
1388
  continue;
1289
1389
  }
1290
1390
  case "/reasoning": {
1291
- if (argument === "") {
1292
- out(paint(ansi.dim, ` reasoning: ${selectionRef.current.reasoningEffort ?? "default"}\n`));
1391
+ if (argument !== "") {
1392
+ await applyReasoningSelection(argument);
1293
1393
  continue;
1294
1394
  }
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
- }
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);
1303
1407
  continue;
1304
1408
  }
1305
1409
  case "/thinking": {
@@ -1381,19 +1485,25 @@ async function run(ctx, options, exit) {
1381
1485
  continue;
1382
1486
  }
1383
1487
  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 {
1488
+ if (argument !== "") {
1392
1489
  permissionPresets.set(agent.session, argument);
1393
- out(paint(ansi.green, ` ✓ permissions: ${argument} (applies to the next tool call)\n`));
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`));
1394
1504
  }
1395
1505
  } catch (error) {
1396
- out(paint(ansi.red, ` ✘ ${describeError(error)}\n`));
1506
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1397
1507
  }
1398
1508
  continue;
1399
1509
  }