@tiens.nguyen/gu-cli 1.0.795 → 1.0.797

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/gu-repl.mjs CHANGED
@@ -138,7 +138,8 @@ import { unapprovedProjectServers } from "./mcp-command.mjs";
138
138
  import { readHooks } from "./hooks-config.mjs";
139
139
  import { readSkillSource, sourceLabel, SKILL_SOURCE_KEY } from "./skill-source.mjs";
140
140
  import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
141
- import { thoughtPrintSeq } from "./terminal-layout.mjs";
141
+ import { questionEchoText, thoughtPrintSeq } from "./terminal-layout.mjs";
142
+ import { embedNotice, embedState } from "./embed-model-command.mjs";
142
143
  import {
143
144
  MODELFALLBACK_PREFIX, fallbackAfterTurn, fallbackHistoryLine, fallbackNotice, keyMissingNotice,
144
145
  parseModelFallback,
@@ -160,7 +161,7 @@ import {
160
161
  registeredRange,
161
162
  selectionKeyAction,
162
163
  } from "./terminal-copy.mjs";
163
- import { createHoverController, applyToggleShift } from "./terminal-hover.mjs";
164
+ import { createHoverController, applyToggleShift, rowsDiffer } from "./terminal-hover.mjs";
164
165
  // The required pair, imported rather than re-typed: the startup "no models" message names them,
165
166
  // and a second copy of these strings would drift from the check that enforces them.
166
167
  import { ACCEPTED_EMBED_MODELS, REQUIRED_AGENT_MODEL, REQUIRED_AGENT_PREFIX } from "./ollama-setup.mjs";
@@ -587,13 +588,15 @@ if (cliVerdict.kind === "run") {
587
588
  } else if (argv.includes("--help") || argv.includes("-h")) {
588
589
  console.log(
589
590
  "gu — terminal agent REPL for the current folder\n\n" +
590
- "Usage: gu [-v|--verbose] [--license <pro token>]\n" +
591
+ "Usage: gu [-v|--verbose] [--log] [--license <pro token>]\n" +
591
592
  " gu status # worker + a table of the running model servers\n" +
592
593
  " gu start|stop|restart|logs # manage the background worker\n" +
593
594
  " gu logs <port|pid> [-f] # tail a running model server's log\n" +
594
595
  " gu doctor|login|set # models / account (same as `gu-cli …`)\n" +
595
596
  " gu rag # is RAG working? tests the embedder end-to-end\n\n" +
596
597
  " -v, --verbose show RAG index-file activity in the bottom bar (each shard written)\n" +
598
+ " --log log this run in detail (~/.gonext/worker.log). Without it only starts,\n" +
599
+ " stops and errors are logged. Ends when this gu closes.\n" +
597
600
  " --license <token> pro license: a first install ASKS how this machine runs (hosted\n" +
598
601
  " client / MLX host / private). Without it, a first install is\n" +
599
602
  " private by default — the whole stack on this machine.\n" +
@@ -613,6 +616,17 @@ if (cliVerdict.kind === "run") {
613
616
  // could not deliver, because agent LOG events stay on the worker's console and never enter
614
617
  // the job stream. It now drives a real, streamable signal.
615
618
  const REPL_VERBOSE = argv.includes("-v") || argv.includes("--verbose");
619
+ // --log (task #352): THIS RUN logs in detail. By default the worker writes only starts, stops and
620
+ // errors; a run started with --log takes a lease (its pid, in ~/.gonext/log-detail.json) that the
621
+ // worker reads, and gives it back on exit. Per run by the owner's rule — nothing is saved, and a
622
+ // run that is killed leaves a lease for a dead process, which is no lease.
623
+ const REPL_LOG = argv.includes("--log");
624
+ if (REPL_LOG) {
625
+ const ll = await import("./log-level.mjs");
626
+ await ll.takeLease().catch(() => {});
627
+ process.on("exit", () => ll.releaseLeaseSync());
628
+ console.log(dim(" --log: detailed logging is on for this run (~/.gonext/worker.log); back to minimal when gu closes."));
629
+ }
616
630
  if (REPL_VERBOSE) {
617
631
  process.env.GONEXT_REPL_VERBOSE = "1";
618
632
  }
@@ -847,7 +861,7 @@ if (!workerKey || !apiBase) {
847
861
  console.error(red(`gu: setup failed: ${e instanceof Error ? e.message : e}`));
848
862
  if (e && e.code === "PAIRING_UNSUPPORTED") {
849
863
  console.error(
850
- "Configure manually: gu-cli set <workerKey> --api-base https://chat-api-v2.gomarsic.cc"
864
+ "Configure manually: gu-cli set <workerKey> --api-base https://chat-api-v2.getmarsic.cc"
851
865
  );
852
866
  }
853
867
  if (e && (e.code === "KEY_REJECTED" || e.code === "KEY_UNCHECKED")) {
@@ -862,7 +876,7 @@ if (!workerKey || !apiBase) {
862
876
  console.error(
863
877
  red("gu: no worker key / API base found.") +
864
878
  `\nRun ${accent("gu-cli login")} to sign in, or configure manually:` +
865
- "\n gu-cli set <workerKey> --api-base https://chat-api-v2.gomarsic.cc"
879
+ "\n gu-cli set <workerKey> --api-base https://chat-api-v2.getmarsic.cc"
866
880
  );
867
881
  process.exit(1);
868
882
  }
@@ -1633,11 +1647,14 @@ let agentMissing = false; // no agent model chosen → agent falls back to the c
1633
1647
  // question to. Seeded from the startup payload; re-checked before a question is refused.
1634
1648
  let noModelAtAll = false;
1635
1649
  let embedMissing = false; // no embed model chosen → RAG is off until /embed-model picks one
1650
+ // #347: WHY it is off — "unset" (none chosen) or "off" (one IS chosen, RAG is switched off) — and
1651
+ // the chosen model's name, so the row never says "no embed model" about a model that is set.
1652
+ let embedWhy = { state: "unset", model: "" };
1636
1653
  // One footer row; the two halves are independent so a model that IS set is never named.
1637
1654
  // ONE SHORT ROW EACH (#317): joined, they ran past 80 columns and were cut mid-word.
1638
1655
  const missingNoticeRows = () => [
1639
1656
  ...(agentMissing ? ["⚠ agent model not set — using the coding model · /agent-model"] : []),
1640
- ...(embedMissing ? ["⚠ no embed model — RAG is off · /embed-model"] : []),
1657
+ ...(embedMissing ? [embedNotice(embedWhy) || "⚠ no embed model — RAG is off · /embed-model"] : []),
1641
1658
  ];
1642
1659
 
1643
1660
  // ANOTHER MODEL STOOD IN (task #343). The agent reports it as a __MODELFALLBACK__:: step; it is
@@ -1662,6 +1679,30 @@ const fallbackNoticeRows = () => {
1662
1679
  ];
1663
1680
  };
1664
1681
 
1682
+ /**
1683
+ * Set the embed warning from what the settings SAY (task #347) — the one place it is decided
1684
+ * after startup's first guess.
1685
+ *
1686
+ * `repair`: an embedder is chosen but RAG is off. On a Local-mode machine the only way to choose
1687
+ * one is /embed-model or setup, and until this fix /embed-model never switched RAG on — so that
1688
+ * state is this bug's leftover, and it is finished here instead of asking the person to choose
1689
+ * again. In Cloud mode the web Settings has its own RAG switch; "off" may be a decision, so it is
1690
+ * reported, not changed.
1691
+ */
1692
+ async function syncEmbedWarning({ repair = false } = {}) {
1693
+ const settings = await fetchWorkerSettings();
1694
+ if (!settings) return; // offline: keep what the bar already says
1695
+ let st = embedState(settings);
1696
+ if (st.state === "off" && repair && isClientExpert()) {
1697
+ const md = await import("./model-doctor.mjs");
1698
+ const saved = await md.saveWorkerSettings(apiBase, workerKey, { ragEnabled: true }).catch(() => false);
1699
+ if (saved) st = { ...st, state: "on" };
1700
+ }
1701
+ embedWhy = st;
1702
+ embedMissing = st.state !== "on";
1703
+ if (barPinned()) paintBar();
1704
+ }
1705
+
1665
1706
  // #307: the calm note after a quick change of mind — dim, not the ⚠ orange of a warning, and in the
1666
1707
  // bar rather than the transcript (which the whole point is to leave untouched).
1667
1708
  let stopNotice = "";
@@ -2418,10 +2459,10 @@ async function runUpdate() {
2418
2459
  if (cli) rows.push({ id: "cli", label: `Update gu to ${cli}`, note: "takes effect after you restart gu" });
2419
2460
  if (!rows.length) return;
2420
2461
  console.log("");
2421
- console.log(dim(` Update (${PICK_HINT}):`));
2462
+ console.log(dim(` Update (${PICK_HINT}):`));
2422
2463
  const idx = await pickFromList(rows.map((r) => r.label + dim(` · ${r.note}`)), 0);
2423
2464
  if (idx < 0) {
2424
- console.log(dim(" (cancelled)\n"));
2465
+ console.log(dim(" (cancelled)\n"));
2425
2466
  paintBar();
2426
2467
  return;
2427
2468
  }
@@ -2448,7 +2489,7 @@ async function runUpdate() {
2448
2489
  r.stderr ? ` — ${String(r.stderr).trim().split("\n").slice(-1)[0]}` : ""}`);
2449
2490
  }
2450
2491
  });
2451
- console.log(accent(` ✓ API updated to ${api}\n`));
2492
+ console.log(accent(` ✓ API updated to ${api}\n`));
2452
2493
  availableUpdates = { ...availableUpdates, api: "", any: Boolean(availableUpdates.cli) };
2453
2494
  } else {
2454
2495
  await withPendingStatus(`updating gu to ${cli}`, async () => {
@@ -2478,12 +2519,12 @@ async function runUpdate() {
2478
2519
  });
2479
2520
  // NOT "updated" — the files changed, THIS process did not. Claiming otherwise is the one
2480
2521
  // way this feature could lie to someone.
2481
- console.log(accent(` ✓ gu ${cli} installed`) +
2522
+ console.log(accent(` ✓ gu ${cli} installed`) +
2482
2523
  dim(` — ${updateRestartNote()}\n`));
2483
2524
  availableUpdates = { ...availableUpdates, cli: "", any: Boolean(availableUpdates.api) };
2484
2525
  }
2485
2526
  } catch (e) {
2486
- console.log(red(` update failed: ${e?.message ?? e}\n`));
2527
+ console.log(red(` update failed: ${e?.message ?? e}\n`));
2487
2528
  }
2488
2529
  paintBar();
2489
2530
  }
@@ -2577,12 +2618,18 @@ const tokenLineText = () => {
2577
2618
  );
2578
2619
  const total = dim(" · ") + dim("total ") + accent(`↓ ${fmtTokens(Math.max(globalOutputTokens, lastTurnTotalOutput))}`);
2579
2620
  // #323: what the last turn's prompt cost, LAST on the row after a "|" — "~" when the cached share
2580
- // was assumed; nothing when the model has no price (never a misleading "| $0"). It survives a
2621
+ // was assumed; "no price" when the model has none (never a misleading "| $0"). It survives a
2581
2622
  // narrow window: "total ↓" is dropped before it is (tokenRowWithPrice).
2582
2623
  return tokenRowWithPrice({
2583
2624
  withTotal: head + total,
2584
2625
  withoutTotal: head,
2585
- price: lastTurnCost?.usd != null ? dim(" | ") + accent(fmtCost(lastTurnCost)) : "",
2626
+ // #349: a model with NO price says so ("no price", dim) — the blank it used to leave read as
2627
+ // the cost feature being broken. Still never "$0": unpriced is not free.
2628
+ price: lastTurnCost?.usd != null
2629
+ ? dim(" | ") + accent(fmtCost(lastTurnCost))
2630
+ : lastTurnCost
2631
+ ? dim(" | no price")
2632
+ : "",
2586
2633
  room: (process.stdout.columns || 80) - 1 - visibleLen(GUTTER),
2587
2634
  });
2588
2635
  };
@@ -3348,6 +3395,7 @@ const hoverCtl = createHoverController({
3348
3395
  pinned: () => barPinned(),
3349
3396
  transcriptRows: () => transcriptRows(),
3350
3397
  blockAtRow: (row) => thoughtBlockAtRow(row),
3398
+ canToggle: (b) => blockCanToggle(b),
3351
3399
  paintRows: (b, on) => paintThoughtRows(b, on),
3352
3400
  replaceRange: (from, to, rows) => transcript.replaceRange(from, to, rows),
3353
3401
  repaintSeq: (b) => thoughtBlockRepaintSeq(b),
@@ -3646,9 +3694,20 @@ function copyTargetAtRow(row) {
3646
3694
  return { block, renderedLines: lines.slice(from, to + 1) };
3647
3695
  }
3648
3696
 
3697
+ /**
3698
+ * Would a click on this block change anything? (#350) — its collapsed and its expanded rows, laid
3699
+ * out for THIS window, are not the same. Asked by the hover (may it light up?), by the click and
3700
+ * by Ctrl+T, so the three can never disagree. See rowsDiffer in terminal-hover.mjs.
3701
+ */
3702
+ function blockCanToggle(block) {
3703
+ if (!block) return false;
3704
+ return rowsDiffer(paintThoughtRows(block, false, true), paintThoughtRows(block, false, false));
3705
+ }
3706
+
3649
3707
  function toggleThoughtAtRow(row) {
3650
3708
  const block = thoughtBlockAtRow(row);
3651
- return block ? toggleThoughtBlock(block) : false;
3709
+ // A block shown whole has no other form: it did not light up, and the click is not for it.
3710
+ return block && blockCanToggle(block) ? toggleThoughtBlock(block) : false;
3652
3711
  }
3653
3712
 
3654
3713
  /**
@@ -3718,8 +3777,12 @@ function toggleThoughtBlock(block) {
3718
3777
 
3719
3778
  /** Expand/collapse the most recently printed thought block. The keyboard path (Ctrl+T). */
3720
3779
  function toggleNewestThought() {
3721
- const block = thoughtBlocks[thoughtBlocks.length - 1];
3722
- if (!barPinned() || !block) return false;
3780
+ if (!barPinned()) return false;
3781
+ // The newest block that HAS another form (#350): Ctrl+T on a thought already shown whole did
3782
+ // nothing visible, while an expandable one sat just above it.
3783
+ let block = null;
3784
+ for (let i = thoughtBlocks.length - 1; i >= 0 && !block; i--) if (blockCanToggle(thoughtBlocks[i])) block = thoughtBlocks[i];
3785
+ if (!block) return false;
3723
3786
  return toggleThoughtBlock(block);
3724
3787
  }
3725
3788
 
@@ -4254,8 +4317,8 @@ if (process.stdin.isTTY) {
4254
4317
  // PROCESS (reported: "PromptCancelled: cancelled … Node.js v22"). Cancel is caught here; any
4255
4318
  // other failure is reported and the session goes on — a keypress must not kill the REPL.
4256
4319
  replaceCodingKey().catch((e) => {
4257
- if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
4258
- else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
4320
+ if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
4321
+ else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
4259
4322
  paintBar();
4260
4323
  });
4261
4324
  return;
@@ -4617,13 +4680,13 @@ const ask = async (q, { prefill = "", hint = [], atomic = false, noCancel = fals
4617
4680
  rl.line = String(prefill);
4618
4681
  rl.cursor = rl.line.length;
4619
4682
  }
4620
- // ELLIPSIS, so a truncation is VISIBLE. Silently cutting "…[https://ollama2.gomarsic.cc/v1]:"
4683
+ // ELLIPSIS, so a truncation is VISIBLE. Silently cutting "…[https://ollama2.getmarsic.cc/v1]:"
4621
4684
  // to "…[https:/" hides the default Enter would accept, which is worse than not showing it —
4622
4685
  // the reader cannot tell whether the value is short or the row lied. Callers should keep the
4623
4686
  // final line short and put the long part above; this is the guard, not the plan.
4624
4687
  // "… " with the space: the label normally ends in ": ", and a truncation that eats the
4625
4688
  // separator leaves the typed text jammed against the last surviving word
4626
- // ("agent + embed ser…ollama2.gomarsic.cc"). Two columns for the ellipsis and its space.
4689
+ // ("agent + embed ser…ollama2.getmarsic.cc"). Two columns for the ellipsis and its space.
4627
4690
  inlinePrompt =
4628
4691
  (visibleLen(label) > budget ? clipVisible(label, budget - 2) + "… " : label) || PROMPT;
4629
4692
  try {
@@ -5603,10 +5666,10 @@ async function runModelDoctor() {
5603
5666
  const dc = await import("./doctor-check.mjs");
5604
5667
  const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
5605
5668
  if (!settings) {
5606
- console.log(red(" couldn't reach the API to read your model settings.\n"));
5669
+ console.log(red(" couldn't reach the API to read your model settings.\n"));
5607
5670
  return;
5608
5671
  }
5609
- console.log(dim(" Asking each model to answer — the real request, not just a ping."));
5672
+ console.log(dim(" Asking each model to answer — the real request, not just a ping."));
5610
5673
  const roles = dc.rolesFromSettings(settings);
5611
5674
  const results = [];
5612
5675
  for (const r of roles) {
@@ -5649,7 +5712,7 @@ async function reconfigureOllamaServer() {
5649
5712
 
5650
5713
  const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
5651
5714
  if (!settings) {
5652
- console.log(red(" couldn't reach the API to read your current settings.\n"));
5715
+ console.log(red(" couldn't reach the API to read your current settings.\n"));
5653
5716
  return;
5654
5717
  }
5655
5718
 
@@ -5678,7 +5741,7 @@ async function reconfigureOllamaServer() {
5678
5741
  console.log(dim(` ${hint()}`));
5679
5742
  // askSecret: masked, kept out of history; Esc throws PromptCancelled → /model is cancelled
5680
5743
  // with nothing changed, which is what "skip all" means here.
5681
- const k = (await askSecret(` API key for ${host} (Enter for none): `)).trim();
5744
+ const k = (await askSecret(` API key for ${host} (Enter for none): `)).trim();
5682
5745
  typedKeys.set(host, k);
5683
5746
  console.log(GUTTER + dim(ui.keyConfirmation(host, k)));
5684
5747
  return k;
@@ -5694,7 +5757,7 @@ async function reconfigureOllamaServer() {
5694
5757
  // instruction, leaving the reader unsure whether their own address needs it. It does not
5695
5758
  // — normalizeOllamaUrl adds it — and displayUrl round-trips, so what is shown can be fed
5696
5759
  // straight back as the default.
5697
- console.log(dim(` ${label} — Enter keeps ${os.displayUrl(dflt)}`));
5760
+ console.log(dim(` ${label} — Enter keeps ${os.displayUrl(dflt)}`));
5698
5761
  console.log(dim(` ${hint()}`));
5699
5762
  // THE ROW MUST SAY WHICH SERVER IT IS ASKING FOR. It said a bare "server:" — chosen to
5700
5763
  // fit the frozen row, which traded away the one thing the label was for. Two questions
@@ -5704,13 +5767,13 @@ async function reconfigureOllamaServer() {
5704
5767
  const typed = (await ask(` ${short}: `)).trim();
5705
5768
  // "s" is the advertised skip (#301); the older words still work.
5706
5769
  if (ui.readStepAnswer(typed).kind === "skip") return null;
5707
- // EVERY READING OF A BARE HOSTNAME, in order. Typing "ollama2.gomarsic.cc" means
5770
+ // EVERY READING OF A BARE HOSTNAME, in order. Typing "ollama2.getmarsic.cc" means
5708
5771
  // https://…:443 behind a proxy, while "192.168.1.40" means http://…:11434 — one guess
5709
5772
  // is right for one of them and produces a timeout that looks like a dead server for the
5710
5773
  // other. See urlCandidates; the load decides, not the heuristic.
5711
5774
  const candidates = os.urlCandidates(typed || dflt);
5712
5775
  if (!candidates.length) {
5713
- console.log(dim(" that is not a server address — try 192.168.1.40 or a full URL"));
5776
+ console.log(dim(" that is not a server address — try 192.168.1.40 or a full URL"));
5714
5777
  continue;
5715
5778
  }
5716
5779
  let n = null;
@@ -5725,7 +5788,7 @@ async function reconfigureOllamaServer() {
5725
5788
  try {
5726
5789
  models = await load(cand, key);
5727
5790
  n = cand;
5728
- if (candidates.length > 1) console.log(dim(` using ${os.displayUrl(cand.url)}`));
5791
+ if (candidates.length > 1) console.log(dim(` using ${os.displayUrl(cand.url)}`));
5729
5792
  return;
5730
5793
  } catch (err) {
5731
5794
  lastErr = err;
@@ -5735,31 +5798,31 @@ async function reconfigureOllamaServer() {
5735
5798
  await tryAll();
5736
5799
  // Refused for want of credentials: ask now, whatever the address looked like, and list again.
5737
5800
  if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus) && !key) {
5738
- console.log(yellow(` ${candidates[0].host} needs an API key to list its models.`));
5801
+ console.log(yellow(` ${candidates[0].host} needs an API key to list its models.`));
5739
5802
  typedKeys.delete(candidates[0].host);
5740
5803
  key = await askKeyFor(candidates[0].host);
5741
5804
  if (key) await tryAll();
5742
5805
  }
5743
5806
  if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus)) {
5744
- console.log(red(` ${candidates[0].host} refused the request (HTTP ${lastErr.httpStatus})${key ? " — that key was not accepted" : ""}.`));
5807
+ console.log(red(` ${candidates[0].host} refused the request (HTTP ${lastErr.httpStatus})${key ? " — that key was not accepted" : ""}.`));
5745
5808
  continue;
5746
5809
  }
5747
5810
  if (!n) {
5748
5811
  if (lastErr?.httpStatus === 404) {
5749
5812
  // IT ANSWERED — it is simply not an Ollama (#43). "Couldn't reach … is Ollama listening"
5750
5813
  // sent people to debug a server that was working.
5751
- for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(` ${l}`));
5814
+ for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(` ${l}`));
5752
5815
  continue;
5753
5816
  }
5754
5817
  // One address typed, one failure reported — naming every candidate would be two errors
5755
5818
  // for one mistake, and neither would be the address they gave.
5756
- console.log(red(` couldn't reach ${candidates[0].host}: ${os.networkReason(lastErr)}`));
5757
- console.log(dim(" check the address, that Ollama is listening on the network, and /proxy."));
5819
+ console.log(red(` couldn't reach ${candidates[0].host}: ${os.networkReason(lastErr)}`));
5820
+ console.log(dim(" check the address, that Ollama is listening on the network, and /proxy."));
5758
5821
  continue;
5759
5822
  }
5760
5823
  const usable = os.usableModelList(models);
5761
5824
  if (!usable.ok) {
5762
- for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(` ${l}`));
5825
+ for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(` ${l}`));
5763
5826
  continue;
5764
5827
  }
5765
5828
  if (require) {
@@ -5768,13 +5831,13 @@ async function reconfigureOllamaServer() {
5768
5831
  // rather than the server.
5769
5832
  const need = os.checkRequired(models);
5770
5833
  if (!need.ok) {
5771
- for (const l of os.describeNotReady(n.host, need.missing)) console.log(yellow(` ${l}`));
5772
- console.log(dim(" (or type s to skip — you can set it later with /model)"));
5834
+ for (const l of os.describeNotReady(n.host, need.missing)) console.log(yellow(` ${l}`));
5835
+ console.log(dim(" (or type s to skip — you can set it later with /model)"));
5773
5836
  continue;
5774
5837
  }
5775
- console.log(dim(` ✓ ${n.host} has the ${os.REQUIRED_AGENT_PREFIX} family and the embedder`));
5838
+ console.log(dim(` ✓ ${n.host} has the ${os.REQUIRED_AGENT_PREFIX} family and the embedder`));
5776
5839
  }
5777
- console.log(dim(` found ${models.length} model${models.length === 1 ? "" : "s"} on ${n.host}`));
5840
+ console.log(dim(` found ${models.length} model${models.length === 1 ? "" : "s"} on ${n.host}`));
5778
5841
  return { normalized: n, models };
5779
5842
  }
5780
5843
  return null;
@@ -5785,17 +5848,17 @@ async function reconfigureOllamaServer() {
5785
5848
  // ask again. Keep it, say where it is changed, and go on to the coder.
5786
5849
  const agentOnProvider = am.agentIsOnProvider(settings);
5787
5850
  const curAgent =
5788
- String(settings.agentModelUrl ?? "").trim() || "https://ollama1.gomarsic.cc/v1";
5851
+ String(settings.agentModelUrl ?? "").trim() || "https://ollama1.getmarsic.cc/v1";
5789
5852
  const curCode = am.coderServerDefault(settings);
5790
5853
 
5791
5854
  let agent = null;
5792
5855
  if (agentOnProvider) {
5793
5856
  const host = hostOf(String(settings.agentModelUrl ?? ""));
5794
- console.log(dim(` The agent model is on ${host} (OpenAI-compatible) — kept as it is; change it with /agent-model.`));
5795
- console.log(dim(" Enter keeps the current address."));
5857
+ console.log(dim(` The agent model is on ${host} (OpenAI-compatible) — kept as it is; change it with /agent-model.`));
5858
+ console.log(dim(" Enter keeps the current address."));
5796
5859
  } else {
5797
- console.log(dim(" TWO servers may be used: the agent + embedder on one, the coder on another."));
5798
- console.log(dim(" Enter keeps the current address."));
5860
+ console.log(dim(" TWO servers may be used: the agent + embedder on one, the coder on another."));
5861
+ console.log(dim(" Enter keeps the current address."));
5799
5862
  agent = await askServer("Agent + embedding server", curAgent, {
5800
5863
  require: true,
5801
5864
  short: "agent + embed server",
@@ -5815,7 +5878,7 @@ async function reconfigureOllamaServer() {
5815
5878
  }
5816
5879
 
5817
5880
  if (!agent && !coder) {
5818
- console.log(dim(" nothing set — models stay as they are.\n"));
5881
+ console.log(dim(" nothing set — models stay as they are.\n"));
5819
5882
  return;
5820
5883
  }
5821
5884
 
@@ -5838,13 +5901,13 @@ async function reconfigureOllamaServer() {
5838
5901
  const size = am.formatSize(m.sizeBytes);
5839
5902
  return `${m.model}${size ? dim(` ${size}`) : ""}${m.model === agentDefault ? dim(" (current)") : ""}`;
5840
5903
  });
5841
- console.log(dim(` AGENT model — routes every turn, writes the plain replies (${PICK_HINT}):`));
5904
+ console.log(dim(` AGENT model — routes every turn, writes the plain replies (${PICK_HINT}):`));
5842
5905
  const pick = await pickFromList(
5843
5906
  labels,
5844
5907
  Math.max(0, cands.findIndex((m) => m.model === agentDefault))
5845
5908
  );
5846
5909
  if (pick < 0) {
5847
- console.log(dim(" (cancelled — nothing changed)\n"));
5910
+ console.log(dim(" (cancelled — nothing changed)\n"));
5848
5911
  return;
5849
5912
  }
5850
5913
  chat = cands[pick].model;
@@ -5863,10 +5926,10 @@ async function reconfigureOllamaServer() {
5863
5926
  const kind = am.isLikelyEmbedder(m) ? dim(" · embedder") : "";
5864
5927
  return `${m.model}${size ? dim(` ${size}`) : ""}${kind}${m.model === roles.code ? dim(" (current)") : ""}`;
5865
5928
  });
5866
- console.log(dim(` CODING model — writes the code; worth waiting for (${PICK_HINT}):`));
5929
+ console.log(dim(` CODING model — writes the code; worth waiting for (${PICK_HINT}):`));
5867
5930
  const idx = await pickFromList(labels, Math.max(0, coder.models.findIndex((m) => m.model === roles.code)));
5868
5931
  if (idx < 0) {
5869
- console.log(dim(" (cancelled — nothing changed)\n"));
5932
+ console.log(dim(" (cancelled — nothing changed)\n"));
5870
5933
  return;
5871
5934
  }
5872
5935
  code = coder.models[idx].model;
@@ -5881,7 +5944,7 @@ async function reconfigureOllamaServer() {
5881
5944
  .flatMap((b) => (Array.isArray(b.models) ? b.models : []))
5882
5945
  );
5883
5946
  console.log(
5884
- dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`)
5947
+ dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`)
5885
5948
  );
5886
5949
  const ticks = await pickManyFromList(
5887
5950
  rest.map((m) => `${m.model}${am.formatSize(m.sizeBytes) ? dim(` ${am.formatSize(m.sizeBytes)}`) : ""}`),
@@ -5890,7 +5953,7 @@ async function reconfigureOllamaServer() {
5890
5953
  );
5891
5954
  // null is Escape and cancels the command; an EMPTY list is a legitimate "offer none".
5892
5955
  if (ticks === null) {
5893
- console.log(dim(" (cancelled — nothing changed)\n"));
5956
+ console.log(dim(" (cancelled — nothing changed)\n"));
5894
5957
  return;
5895
5958
  }
5896
5959
  extras = ticks.map((i) => rest[i].model);
@@ -5936,7 +5999,7 @@ async function reconfigureOllamaServer() {
5936
5999
  md.saveWorkerSettings(apiBase, workerKey, patch)
5937
6000
  );
5938
6001
  if (!saved) {
5939
- console.log(red(" couldn't save that to this machine — nothing changed."));
6002
+ console.log(red(" couldn't save that to this machine — nothing changed."));
5940
6003
  console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
5941
6004
  return;
5942
6005
  }
@@ -5956,13 +6019,13 @@ async function reconfigureOllamaServer() {
5956
6019
  const url = used.find((u) => hostnameOf(u) === host);
5957
6020
  const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), url);
5958
6021
  if (!found) {
5959
- console.log(yellow(` no server row holds ${host} yet — add its key with ctrl+k in /model.`));
6022
+ console.log(yellow(` no server row holds ${host} yet — add its key with ctrl+k in /model.`));
5960
6023
  continue;
5961
6024
  }
5962
6025
  const ok = await withPendingStatus("saving the key", () => addCodingKey(host, k, found.id));
5963
6026
  console.log(ok
5964
6027
  ? GUTTER + dim(`✓ key stored for ${host} — the server's default key`)
5965
- : red(` couldn't store the key for ${host} — add it with ctrl+k.`));
6028
+ : red(` couldn't store the key for ${host} — add it with ctrl+k.`));
5966
6029
  }
5967
6030
  }
5968
6031
  }
