@tiens.nguyen/gu-cli 1.0.876 → 1.0.877

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/gu-repl.mjs +172 -60
  2. package/package.json +1 -1
package/gu-repl.mjs CHANGED
@@ -547,6 +547,10 @@ const waitPaint = (n, s) => (REPL_COLOR ? color256(n, s) : white(s));
547
547
  // cyan only with `gu --color`.
548
548
  const thoughtPaint = (s) => (REPL_COLOR ? accent(s) : white(s));
549
549
  const orange = (s) => color256(208, s); // peak-token label (task #84)
550
+ // A menu's ACTION rows (pick another model…, API keys…, add a provider…) in orange, apart from the
551
+ // models above them (#248). \x1b[22m first: an unselected picker row is wrapped in dim, which would
552
+ // turn the orange grey. Plain text when colour is off (NO_COLOR, TERM=dumb).
553
+ const actionRow = (s) => (dim("x") === "x" ? s : `\x1b[22m${orange(s)}`);
550
554
  // Compact token count: 950 → "950", 12300 → "12.3k", 1500000 → "1.5M" (task #83).
551
555
  const fmtTokens = (n) => {
552
556
  if (n < 1000) return String(n);
@@ -6318,7 +6322,7 @@ async function manageCodingProviders({ back = false } = {}) {
6318
6322
  `${p.model || dim("(no model set)")}${dim(` · ${p.host}`)}${p.enabled ? "" : dim(" · off")}`
6319
6323
  );
6320
6324
  const ADD = labels.length;
6321
- labels.push("add another provider — Claude API, OpenAI, Kimi…");
6325
+ labels.push(actionRow("add another provider — Claude API, OpenAI, Kimi…"));
6322
6326
  console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
6323
6327
  const idx = await pickFromList(labels, selAt, { back });
6324
6328
  if (idx === PICK_BACK) return BACK;
@@ -6355,11 +6359,12 @@ async function manageCodingProviders({ back = false } = {}) {
6355
6359
  }
6356
6360
  }
6357
6361
 
