@tiens.nguyen/gu-cli 1.0.877 → 1.0.879

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.
Files changed (2) hide show
  1. package/gu-repl.mjs +23 -8
  2. package/package.json +1 -1
package/gu-repl.mjs CHANGED
@@ -550,6 +550,7 @@ const orange = (s) => color256(208, s); // peak-token label (task #84)
550
550
  // A menu's ACTION rows (pick another model…, API keys…, add a provider…) in orange, apart from the
551
551
  // models above them (#248). \x1b[22m first: an unselected picker row is wrapped in dim, which would
552
552
  // turn the orange grey. Plain text when colour is off (NO_COLOR, TERM=dumb).
553
+ const ACTION_ROW_START = "\x1b[22m\x1b[38;5;208m";
553
554
  const actionRow = (s) => (dim("x") === "x" ? s : `\x1b[22m${orange(s)}`);
554
555
  // Compact token count: 950 → "950", 12300 → "12.3k", 1500000 → "1.5M" (task #83).
555
556
  const fmtTokens = (n) => {
@@ -568,6 +569,10 @@ const fmtTokens = (n) => {
568
569
  * naming it here would imply the picker is something you have to break out of.
569
570
  */
570
571
  const PICK_HINT = "↑/↓ then Enter · Esc to cancel";
572
+ // #249: a list whose last rows are orange actions says Esc UNDER them (pickFromList's footer);
573
+ // its header keeps only the arrows, so the instruction is not given twice.
574
+ const PICK_ARROWS = "↑/↓ then Enter";
575
+ const ESC_FOOTER = "Esc to cancel";
571
576
  const PICK_MANY_HINT = "↑/↓ · space toggles · ctrl+a all · Enter saves · Esc cancels";
572
577
  // How many EXTRA models one server offers alongside its default. Task #245 made it a rule, not a
573
578
  // readability cap: a server URL offers AT MOST 3 models, default included (MAX_MODELS_PER_SERVER
@@ -7157,8 +7162,8 @@ async function chooseModel() {
7157
7162
  // enough in name to have already been mixed up. The fix is to say which one THIS is — not to
7158
7163
  // print the other two here: the agent model and the embedder are independent settings with
7159
7164
  // their own commands, and listing them together implies a relationship that does not exist.
7160
- console.log(dim(` Choose the CODING model — the one that writes code (${PICK_HINT}):`));
7161
- const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
7165
+ console.log(dim(` Choose the CODING model — the one that writes code (${PICK_ARROWS}):`));
7166
+ const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0, { footer: ESC_FOOTER });
7162
7167
  if (idx < 0) {
7163
7168
  console.log(dim(" (cancelled)\n"));
7164
7169
  return;
@@ -8883,12 +8888,12 @@ async function chooseEmbedModel() {
8883
8888
  const ADD = labels.length;
8884
8889
  labels.push(actionRow("add a provider — OpenAI, Ollama…"));
8885
8890
  const head = ` Embedding model (now ${current} on ${host || currentUrl})`;
8886
- const hint = `(${PICK_HINT}):`;
8891
+ const hint = `(${PICK_ARROWS}):`;
8887
8892
  const cols = Math.max(20, process.stdout.columns || 80);
8888
8893
  console.log(dim(head.length + 2 + hint.length < cols ? `${head} ${hint}` : `${head}\n ${hint}`));
8889
8894
  // The cursor starts on the one in use: the set one, else the default.
8890
8895
  const inUse = rows.findIndex((r) => ec.sameEmbedModel(r.model, current));
8891
- const mi = await pickFromList(labels, Math.max(0, inUse));
8896
+ const mi = await pickFromList(labels, Math.max(0, inUse), { footer: ESC_FOOTER });
8892
8897
  if (mi < 0) {
8893
8898
  console.log(dim(" (cancelled)\n"));
8894
8899
  return;
@@ -9078,10 +9083,10 @@ async function chooseAgentModel() {
9078
9083
  labels.push(actionRow("add a provider — Claude API, OpenAI, Kimi, Ollama…"));
9079
9084
  // The hint drops to its own row when the header would wrap (80 columns over SSH).
9080
9085
  const head = ` Agent model — the one that TALKS (now ${current || "unset"}${curHost ? ` on ${curHost}` : ""})`;
9081
- const hint = `(${PICK_HINT}):`;
9086
+ const hint = `(${PICK_ARROWS}):`;
9082
9087
  const cols = Math.max(20, process.stdout.columns || 80);
9083
9088
  console.log(dim(head.length + 2 + hint.length < cols ? `${head} ${hint}` : `${head}\n ${hint}`));
9084
- const idx = await pickFromList(labels, Math.max(0, saved.findIndex((c) => c.current)));
9089
+ const idx = await pickFromList(labels, Math.max(0, saved.findIndex((c) => c.current)), { footer: ESC_FOOTER });
9085
9090
  if (idx < 0) {
9086
9091
  console.log(dim(" (cancelled)\n"));
9087
9092
  return;
@@ -11245,6 +11250,8 @@ let listChecked = new Set();
11245
11250
  // arrive after the choosing is over.
11246
11251
  let listMax = 0;
11247
11252
  let listNote = "";
11253
+ /** #249: a dim line under the rows (pickFromList's `footer`), drawn and erased with the block. */
11254
+ let listFooter = "";
11248
11255
  // When true, ctrl+d in a single-select picker finishes with a DELETE request for the
11249
11256
  // highlighted row instead of choosing it (task #222). Off unless a caller asks for it, so no
11250
11257
  // existing picker gains a destructive key by accident.
@@ -11274,7 +11281,7 @@ function drawList(redraw) {
11274
11281
  // provider's /v1/models was taller than the terminal: the screen scrolled, the highlighted row
11275
11282
  // went off the top, and the ESC[nA walk below then counted rows that had moved — "cannot scroll
11276
11283
  // up or down, stuck here". Two rows of margin keep the block clear of the line above it.
11277
- const maxBlock = Math.max(5, viewportHeight() - 2) - (listNote ? 1 : 0) - (listHeader ? listHeaderRows : 0);
11284
+ const maxBlock = Math.max(5, viewportHeight() - 2) - (listNote ? 1 : 0) - (listFooter ? 1 : 0) - (listHeader ? listHeaderRows : 0);
11278
11285
  const win = listWindow(listItems.length, listSel, listTop, maxBlock);
11279
11286
  listTop = win.top;
11280
11287
  const row = (i) => {
@@ -11284,7 +11291,11 @@ function drawList(redraw) {
11284
11291
  const box = listMulti ? (listChecked.has(i) ? "[x] " : "[ ] ") : "";
11285
11292
  const left = listDeadline && i === listTimeoutIndex ? " (auto in " + countdownLabel(listDeadline - Date.now()) + ")" : "";
11286
11293
  const body = box + listItems[i] + left;
11287
- return clipVisible(GUTTER + (i === listSel ? accent("▸ " + body) : dim(" " + body)), width - 1);
11294
+ // An orange action row (#249) gets an orange ▸ too — the cursor in the row's own colour.
11295
+ const cursor = i === listSel
11296
+ ? (String(listItems[i]).startsWith(ACTION_ROW_START) ? orange("▸ ") + body : accent("▸ " + body))
11297
+ : dim(" " + body);
11298
+ return clipVisible(GUTTER + cursor, width - 1);
11288
11299
  };
11289
11300
  const marker = (text) => clipVisible(GUTTER + dim(text), width - 1);
11290
11301
  const rows = [];
@@ -11302,6 +11313,8 @@ function drawList(redraw) {
11302
11313
  // The cap's message rides UNDER the list, in the same block, so it is erased and redrawn with
11303
11314
  // it — a note printed into the scrolling output would stay on screen after the list is gone.
11304
11315
  if (listNote) rows.push(clipVisible(GUTTER + yellow(listNote), width - 1));
11316
+ // Indented like the rows' text (past the "▸ " column), not like the ▸ itself.
11317
+ if (listFooter) rows.push(clipVisible(GUTTER + dim(" " + listFooter), width - 1));
11305
11318
  // On a redraw the caret sits one row BELOW the block (each draw ends with "\n").
11306
11319
  const up = redraw ? physicalRows(listRowsDrawn, width) : 0;
11307
11320
  // #321: what the screen held under the picker when it opened, and how far it reaches — so the
@@ -11346,6 +11359,7 @@ function finishList(index) {
11346
11359
  listChecked = new Set();
11347
11360
  listMax = 0;
11348
11361
  listNote = "";
11362
+ listFooter = "";
11349
11363
  const allowDelete = listAllowDelete;
11350
11364
  const deleting = listDeleteRequested;
11351
11365
  listAllowDelete = false;
@@ -11486,6 +11500,7 @@ function pickFromList(items, initialSel = 0, opts = {}) {
11486
11500
  return;
11487
11501
  }
11488
11502
  listMulti = false; // explicit: a cancelled multi-select must not leak its mode into this one
11503
+ listFooter = typeof opts.footer === "string" ? opts.footer : "";
11489
11504
  listAllowDelete = opts.allowDelete === true;
11490
11505
  listDeleteRequested = false;
11491
11506
  listKeys = opts.keys && typeof opts.keys === "object" ? opts.keys : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.877",
3
+ "version": "1.0.879",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",