@@ -5980,11 +6043,11 @@ async function reconfigureOllamaServer() {
5980
6043
  for (const u of cloudUrls) {
5981
6044
  const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), u);
5982
6045
  if (found && !am.boundKeyFor(keys, found.id)) {
5983
- console.log(yellow(` ${hostOf(u)} is the Ollama cloud — it needs its own API key (the model list worked without one).`));
6046
+ console.log(yellow(` ${hostOf(u)} is the Ollama cloud — it needs its own API key (the model list worked without one).`));
5984
6047
  // The shared key prompt (task #245); the server's first key becomes its default.
5985
6048
  const { name, secret } = await askServerKey({ host: hostOf(u) });
5986
6049
  if (secret && !(await withPendingStatus("saving the key", () => addCodingKey(name, secret, found.id)))) {
5987
- console.log(red(" couldn't save the key — add it with ctrl+k.\n"));
6050
+ console.log(red(" couldn't save the key — add it with ctrl+k.\n"));
5988
6051
  }
5989
6052
  }
5990
6053
  }
@@ -6020,7 +6083,7 @@ async function reconfigureOllamaServer() {
6020
6083
  if (g !== null) globalOutputTokens = g;
6021
6084
  }
6022
6085
  paintBar(); // …and show all of it now, not at the next keystroke