6358
- /** Point a provider at a different model. */
6359
- async function changeProviderModel(ps, rows, p) {
6360
- const md = await import("./model-doctor.mjs");
6362
+ /**
6363
+ * What provider `p` offers, asked with ITS OWN key (#266) — its problems printed as it goes.
6364
+ * Shared by "change the model…" and /model's "pick another model on <host>…" (#248). → [ids]
6365
+ */
6366
+ async function listProviderModels(ps, p) {
6361
6367
  const { proxyFetch } = await import("./proxy-dispatcher.mjs");
6362
-
6363
6368
  // LISTED WITH THIS PROVIDER'S OWN KEY (#266). This used to probe with NO key, on purpose: the
6364
6369
  // only key the terminal could fetch was the ACCOUNT DEFAULT, which may belong to another provider.
6365
6370
  // Since #245 a key belongs to its server, so the key used is the one bound to THIS row — the same
@@ -6407,6 +6412,13 @@ async function changeProviderModel(ps, rows, p) {
6407
6412
  }
6408
6413
  }
6409
6414
 
6415
+ return models;
6416
+ }
6417
+
6418
+ /** Point a provider at a different model. */
6419
+ async function changeProviderModel(ps, rows, p) {
6420
+ const md = await import("./model-doctor.mjs");
6421
+ const models = await listProviderModels(ps, p);
6410
6422
  let model = "";
6411
6423
  if (models.length) {
6412
6424
  model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model, { back: true });
@@ -7118,17 +7130,28 @@ async function chooseModel() {
7118
7130
  // OpenAI-compatible providers are both "OpenAI-compatible", so the kind names neither.
7119
7131
  const { labels, tags, showTag } = codingChoiceLabels(choices, def);
7120
7132
  const activeIdx = choices.findIndex((c) => c.id === active);
7133
+ // THE CURRENT CODER'S KEYS, ON THIS LIST (#248) — the manager ctrl+k opens, for a provider that
7134
+ // takes a key (OpenAI-compatible, ollama.com). Before the add row, so no model row moves.
7135
+ const activeChoice = choices[activeIdx >= 0 ? activeIdx : choices.findIndex((c) => c.id === def)];
7136
+ const activeUrl = String(activeChoice?.url ?? "");
7137
+ const keyHost = activeChoice && ((activeChoice.kind ?? "") === "openai" || /(^|\.)ollama\.com$/i.test(hostOf(activeUrl))) ? hostOf(activeUrl) : "";
7138
+ // …and the rest of that server's models, the same row /agent-model has (#248). Not for local MLX.
7139
+ const modelsHost = activeChoice && ["openai", "ollama"].includes(activeChoice.kind ?? "") && activeUrl ? hostOf(activeUrl) : "";
7140
+ const MODELS = modelsHost ? labels.length : -1;
7141
+ if (modelsHost) labels.push(actionRow(`pick another model on ${modelsHost}…`));
7142
+ const KEYS = keyHost ? labels.length : -1;
7143
+ if (keyHost) labels.push(actionRow(`API keys for ${keyHost} — switch, add or remove…`));
7121
7144
  // Task #223. Appended LAST, after the models: a new row must never shift an index someone
7122
7145
  // already has muscle memory for.
7123
7146
  //
7124
7147
  // "add or change" rather than "add", because add-only was a one-way door: a model id typed
7125
7148
  // wrong could not be corrected from anywhere, here or in web Settings. The entry leads to the
7126
7149
  // add flow directly when there is nothing to change, so it costs a word and no extra step.
7127
- labels.push(
7150
+ labels.push(actionRow(
7128
7151
  choices.some((c) => (c.kind ?? "") === "openai")
7129
7152
  ? "add or change a provider — Claude API, OpenAI, Kimi…"
7130
7153
  : "add a provider — Claude API, OpenAI, Kimi…"
7131
- );
7154
+ ));
7132
7155
  // THREE MODELS ARE IN PLAY AND THIS COMMAND ONLY CHANGES ONE (task #169 req 5). "/model" has
7133
7156
  // meant the CODING model since task #44, but "agent model" and "agent coding model" are close
7134
7157
  // enough in name to have already been mixed up. The fix is to say which one THIS is — not to
@@ -7140,7 +7163,15 @@ async function chooseModel() {
7140
7163
  console.log(dim(" (cancelled)\n"));
7141
7164
  return;
7142
7165
  }
7143
- // The one appended entry sits past the last model.
7166
+ if (idx === MODELS) {
7167
+ if ((await pickModelOnCoderServer(activeChoice, def)) === BACK) return chooseModel();
7168
+ return;
7169
+ }
7170
+ if (idx === KEYS) {
7171
+ if ((await replaceCodingKey({ back: true })) === BACK) return chooseModel();
7172
+ return;
7173
+ }
7174
+ // The add entry sits past the last model (and the keys row).
7144
7175
  if (idx >= choices.length) {
7145
7176
  if ((await manageCodingProviders({ back: true })) === BACK) return chooseModel();
7146
7177
  return;
@@ -7148,6 +7179,62 @@ async function chooseModel() {
7148
7179
  await applyChosenCoder(choices[idx], { def, tag: showTag ? tags[idx] : "" });
7149
7180
  }
7150
7181
 
7182
+ /**
7183
+ * /model's "pick another model on <host>…" (#248): the current coder's server, asked for everything
7184
+ * it has (with its own key). The pick is ADDED to that provider's models — its main model stays —
7185
+ * and becomes the coder. `← back` returns BACK to /model.
7186
+ */
7187
+ async function pickModelOnCoderServer(active, def) {
7188
+ const ps = await import("./coding-provider-setup.mjs");
7189
+ const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
7190
+ if (!settings) {
7191
+ console.log(red(" couldn't reach the API to read your providers.\n"));
7192
+ return;
7193
+ }
7194
+ const rows = ps.existingBackendRows(settings);
7195
+ const same = (u) => ps.ensureV1Root(String(u ?? "")).toLowerCase();
7196
+ const p = ps.manageableProviders(rows).find((x) => same(x.url) === same(active.url));
7197
+ // No saved row behind this coder (an account default from elsewhere): the provider manager.
7198
+ if (!p) return manageCodingProviders({ back: true });
7199
+ const models = await listProviderModels(ps, p);
7200
+ let model;
7201
+ if (models.length) {
7202
+ model = await chooseCodingModelFrom(ps, models, `Which model on ${p.host}?`, active.model, { back: true });
7203
+ if (model === BACK) return BACK;
7204
+ } else {
7205
+ // Not listed (no key, refused, down) — the lines above said so: the name is typed instead.
7206
+ model = (await ask(" model name (Enter to cancel): ")).trim() || null;
7207
+ }
7208
+ if (model === null) {
7209
+ console.log(dim(" (nothing changed)\n"));
7210
+ return;
7211
+ }
7212
+ const row = rows.find((b) => (b.id ?? b.kind) === p.id);
7213
+ const extras = Array.isArray(row?.models) ? row.models : [];
7214
+ if (model !== p.model && !extras.includes(model)) {
7215
+ const md = await import("./model-doctor.mjs");
7216
+ const saved = await withPendingStatus("saving", () =>
7217
+ md.saveWorkerSettings(apiBase, workerKey, {
7218
+ agentCodingBackends: rows.map((b) => ((b.id ?? b.kind) === p.id ? { ...b, models: [...extras, model] } : b)),
7219
+ })
7220
+ );
7221
+ if (!saved) {
7222
+ console.log(red(" couldn't save that — nothing changed.\n"));
7223
+ return;
7224
+ }
7225
+ }
7226
+ // The choice as the API now lists it — its id is the API's to make, not this side's.
7227
+ const probe = await withPendingStatus("loading models", () => fetchAgentPayload([{ role: "user", content: "ping" }])).catch(() => null);
7228
+ const chosen = (Array.isArray(probe?.codingChoices) ? probe.codingChoices : []).find(
7229
+ (c) => c.model === model && same(c.url) === same(p.url)
7230
+ );
7231
+ if (!chosen) {
7232
+ console.log(accent(` ✓ ${model} added to ${p.host}`) + dim(" · pick it in /model\n"));
7233
+ return;
7234
+ }
7235
+ await applyChosenCoder(chosen, { def: String(probe?.codingDefault ?? def ?? "").trim() });
7236
+ }
7237
+
7151
7238
  /**
7152
7239
  * Make `chosen` ({ id, model, kind, url }) the coding model: session state, saved choice, the key
7153
7240
  * check and the lifetime token total. The tail of /model, extracted so /quick-models' ← / → (#233)
@@ -8746,25 +8833,6 @@ async function confirmEmbedSwitch(ec, current, chosen) {
8746
8833
  return { changed: true, plan };
8747
8834
  }
8748
8835
 
8749
- // WHICH EMBEDDER, ASKED ONCE. The model sub-flow of /embed-model: the curated picker, then the
8750
- // shared recognition step. Returns { changed, chosen, plan }.
8751
- async function pickEmbedModel(ec, current, { back = false } = {}) {
8752
- const rows = ec.embedChoiceRows(current);
8753
- console.log(dim(` Choose the embedder (${PICK_HINT}):`));
8754
- const labels = rows.map(
8755
- (r) => `${r.model}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}` + (r.current ? accent(" (current)") : "")
8756
- );
8757
- const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)), { back });
8758
- if (idx === PICK_BACK) return BACK;
8759
- if (idx < 0) {
8760
- console.log(dim(" (cancelled)\n"));
8761
- return { changed: false, chosen: "", plan: null };
8762
- }
8763
- const chosen = rows[idx].model;
8764
- const sw = await confirmEmbedSwitch(ec, current, chosen);
8765
- return { changed: sw.changed, chosen: sw.changed ? chosen : "", plan: sw.plan };
8766
- }
8767
-
8768
8836
  // WHAT A SAVED CHANGE SOUNDS LIKE. Success lines used to live at the end of one long function;
8769
8837
  // four exit paths now say them, so they are said once here: the embedder line, the rebuild note
8770
8838
  // that only a REAL model change earns, and the pull hint — Ollama serves the embedder itself, so
@@ -8791,31 +8859,57 @@ async function chooseEmbedModel() {
8791
8859
  const current = ec.currentEmbedModel(settings);
8792
8860
  const currentUrl = String(settings.ragEmbedUrl || "").trim() || "http://127.0.0.1:11434";
8793
8861
  const host = hostOf(currentUrl);
8794
- // WHEN TO SHOW THE KEY ROW (embedMenuOptions' keyRequired): a key is clearly the question when
8862
+ // WHEN TO SHOW THE KEY ROW (keyRequired): a key is clearly the question when
8795
8863
  // the server is ollama.com (it rejects anonymous calls) or already looks OpenAI-compatible (the
8796
8864
  // /v1 shape), or when one is already bound — replacing the key people can no longer read is the
8797
8865
  // very thing they come back for. A keyless localhost Ollama sees three rows instead.
8798
8866
  const keyRequired = isOllamaCloudUrl(currentUrl) || /\/v1\/?$/i.test(currentUrl) ||
8799
8867
  Boolean(String(settings.ragEmbedKeyId ?? "").trim());
8800
- const menu = ec.embedMenuOptions({ current, host, keyRequired });
8801
- console.log(dim(` Embedding model (currently ${current} on ${host || currentUrl})`));
8802
- const mi = await pickFromList(menu.map((o) => o.label), 0);
8803
- const action = mi >= 0 ? menu[mi].action : "cancel";
8804
- if (action === "cancel") {
8868
+ // ONE LIST, THE SHAPE OF /model AND /agent-model (#248): the embedders, the current one marked,
8869
+ // then this server's own rows in orange — its other models, its keys — and add a provider. It was
8870
+ // a three-row menu first ("switch the embedder model…", "use another provider…", the key), one
8871
+ // step more than the other two commands.
8872
+ // "(current)" only for an embedder that was SET — nothing set means the default, which its own
8873
+ // note already says. Right after the name: at the end of the row an 80-column clip cut it off.
8874
+ const isSet = Boolean(String(settings.ragEmbedModel ?? "").trim());
8875
+ const rows = ec.embedChoiceRows(isSet ? current : "");
8876
+ const labels = rows.map(
8877
+ (r) => `${r.model}${r.current ? accent(" (current)") : ""}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}`
8878
+ );
8879
+ const MODELS = labels.length;
8880
+ labels.push(actionRow(`pick another model on ${host || currentUrl}…`));
8881
+ const KEYS = keyRequired ? labels.length : -1;
8882
+ if (keyRequired) labels.push(actionRow(`API keys for ${host || currentUrl} — switch, add or remove…`));
8883
+ const ADD = labels.length;
8884
+ labels.push(actionRow("add a provider — OpenAI, Ollama…"));
8885
+ const head = ` Embedding model (now ${current} on ${host || currentUrl})`;
8886
+ const hint = `(${PICK_HINT}):`;
8887
+ const cols = Math.max(20, process.stdout.columns || 80);
8888
+ console.log(dim(head.length + 2 + hint.length < cols ? `${head} ${hint}` : `${head}\n ${hint}`));
8889
+ // The cursor starts on the one in use: the set one, else the default.
8890
+ const inUse = rows.findIndex((r) => ec.sameEmbedModel(r.model, current));
8891
+ const mi = await pickFromList(labels, Math.max(0, inUse));
8892
+ if (mi < 0) {
8805
8893
  console.log(dim(" (cancelled)\n"));
8806
8894
  return;
8807
8895
  }
8896
+ const action = mi === MODELS ? "server" : mi === KEYS ? "key" : mi === ADD ? "provider" : "model";
8808
8897
 
8809
8898
  // EVERY ARM IS THE ONE ADD-SERVER SCREEN (task #245) — the same URL → key → models → save as
8810
- // /model and /agent-model. The curated arm has already chosen the model (and passed the
8899
+ // /model and /agent-model. A listed embedder has been chosen already (and passed the
8811
8900
  // recognition wall), so it only asks WHERE; the other two list what the server has.
8812
- // "← back" in any arm returns to this menu (#242).
8901
+ // "← back" in any arm returns to this list (#242).
8813
8902
  if (action === "model") {
8814
- const picked = await pickEmbedModel(ec, current, { back: true });
8815
- if (picked === BACK) return chooseEmbedModel();
8816
- const { changed, chosen, plan } = picked;
8817
- if (!changed) return;
8818
- await addServer({ role: "embed", dialect: "ollama", model: chosen, plan });
8903
+ const chosen = rows[mi].model;
8904
+ const sw = await confirmEmbedSwitch(ec, current, chosen);
8905
+ if (!sw.changed) return;
8906
+ await addServer({ role: "embed", dialect: "ollama", model: chosen, plan: sw.plan });
8907
+ return;
8908
+ }
8909
+ if (action === "server") {
8910
+ // That server's own list, with its key — the add-server screen with the URL known.
8911
+ const dialect = /\/v1\/?$/i.test(currentUrl) ? "openai" : "ollama";
8912
+ if ((await addServer({ role: "embed", dialect, url: currentUrl, back: true })) === BACK) return chooseEmbedModel();
8819
8913
  return;
8820
8914
  }
8821
8915
  if (action === "provider") {
@@ -8970,12 +9064,18 @@ async function chooseAgentModel() {
8970
9064
  if (!saved.length) return agentModelOnServer();
8971
9065
  const current = am.currentAgentModel(settings);
8972
9066
  const curHost = settings.agentModelUrl ? hostOf(String(settings.agentModelUrl)) : "";
9067
+ // THE CURRENT PROVIDER'S OWN ROWS, ON THIS LIST (#248). They were one step down, behind "other
9068
+ // models, providers and API keys…" — which read like the "add a provider" row under it.
9069
+ const source = am.agentListSource(settings);
9070
+ const needsKey = am.agentSourceNeedsKey(source);
8973
9071
  for (;;) {
8974
9072
  const labels = saved.map((c) => c.label + (c.current ? accent(" (current)") : ""));
8975
- const MORE = labels.length;
8976
- labels.push("other models, providers and API keys…");
9073
+ const MODELS = curHost ? labels.length : -1;
9074
+ if (curHost) labels.push(actionRow(`pick another model on ${curHost}…`));
9075
+ const KEYS = curHost && needsKey ? labels.length : -1;
9076
+ if (KEYS >= 0) labels.push(actionRow(`API keys for ${curHost} — switch, add or remove…`));
8977
9077
  const ADD = labels.length;
8978
- labels.push("add a provider — Claude API, OpenAI, Kimi, Ollama…");
9078
+ labels.push(actionRow("add a provider — Claude API, OpenAI, Kimi, Ollama…"));
8979
9079
  // The hint drops to its own row when the header would wrap (80 columns over SSH).
8980
9080
  const head = ` Agent model — the one that TALKS (now ${current || "unset"}${curHost ? ` on ${curHost}` : ""})`;
8981
9081
  const hint = `(${PICK_HINT}):`;
@@ -8986,8 +9086,15 @@ async function chooseAgentModel() {
8986
9086
  console.log(dim(" (cancelled)\n"));
8987
9087
  return;
8988
9088
  }
8989
- if (idx === MORE) {
8990
- if ((await agentModelOnServer({ back: true })) === BACK) continue;
9089
+ if (idx === MODELS) {
9090
+ // Straight to that server's full list; when it can't be listed, its recovery menu instead.
9091
+ if ((await agentModelOnServer({ back: true, start: "models" })) === BACK) continue;
9092
+ return;
9093
+ }
9094
+ if (idx === KEYS) {
9095
+ const rowId = String(source.row?.id ?? "").trim() || String(source.row?.kind ?? "").trim();
9096
+ if (!rowId) return enterAgentProviderKey(source.row, curHost);
9097
+ if ((await manageServerKeys({ role: "agent", backendId: rowId, host: curHost, model: current, back: true })) === BACK) continue;
8991
9098
  return;
8992
9099
  }
8993
9100
  if (idx === ADD) {
@@ -9025,10 +9132,12 @@ async function useSavedAgentChoice(c) {
9025
9132
 
9026
9133
  /**
9027
9134
  * One server's view: its full model list, other providers, its keys — what /agent-model was before
9028
- * #246, now behind "other models, providers and API keys…" when something is already set up.
9029
- * `back`: opened from that list, so its first menu offers "← back".
9135
+ * #246. With models already set up, /agent-model's list opens it as "pick another model on <host>…"
9136
+ * (#248): `start: "models"` skips the what-to-change menu and goes to the model list.
9137
+ * `back`: opened from that list, so its first screen offers "← back" and returns BACK.
9030
9138
  */
9031
- async function agentModelOnServer({ back = false } = {}) {
9139
+ async function agentModelOnServer({ back = false, start = "menu" } = {}) {
9140
+ const again = () => agentModelOnServer({ back, start });
9032
9141
  const am = await import("./agent-model-command.mjs");
9033
9142
  const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
9034
9143
  if (!settings) {
@@ -9113,7 +9222,7 @@ async function agentModelOnServer({ back = false } = {}) {
9113
9222
  if (action === "key") await enterAgentProviderKey(source.row, listHost);
9114
9223
  else if (action === "provider") {
9115
9224
  // "← back" on "Which provider?" returns to this menu (#247).
9116
- if ((await addServer({ role: "agent", dialect: "openai", back: true })) === BACK) return agentModelOnServer({ back });
9225
+ if ((await addServer({ role: "agent", dialect: "openai", back: true })) === BACK) return again();
9117
9226
  } else console.log("");
9118
9227
  return;
9119
9228
  }
@@ -9121,18 +9230,21 @@ async function agentModelOnServer({ back = false } = {}) {
9121
9230
  // WHAT TO CHANGE, FIRST (#44). The ways to use an Ollama server or another provider used to sit
9122
9231
  // after every model on this one — 130 rows down on api.openai.com — which read as "OpenAI models
9123
9232
  // are all you can choose". A short menu of what to change comes before any list.
9124
- console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
9125
- const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
9126
- const first = await pickFromList(menu.map((o) => o.label), 0, { back });
9127
- if (first === PICK_BACK) return BACK;
9128
- const firstAction = first >= 0 ? menu[first].action : "cancel";
9233
+ let firstAction = "models";
9234
+ if (start !== "models") {
9235
+ console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
9236
+ const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
9237
+ const first = await pickFromList(menu.map((o) => o.label), 0, { back });
9238
+ if (first === PICK_BACK) return BACK;
9239
+ firstAction = first >= 0 ? menu[first].action : "cancel";
9240
+ }
9129
9241
  if (firstAction === "cancel") {
9130
9242
  console.log(dim(" (cancelled)\n"));
9131
9243
  return;
9132
9244
  }
9133
9245
  // "← back" in any of these returns to this menu (#242) — read again, as on a first open.
9134
9246
  if (firstAction === "provider") {
9135
- if ((await chooseAgentProvider({ back: true })) === BACK) return agentModelOnServer({ back });
9247
+ if ((await chooseAgentProvider({ back: true })) === BACK) return again();
9136
9248
  return;
9137
9249
  }
9138
9250
  if (firstAction === "key") {
@@ -9140,7 +9252,7 @@ async function agentModelOnServer({ back = false } = {}) {
9140
9252
  const rowId = String(source.row?.id ?? "").trim() || String(source.row?.kind ?? "").trim();
9141
9253
  if (!rowId) return enterAgentProviderKey(source.row, listHost);
9142
9254
  const r = await manageServerKeys({ role: "agent", backendId: rowId, host: listHost, model: am.currentAgentModel(settings), back: true });
9143
- if (r === BACK) return agentModelOnServer({ back });
9255
+ if (r === BACK) return again();
9144
9256
  return;
9145
9257
  }
9146
9258
 
@@ -9173,7 +9285,8 @@ async function agentModelOnServer({ back = false } = {}) {
9173
9285
  // No other entries here any more (#44): the ways to change the provider are on the menu above,
9174
9286
  // where they can be seen, rather than after every model.
9175
9287
  const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)), { back: true });
9176
- if (idx === PICK_BACK) return agentModelOnServer({ back });
9288
+ // Opened as "pick another model on <host>…", back is /agent-model's list; else this menu.
9289
+ if (idx === PICK_BACK) return start === "models" ? BACK : again();
9177
9290
  if (idx < 0) {
9178
9291
  console.log(dim(" (cancelled)\n"));
9179
9292
  return;
@@ -10356,7 +10469,7 @@ async function promptNewCodingKey(backendId = "", provider = "") {
10356
10469
  return id;
10357
10470
  }
10358
10471
 
10359
- async function replaceCodingKey() {
10472
+ async function replaceCodingKey({ back = false } = {}) {
10360
10473
  const ks = await withPendingStatus("checking the coding model", () =>
10361
10474
  coderKeyState(sessionCodingModel)
10362
10475
  );
@@ -10391,8 +10504,7 @@ async function replaceCodingKey() {
10391
10504
  const keys = allKeys ? ps.keysForBackend(allKeys, ks.backendId) : null;
10392
10505
  if (keys && keys.length >= 1) {
10393
10506
  // ONE SERVER'S KEYS (task #245): the same picker /agent-model and /embed-model open.
10394
- await manageServerKeys({ role: "coding", backendId: ks.backendId, host: ks.provider || ks.backendId || "this server", model: ks.model });
10395
- return;
10507
+ return manageServerKeys({ role: "coding", backendId: ks.backendId, host: ks.provider || ks.backendId || "this server", model: ks.model, back });
10396
10508
  }
10397
10509
 
10398
10510
  // NO KEYS YET, BUT AN API THAT UNDERSTANDS NAMES — so ask for one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.876",
3
+ "version": "1.0.877",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",