@tiens.nguyen/gu-cli 1.0.876 → 1.0.878
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 +186 -65
- 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);
|
|
@@ -564,6 +568,10 @@ const fmtTokens = (n) => {
|
|
|
564
568
|
* naming it here would imply the picker is something you have to break out of.
|
|
565
569
|
*/
|
|
566
570
|
const PICK_HINT = "↑/↓ then Enter · Esc to cancel";
|
|
571
|
+
// #249: a list whose last rows are orange actions says Esc UNDER them (pickFromList's footer);
|
|
572
|
+
// its header keeps only the arrows, so the instruction is not given twice.
|
|
573
|
+
const PICK_ARROWS = "↑/↓ then Enter";
|
|
574
|
+
const ESC_FOOTER = "Esc to cancel";
|
|
567
575
|
const PICK_MANY_HINT = "↑/↓ · space toggles · ctrl+a all · Enter saves · Esc cancels";
|
|
568
576
|
// How many EXTRA models one server offers alongside its default. Task #245 made it a rule, not a
|
|
569
577
|
// readability cap: a server URL offers AT MOST 3 models, default included (MAX_MODELS_PER_SERVER
|
|
@@ -6318,7 +6326,7 @@ async function manageCodingProviders({ back = false } = {}) {
|
|
|
6318
6326
|
`${p.model || dim("(no model set)")}${dim(` · ${p.host}`)}${p.enabled ? "" : dim(" · off")}`
|
|
6319
6327
|
);
|
|
6320
6328
|
const ADD = labels.length;
|
|
6321
|
-
labels.push("add another provider — Claude API, OpenAI, Kimi…");
|
|
6329
|
+
labels.push(actionRow("add another provider — Claude API, OpenAI, Kimi…"));
|
|
6322
6330
|
console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
|
|
6323
6331
|
const idx = await pickFromList(labels, selAt, { back });
|
|
6324
6332
|
if (idx === PICK_BACK) return BACK;
|
|
@@ -6355,11 +6363,12 @@ async function manageCodingProviders({ back = false } = {}) {
|
|
|
6355
6363
|
}
|
|
6356
6364
|
}
|
|
6357
6365
|
|
|
6358
|
-
/**
|
|
6359
|
-
|
|
6360
|
-
|
|
6366
|
+
/**
|
|
6367
|
+
* What provider `p` offers, asked with ITS OWN key (#266) — its problems printed as it goes.
|
|
6368
|
+
* Shared by "change the model…" and /model's "pick another model on <host>…" (#248). → [ids]
|
|
6369
|
+
*/
|
|
6370
|
+
async function listProviderModels(ps, p) {
|
|
6361
6371
|
const { proxyFetch } = await import("./proxy-dispatcher.mjs");
|
|
6362
|
-
|
|
6363
6372
|
// LISTED WITH THIS PROVIDER'S OWN KEY (#266). This used to probe with NO key, on purpose: the
|
|
6364
6373
|
// only key the terminal could fetch was the ACCOUNT DEFAULT, which may belong to another provider.
|
|
6365
6374
|
// Since #245 a key belongs to its server, so the key used is the one bound to THIS row — the same
|
|
@@ -6407,6 +6416,13 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6407
6416
|
}
|
|
6408
6417
|
}
|
|
6409
6418
|
|
|
6419
|
+
return models;
|
|
6420
|
+
}
|
|
6421
|
+
|
|
6422
|
+
/** Point a provider at a different model. */
|
|
6423
|
+
async function changeProviderModel(ps, rows, p) {
|
|
6424
|
+
const md = await import("./model-doctor.mjs");
|
|
6425
|
+
const models = await listProviderModels(ps, p);
|
|
6410
6426
|
let model = "";
|
|
6411
6427
|
if (models.length) {
|
|
6412
6428
|
model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model, { back: true });
|
|
@@ -7118,29 +7134,48 @@ async function chooseModel() {
|
|
|
7118
7134
|
// OpenAI-compatible providers are both "OpenAI-compatible", so the kind names neither.
|
|
7119
7135
|
const { labels, tags, showTag } = codingChoiceLabels(choices, def);
|
|
7120
7136
|
const activeIdx = choices.findIndex((c) => c.id === active);
|
|
7137
|
+
// THE CURRENT CODER'S KEYS, ON THIS LIST (#248) — the manager ctrl+k opens, for a provider that
|
|
7138
|
+
// takes a key (OpenAI-compatible, ollama.com). Before the add row, so no model row moves.
|
|
7139
|
+
const activeChoice = choices[activeIdx >= 0 ? activeIdx : choices.findIndex((c) => c.id === def)];
|
|
7140
|
+
const activeUrl = String(activeChoice?.url ?? "");
|
|
7141
|
+
const keyHost = activeChoice && ((activeChoice.kind ?? "") === "openai" || /(^|\.)ollama\.com$/i.test(hostOf(activeUrl))) ? hostOf(activeUrl) : "";
|
|
7142
|
+
// …and the rest of that server's models, the same row /agent-model has (#248). Not for local MLX.
|
|
7143
|
+
const modelsHost = activeChoice && ["openai", "ollama"].includes(activeChoice.kind ?? "") && activeUrl ? hostOf(activeUrl) : "";
|
|
7144
|
+
const MODELS = modelsHost ? labels.length : -1;
|
|
7145
|
+
if (modelsHost) labels.push(actionRow(`pick another model on ${modelsHost}…`));
|
|
7146
|
+
const KEYS = keyHost ? labels.length : -1;
|
|
7147
|
+
if (keyHost) labels.push(actionRow(`API keys for ${keyHost} — switch, add or remove…`));
|
|
7121
7148
|
// Task #223. Appended LAST, after the models: a new row must never shift an index someone
|
|
7122
7149
|
// already has muscle memory for.
|
|
7123
7150
|
//
|
|
7124
7151
|
// "add or change" rather than "add", because add-only was a one-way door: a model id typed
|
|
7125
7152
|
// wrong could not be corrected from anywhere, here or in web Settings. The entry leads to the
|
|
7126
7153
|
// add flow directly when there is nothing to change, so it costs a word and no extra step.
|
|
7127
|
-
labels.push(
|
|
7154
|
+
labels.push(actionRow(
|
|
7128
7155
|
choices.some((c) => (c.kind ?? "") === "openai")
|
|
7129
7156
|
? "add or change a provider — Claude API, OpenAI, Kimi…"
|
|
7130
7157
|
: "add a provider — Claude API, OpenAI, Kimi…"
|
|
7131
|
-
);
|
|
7158
|
+
));
|
|
7132
7159
|
// THREE MODELS ARE IN PLAY AND THIS COMMAND ONLY CHANGES ONE (task #169 req 5). "/model" has
|
|
7133
7160
|
// meant the CODING model since task #44, but "agent model" and "agent coding model" are close
|
|
7134
7161
|
// enough in name to have already been mixed up. The fix is to say which one THIS is — not to
|
|
7135
7162
|
// print the other two here: the agent model and the embedder are independent settings with
|
|
7136
7163
|
// their own commands, and listing them together implies a relationship that does not exist.
|
|
7137
|
-
console.log(dim(` Choose the CODING model — the one that writes code (${
|
|
7138
|
-
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
7164
|
+
console.log(dim(` Choose the CODING model — the one that writes code (${PICK_ARROWS}):`));
|
|
7165
|
+
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0, { footer: ESC_FOOTER });
|
|
7139
7166
|
if (idx < 0) {
|
|
7140
7167
|
console.log(dim(" (cancelled)\n"));
|
|
7141
7168
|
return;
|
|
7142
7169
|
}
|
|
7143
|
-
|
|
7170
|
+
if (idx === MODELS) {
|
|
7171
|
+
if ((await pickModelOnCoderServer(activeChoice, def)) === BACK) return chooseModel();
|
|
7172
|
+
return;
|
|
7173
|
+
}
|
|
7174
|
+
if (idx === KEYS) {
|
|
7175
|
+
if ((await replaceCodingKey({ back: true })) === BACK) return chooseModel();
|
|
7176
|
+
return;
|
|
7177
|
+
}
|
|
7178
|
+
// The add entry sits past the last model (and the keys row).
|
|
7144
7179
|
if (idx >= choices.length) {
|
|
7145
7180
|
if ((await manageCodingProviders({ back: true })) === BACK) return chooseModel();
|
|
7146
7181
|
return;
|
|
@@ -7148,6 +7183,62 @@ async function chooseModel() {
|
|
|
7148
7183
|
await applyChosenCoder(choices[idx], { def, tag: showTag ? tags[idx] : "" });
|
|
7149
7184
|
}
|
|
7150
7185
|
|
|
7186
|
+
/**
|
|
7187
|
+
* /model's "pick another model on <host>…" (#248): the current coder's server, asked for everything
|
|
7188
|
+
* it has (with its own key). The pick is ADDED to that provider's models — its main model stays —
|
|
7189
|
+
* and becomes the coder. `← back` returns BACK to /model.
|
|
7190
|
+
*/
|
|
7191
|
+
async function pickModelOnCoderServer(active, def) {
|
|
7192
|
+
const ps = await import("./coding-provider-setup.mjs");
|
|
7193
|
+
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
7194
|
+
if (!settings) {
|
|
7195
|
+
console.log(red(" couldn't reach the API to read your providers.\n"));
|
|
7196
|
+
return;
|
|
7197
|
+
}
|
|
7198
|
+
const rows = ps.existingBackendRows(settings);
|
|
7199
|
+
const same = (u) => ps.ensureV1Root(String(u ?? "")).toLowerCase();
|
|
7200
|
+
const p = ps.manageableProviders(rows).find((x) => same(x.url) === same(active.url));
|
|
7201
|
+
// No saved row behind this coder (an account default from elsewhere): the provider manager.
|
|
7202
|
+
if (!p) return manageCodingProviders({ back: true });
|
|
7203
|
+
const models = await listProviderModels(ps, p);
|
|
7204
|
+
let model;
|
|
7205
|
+
if (models.length) {
|
|
7206
|
+
model = await chooseCodingModelFrom(ps, models, `Which model on ${p.host}?`, active.model, { back: true });
|
|
7207
|
+
if (model === BACK) return BACK;
|
|
7208
|
+
} else {
|
|
7209
|
+
// Not listed (no key, refused, down) — the lines above said so: the name is typed instead.
|
|
7210
|
+
model = (await ask(" model name (Enter to cancel): ")).trim() || null;
|
|
7211
|
+
}
|
|
7212
|
+
if (model === null) {
|
|
7213
|
+
console.log(dim(" (nothing changed)\n"));
|
|
7214
|
+
return;
|
|
7215
|
+
}
|
|
7216
|
+
const row = rows.find((b) => (b.id ?? b.kind) === p.id);
|
|
7217
|
+
const extras = Array.isArray(row?.models) ? row.models : [];
|
|
7218
|
+
if (model !== p.model && !extras.includes(model)) {
|
|
7219
|
+
const md = await import("./model-doctor.mjs");
|
|
7220
|
+
const saved = await withPendingStatus("saving", () =>
|
|
7221
|
+
md.saveWorkerSettings(apiBase, workerKey, {
|
|
7222
|
+
agentCodingBackends: rows.map((b) => ((b.id ?? b.kind) === p.id ? { ...b, models: [...extras, model] } : b)),
|
|
7223
|
+
})
|
|
7224
|
+
);
|
|
7225
|
+
if (!saved) {
|
|
7226
|
+
console.log(red(" couldn't save that — nothing changed.\n"));
|
|
7227
|
+
return;
|
|
7228
|
+
}
|
|
7229
|
+
}
|
|
7230
|
+
// The choice as the API now lists it — its id is the API's to make, not this side's.
|
|
7231
|
+
const probe = await withPendingStatus("loading models", () => fetchAgentPayload([{ role: "user", content: "ping" }])).catch(() => null);
|
|
7232
|
+
const chosen = (Array.isArray(probe?.codingChoices) ? probe.codingChoices : []).find(
|
|
7233
|
+
(c) => c.model === model && same(c.url) === same(p.url)
|
|
7234
|
+
);
|
|
7235
|
+
if (!chosen) {
|
|
7236
|
+
console.log(accent(` ✓ ${model} added to ${p.host}`) + dim(" · pick it in /model\n"));
|
|
7237
|
+
return;
|
|
7238
|
+
}
|
|
7239
|
+
await applyChosenCoder(chosen, { def: String(probe?.codingDefault ?? def ?? "").trim() });
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7151
7242
|
/**
|
|
7152
7243
|
* Make `chosen` ({ id, model, kind, url }) the coding model: session state, saved choice, the key
|
|
7153
7244
|
* check and the lifetime token total. The tail of /model, extracted so /quick-models' ← / → (#233)
|
|
@@ -8746,25 +8837,6 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8746
8837
|
return { changed: true, plan };
|
|
8747
8838
|
}
|
|
8748
8839
|
|
|
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
8840
|
// WHAT A SAVED CHANGE SOUNDS LIKE. Success lines used to live at the end of one long function;
|
|
8769
8841
|
// four exit paths now say them, so they are said once here: the embedder line, the rebuild note
|
|
8770
8842
|
// that only a REAL model change earns, and the pull hint — Ollama serves the embedder itself, so
|
|
@@ -8791,31 +8863,57 @@ async function chooseEmbedModel() {
|
|
|
8791
8863
|
const current = ec.currentEmbedModel(settings);
|
|
8792
8864
|
const currentUrl = String(settings.ragEmbedUrl || "").trim() || "http://127.0.0.1:11434";
|
|
8793
8865
|
const host = hostOf(currentUrl);
|
|
8794
|
-
// WHEN TO SHOW THE KEY ROW (
|
|
8866
|
+
// WHEN TO SHOW THE KEY ROW (keyRequired): a key is clearly the question when
|
|
8795
8867
|
// the server is ollama.com (it rejects anonymous calls) or already looks OpenAI-compatible (the
|
|
8796
8868
|
// /v1 shape), or when one is already bound — replacing the key people can no longer read is the
|
|
8797
8869
|
// very thing they come back for. A keyless localhost Ollama sees three rows instead.
|
|
8798
8870
|
const keyRequired = isOllamaCloudUrl(currentUrl) || /\/v1\/?$/i.test(currentUrl) ||
|
|
8799
8871
|
Boolean(String(settings.ragEmbedKeyId ?? "").trim());
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8872
|
+
// ONE LIST, THE SHAPE OF /model AND /agent-model (#248): the embedders, the current one marked,
|
|
8873
|
+
// then this server's own rows in orange — its other models, its keys — and add a provider. It was
|
|
8874
|
+
// a three-row menu first ("switch the embedder model…", "use another provider…", the key), one
|
|
8875
|
+
// step more than the other two commands.
|
|
8876
|
+
// "(current)" only for an embedder that was SET — nothing set means the default, which its own
|
|
8877
|
+
// note already says. Right after the name: at the end of the row an 80-column clip cut it off.
|
|
8878
|
+
const isSet = Boolean(String(settings.ragEmbedModel ?? "").trim());
|
|
8879
|
+
const rows = ec.embedChoiceRows(isSet ? current : "");
|
|
8880
|
+
const labels = rows.map(
|
|
8881
|
+
(r) => `${r.model}${r.current ? accent(" (current)") : ""}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}`
|
|
8882
|
+
);
|
|
8883
|
+
const MODELS = labels.length;
|
|
8884
|
+
labels.push(actionRow(`pick another model on ${host || currentUrl}…`));
|
|
8885
|
+
const KEYS = keyRequired ? labels.length : -1;
|
|
8886
|
+
if (keyRequired) labels.push(actionRow(`API keys for ${host || currentUrl} — switch, add or remove…`));
|
|
8887
|
+
const ADD = labels.length;
|
|
8888
|
+
labels.push(actionRow("add a provider — OpenAI, Ollama…"));
|
|
8889
|
+
const head = ` Embedding model (now ${current} on ${host || currentUrl})`;
|
|
8890
|
+
const hint = `(${PICK_ARROWS}):`;
|
|
8891
|
+
const cols = Math.max(20, process.stdout.columns || 80);
|
|
8892
|
+
console.log(dim(head.length + 2 + hint.length < cols ? `${head} ${hint}` : `${head}\n ${hint}`));
|
|
8893
|
+
// The cursor starts on the one in use: the set one, else the default.
|
|
8894
|
+
const inUse = rows.findIndex((r) => ec.sameEmbedModel(r.model, current));
|
|
8895
|
+
const mi = await pickFromList(labels, Math.max(0, inUse), { footer: ESC_FOOTER });
|
|
8896
|
+
if (mi < 0) {
|
|
8805
8897
|
console.log(dim(" (cancelled)\n"));
|
|
8806
8898
|
return;
|
|
8807
8899
|
}
|
|
8900
|
+
const action = mi === MODELS ? "server" : mi === KEYS ? "key" : mi === ADD ? "provider" : "model";
|
|
8808
8901
|
|
|
8809
8902
|
// EVERY ARM IS THE ONE ADD-SERVER SCREEN (task #245) — the same URL → key → models → save as
|
|
8810
|
-
// /model and /agent-model.
|
|
8903
|
+
// /model and /agent-model. A listed embedder has been chosen already (and passed the
|
|
8811
8904
|
// recognition wall), so it only asks WHERE; the other two list what the server has.
|
|
8812
|
-
// "← back" in any arm returns to this
|
|
8905
|
+
// "← back" in any arm returns to this list (#242).
|
|
8813
8906
|
if (action === "model") {
|
|
8814
|
-
const
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8907
|
+
const chosen = rows[mi].model;
|
|
8908
|
+
const sw = await confirmEmbedSwitch(ec, current, chosen);
|
|
8909
|
+
if (!sw.changed) return;
|
|
8910
|
+
await addServer({ role: "embed", dialect: "ollama", model: chosen, plan: sw.plan });
|
|
8911
|
+
return;
|
|
8912
|
+
}
|
|
8913
|
+
if (action === "server") {
|
|
8914
|
+
// That server's own list, with its key — the add-server screen with the URL known.
|
|
8915
|
+
const dialect = /\/v1\/?$/i.test(currentUrl) ? "openai" : "ollama";
|
|
8916
|
+
if ((await addServer({ role: "embed", dialect, url: currentUrl, back: true })) === BACK) return chooseEmbedModel();
|
|
8819
8917
|
return;
|
|
8820
8918
|
}
|
|
8821
8919
|
if (action === "provider") {
|
|
@@ -8970,24 +9068,37 @@ async function chooseAgentModel() {
|
|
|
8970
9068
|
if (!saved.length) return agentModelOnServer();
|
|
8971
9069
|
const current = am.currentAgentModel(settings);
|
|
8972
9070
|
const curHost = settings.agentModelUrl ? hostOf(String(settings.agentModelUrl)) : "";
|
|
9071
|
+
// THE CURRENT PROVIDER'S OWN ROWS, ON THIS LIST (#248). They were one step down, behind "other
|
|
9072
|
+
// models, providers and API keys…" — which read like the "add a provider" row under it.
|
|
9073
|
+
const source = am.agentListSource(settings);
|
|
9074
|
+
const needsKey = am.agentSourceNeedsKey(source);
|
|
8973
9075
|
for (;;) {
|
|
8974
9076
|
const labels = saved.map((c) => c.label + (c.current ? accent(" (current)") : ""));
|
|
8975
|
-
const
|
|
8976
|
-
labels.push(
|
|
9077
|
+
const MODELS = curHost ? labels.length : -1;
|
|
9078
|
+
if (curHost) labels.push(actionRow(`pick another model on ${curHost}…`));
|
|
9079
|
+
const KEYS = curHost && needsKey ? labels.length : -1;
|
|
9080
|
+
if (KEYS >= 0) labels.push(actionRow(`API keys for ${curHost} — switch, add or remove…`));
|
|
8977
9081
|
const ADD = labels.length;
|
|
8978
|
-
labels.push("add a provider — Claude API, OpenAI, Kimi, Ollama…");
|
|
9082
|
+
labels.push(actionRow("add a provider — Claude API, OpenAI, Kimi, Ollama…"));
|
|
8979
9083
|
// The hint drops to its own row when the header would wrap (80 columns over SSH).
|
|
8980
9084
|
const head = ` Agent model — the one that TALKS (now ${current || "unset"}${curHost ? ` on ${curHost}` : ""})`;
|
|
8981
|
-
const hint = `(${
|
|
9085
|
+
const hint = `(${PICK_ARROWS}):`;
|
|
8982
9086
|
const cols = Math.max(20, process.stdout.columns || 80);
|
|
8983
9087
|
console.log(dim(head.length + 2 + hint.length < cols ? `${head} ${hint}` : `${head}\n ${hint}`));
|
|
8984
|
-
const idx = await pickFromList(labels, Math.max(0, saved.findIndex((c) => c.current)));
|
|
9088
|
+
const idx = await pickFromList(labels, Math.max(0, saved.findIndex((c) => c.current)), { footer: ESC_FOOTER });
|
|
8985
9089
|
if (idx < 0) {
|
|
8986
9090
|
console.log(dim(" (cancelled)\n"));
|
|
8987
9091
|
return;
|
|
8988
9092
|
}
|
|
8989
|
-
if (idx ===
|
|
8990
|
-
|
|
9093
|
+
if (idx === MODELS) {
|
|
9094
|
+
// Straight to that server's full list; when it can't be listed, its recovery menu instead.
|
|
9095
|
+
if ((await agentModelOnServer({ back: true, start: "models" })) === BACK) continue;
|
|
9096
|
+
return;
|
|
9097
|
+
}
|
|
9098
|
+
if (idx === KEYS) {
|
|
9099
|
+
const rowId = String(source.row?.id ?? "").trim() || String(source.row?.kind ?? "").trim();
|
|
9100
|
+
if (!rowId) return enterAgentProviderKey(source.row, curHost);
|
|
9101
|
+
if ((await manageServerKeys({ role: "agent", backendId: rowId, host: curHost, model: current, back: true })) === BACK) continue;
|
|
8991
9102
|
return;
|
|
8992
9103
|
}
|
|
8993
9104
|
if (idx === ADD) {
|
|
@@ -9025,10 +9136,12 @@ async function useSavedAgentChoice(c) {
|
|
|
9025
9136
|
|
|
9026
9137
|
/**
|
|
9027
9138
|
* One server's view: its full model list, other providers, its keys — what /agent-model was before
|
|
9028
|
-
* #246
|
|
9029
|
-
* `
|
|
9139
|
+
* #246. With models already set up, /agent-model's list opens it as "pick another model on <host>…"
|
|
9140
|
+
* (#248): `start: "models"` skips the what-to-change menu and goes to the model list.
|
|
9141
|
+
* `back`: opened from that list, so its first screen offers "← back" and returns BACK.
|
|
9030
9142
|
*/
|
|
9031
|
-
async function agentModelOnServer({ back = false } = {}) {
|
|
9143
|
+
async function agentModelOnServer({ back = false, start = "menu" } = {}) {
|
|
9144
|
+
const again = () => agentModelOnServer({ back, start });
|
|
9032
9145
|
const am = await import("./agent-model-command.mjs");
|
|
9033
9146
|
const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
|
|
9034
9147
|
if (!settings) {
|
|
@@ -9113,7 +9226,7 @@ async function agentModelOnServer({ back = false } = {}) {
|
|
|
9113
9226
|
if (action === "key") await enterAgentProviderKey(source.row, listHost);
|
|
9114
9227
|
else if (action === "provider") {
|
|
9115
9228
|
// "← back" on "Which provider?" returns to this menu (#247).
|
|
9116
|
-
if ((await addServer({ role: "agent", dialect: "openai", back: true })) === BACK) return
|
|
9229
|
+
if ((await addServer({ role: "agent", dialect: "openai", back: true })) === BACK) return again();
|
|
9117
9230
|
} else console.log("");
|
|
9118
9231
|
return;
|
|
9119
9232
|
}
|
|
@@ -9121,18 +9234,21 @@ async function agentModelOnServer({ back = false } = {}) {
|
|
|
9121
9234
|
// WHAT TO CHANGE, FIRST (#44). The ways to use an Ollama server or another provider used to sit
|
|
9122
9235
|
// after every model on this one — 130 rows down on api.openai.com — which read as "OpenAI models
|
|
9123
9236
|
// are all you can choose". A short menu of what to change comes before any list.
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9237
|
+
let firstAction = "models";
|
|
9238
|
+
if (start !== "models") {
|
|
9239
|
+
console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
|
|
9240
|
+
const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
|
|
9241
|
+
const first = await pickFromList(menu.map((o) => o.label), 0, { back });
|
|
9242
|
+
if (first === PICK_BACK) return BACK;
|
|
9243
|
+
firstAction = first >= 0 ? menu[first].action : "cancel";
|
|
9244
|
+
}
|
|
9129
9245
|
if (firstAction === "cancel") {
|
|
9130
9246
|
console.log(dim(" (cancelled)\n"));
|
|
9131
9247
|
return;
|
|
9132
9248
|
}
|
|
9133
9249
|
// "← back" in any of these returns to this menu (#242) — read again, as on a first open.
|
|
9134
9250
|
if (firstAction === "provider") {
|
|
9135
|
-
if ((await chooseAgentProvider({ back: true })) === BACK) return
|
|
9251
|
+
if ((await chooseAgentProvider({ back: true })) === BACK) return again();
|
|
9136
9252
|
return;
|
|
9137
9253
|
}
|
|
9138
9254
|
if (firstAction === "key") {
|
|
@@ -9140,7 +9256,7 @@ async function agentModelOnServer({ back = false } = {}) {
|
|
|
9140
9256
|
const rowId = String(source.row?.id ?? "").trim() || String(source.row?.kind ?? "").trim();
|
|
9141
9257
|
if (!rowId) return enterAgentProviderKey(source.row, listHost);
|
|
9142
9258
|
const r = await manageServerKeys({ role: "agent", backendId: rowId, host: listHost, model: am.currentAgentModel(settings), back: true });
|
|
9143
|
-
if (r === BACK) return
|
|
9259
|
+
if (r === BACK) return again();
|
|
9144
9260
|
return;
|
|
9145
9261
|
}
|
|
9146
9262
|
|
|
@@ -9173,7 +9289,8 @@ async function agentModelOnServer({ back = false } = {}) {
|
|
|
9173
9289
|
// No other entries here any more (#44): the ways to change the provider are on the menu above,
|
|
9174
9290
|
// where they can be seen, rather than after every model.
|
|
9175
9291
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)), { back: true });
|
|
9176
|
-
|
|
9292
|
+
// Opened as "pick another model on <host>…", back is /agent-model's list; else this menu.
|
|
9293
|
+
if (idx === PICK_BACK) return start === "models" ? BACK : again();
|
|
9177
9294
|
if (idx < 0) {
|
|
9178
9295
|
console.log(dim(" (cancelled)\n"));
|
|
9179
9296
|
return;
|
|
@@ -10356,7 +10473,7 @@ async function promptNewCodingKey(backendId = "", provider = "") {
|
|
|
10356
10473
|
return id;
|
|
10357
10474
|
}
|
|
10358
10475
|
|
|
10359
|
-
async function replaceCodingKey() {
|
|
10476
|
+
async function replaceCodingKey({ back = false } = {}) {
|
|
10360
10477
|
const ks = await withPendingStatus("checking the coding model", () =>
|
|
10361
10478
|
coderKeyState(sessionCodingModel)
|
|
10362
10479
|
);
|
|
@@ -10391,8 +10508,7 @@ async function replaceCodingKey() {
|
|
|
10391
10508
|
const keys = allKeys ? ps.keysForBackend(allKeys, ks.backendId) : null;
|
|
10392
10509
|
if (keys && keys.length >= 1) {
|
|
10393
10510
|
// ONE SERVER'S KEYS (task #245): the same picker /agent-model and /embed-model open.
|
|
10394
|
-
|
|
10395
|
-
return;
|
|
10511
|
+
return manageServerKeys({ role: "coding", backendId: ks.backendId, host: ks.provider || ks.backendId || "this server", model: ks.model, back });
|
|
10396
10512
|
}
|
|
10397
10513
|
|
|
10398
10514
|
// NO KEYS YET, BUT AN API THAT UNDERSTANDS NAMES — so ask for one.
|
|
@@ -11133,6 +11249,8 @@ let listChecked = new Set();
|
|
|
11133
11249
|
// arrive after the choosing is over.
|
|
11134
11250
|
let listMax = 0;
|
|
11135
11251
|
let listNote = "";
|
|
11252
|
+
/** #249: a dim line under the rows (pickFromList's `footer`), drawn and erased with the block. */
|
|
11253
|
+
let listFooter = "";
|
|
11136
11254
|
// When true, ctrl+d in a single-select picker finishes with a DELETE request for the
|
|
11137
11255
|
// highlighted row instead of choosing it (task #222). Off unless a caller asks for it, so no
|
|
11138
11256
|
// existing picker gains a destructive key by accident.
|
|
@@ -11162,7 +11280,7 @@ function drawList(redraw) {
|
|
|
11162
11280
|
// provider's /v1/models was taller than the terminal: the screen scrolled, the highlighted row
|
|
11163
11281
|
// went off the top, and the ESC[nA walk below then counted rows that had moved — "cannot scroll
|
|
11164
11282
|
// up or down, stuck here". Two rows of margin keep the block clear of the line above it.
|
|
11165
|
-
const maxBlock = Math.max(5, viewportHeight() - 2) - (listNote ? 1 : 0) - (listHeader ? listHeaderRows : 0);
|
|
11283
|
+
const maxBlock = Math.max(5, viewportHeight() - 2) - (listNote ? 1 : 0) - (listFooter ? 1 : 0) - (listHeader ? listHeaderRows : 0);
|
|
11166
11284
|
const win = listWindow(listItems.length, listSel, listTop, maxBlock);
|
|
11167
11285
|
listTop = win.top;
|
|
11168
11286
|
const row = (i) => {
|
|
@@ -11190,6 +11308,7 @@ function drawList(redraw) {
|
|
|
11190
11308
|
// The cap's message rides UNDER the list, in the same block, so it is erased and redrawn with
|
|
11191
11309
|
// it — a note printed into the scrolling output would stay on screen after the list is gone.
|
|
11192
11310
|
if (listNote) rows.push(clipVisible(GUTTER + yellow(listNote), width - 1));
|
|
11311
|
+
if (listFooter) rows.push(clipVisible(GUTTER + dim(listFooter), width - 1));
|
|
11193
11312
|
// On a redraw the caret sits one row BELOW the block (each draw ends with "\n").
|
|
11194
11313
|
const up = redraw ? physicalRows(listRowsDrawn, width) : 0;
|
|
11195
11314
|
// #321: what the screen held under the picker when it opened, and how far it reaches — so the
|
|
@@ -11234,6 +11353,7 @@ function finishList(index) {
|
|
|
11234
11353
|
listChecked = new Set();
|
|
11235
11354
|
listMax = 0;
|
|
11236
11355
|
listNote = "";
|
|
11356
|
+
listFooter = "";
|
|
11237
11357
|
const allowDelete = listAllowDelete;
|
|
11238
11358
|
const deleting = listDeleteRequested;
|
|
11239
11359
|
listAllowDelete = false;
|
|
@@ -11374,6 +11494,7 @@ function pickFromList(items, initialSel = 0, opts = {}) {
|
|
|
11374
11494
|
return;
|
|
11375
11495
|
}
|
|
11376
11496
|
listMulti = false; // explicit: a cancelled multi-select must not leak its mode into this one
|
|
11497
|
+
listFooter = typeof opts.footer === "string" ? opts.footer : "";
|
|
11377
11498
|
listAllowDelete = opts.allowDelete === true;
|
|
11378
11499
|
listDeleteRequested = false;
|
|
11379
11500
|
listKeys = opts.keys && typeof opts.keys === "object" ? opts.keys : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.878",
|
|
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",
|