6023
- console.log(accent(" ✓ models saved"));
6086
+ console.log(accent(" ✓ models saved"));
6024
6087
  if (agent) {
6025
6088
  console.log(dim(` chat ${chat} @ ${agent.normalized.host}`));
6026
6089
  console.log(dim(` RAG ${os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL} @ ${agent.normalized.host}`));
@@ -6038,9 +6101,9 @@ async function reconfigureOllamaServer() {
6038
6101
  // falsely scream "changed" while the seeded value is exactly what settings now hold.
6039
6102
  const seededEmbed = agent ? (os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL) : "";
6040
6103
  if (agent && String(settings.ragEmbedModel ?? "") !== seededEmbed) {
6041
- console.log(dim(" the embedder changed — existing RAG indexes need rebuilding (/rag)."));
6104
+ console.log(dim(" the embedder changed — existing RAG indexes need rebuilding (/rag)."));
6042
6105
  }
6043
- console.log(dim(" takes effect on your next question.\n"));
6106
+ console.log(dim(" takes effect on your next question.\n"));
6044
6107
  }
6045
6108
 
6046
6109
  // ---------- fetch the ready-to-run agent payload from user settings ----------
@@ -6091,7 +6154,7 @@ async function reconfigureOllamaServer() {
6091
6154
  */
6092
6155
  async function pickExtraModels(rest, preChecked = []) {
6093
6156
  if (rest.length === 0) return [];
6094
- console.log(dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`));
6157
+ console.log(dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`));
6095
6158
  const already = new Set(preChecked);
6096
6159
  const ticks = await pickManyFromList(
6097
6160
  rest,
@@ -6137,7 +6200,7 @@ async function chooseCodingModelFrom(ps, all, question, current) {
6137
6200
  const idx = await pickFromList(rows, at >= 0 ? at : 0);
6138
6201
  if (idx < 0) return null;
6139
6202
  if (idx === list.length) {
6140
- const typed = (await ask(" model name: ")).trim();
6203
+ const typed = (await ask(" model name: ")).trim();
6141
6204
  return typed || null;
6142
6205
  }
6143
6206
  if (idx === list.length + 1) {
@@ -6165,11 +6228,11 @@ async function chooseCodingModelFrom(ps, all, question, current) {
6165
6228
  async function chooseFromModelList(all, question, current = 0) {
6166
6229
  const rows = [...all, "type a model name…"];
6167
6230
  const at = typeof current === "string" ? all.indexOf(current) : Number(current) || 0;
6168
- console.log(dim(` ${question} (${PICK_HINT})`));
6231
+ console.log(dim(` ${question} (${PICK_HINT})`));
6169
6232
  const idx = await pickFromList(rows, at >= 0 ? at : 0);
6170
6233
  if (idx < 0) return null;
6171
6234
  if (idx === all.length) {
6172
- const typed = (await ask(" model name: ")).trim();
6235
+ const typed = (await ask(" model name: ")).trim();
6173
6236
  return typed || null;
6174
6237
  }
6175
6238
  return all[idx];
@@ -6179,7 +6242,7 @@ async function manageCodingProviders() {
6179
6242
  const ps = await import("./coding-provider-setup.mjs");
6180
6243
  const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
6181
6244
  if (!settings) {
6182
- console.log(red(" couldn't reach the API to read your current settings.\n"));
6245
+ console.log(red(" couldn't reach the API to read your current settings.\n"));
6183
6246
  return;
6184
6247
  }
6185
6248
  const rows = ps.existingBackendRows(settings);
@@ -6196,10 +6259,10 @@ async function manageCodingProviders() {
6196
6259
  );
6197
6260
  const ADD = labels.length;
6198
6261
  labels.push("add another provider — Claude API, OpenAI, Kimi…");
6199
- console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
6262
+ console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
6200
6263
  const idx = await pickFromList(labels, 0);
6201
6264
  if (idx < 0) {
6202
- console.log(dim(" (cancelled)\n"));
6265
+ console.log(dim(" (cancelled)\n"));
6203
6266
  return;
6204
6267
  }
6205
6268
  if (idx === ADD) {
@@ -6214,7 +6277,7 @@ async function manageCodingProviders() {
6214
6277
  1
6215
6278
  );
6216
6279
  if (action <= 0) {
6217
- console.log(dim(" (nothing changed)\n"));
6280
+ console.log(dim(" (nothing changed)\n"));
6218
6281
  return;
6219
6282
  }
6220
6283
  if (action === 1) await changeProviderModel(ps, rows, p);
@@ -6273,12 +6336,12 @@ async function changeProviderModel(ps, rows, p) {
6273
6336
  if (models.length) {
6274
6337
  model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model);
6275
6338
  if (model === null) {
6276
- console.log(dim(" (nothing changed)\n"));
6339
+ console.log(dim(" (nothing changed)\n"));
6277
6340
  return;
6278
6341
  }
6279
6342
  } else {
6280
6343
  console.log(GUTTER + dim(`Enter keeps ${p.model || "it unset"}`));
6281
- model = (await ask(" model name: ")).trim();
6344
+ model = (await ask(" model name: ")).trim();
6282
6345
  }
6283
6346
  if (!model) {
6284
6347
  console.log(GUTTER + dim("(nothing changed)\n"));
@@ -6302,7 +6365,7 @@ async function changeProviderModel(ps, rows, p) {
6302
6365
  })
6303
6366
  );
6304
6367
  if (!saved) {
6305
- console.log(red(" couldn't save that — nothing changed.\n"));
6368
+ console.log(red(" couldn't save that — nothing changed.\n"));
6306
6369
  return;
6307
6370
  }
6308
6371
  // THE FOLDER'S PICK NAMES THE OLD MODEL. Leaving it would send a name this provider no longer
@@ -6316,14 +6379,14 @@ async function changeProviderModel(ps, rows, p) {
6316
6379
  await syncActiveCoderWhere();
6317
6380
  console.log(GUTTER + dim("this folder's pick named the old model — cleared it."));
6318
6381
  }
6319
- console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
6382
+ console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
6320
6383
  }
6321
6384
 
6322
6385
  /** Point a provider at a different URL, keeping its id so saved picks still resolve. */
6323
6386
  async function changeProviderUrl(ps, rows, p) {
6324
6387
  const md = await import("./model-doctor.mjs");
6325
6388
  console.log("\n" + GUTTER + dim(`Enter keeps ${p.url}`));
6326
- const typed = (await ask(" provider URL: ")).trim();
6389
+ const typed = (await ask(" provider URL: ")).trim();
6327
6390
  if (!typed || typed === p.url) {
6328
6391
  console.log(GUTTER + dim("(nothing changed)\n"));
6329
6392
  return;
@@ -6348,12 +6411,12 @@ async function changeProviderUrl(ps, rows, p) {
6348
6411
  })
6349
6412
  );
6350
6413
  if (!saved) {
6351
- console.log(red(" couldn't save that — nothing changed.\n"));
6414
+ console.log(red(" couldn't save that — nothing changed.\n"));
6352
6415
  return;
6353
6416
  }
6354
6417
  // The KEY still belongs to this row, because the row id did not change — which is the whole
6355
6418
  // reason an id is not derived from the URL.
6356
- console.log(accent(` ✓ ${p.host} → ${r.host}`) + dim(" · its key still applies\n"));
6419
+ console.log(accent(` ✓ ${p.host} → ${r.host}`) + dim(" · its key still applies\n"));
6357
6420
  }
6358
6421
 
6359
6422
  /** Remove a provider, and the keys that would be stranded by its removal. */
@@ -6386,7 +6449,7 @@ async function removeCodingProvider(ps, rows, p) {
6386
6449
  })
6387
6450
  );
6388
6451
  if (!saved) {
6389
- console.log(red(" couldn't save that — nothing was removed.\n"));
6452
+ console.log(red(" couldn't save that — nothing was removed.\n"));
6390
6453
  return;
6391
6454
  }
6392
6455
  // THE ROW GOES FIRST. If a key delete fails afterwards the key is merely orphaned, which is
@@ -6410,7 +6473,7 @@ async function removeCodingProvider(ps, rows, p) {
6410
6473
  console.log(GUTTER + dim("this folder was using it — cleared the pick.\n"));
6411
6474
  }
6412
6475
  console.log(
6413
- accent(` ✓ removed ${p.host}`) +
6476
+ accent(` ✓ removed ${p.host}`) +
6414
6477
  (stranded.length ? dim(` · ${deleted} key${deleted === 1 ? "" : "s"} deleted`) : "") +
6415
6478
  "\n"
6416
6479
  );
@@ -6451,7 +6514,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6451
6514
 
6452
6515
  const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
6453
6516
  if (!settings) {
6454
- console.log(red(" couldn't reach the API to read your current settings.\n"));
6517
+ console.log(red(" couldn't reach the API to read your current settings.\n"));
6455
6518
  return;
6456
6519
  }
6457
6520
  // EVERY ROW THAT IS ALREADY THERE, including the synthesized one a pre-#123 account keeps in
@@ -6477,10 +6540,10 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6477
6540
  // without Claude, which serves no embeddings).
6478
6541
  const presets = ps.providerPresets(role);
6479
6542
  const rowsShown = [...presets.map((p) => `${p.name}${dim(` · ${p.url}`)}`), "another URL…"];
6480
- console.log(dim(` Which provider? (${PICK_HINT})`));
6543
+ console.log(dim(` Which provider? (${PICK_HINT})`));
6481
6544
  const pi = await pickFromList(rowsShown, 0);
6482
6545
  if (pi < 0) {
6483
- console.log(dim(" (nothing saved)\n"));
6546
+ console.log(dim(" (nothing saved)\n"));
6484
6547
  return;
6485
6548
  }
6486
6549
  if (pi < presets.length) target = as.serverTarget("openai", presets[pi].url);
@@ -6488,12 +6551,12 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6488
6551
  if (!target && !presetUrl) {
6489
6552
  const dflt = as.defaultServerUrl(role, dialect, settings, existing);
6490
6553
  console.log(dim(dialect === "ollama"
6491
- ? " An Ollama server: this machine, a box on your network, or https://ollama.com (needs a key)."
6492
- : " Any OpenAI-compatible endpoint: OpenRouter, Together, vLLM, LM Studio, …"));
6554
+ ? " An Ollama server: this machine, a box on your network, or https://ollama.com (needs a key)."
6555
+ : " Any OpenAI-compatible endpoint: OpenRouter, Together, vLLM, LM Studio, …"));
6493
6556
  // The address goes ABOVE, in full, and Enter's meaning is said out loud (46d1524e).
6494
6557
  console.log(dim(dflt ? ` Enter uses ${dflt} · Esc cancels\n` : " Esc cancels.\n"));
6495
6558
  for (let attempt = 0; attempt < 3 && !target; attempt++) {
6496
- const typed = await ask(" server URL: ");
6559
+ const typed = await ask(" server URL: ");
6497
6560
  if (typed === null || as.isCancelWord(typed)) break;
6498
6561
  const raw = typed.trim() || dflt;
6499
6562
  if (!raw) break;
@@ -6510,7 +6573,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6510
6573
  }
6511
6574
  }
6512
6575
  if (!target) {
6513
- console.log(dim(" (nothing saved)\n"));
6576
+ console.log(dim(" (nothing saved)\n"));
6514
6577
  return;
6515
6578
  }
6516
6579
  const host = target.host;
@@ -6532,7 +6595,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6532
6595
  // THE ROW CAP BEFORE ANY QUESTION about a new server: exceeding it is a silent 200 with no row,
6533
6596
  // and asking for a key and a model first would be the worst order to discover that in.
6534
6597
  if (!found && existing.length >= ps.MAX_BACKENDS) {
6535
- console.log(yellow(` ${ps.MAX_BACKENDS} servers is the limit — remove one in /model → manage providers first.\n`));
6598
+ console.log(yellow(` ${ps.MAX_BACKENDS} servers is the limit — remove one in /model → manage providers first.\n`));
6536
6599
  return;
6537
6600
  }
6538
6601
  const need = as.keyRequirement(target);
@@ -6577,7 +6640,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6577
6640
  let probe = await list();
6578
6641
  // THE STORED KEY WAS REFUSED — ask for the right one once, rather than saving nothing.
6579
6642
  if ((probe.status === 401 || probe.status === 403) && !typedKey && key) {
6580
- console.log(yellow(` ${host} refused the stored key (HTTP ${probe.status}) — enter the right one.`));
6643
+ console.log(yellow(` ${host} refused the stored key (HTTP ${probe.status}) — enter the right one.`));
6581
6644
  if (!(await askKey()) || !key) return;
6582
6645
  probe = await list();
6583
6646
  }
@@ -6612,11 +6675,11 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6612
6675
  ? await chooseCodingModelFrom(ps, models, R.question, 0)
6613
6676
  : await chooseFromModelList(models, R.question, Math.max(0, models.indexOf(current)));
6614
6677
  if (model === null) {
6615
- console.log(dim(" (cancelled — nothing was saved)\n"));
6678
+ console.log(dim(" (cancelled — nothing was saved)\n"));
6616
6679
  return;
6617
6680
  }
6618
6681
  } else {
6619
- model = String((await ask(" model name: ")) ?? "").trim();
6682
+ model = String((await ask(" model name: ")) ?? "").trim();
6620
6683
  }
6621
6684
  if (!model) {
6622
6685
  console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
@@ -6633,7 +6696,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6633
6696
  if (!sw.changed) return;
6634
6697
  plan = sw.plan;
6635
6698
  } else if (as.serverTarget(target.dialect, String(settings.ragEmbedUrl ?? "")).root === target.root && !typedKey) {
6636
- console.log(dim(` already embedding with ${model} on ${host} — nothing changed.\n`));
6699
+ console.log(dim(` already embedding with ${model} on ${host} — nothing changed.\n`));
6637
6700
  return;
6638
6701
  }
6639
6702
  }
@@ -6662,7 +6725,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6662
6725
  } else {
6663
6726
  const r = as.rowForSave(role, { existing: found.row, model });
6664
6727
  if (r.full) {
6665
- console.log(yellow(` ${host} already offers ${ps.MAX_MODELS_PER_SERVER} models — remove one in /model → manage providers first.\n`));
6728
+ console.log(yellow(` ${host} already offers ${ps.MAX_MODELS_PER_SERVER} models — remove one in /model → manage providers first.\n`));
6666
6729
  return;
6667
6730
  }
6668
6731
  if (r.changed) rows = existing.map((b) => (b === found.row ? r.row : b));
@@ -6670,7 +6733,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6670
6733
  if (rows !== existing) {
6671
6734
  const ok = await withPendingStatus(`saving ${host}`, () => md.saveWorkerSettings(apiBase, workerKey, { agentCodingBackends: rows }));
6672
6735
  if (!ok) {
6673
- console.log(red(` couldn't save ${host} — nothing changed.`));
6736
+ console.log(red(` couldn't save ${host} — nothing changed.`));
6674
6737
  console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
6675
6738
  return;
6676
6739
  }
@@ -6679,7 +6742,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6679
6742
  if (typedKey) {
6680
6743
  const id = await withPendingStatus("saving the key", () => addCodingKey(keyName, key, rowId));
6681
6744
  if (!id) {
6682
- console.log(yellow(` ${host} was saved, but its key was not — nothing else changed.`));
6745
+ console.log(yellow(` ${host} was saved, but its key was not — nothing else changed.`));
6683
6746
  console.log(GUTTER + dim("try again, or add the key with ctrl+k.\n"));
6684
6747
  return;
6685
6748
  }
@@ -6697,18 +6760,20 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6697
6760
  console.log(GUTTER + dim("the banner shows the new model next time gu starts.\n"));
6698
6761
  } else if (rows !== existing || typedKey) {
6699
6762
  // The SERVER landed — say so, or the user re-enters a key that is already stored.
6700
- console.log(yellow(` ${host} was saved, but the agent model was not pointed at it.`));
6763
+ console.log(yellow(` ${host} was saved, but the agent model was not pointed at it.`));
6701
6764
  console.log(GUTTER + dim("run /agent-model to finish, or check the API is reachable.\n"));
6702
6765
  }
6703
6766
  return;
6704
6767
  }
6705
6768
  if (role === "embed") {
6706
6769
  // ragEmbedKeyId "" — the server's own keys decide now (its default, or this folder's pick).
6707
- const fields = { ragEmbedUrl: target.roleUrl("embed"), ragEmbedModel: model, ragEmbedKeyId: "" };
6770
+ // …and ragEnabled: true (#347): choosing an embedder IS turning RAG on. Without it the choice
6771
+ // was stored and never took effect, and the "no embed model" warning came back at every start.
6772
+ const fields = ec.embedSaveFields({ url: target.roleUrl("embed"), model });
6708
6773
  if (!(await saveEmbedSettings(fields))) return;
6709
6774
  if (plan) announceEmbedSaved({ chosen: model, plan, url: target.roleUrl("embed") });
6710
6775
  else {
6711
- console.log(accent(` ✓ embedding server → ${host}`));
6776
+ console.log(accent(` ✓ embedding server → ${host}`));
6712
6777
  console.log(dim(` ${model} did not change, so no knowledge base rebuilds — the same model just answers from ${host}.\n`));
6713
6778
  }
6714
6779
  return;
@@ -6736,7 +6801,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
6736
6801
  (c) => c.model === model && (c.backendId ? c.backendId === backendId : sameHost(c.url, url))
6737
6802
  );
6738
6803
  if (!offered) {
6739
- console.log(yellow(` ${host} was saved, but this API is not offering ${model} yet.`));
6804
+ console.log(yellow(` ${host} was saved, but this API is not offering ${model} yet.`));
6740
6805
  console.log(GUTTER + dim("run /model — if it is missing there too, the API may be older than"));
6741
6806
  console.log(GUTTER + dim("this feature; restart it with `gu api restart`.\n"));
6742
6807
  return;
@@ -6750,7 +6815,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
6750
6815
  ])
6751
6816
  );
6752
6817
  if (lost.length) {
6753
- console.log(yellow(` ⚠ this API dropped ${lost.length === 1 ? "a provider" : `${lost.length} providers`} when saving:`));
6818
+ console.log(yellow(` ⚠ this API dropped ${lost.length === 1 ? "a provider" : `${lost.length} providers`} when saving:`));
6754
6819
  for (const u of lost) console.log(GUTTER + dim(` ${u}`));
6755
6820
  if (ps.apiKeysBackendsByKind(after?.codingChoices)) {
6756
6821
  console.log(GUTTER + dim("this API identifies a backend by its KIND, so it holds ONE"));
@@ -6774,8 +6839,8 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
6774
6839
  // The key question for this coder is answered — leaving the flag set would warn about a key
6775
6840
  // that was just entered, which reads as the save having failed.
6776
6841
  codingKeyMissingKnown = false;
6777
- console.log(accent(` ✓ coding model → ${model}`) + dim(` · ${host}`));
6778
- if (key) console.log(dim(` API key ${keyName || "saved"} ····${key.slice(-4)} · ${host}'s key\n`));
6842
+ console.log(accent(` ✓ coding model → ${model}`) + dim(` · ${host}`));
6843
+ if (key) console.log(dim(` API key ${keyName || "saved"} ····${key.slice(-4)} · ${host}'s key\n`));
6779
6844
  // A HOSTED SERVER NEEDS ITS WINDOW SET: with nothing set gu assumes 24,000 prompt tokens and
6780
6845
  // compacts there — on a large hosted model that throws history away every turn.
6781
6846
  if (ps.shouldMentionContextWindow(locality)) {
@@ -6795,7 +6860,7 @@ async function chooseModel() {
6795
6860
  fetchAgentPayload([{ role: "user", content: "ping" }])
6796
6861
  );
6797
6862
  } catch (err) {
6798
- console.log(red(` ${describeApiFailure(err, "couldn't load models")}\n`));
6863
+ console.log(red(` ${describeApiFailure(err, "couldn't load models")}\n`));
6799
6864
  return;
6800
6865
  }
6801
6866
  const allowed = Array.isArray(probe?.codingAllowed) ? probe.codingAllowed : [];
@@ -6816,7 +6881,7 @@ async function chooseModel() {
6816
6881
  // choices while still occupying its URL, so sending this branch straight to the ADD flow
6817
6882
  // would meet "already configured here" and dead-end. The manager fixes such a row, and falls
6818
6883
  // through to add when there is genuinely nothing there.
6819
- console.log(dim(" No coding model configured."));
6884
+ console.log(dim(" No coding model configured."));
6820
6885
  const go = await pickFromList(
6821
6886
  [dim("leave it"), "add or change a provider — Claude API, OpenAI, Kimi…", "change the Ollama server this machine uses…"],
6822
6887
  1
@@ -6834,7 +6899,7 @@ async function chooseModel() {
6834
6899
  // symptom of being pointed at the wrong box, and the old message sent people to a web app a
6835
6900
  // Client Expert machine does not have.
6836
6901
  if (choices.length === 1) {
6837
- console.log(dim(` Only one coding model is available: ${choices[0].model}.`));
6902
+ console.log(dim(` Only one coding model is available: ${choices[0].model}.`));
6838
6903
  // APPENDED AFTER the existing entry, here and below, so that entry keeps the index anyone
6839
6904
  // who has used it already reaches for.
6840
6905
  const go = await pickFromList(
@@ -6882,10 +6947,10 @@ async function chooseModel() {
6882
6947
  // enough in name to have already been mixed up. The fix is to say which one THIS is — not to
6883
6948
  // print the other two here: the agent model and the embedder are independent settings with
6884
6949
  // their own commands, and listing them together implies a relationship that does not exist.
6885
- console.log(dim(` Choose the CODING model — the one that writes code (${PICK_HINT}):`));
6950
+ console.log(dim(` Choose the CODING model — the one that writes code (${PICK_HINT}):`));
6886
6951
  const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
6887
6952
  if (idx < 0) {
6888
- console.log(dim(" (cancelled)\n"));
6953
+ console.log(dim(" (cancelled)\n"));
6889
6954
  return;
6890
6955
  }
6891
6956
  // The appended entries sit past the last model, so they are the only indices without a choice.
@@ -6915,7 +6980,7 @@ async function chooseModel() {
6915
6980
  const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
6916
6981
  console.log(
6917
6982
  accent(
6918
- ` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
6983
+ ` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
6919
6984
  )
6920
6985
  );
6921
6986
  // DOES THIS ONE NEED AN API KEY? (task #181) Reported live on Windows 11 / Client Expert:
@@ -6968,7 +7033,7 @@ async function chooseModel() {
6968
7033
  host: ks.provider,
6969
7034
  });
6970
7035
  console.log(
6971
- dim(" API key ") + accent(line.text) +
7036
+ dim(" API key ") + accent(line.text) +
6972
7037
  (line.note ? orange(` · ${line.note}`) : "") +
6973
7038
  dim(` · ctrl+k to ${line.verb} it`)
6974
7039
  );
@@ -6977,7 +7042,7 @@ async function chooseModel() {
6977
7042
  // the more common one — "how do I change the model" — with no visible answer at all. Reported
6978
7043
  // three times from exactly this view, twice after being told where the picker entry was.
6979
7044
  // /provider is hidden (see HIDDEN_COMMANDS): point at the row of /model that does the same.
6980
- console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
7045
+ console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
6981
7046
  }
6982
7047
  // Task #125: the GLOBAL lifetime total is per-backend, so re-fetch it for the newly
6983
7048
  // selected backend NOW — otherwise the footer would keep showing the previous backend's
@@ -7331,13 +7396,13 @@ async function runSwitchSkillCommand(line) {
7331
7396
  }
7332
7397
  } else {
7333
7398
  const options = ["claude", "gu"];
7334
- console.log(dim(` Whose skills, MCP servers and hooks this folder uses (${PICK_HINT}):`));
7399
+ console.log(dim(` Whose skills, MCP servers and hooks this folder uses (${PICK_HINT}):`));
7335
7400
  const idx = await pickFromList(
7336
7401
  options.map((o) => sourceLabel(o) + dim(` · ${where(o)}`) + (o === current ? dim(" · in use") : "")),
7337
7402
  Math.max(0, options.indexOf(current))
7338
7403
  );
7339
7404
  if (idx < 0) {
7340
- console.log(dim(" (cancelled)\n"));
7405
+ console.log(dim(" (cancelled)\n"));
7341
7406
  paintBar();
7342
7407
  return;
7343
7408
  }
@@ -7355,7 +7420,7 @@ async function runSwitchSkillCommand(line) {
7355
7420
  _skillScan = { at: 0, skills: [] }; // the "/" menu and Tab follow at once
7356
7421
  const say = (l) => console.log(GUTTER + l);
7357
7422
  say(accent(`✓ ${pick === current ? "this folder uses" : "this folder now uses"} ${sourceLabel(pick)}`));
7358
- say(dim(` skills, MCP servers and hooks are read from ${where(pick)}`));
7423
+ say(dim(` skills, MCP servers and hooks are read from ${where(pick)}`));
7359
7424
  // What was found there — and what was not, said rather than shown as an empty list.
7360
7425
  try {
7361
7426
  const sk = discoverSkills({ cwd: process.cwd(), reserved: COMMAND_NAMES });
@@ -7367,20 +7432,20 @@ async function runSwitchSkillCommand(line) {
7367
7432
  say(dim(` ${count(sk.skills.length, "skill")}${sk.skills.length ? ` (${sk.skills.map((x) => "/" + x.name).join(", ")})` : ""} · ${count(running, "MCP server")} · ${count(nHooks(hk.user) + (hk.projectApproved ? nHooks(hk.project) : 0), "hook")}`));
7368
7433
  // Nothing there at all is a normal state, not a fault: say so, and that gu needs none of it.
7369
7434
  if (!sk.skills.length && !mcp.servers.length && !nHooks(hk.user) && !nHooks(hk.project) && !hk.notes.length) {
7370
- say(dim(" Nothing is set up there yet — gu works the same without any of them."));
7435
+ say(dim(" Nothing is set up there yet — gu works the same without any of them."));
7371
7436
  }
7372
7437
  for (const w of [...sk.warnings, ...mcp.warnings, ...hk.warnings]) say(yellow(" " + w));
7373
7438
  const unsupported = mcp.servers.filter((x) => x.error);
7374
7439
  if (unsupported.length) say(yellow(` ${count(unsupported.length, "MCP server")} cannot run here: ${unsupported.map((x) => `${x.name} (${x.error})`).join("; ")}`));
7375
7440
  const waiting = mcp.servers.filter((x) => x.scope === "project" && !x.approved && !x.error);
7376
7441
  if (waiting.length) say(yellow(` ${count(waiting.length, "project MCP server")} waiting for your approval: ${waiting.map((x) => x.name).join(", ")} — /mcp approve <name>`));
7377
- if (hk.projectPending) say(yellow(` this folder's ${hk.projectFile.replace(/^\.\//, "")} has hooks that will not run until you approve them — /hooks approve`));
7442
+ if (hk.projectPending) say(yellow(` this folder's ${hk.projectFile.replace(/^\.\//, "")} has hooks that will not run until you approve them — /hooks approve`));
7378
7443
  if (hk.notes.length) say(yellow(` ${count(hk.notes.length, "hook")} in Claude's settings ${hk.notes.length === 1 ? "is" : "are"} NOT run by gu — /hooks says which and why`));
7379
- if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(dim(" Claude's hooks get Claude's event and tool names from gu, so they record here as they do there."));
7444
+ if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(dim(" Claude's hooks get Claude's event and tool names from gu, so they record here as they do there."));
7380
7445
  } catch (err) {
7381
- say(yellow(` (could not read it back: ${err.message})`));
7446
+ say(yellow(` (could not read it back: ${err.message})`));
7382
7447
  }
7383
- say(dim(` It holds from your next question. ${pick === "claude" ? "/switch-skill gu" : "/switch-skill claude"} switches back.`));
7448
+ say(dim(` It holds from your next question. ${pick === "claude" ? "/switch-skill gu" : "/switch-skill claude"} switches back.`));
7384
7449
  console.log("");
7385
7450
  if (barPinned()) paintBar();
7386
7451
  }
@@ -7529,7 +7594,7 @@ async function runUsePresetsCommand(line) {
7529
7594
  console.log(GUTTER + dim("/save-preset pastes one in — copy it from another computer's /open-presets.") + "\n");
7530
7595
  return;
7531
7596
  }
7532
- console.log(dim(` Presets (${ed.PICKER_HINT}):`));
7597
+ console.log(dim(` Presets (${ed.PICKER_HINT}):`));
7533
7598
  const pick = await pickFromList(found, 0, { keys: ed.PICKER_KEYS });
7534
7599
  if (!pick || pick.index < 0) {
7535
7600
  console.log(GUTTER + dim("nothing changed.") + "\n");
@@ -7608,7 +7673,7 @@ async function runSaveSettingsCommand(line) {
7608
7673
  // have to type is a name — and Enter keeps gu-settings.json, as before.
7609
7674
  let typed = settingsArg(line, "/save-settings");
7610
7675
  if (!typed) {
7611
- const answer = await ask(` file name in ${home} [${st.DEFAULT_SETTINGS_NAME}]: `);
7676
+ const answer = await ask(` file name in ${home} [${st.DEFAULT_SETTINGS_NAME}]: `);
7612
7677
  if (answer === null) {
7613
7678
  console.log(GUTTER + dim(" (cancelled — nothing was written)\n"));
7614
7679
  return;
@@ -7668,7 +7733,7 @@ async function runLoadSettingsCommand(line) {
7668
7733
  if (!typed) {
7669
7734
  const found = st.settingsFilesIn(listJsonFiles(home));
7670
7735
  if (found.length > 1) {
7671
- console.log(dim(` Settings files in ${home} (${PICK_HINT}):`));
7736
+ console.log(dim(` Settings files in ${home} (${PICK_HINT}):`));
7672
7737
  const i = await pickFromList(found, 0);
7673
7738
  if (i < 0) {
7674
7739
  console.log(GUTTER + dim(" (cancelled — nothing changed)\n"));
@@ -7849,6 +7914,7 @@ async function noteTurnCost(inputTokens, breakdown, coder = null) {
7849
7914
  const c = mp.turnCost({ input: inputTokens, ...cache, price });
7850
7915
  if (!c) {
7851
7916
  lastTurnCost = { usd: null, model, host: mp.hostOf(url) };
7917
+ if (barPinned()) paintBar(); // #349: "no price" is shown, so it is painted like a cost is
7852
7918
  return;
7853
7919
  }
7854
7920
  lastTurnCost = { ...c, input: inputTokens, cacheReported: cache.cacheReported, model, host: mp.hostOf(url) };
@@ -7891,7 +7957,7 @@ async function runPricesCommand(line = "/prices") {
7891
7957
  console.log(GUTTER + dim(pricePublishedAt ? `from gu's tables published ${new Date(pricePublishedAt).toLocaleString()}` : "gu's built-in tables (none published applied yet)"));
7892
7958
  if (priceUpdate) console.log(GUTTER + accent(`${mp.priceUpdateLabel(priceUpdate.publishedAt)} — click it below, or /prices update`));
7893
7959
  if (!mp.findPrice(rows, url, model)) {
7894
- console.log(GUTTER + yellow(`no price for ${model || "the coder"} at ${mp.hostOf(url) || "its server"} — no cost is shown for its turns.`));
7960
+ for (const r of wrapBlock(mp.noPriceLine({ model, host: mp.hostOf(url) }), GUTTER, width)) console.log(GUTTER + yellow(r));
7895
7961
  }
7896
7962
  if (lastTurnCost?.usd != null) {
7897
7963
  const share = lastTurnCost.input ? Math.round((100 * lastTurnCost.cachedTokens) / lastTurnCost.input) : 0;
@@ -8017,11 +8083,11 @@ async function runClearLogs() {
8017
8083
  const question = (q) => {
8018
8084
  const hint = `(${PICK_HINT}):`;
8019
8085
  console.log("");
8020
- if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
8021
- else console.log(dim(` ${q}\n ${hint}`));
8086
+ if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
8087
+ else console.log(dim(` ${q}\n ${hint}`));
8022
8088
  };
8023
8089
  const cancelled = () => {
8024
- console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
8090
+ console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
8025
8091
  paintBar();
8026
8092
  };
8027
8093
 
@@ -8122,7 +8188,7 @@ async function saveEmbedSettings(fields) {
8122
8188
  const saved = await withPendingStatus("saving your embedder", () =>
8123
8189
  md.saveWorkerSettings(apiBase, workerKey, fields)
8124
8190
  );
8125
- if (!saved) console.log(red(" couldn't save that to your account — nothing changed.\n"));
8191
+ if (!saved) console.log(red(" couldn't save that to your account — nothing changed.\n"));
8126
8192
  return saved;
8127
8193
  }
8128
8194
 
@@ -8140,7 +8206,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
8140
8206
  }
8141
8207
  const plan = ec.embedChangePlan({ current, chosen, indexes });
8142
8208
  if (!plan.changed) {
8143
- console.log(dim(` already using ${current} — nothing changed.\n`));
8209
+ console.log(dim(` already using ${current} — nothing changed.\n`));
8144
8210
  return { changed: false, plan: null };
8145
8211
  }
8146
8212
  // "Are you sure?" with nothing behind it is a prompt people learn to dismiss; the four projects
@@ -8148,7 +8214,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
8148
8214
  for (const l of ec.describeEmbedChange(plan)) console.log(GUTTER + dim(l));
8149
8215
  const go = await pickFromList([`switch to ${chosen}`, `keep ${current}`], plan.stale.length ? 1 : 0);
8150
8216
  if (go !== 0) {
8151
- console.log(dim(" (kept)\n"));
8217
+ console.log(dim(" (kept)\n"));
8152
8218
  return { changed: false, plan: null };
8153
8219
  }
8154
8220
  return { changed: true, plan };
@@ -8158,13 +8224,13 @@ async function confirmEmbedSwitch(ec, current, chosen) {
8158
8224
  // shared recognition step. Returns { changed, chosen, plan }.
8159
8225
  async function pickEmbedModel(ec, current) {
8160
8226
  const rows = ec.embedChoiceRows(current);
8161
- console.log(dim(` Choose the embedder (${PICK_HINT}):`));
8227
+ console.log(dim(` Choose the embedder (${PICK_HINT}):`));
8162
8228
  const labels = rows.map(
8163
8229
  (r) => `${r.model}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}` + (r.current ? accent(" (current)") : "")
8164
8230
  );
8165
8231
  const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
8166
8232
  if (idx < 0) {
8167
- console.log(dim(" (cancelled)\n"));
8233
+ console.log(dim(" (cancelled)\n"));
8168
8234
  return { changed: false, chosen: "", plan: null };
8169
8235
  }
8170
8236
  const chosen = rows[idx].model;
@@ -8178,21 +8244,21 @@ async function pickEmbedModel(ec, current) {
8178
8244
  // a model that was never pulled fails at the first search, not here. Say it once rather than let
8179
8245
  // it surface as "RAG is broken".
8180
8246
  function announceEmbedSaved({ chosen, plan, url }) {
8181
- console.log(accent(` ✓ embedding model → ${chosen}` + (url ? ` on ${hostOf(url)}` : "")));
8247
+ console.log(accent(` ✓ embedding model → ${chosen}` + (url ? ` on ${hostOf(url)}` : "")));
8182
8248
  if (plan?.stale.length) {
8183
8249
  console.log(
8184
8250
  dim(` ${plan.stale.length} knowledge base${plan.stale.length === 1 ? "" : "s"} ` +
8185
8251
  "will rebuild on the next search — the first one will be slower.")
8186
8252
  );
8187
8253
  }
8188
- console.log(dim(` Make sure it is pulled there: ollama pull ${chosen}\n`));
8254
+ console.log(dim(` Make sure it is pulled there: ollama pull ${chosen}\n`));
8189
8255
  }
8190
8256
 
8191
8257
  async function chooseEmbedModel() {
8192
8258
  const ec = await import("./embed-model-command.mjs");
8193
8259
  const settings = await withPendingStatus("reading your embedder", fetchWorkerSettings);
8194
8260
  if (!settings) {
8195
- console.log(red(" couldn't reach the API to read your current embedder.\n"));
8261
+ console.log(red(" couldn't reach the API to read your current embedder.\n"));
8196
8262
  return;
8197
8263
  }
8198
8264
  const current = ec.currentEmbedModel(settings);
@@ -8205,11 +8271,11 @@ async function chooseEmbedModel() {
8205
8271
  const keyRequired = isOllamaCloudUrl(currentUrl) || /\/v1\/?$/i.test(currentUrl) ||
8206
8272
  Boolean(String(settings.ragEmbedKeyId ?? "").trim());
8207
8273
  const menu = ec.embedMenuOptions({ current, host, keyRequired });
8208
- console.log(dim(` Embedding model (currently ${current} on ${host || currentUrl})`));
8274
+ console.log(dim(` Embedding model (currently ${current} on ${host || currentUrl})`));
8209
8275
  const mi = await pickFromList(menu.map((o) => o.label), 0);
8210
8276
  const action = mi >= 0 ? menu[mi].action : "cancel";
8211
8277
  if (action === "cancel") {
8212
- console.log(dim(" (cancelled)\n"));
8278
+ console.log(dim(" (cancelled)\n"));
8213
8279
  return;
8214
8280
  }
8215
8281
 
@@ -8250,12 +8316,12 @@ async function saveAgentModel(agentUrl, model, host) {
8250
8316
  md.saveWorkerSettings(apiBase, workerKey, { agentModelUrl: agentUrl, agentModelName: model, agentModel: model })
8251
8317
  );
8252
8318
  if (!ok) {
8253
- console.log(red(` couldn't save the agent model — nothing changed.`));
8319
+ console.log(red(` couldn't save the agent model — nothing changed.`));
8254
8320
  console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
8255
8321
  return false;
8256
8322
  }
8257
- console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
8258
- console.log(dim(" takes effect on your next question.\n"));
8323
+ console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
8324
+ console.log(dim(" takes effect on your next question.\n"));
8259
8325
  return true;
8260
8326
  }
8261
8327
 
@@ -8268,7 +8334,7 @@ async function askKeyName(prompt = " key name: ") {
8268
8334
  for (let attempt = 0; attempt < 2; attempt++) {
8269
8335
  const name = (await ask(prompt)).trim();
8270
8336
  if (!am.looksLikeSecret(name)) return name;
8271
- console.log(yellow(" that looks like the API key itself — the NAME is shown on screen, so it must not be the key."));
8337
+ console.log(yellow(" that looks like the API key itself — the NAME is shown on screen, so it must not be the key."));
8272
8338
  console.log(GUTTER + dim('type a short label like "work", or press Enter for none — the key comes next.'));
8273
8339
  }
8274
8340
  return "";
@@ -8289,7 +8355,7 @@ async function askServerKey({ host = "", nameHint = '"work", "personal"', option
8289
8355
  console.log("\n" + GUTTER + dim(`A name just tells keys apart — ${nameHint}.`));
8290
8356
  const name = (await askKeyName()).trim();
8291
8357
  console.log(GUTTER + dim(SECRET_KEY_HINT));
8292
- const secret = (await askSecret(` API key${host ? ` for ${host}` : ""}${name ? ` (${name})` : ""}: `)).trim();
8358
+ const secret = (await askSecret(` API key${host ? ` for ${host}` : ""}${name ? ` (${name})` : ""}: `)).trim();
8293
8359
  if (!secret) {
8294
8360
  console.log(GUTTER + dim(optional ? "(no key — the server will be called anonymously)" : "(cancelled — nothing was saved)\n"));
8295
8361
  return { name, secret: "" };
@@ -8324,15 +8390,15 @@ async function chooseAgentProvider() {
8324
8390
  const am = await import("./agent-model-command.mjs");
8325
8391
  const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
8326
8392
  if (!settings) {
8327
- console.log(red(" couldn't reach the API to read your providers.\n"));
8393
+ console.log(red(" couldn't reach the API to read your providers.\n"));
8328
8394
  return;
8329
8395
  }
8330
8396
  const choices = am.agentProviderChoices(settings);
8331
8397
  if (choices.length === 1) return addServer({ role: "agent", dialect: "openai" });
8332
- console.log(dim(` Which OpenAI-compatible provider should the agent use? (${PICK_HINT})`));
8398
+ console.log(dim(` Which OpenAI-compatible provider should the agent use? (${PICK_HINT})`));
8333
8399
  const idx = await pickFromList(choices.map((c) => c.label), 0);
8334
8400
  if (idx < 0) {
8335
- console.log(dim(" (cancelled)\n"));
8401
+ console.log(dim(" (cancelled)\n"));
8336
8402
  return;
8337
8403
  }
8338
8404
  const pick = choices[idx];
@@ -8354,7 +8420,7 @@ async function chooseAgentModel() {
8354
8420
  const am = await import("./agent-model-command.mjs");
8355
8421
  const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
8356
8422
  if (!settings) {
8357
- console.log(red(" couldn't reach the API to read your current agent model.\n"));
8423
+ console.log(red(" couldn't reach the API to read your current agent model.\n"));
8358
8424
  return;
8359
8425
  }
8360
8426
  const current = am.currentAgentModel(settings);
@@ -8440,12 +8506,12 @@ async function chooseAgentModel() {
8440
8506
  // WHAT TO CHANGE, FIRST (#44). The ways to use an Ollama server or another provider used to sit
8441
8507
  // after every model on this one — 130 rows down on api.openai.com — which read as "OpenAI models
8442
8508
  // are all you can choose". A short menu of what to change comes before any list.
8443
- console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
8509
+ console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
8444
8510
  const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
8445
8511
  const first = await pickFromList(menu.map((o) => o.label), 0);
8446
8512
  const firstAction = first >= 0 ? menu[first].action : "cancel";
8447
8513
  if (firstAction === "cancel") {
8448
- console.log(dim(" (cancelled)\n"));
8514
+ console.log(dim(" (cancelled)\n"));
8449
8515
  return;
8450
8516
  }
8451
8517
  if (firstAction === "ollama") return addServer({ role: "agent", dialect: "ollama" });
@@ -8459,7 +8525,7 @@ async function chooseAgentModel() {
8459
8525
  }
8460
8526
 
8461
8527
  const rows = am.agentModelRows(models, current);
8462
- console.log(dim(` Agent model — the one that TALKS (currently ${current || "unset"})`));
8528
+ console.log(dim(` Agent model — the one that TALKS (currently ${current || "unset"})`));
8463
8529
  // Strip whichever list endpoint was asked, so the header names the SERVER rather than the path.
8464
8530
  console.log(
8465
8531
  dim(` ${models.length} on ${url.replace(/\/api\/tags$/, "").replace(/\/models$/, "")} (${PICK_HINT}):`)
@@ -8488,12 +8554,12 @@ async function chooseAgentModel() {
8488
8554
  // where they can be seen, rather than after every model.
8489
8555
  const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
8490
8556
  if (idx < 0) {
8491
- console.log(dim(" (cancelled)\n"));
8557
+ console.log(dim(" (cancelled)\n"));
8492
8558
  return;
8493
8559
  }
8494
8560
  const chosen = rows[idx].model;
8495
8561
  if (chosen === current) {
8496
- console.log(dim(` already using ${current} — nothing changed.\n`));
8562
+ console.log(dim(` already using ${current} — nothing changed.\n`));
8497
8563
  return;
8498
8564
  }
8499
8565
  if (rows[idx].embedder) {
@@ -8504,7 +8570,7 @@ async function chooseAgentModel() {
8504
8570
  console.log(GUTTER + yellow("produce nonsense rather than an error."));
8505
8571
  const go = await pickFromList([`use it anyway`, `pick something else`], 1);
8506
8572
  if (go !== 0) {
8507
- console.log(dim(" (kept)\n"));
8573
+ console.log(dim(" (kept)\n"));
8508
8574
  return;
8509
8575
  }
8510
8576
  }
@@ -8517,15 +8583,15 @@ async function chooseAgentModel() {
8517
8583
  agentModel: chosen,
8518
8584
  });
8519
8585
  if (!saved) {
8520
- console.log(red(` couldn't save that to your account — still using ${current || "unset"}.`));
8586
+ console.log(red(` couldn't save that to your account — still using ${current || "unset"}.`));
8521
8587
  // The likeliest cause by far, and it is invisible otherwise: this field only became writable
8522
8588
  // through the worker route in task #169, so an older local API silently ignores it.
8523
8589
  console.log(GUTTER + dim("if this machine's local API predates /agent-model, restart it with"));
8524
8590
  console.log(GUTTER + dim("`gu api restart` to pick up the current version.\n"));
8525
8591
  return;
8526
8592
  }
8527
- console.log(accent(` ✓ agent model → ${chosen}`));
8528
- console.log(dim(" takes effect on your next question.\n"));
8593
+ console.log(accent(` ✓ agent model → ${chosen}`));
8594
+ console.log(dim(" takes effect on your next question.\n"));
8529
8595
  }
8530
8596
 
8531
8597
  // Probe whether SSH KEY auth already works for a server (so we can tell the user to run
@@ -8550,7 +8616,7 @@ async function checkServerKeyAuth(s) {
8550
8616
  if (ok) console.log(dim(" ✓ key auth works — deploys will connect without a password."));
8551
8617
  else
8552
8618
  console.log(
8553
- yellow(" key auth not set up yet.") +
8619
+ yellow(" key auth not set up yet.") +
8554
8620
  dim(` Run once in your terminal: `) +
8555
8621
  `ssh-copy-id ${s.user}@${s.host}`
8556
8622
  );
@@ -8750,20 +8816,20 @@ async function runS3Command(line) {
8750
8816
  // and, for the secret, does not have to know it.
8751
8817
  const keep = (typed, was) => (String(typed ?? "").trim() ? String(typed).trim() : was);
8752
8818
  const location = keep(
8753
- await ask(` Bucket (ARN, s3://… or name)${current.location ? ` [${current.location}]` : ""}: `),
8819
+ await ask(` Bucket (ARN, s3://… or name)${current.location ? ` [${current.location}]` : ""}: `),
8754
8820
  current.location
8755
8821
  );
8756
8822
  const region = keep(
8757
- await ask(` Region [${current.region || "ap-southeast-1"}]: `),
8823
+ await ask(` Region [${current.region || "ap-southeast-1"}]: `),
8758
8824
  current.region || "ap-southeast-1"
8759
8825
  );
8760
8826
  const accessKeyId = keep(
8761
- await ask(` AWS access key id${current.accessKeyId ? ` [${s3.maskKeyId(current.accessKeyId)}]` : ""}: `),
8827
+ await ask(` AWS access key id${current.accessKeyId ? ` [${s3.maskKeyId(current.accessKeyId)}]` : ""}: `),
8762
8828
  current.accessKeyId
8763
8829
  );
8764
8830
  // askSecret, never ask: the echo is muted and the value never reaches the scrollback (#127).
8765
8831
  const typedSecret = String(
8766
- await askSecret(` AWS secret${current.secretConfigured ? " [Enter keeps the stored one]" : ""}:`)
8832
+ await askSecret(` AWS secret${current.secretConfigured ? " [Enter keeps the stored one]" : ""}:`)
8767
8833
  ).trim();
8768
8834
 
8769
8835
  if (!location || !accessKeyId || !(typedSecret || current.secretConfigured)) {
@@ -8960,16 +9026,16 @@ async function chooseServer() {
8960
9026
  );
8961
9027
  const data = await res.json().catch(() => ({}));
8962
9028
  if (!res.ok) {
8963
- console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
9029
+ console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
8964
9030
  return;
8965
9031
  }
8966
9032
  servers = Array.isArray(data?.servers) ? data.servers : [];
8967
9033
  } catch (err) {
8968
- console.log(red(` ${describeApiFailure(err, "couldn't load servers")}\n`));
9034
+ console.log(red(` ${describeApiFailure(err, "couldn't load servers")}\n`));
8969
9035
  return;
8970
9036
  }
8971
9037
  if (servers.length === 0) {
8972
- console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
9038
+ console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
8973
9039
  return;
8974
9040
  }
8975
9041
  // Arrow-key picker: ↑/↓ to move, Enter to choose (no number typing). The list ends with
@@ -8977,21 +9043,21 @@ async function chooseServer() {
8977
9043
  const labels = servers.map((s) => `${s.name} (${s.user}@${s.host})`);
8978
9044
  labels.push("none (clear selection)");
8979
9045
  const activeIdx = servers.findIndex((s) => s.host === selectedServer?.host);
8980
- console.log(dim(` Choose a deployment server (${PICK_HINT}):`));
9046
+ console.log(dim(` Choose a deployment server (${PICK_HINT}):`));
8981
9047
  const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
8982
9048
  if (idx < 0) {
8983
- console.log(dim(" (cancelled)\n"));
9049
+ console.log(dim(" (cancelled)\n"));
8984
9050
  return;
8985
9051
  }
8986
9052
  if (idx === servers.length) {
8987
9053
  // the trailing "none" row
8988
9054
  selectedServer = null;
8989
- console.log(dim(" ✓ deployment server cleared.\n"));
9055
+ console.log(dim(" ✓ deployment server cleared.\n"));
8990
9056
  return;
8991
9057
  }
8992
9058
  const s = servers[idx];
8993
9059
  selectedServer = { name: s.name, host: s.host, user: s.user };
8994
- console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
9060
+ console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
8995
9061
  await checkServerKeyAuth(selectedServer);
8996
9062
  console.log("");
8997
9063
  }
@@ -9083,10 +9149,10 @@ async function showOtherAgents() {
9083
9149
  return;
9084
9150
  }
9085
9151
  console.log("");
9086
- console.log(dim(` Your other running agents — pick one to act on (${PICK_HINT}):`));
9152
+ console.log(dim(` Your other running agents — pick one to act on (${PICK_HINT}):`));
9087
9153
  const which = await pickFromList(items.map((i) => i.label), 0);
9088
9154
  if (which < 0) {
9089
- console.log(dim(" (cancelled)\n"));
9155
+ console.log(dim(" (cancelled)\n"));
9090
9156
  paintBar();
9091
9157
  return;
9092
9158
  }
@@ -9109,10 +9175,10 @@ async function showOtherAgents() {
9109
9175
  { id: "stop", label: "Stop it", note: "ends the run — its unsaved work is lost" },
9110
9176
  ];
9111
9177
  console.log("");
9112
- console.log(dim(` ${shortName} (${PICK_HINT}):`));
9178
+ console.log(dim(` ${shortName} (${PICK_HINT}):`));
9113
9179
  const act = await pickFromList(actions.map((a) => a.label + dim(` · ${a.note}`)), 0);
9114
9180
  if (act < 0) {
9115
- console.log(dim(" (cancelled)\n"));
9181
+ console.log(dim(" (cancelled)\n"));
9116
9182
  paintBar();
9117
9183
  return;
9118
9184
  }
@@ -9410,9 +9476,9 @@ async function promptNewCodingKey(backendId = "", provider = "") {
9410
9476
  const id = await addCodingKey(name, secret, backendId);
9411
9477
  console.log(
9412
9478
  id
9413
- ? accent(` ✓ added ${name || "Unknown"}`) +
9479
+ ? accent(` ✓ added ${name || "Unknown"}`) +
9414
9480
  (backendId && provider ? dim(` · for ${provider}`) : "") + "\n"
9415
- : red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
9481
+ : red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
9416
9482
  );
9417
9483
  return id;
9418
9484
  }
@@ -9505,9 +9571,9 @@ async function replaceCodingKey() {
9505
9571
  console.log(
9506
9572
  ok
9507
9573
  ? clearing
9508
- ? accent(` ✓ ${ks.model} API key removed`) + dim(" — coding falls back to your local model\n")
9509
- : accent(` ✓ ${ks.model} API key saved\n`)
9510
- : red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
9574
+ ? accent(` ✓ ${ks.model} API key removed`) + dim(" — coding falls back to your local model\n")
9575
+ : accent(` ✓ ${ks.model} API key saved\n`)
9576
+ : red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
9511
9577
  );
9512
9578
  paintBar();
9513
9579
  }
@@ -9537,7 +9603,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
9537
9603
  backendId ? fetchServerKeys(backendId) : fetchCodingKeys()
9538
9604
  );
9539
9605
  if (!keys) {
9540
- console.log(red(` couldn't read the keys for ${host} — is this machine's API up to date? (\`gu api restart\`)\n`));
9606
+ console.log(red(` couldn't read the keys for ${host} — is this machine's API up to date? (\`gu api restart\`)\n`));
9541
9607
  paintBar();
9542
9608
  return "";
9543
9609
  }
@@ -9582,7 +9648,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
9582
9648
  const fresh = await fetchServerKeys(backendId);
9583
9649
  const added = fresh?.find((k) => k.id === id);
9584
9650
  if (added?.default) {
9585
- console.log(accent(` ✓ ${added.name} is ${host}'s default key\n`));
9651
+ console.log(accent(` ✓ ${added.name} is ${host}'s default key\n`));
9586
9652
  paintBar();
9587
9653
  return id;
9588
9654
  }
@@ -9594,7 +9660,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
9594
9660
  if (!key.backendId && backendId) {
9595
9661
  const ok = await withPendingStatus(`tying ${key.name} to ${host}`, () => bindKeyToServer(key.id, backendId));
9596
9662
  if (!ok) {
9597
- console.log(red(` couldn't tie ${key.name} to ${host} — nothing changed.\n`));
9663
+ console.log(red(` couldn't tie ${key.name} to ${host} — nothing changed.\n`));
9598
9664
  paintBar();
9599
9665
  return "";
9600
9666
  }
@@ -9613,10 +9679,10 @@ async function applyKeyPick({ role, backendId, host, key }) {
9613
9679
  let scope = "folder";
9614
9680
  if (!backendId) {
9615
9681
  await saveFolderKeyPick(role, key.id);
9616
- console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail}\n`));
9682
+ console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail}\n`));
9617
9683
  } else if (key.default) {
9618
9684
  await saveFolderKeyPick(role, "");
9619
- console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · ${host}'s default\n`));
9685
+ console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · ${host}'s default\n`));
9620
9686
  } else {
9621
9687
  const choices = sk.scopeChoices({ host, role });
9622
9688
  console.log("\n" + GUTTER + dim(`Use ${key.name} for…`));
@@ -9630,16 +9696,16 @@ async function applyKeyPick({ role, backendId, host, key }) {
9630
9696
  if (scope === "default") {
9631
9697
  const ok = await withPendingStatus(`making ${key.name} the default for ${host}`, () => setServerDefaultKey(backendId, key.id));
9632
9698
  if (!ok) {
9633
- console.log(red(" couldn't change the default — nothing changed.\n"));
9699
+ console.log(red(" couldn't change the default — nothing changed.\n"));
9634
9700
  paintBar();
9635
9701
  return "";
9636
9702
  }
9637
9703
  // This folder follows the default now, rather than pinning a copy of it.
9638
9704
  await saveFolderKeyPick(role, "");
9639
- console.log(accent(` ✓ ${key.name} is now ${host}'s default`) + dim(` ····${key.tail} · every folder without its own pick\n`));
9705
+ console.log(accent(` ✓ ${key.name} is now ${host}'s default`) + dim(` ····${key.tail} · every folder without its own pick\n`));
9640
9706
  } else {
9641
9707
  await saveFolderKeyPick(role, key.id);
9642
- console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · this folder only\n`));
9708
+ console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · this folder only\n`));
9643
9709
  }
9644
9710
  }
9645
9711
  if (role === "coding") {
@@ -9693,7 +9759,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
9693
9759
  }
9694
9760
  const out = await deleteCodingKey(key.id);
9695
9761
  if (!out.ok) {
9696
- console.log(red(" couldn't delete the key — try again, or remove it in web Settings → Agent\n"));
9762
+ console.log(red(" couldn't delete the key — try again, or remove it in web Settings → Agent\n"));
9697
9763
  paintBar();
9698
9764
  return;
9699
9765
  }
@@ -9702,7 +9768,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
9702
9768
  const mine = r === "agent" ? sessionAgentKeyId : r === "embed" ? sessionEmbedKeyId : sessionCodingKeyId;
9703
9769
  if (mine === key.id) await saveFolderKeyPick(r, "");
9704
9770
  }
9705
- console.log(accent(` ✓ deleted ${out.name || key.name}`) + dim(` ····${key.tail} · ${out.remaining} key(s) left\n`));
9771
+ console.log(accent(` ✓ deleted ${out.name || key.name}`) + dim(` ····${key.tail} · ${out.remaining} key(s) left\n`));
9706
9772
  paintBar();
9707
9773
  }
9708
9774
 
@@ -9970,7 +10036,7 @@ async function approvalPrompt(command) {
9970
10036
  ok = (await runS3Command("/s3")) === true;
9971
10037
  } catch (e) {
9972
10038
  if (!wasPromptCancelled(e)) throw e;
9973
- console.log(dim(" (cancelled — the file is saved on this computer)\n"));
10039
+ console.log(dim(" (cancelled — the file is saved on this computer)\n"));
9974
10040
  } finally {
9975
10041
  following = wasFollowing;
9976
10042
  }
@@ -11088,6 +11154,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11088
11154
  // whether the user has clicked to expand it. Both reset when the step's Thought changes.
11089
11155
  let fullLiveThought = "";
11090
11156
  let thoughtExpanded = false;
11157
+ let liveExpandable = false; // the live thought line has more to show than fits (set each tick)
11091
11158
  // Task #148: the CURRENT step's raw model output, accumulated in full (fenceThought stops at
11092
11159
  // 400 chars — enough for a one-line label, not for the paragraphs we now print). Bounded so a
11093
11160
  // runaway generation cannot grow without limit; the prose we keep is far shorter anyway.
@@ -11298,6 +11365,9 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11298
11365
  // "almost done" > "Thinking". When clickable we drive line 1 from the SAME fuller string, so
11299
11366
  // the grey expansion CONTINUES where line 1 was cut off instead of repeating the head.
11300
11367
  const clickable = !!fullLiveThought && !runningCmd && fullLiveThought.length > max;
11368
+ // #350: the click and Ctrl+T ask the SAME question the highlight does. They used to accept
11369
+ // any thought at all, so a line that was already whole took the click and redrew for nothing.
11370
+ liveExpandable = clickable;
11301
11371
  let label, rest;
11302
11372
  if (clickable) {
11303
11373
  // Split at a WORD boundary near the line-1 width so nothing is cut mid-word: the last
@@ -11450,14 +11520,14 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11450
11520
  if (row < top || row > top + statusContentLines - LEAD_ROWS - WORD_GAP - 2) return;
11451
11521
  // Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
11452
11522
  // so we don't flicker an empty expand.
11453
- if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
11523
+ if (!thoughtExpanded && !liveExpandable) return;
11454
11524
  toggleThought();
11455
11525
  };
11456
11526
  // Same expand/collapse, reachable without the mouse: Ctrl+T while a turn runs (see
11457
11527
  // toggleActiveThought). Ignored when there's nothing more to reveal, so it never flickers an
11458
11528
  // empty expansion.
11459
11529
  const toggleThought = () => {
11460
- if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
11530
+ if (!thoughtExpanded && !liveExpandable) return;
11461
11531
  thoughtExpanded = !thoughtExpanded;
11462
11532
  clearStatus(); // next tick (≤120ms) redraws at the new size; keeps cursor math correct
11463
11533
  };
@@ -12457,7 +12527,10 @@ async function main() {
12457
12527
  // reaches the model over HTTP), so it runs HERE, where the files are, and calls the
12458
12528
  // model machine for tokens. That needs a worker here like any other machine. The earlier
12459
12529
  // build suppressed the daemon entirely, which is why this comment exists.
12460
- if (!status.running) {
12530
+ // GU_NO_WORKER_AUTOSTART=1 (task #351): the terminal UI tests play a scripted turn from a fake
12531
+ // API — no worker is needed, and one started for a throwaway home outlived the test (on
12532
+ // Windows it would be a scheduled task). Nothing a user sets; without it, as always:
12533
+ if (!status.running && process.env.GU_NO_WORKER_AUTOSTART !== "1") {
12461
12534
  // NO LONGER A QUESTION (task #159, user 2026-08-09: "it need to run otherwise cannot work,
12462
12535
  // so, just notify"). It used to ask, and a "no" was then remembered per folder — but the
12463
12536
  // only workable answer was yes: without a worker nothing claims your turns, so a question
@@ -12471,7 +12544,7 @@ async function main() {
12471
12544
  // meaningfully refuse.
12472
12545
  const steps = await import("./first-run-steps.mjs");
12473
12546
  const step = steps.stepById("daemon");
12474
- console.log(dim(` ${step.doing}…`));
12547
+ console.log(dim(` ${step.doing}…`));
12475
12548
  let started = false;
12476
12549
  let why = "";
12477
12550
  try {
@@ -12479,7 +12552,7 @@ async function main() {
12479
12552
  started = !!r.started;
12480
12553
  localDaemonRunning = started || localDaemonRunning;
12481
12554
  if (started) {
12482
- console.log(dim(` started background worker (pid ${r.pid}) · logs ${ctl.LOG_FILE}`));
12555
+ console.log(dim(` started background worker (pid ${r.pid}) · logs ${ctl.LOG_FILE}`));
12483
12556
  } else {
12484
12557
  why = r.reason || "the worker did not report a pid";
12485
12558
  }
@@ -12493,7 +12566,7 @@ async function main() {
12493
12566
  await recordStepOutcome("daemon", started, why);
12494
12567
  if (!started) {
12495
12568
  console.log(
12496
- yellow(" the background worker did not start") +
12569
+ yellow(" the background worker did not start") +
12497
12570
  dim(` — ${why}\n ${step.why}.\n Try \`gu start\` to see the error in full.\n`)
12498
12571
  );
12499
12572
  }
@@ -12613,7 +12686,12 @@ async function main() {
12613
12686
  // private mode's local API can itself be unconfigured.
12614
12687
  if (p.agentMissing === true) agentMissing = true;
12615
12688
  noModelAtAll = nothingToAnswerWith(p);
12616
- if (p.ragEnabled !== true) embedMissing = true;
12689
+ if (p.ragEnabled !== true) {
12690
+ embedMissing = true;
12691
+ // …but "off" has two causes, and only the settings can tell them apart (#347). In the
12692
+ // background: startup never waits on it, and the bar is repainted when the answer is in.
12693
+ void syncEmbedWarning({ repair: true });
12694
+ }
12617
12695
  // A RESTORED PICK MUST STILL EXIST. The stored id names a model on the box this folder was
12618
12696
  // last pointed at — and the box can change (`/model` → change the Ollama server), a model
12619
12697
  // can be removed, or the folder can be opened on another machine entirely. Sending a name
@@ -12797,7 +12875,10 @@ async function main() {
12797
12875
  // check used to sit INSIDE the "chat model is down" branch, so a client pointing at
12798
12876
  // a healthy remote model was never offered them and every turn died with
12799
12877
  // "No module named 'smolagents'". Independent now.
12800
- if (!(await md.hasAgentLibs())) {
12878
+ // GU_NO_AGENT_DEPS_PROMPT=1 (task #351): the terminal UI tests run no agent — on a machine
12879
+ // without the framework every test's throwaway home answered "yes" and sat through a pip
12880
+ // install (found on the Ubuntu server: 21 seconds of downloads, then a timeout).
12881
+ if (process.env.GU_NO_AGENT_DEPS_PROMPT !== "1" && !(await md.hasAgentLibs())) {
12801
12882
  const skipDeps = await loadSessionSkip(resolve(process.cwd()), "skipDepsPrompt");
12802
12883
  if (!skipDeps) {
12803
12884
  console.log(
@@ -12838,7 +12919,7 @@ async function main() {
12838
12919
  } else {
12839
12920
  await saveSessionSkip(resolve(process.cwd()), "skipDepsPrompt", true);
12840
12921
  console.log(
12841
- dim(" Skipped — install later with `gu doctor`") +
12922
+ dim(" Skipped — install later with `gu doctor`") +
12842
12923
  dim(clientModeBanner ? " --client" : "") +
12843
12924
  dim("; I won't ask again in this folder.\n")
12844
12925
  );
@@ -12872,7 +12953,7 @@ async function main() {
12872
12953
  );
12873
12954
  if (skipSetup) {
12874
12955
  console.log(
12875
- dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
12956
+ dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
12876
12957
  dim("any time.\n")
12877
12958
  );
12878
12959
  }
@@ -12900,7 +12981,7 @@ async function main() {
12900
12981
  await ctl2.setClientMode(true);
12901
12982
  const st = await ctl2.daemonStatus();
12902
12983
  console.log(
12903
- dim(" Skipped — this machine won't SERVE models; the agent still runs here and\n") +
12984
+ dim(" Skipped — this machine won't SERVE models; the agent still runs here and\n") +
12904
12985
  dim(" calls your model machine over the network. Point the Agent model URL at\n") +
12905
12986
  dim(" that machine (not 127.0.0.1) — `gu status` shows what it resolves to.\n") +
12906
12987
  dim(" `gu doctor` sets models up here · `/host-mode` undoes this.\n")
@@ -13078,8 +13159,8 @@ async function main() {
13078
13159
  const started = await tryStartLocalApi();
13079
13160
  console.error(
13080
13161
  started
13081
- ? dim(" Started it — run `gu` again.\n")
13082
- : dim(" Start it with: gu-cli api start") +
13162
+ ? dim(" Started it — run `gu` again.\n")
13163
+ : dim(" Start it with: gu-cli api start") +
13083
13164
  dim("\n If that fails, check MongoDB is running and see ~/.gonext/local-api.log\n")
13084
13165
  );
13085
13166
  }
@@ -13338,7 +13419,12 @@ async function main() {
13338
13419
  // TRANSCRIPT: the echo is written exactly once per submitted line and never repainted,
13339
13420
  // so there is nothing here for a blank to accumulate against.
13340
13421
  // As typed, not as sent: "[Image #1]", never the file path the agent needs (#51).
13341
- const echo = PROMPT_PAST + imageMarkersForDisplay(line) + "\n\n";
13422
+ // Every row under the first one's text (#348): a pasted multi-line question used to be
13423
+ // echoed with only its first line indented, the rest hard against the left edge.
13424
+ const echo = questionEchoText(imageMarkersForDisplay(line), {
13425
+ width: process.stdout.columns || 80,
13426
+ paintPrompt: () => PROMPT_PAST,
13427
+ }) + "\n\n";
13342
13428
  if (stopNotice) {
13343
13429
  stopNotice = "";
13344
13430
  if (stopNoticeTimer) clearTimeout(stopNoticeTimer);
@@ -13418,7 +13504,10 @@ async function main() {
13418
13504
  }
13419
13505
  if (line === "/embed-model") {
13420
13506
  await chooseEmbedModel();
13421
- embedMissing = false; // Task #14: an embed model was just chosen → clear its warning
13507
+ // Read back what is SAVED (#347). The warning used to be cleared here whatever happened —
13508
+ // a cancelled menu or a failed save removed it for the session, and it returned at the
13509
+ // next start as if the choice had been forgotten.
13510
+ await syncEmbedWarning({ repair: false });
13422
13511
  continue;
13423
13512
  }
13424
13513
  if (line === "/server") {
@@ -13520,7 +13609,7 @@ async function main() {
13520
13609
  const direct = line === "/rag-local" ? "local" : line === "/rag-cloud" ? "both" : null;
13521
13610
  let chosen = direct;
13522
13611
  if (!chosen) {
13523
- console.log(dim(` Where this folder's RAG knowledge base lives (${PICK_HINT}):`));
13612
+ console.log(dim(` Where this folder's RAG knowledge base lives (${PICK_HINT}):`));
13524
13613
  const idx = await pickFromList(
13525
13614
  RAG_STORAGE.map(
13526
13615
  (r) =>
@@ -13531,7 +13620,7 @@ async function main() {
13531
13620
  Math.max(0, RAG_STORAGE.findIndex((r) => r.id === sessionRagStorage))
13532
13621
  );
13533
13622
  if (idx < 0) {
13534
- console.log(dim(" (cancelled)\n"));
13623
+ console.log(dim(" (cancelled)\n"));
13535
13624
  continue;
13536
13625
  }
13537
13626
  chosen = RAG_STORAGE[idx].id;
@@ -13547,7 +13636,7 @@ async function main() {
13547
13636
  // switching to cloud leaves the local shards on disk unread, and switching to local
13548
13637
  // leaves the S3 ones untouched — either way the new store is built by the next index.
13549
13638
  console.log(
13550
- accent(` ✓ RAG storage → ${row.label}`) +
13639
+ accent(` ✓ RAG storage → ${row.label}`) +
13551
13640
  dim(`\n ${row.note}`) +
13552
13641
  dim("\n Existing shards are not moved — the new store fills as this folder is re-indexed.\n")
13553
13642
  );
@@ -13654,7 +13743,7 @@ async function main() {
13654
13743
  if (!wasPromptCancelled(e)) throw e;
13655
13744
  // The same words the pickers print, for the same event — a cancel should not read as one
13656
13745
  // thing when the answer was typed and another when it was arrowed to.
13657
- console.log(dim(" (cancelled)\n"));
13746
+ console.log(dim(" (cancelled)\n"));
13658
13747
  continue;
13659
13748
  }
13660
13749
  // SWITCH TO ANOTHER AGENT (task #211) — chosen with the mouse in showOtherAgents, delivered