@tiens.nguyen/gu-cli 1.0.694 → 1.0.696
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/accent.mjs +99 -0
- package/client-expert-run.mjs +2 -1
- package/coding-key-prompt.mjs +44 -3
- package/gonext_agent_chat.py +3 -3
- package/gu-cli.mjs +14 -5
- package/gu-repl.mjs +92 -85
- package/package.json +2 -1
- package/terminal-viewport.mjs +10 -9
package/accent.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE BRAND COLOUR, IN A TERMINAL.
|
|
3
|
+
*
|
|
4
|
+
* gu's accent was ANSI cyan — one of the original sixteen colours, chosen when the only question
|
|
5
|
+
* was "which of these eight-ish names looks least bad". The web app has a real palette now, built
|
|
6
|
+
* around indigo, and the terminal is the face of this product people see every day: two different
|
|
7
|
+
* accents makes them look like two different programs.
|
|
8
|
+
*
|
|
9
|
+
* THE COLOUR IS #a5b4fc — the web app's --accent-fg, the same token that paints the block wordmark
|
|
10
|
+
* on the landing page. Not --accent (#6366f1): that one is a BUTTON FILL, and as text on a dark
|
|
11
|
+
* terminal it is too dark to read comfortably. The web app makes exactly the same distinction.
|
|
12
|
+
*
|
|
13
|
+
* WHICH IS A PROBLEM, BECAUSE TERMINALS DISAGREE ABOUT COLOUR. There is no "purple" to ask for —
|
|
14
|
+
* there are three separate mechanisms, and a terminal supports one of them:
|
|
15
|
+
*
|
|
16
|
+
* 24-bit \x1b[38;2;165;180;252m the exact colour. iTerm2, VS Code, Windows Terminal,
|
|
17
|
+
* kitty, alacritty, most modern emulators.
|
|
18
|
+
* 256 \x1b[38;5;147m the nearest cell of the xterm cube (#afafff). Apple's
|
|
19
|
+
* Terminal.app is here — it has never supported 24-bit.
|
|
20
|
+
* 16 \x1b[35m magenta, the only purple the original palette has. Old
|
|
21
|
+
* terminals, CI logs, anything reporting a TERM it cannot
|
|
22
|
+
* back up.
|
|
23
|
+
*
|
|
24
|
+
* Sending a 24-bit sequence to a terminal that does not understand it does not degrade — some
|
|
25
|
+
* print the escape as text, across the banner. So the capability is detected rather than assumed,
|
|
26
|
+
* and the fallbacks are deliberate rather than accidental.
|
|
27
|
+
*
|
|
28
|
+
* ESCAPE HATCHES, because a colour is a preference and someone will hate this one:
|
|
29
|
+
* NO_COLOR=1 no colour at all (the standard; https://no-color.org)
|
|
30
|
+
* GU_ACCENT=cyan the old cyan, exactly as it was
|
|
31
|
+
* GU_ACCENT=off same as NO_COLOR, for people who only want gu quiet
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** The web app's --accent-fg. Keep in step with web-v2/src/App.css. */
|
|
35
|
+
export const ACCENT_RGB = [165, 180, 252];
|
|
36
|
+
/** The nearest xterm-256 cube cell: #afafff. */
|
|
37
|
+
export const ACCENT_256 = 147;
|
|
38
|
+
|
|
39
|
+
export const TRUECOLOR = `\x1b[38;2;${ACCENT_RGB.join(";")}m`;
|
|
40
|
+
export const COLOR_256 = `\x1b[38;5;${ACCENT_256}m`;
|
|
41
|
+
export const COLOR_16 = "\x1b[35m";
|
|
42
|
+
export const CYAN = "\x1b[36m";
|
|
43
|
+
export const RESET = "\x1b[0m";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The escape sequence this environment should get, or "" for no colour.
|
|
47
|
+
*
|
|
48
|
+
* Pure, and takes the environment, so every branch is testable — the alternative is a colour
|
|
49
|
+
* nobody can check without owning six terminal emulators.
|
|
50
|
+
*/
|
|
51
|
+
export function accentPrefix(env = process.env) {
|
|
52
|
+
const pref = String(env.GU_ACCENT ?? "").trim().toLowerCase();
|
|
53
|
+
if (pref === "off" || pref === "none") return "";
|
|
54
|
+
if (pref === "cyan") return CYAN;
|
|
55
|
+
// NO_COLOR: "when present and not an empty string, regardless of its value".
|
|
56
|
+
if (env.NO_COLOR != null && String(env.NO_COLOR) !== "") return "";
|
|
57
|
+
|
|
58
|
+
const term = String(env.TERM ?? "").toLowerCase();
|
|
59
|
+
if (term === "dumb") return "";
|
|
60
|
+
|
|
61
|
+
const colorterm = String(env.COLORTERM ?? "").trim().toLowerCase();
|
|
62
|
+
if (colorterm === "truecolor" || colorterm === "24bit") return TRUECOLOR;
|
|
63
|
+
// Windows Terminal supports 24-bit but sets neither TERM nor COLORTERM.
|
|
64
|
+
if (env.WT_SESSION) return TRUECOLOR;
|
|
65
|
+
if (/truecolor|direct/.test(term)) return TRUECOLOR;
|
|
66
|
+
if (term.includes("256color")) return COLOR_256;
|
|
67
|
+
|
|
68
|
+
// Everything else — including a TERM we have never heard of, and Windows consoles that report
|
|
69
|
+
// nothing — gets the sixteen-colour magenta. It is the conservative answer: every terminal that
|
|
70
|
+
// renders colour at all renders this one.
|
|
71
|
+
return COLOR_16;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Which mechanism was chosen. For `gu doctor`-style reporting, and for the tests. */
|
|
75
|
+
export function accentMode(env = process.env) {
|
|
76
|
+
const prefix = accentPrefix(env);
|
|
77
|
+
if (prefix === "") return "none";
|
|
78
|
+
if (prefix === TRUECOLOR) return "truecolor";
|
|
79
|
+
if (prefix === COLOR_256) return "256";
|
|
80
|
+
if (prefix === CYAN) return "cyan";
|
|
81
|
+
return "16";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Wrap a string in the accent colour.
|
|
86
|
+
*
|
|
87
|
+
* Resolved ONCE at module load, like the colour helpers it replaces: these are called thousands of
|
|
88
|
+
* times while drawing a frame, and re-reading the environment for each would be work done to answer
|
|
89
|
+
* a question that cannot change.
|
|
90
|
+
*/
|
|
91
|
+
const PREFIX = accentPrefix();
|
|
92
|
+
export function accent(s) {
|
|
93
|
+
return PREFIX ? `${PREFIX}${s}${RESET}` : String(s);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Bold accent — the startup banner's name. */
|
|
97
|
+
export function accentBold(s) {
|
|
98
|
+
return PREFIX ? `\x1b[1m${PREFIX}${s}${RESET}` : String(s);
|
|
99
|
+
}
|
package/client-expert-run.mjs
CHANGED
|
@@ -74,7 +74,8 @@ async function fetchOllamaTags(url) {
|
|
|
74
74
|
|
|
75
75
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
76
76
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
77
|
-
|
|
77
|
+
// The brand accent, shared with the REPL — see accent.mjs.
|
|
78
|
+
import { accent as cyan } from "./accent.mjs";
|
|
78
79
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
79
80
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
80
81
|
|
package/coding-key-prompt.mjs
CHANGED
|
@@ -52,6 +52,34 @@ export function isKeylessCodingHost(url) {
|
|
|
52
52
|
return host === "gomarsic.cc" || host.endsWith(".gomarsic.cc");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* TRUE only for the Ollama CLOUD (https://ollama.com, or a subdomain of it). Mirrors
|
|
57
|
+
* isOllamaCloudUrl in api/src/store/settingsTypes.ts, web/src/lib/coderChoices.mjs and
|
|
58
|
+
* _isOllamaCloudUrl in gu-cli.mjs — keep them in step.
|
|
59
|
+
*/
|
|
60
|
+
export function isOllamaCloudUrl(url) {
|
|
61
|
+
const v = String(url ?? "").trim();
|
|
62
|
+
if (!/^https:\/\//i.test(v)) return false;
|
|
63
|
+
let host;
|
|
64
|
+
try {
|
|
65
|
+
host = new URL(v).hostname.toLowerCase().replace(/\.$/, "");
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return host === "ollama.com" || host.endsWith(".ollama.com");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* DOES THIS CODER NEED AN API KEY? — an OpenAI-compatible provider, or Ollama's paid CLOUD.
|
|
74
|
+
*
|
|
75
|
+
* Reported: /model on kimi-k3 at https://ollama.com said "needs no API key — nothing to set", so
|
|
76
|
+
* there was no way to give it one. Every "needs a key" check had been `kind === "openai"`, on the
|
|
77
|
+
* belief that Ollama never authenticates — true of a local or LAN Ollama, false of ollama.com.
|
|
78
|
+
*/
|
|
79
|
+
export function codingNeedsKey(kind, url) {
|
|
80
|
+
return kind === "openai" || (kind === "ollama" && isOllamaCloudUrl(url));
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
/**
|
|
56
84
|
* @param payload the /agent-payload `payload` object (reads codingKeyMissing/codingKeyModel).
|
|
57
85
|
* @param deps.askSecret async (prompt) => string — masked input; "" means the user skipped.
|
|
@@ -187,9 +215,10 @@ export function codingKeyFallbackLine(kj, jobId = "") {
|
|
|
187
215
|
* when a choice HAS been made, and when only one key exists — with one there is nothing to
|
|
188
216
|
* choose, and a warning on a correct setup is how warnings stop being read.
|
|
189
217
|
*/
|
|
190
|
-
export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys = null } = {}) {
|
|
218
|
+
export function unchosenKeyWarning({ codingKind = "", codingUrl = "", selectedKeyId = "", keys = null } = {}) {
|
|
191
219
|
const quiet = { warn: false, using: null, why: "" };
|
|
192
|
-
|
|
220
|
+
// Local Ollama and MLX need no credential; the Ollama cloud does.
|
|
221
|
+
if (!codingNeedsKey(codingKind, codingUrl)) return quiet;
|
|
193
222
|
if (!Array.isArray(keys) || keys.length < 2) return quiet;
|
|
194
223
|
if (String(selectedKeyId ?? "").trim()) return quiet;
|
|
195
224
|
return { warn: true, using: keys[0], why: "none-chosen" };
|
|
@@ -200,7 +229,8 @@ export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys =
|
|
|
200
229
|
*
|
|
201
230
|
* REPORTED: the bar's "key not chosen" mark only appeared after running /model. The startup gate
|
|
202
231
|
* read `payload.codingKind`, and a payload fetched with NO override describes the ACCOUNT DEFAULT
|
|
203
|
-
* — on the reporting machine
|
|
232
|
+
* — on the reporting machine kimi-k3 at ollama.com, kind "ollama" (which, it turned out later,
|
|
233
|
+
* DOES need a key: see codingNeedsKey) —
|
|
204
234
|
* while the folder was pointed at gpt-5, kind "openai". So the check answered a question about a
|
|
205
235
|
* coder the folder was not using, said "nothing to warn about", and stayed silent. /model looked
|
|
206
236
|
* right because it re-probes WITH the override.
|
|
@@ -212,6 +242,17 @@ export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys =
|
|
|
212
242
|
* Matched by id OR by bare model name, mirroring that validation: a session saved before ids were
|
|
213
243
|
* qualified ("<kind>::<model>", task #123) stores the bare form and is still a valid pick.
|
|
214
244
|
*/
|
|
245
|
+
/** The folder's coder — its kind AND its URL, which together decide whether it needs a key. */
|
|
246
|
+
export function effectiveCoder({ sessionCodingModel = "", choices = null, defaultKind = "", defaultUrl = "" } = {}) {
|
|
247
|
+
const want = String(sessionCodingModel ?? "").trim();
|
|
248
|
+
const list = Array.isArray(choices) ? choices : [];
|
|
249
|
+
if (want) {
|
|
250
|
+
const hit = list.find((c) => c?.id === want || c?.model === want);
|
|
251
|
+
if (hit) return { kind: String(hit.kind ?? ""), url: String(hit.url ?? "") };
|
|
252
|
+
}
|
|
253
|
+
return { kind: String(defaultKind ?? ""), url: String(defaultUrl ?? "") };
|
|
254
|
+
}
|
|
255
|
+
|
|
215
256
|
export function effectiveCoderKind({
|
|
216
257
|
sessionCodingModel = "",
|
|
217
258
|
choices = null,
|
package/gonext_agent_chat.py
CHANGED
|
@@ -9707,9 +9707,9 @@ def run_agent_chat(cfg):
|
|
|
9707
9707
|
raise _AgentConfigError(
|
|
9708
9708
|
f"The agent coding backend ({_host}) rejected the request: "
|
|
9709
9709
|
"NO coding API key was sent, so the local agent key was "
|
|
9710
|
-
"used instead.
|
|
9711
|
-
"
|
|
9712
|
-
"
|
|
9710
|
+
"used instead. Enter the API key in Settings → Agent for "
|
|
9711
|
+
"this coding backend (OpenAI-compatible, or Ollama with an "
|
|
9712
|
+
"https://ollama.com URL — local Ollama needs no key)."
|
|
9713
9713
|
) from e
|
|
9714
9714
|
if "404" in emsg and "not found" in emsg.lower():
|
|
9715
9715
|
# Probe with the CODING key when there is one: using the agent
|
package/gu-cli.mjs
CHANGED
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
// FIRST, before anything that could itself fail on an old runtime: npm only WARNS about
|
|
13
13
|
// `engines`, so an unsupported Node installs fine and then breaks somewhere unrelated.
|
|
14
14
|
import { requireSupportedNode } from "./node-version.mjs";
|
|
15
|
+
// The brand accent, shared with the REPL — see accent.mjs.
|
|
16
|
+
import { accent } from "./accent.mjs";
|
|
15
17
|
requireSupportedNode();
|
|
16
18
|
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
17
19
|
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
18
20
|
import { createHash } from "node:crypto";
|
|
19
21
|
import { createTaskPool, createFolderQueue } from "./job-pools.mjs";
|
|
20
22
|
import { ragJobConfig } from "./rag-job-config.mjs";
|
|
21
|
-
import { codingKeyFallbackLine } from "./coding-key-prompt.mjs";
|
|
23
|
+
import { codingKeyFallbackLine, isOllamaCloudUrl } from "./coding-key-prompt.mjs";
|
|
22
24
|
// Pure layout helpers — safe to import here (no side effects on load, unlike gu-repl.mjs).
|
|
23
25
|
// Shared so the mid-run picker and the REPL's post-stop advice cannot word the same budget
|
|
24
26
|
// differently.
|
|
@@ -1207,7 +1209,7 @@ if (
|
|
|
1207
1209
|
// shared ones, and `node --check` cannot see a missing helper — it is a syntax checker, not a
|
|
1208
1210
|
// resolver. That precise shape has crashed this CLI before.
|
|
1209
1211
|
const bold = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
1210
|
-
const cyan = (t) =>
|
|
1212
|
+
const cyan = (t) => accent(t);
|
|
1211
1213
|
const red = (t) => `\x1b[31m${t}\x1b[0m`;
|
|
1212
1214
|
const dim = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
1213
1215
|
console.log("\n" + bold("proxy") + dim(" — can this machine route model calls through a proxy?\n"));
|
|
@@ -1238,7 +1240,7 @@ if (args[0] === "rag" && (args.includes("--test") || args[1] === "test")) {
|
|
|
1238
1240
|
// cannot see a missing one — it is a syntax checker, not a resolver. That exact shape has
|
|
1239
1241
|
// crashed this CLI before.
|
|
1240
1242
|
const bold = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
1241
|
-
const cyan = (t) =>
|
|
1243
|
+
const cyan = (t) => accent(t);
|
|
1242
1244
|
const red = (t) => `\x1b[31m${t}\x1b[0m`;
|
|
1243
1245
|
const dim = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
1244
1246
|
console.log("\n" + bold("rag") + dim(" — can this machine embed and retrieve?\n"));
|
|
@@ -3263,6 +3265,9 @@ function _isLocalCodingUrl(baseURL) {
|
|
|
3263
3265
|
return !host.includes(".") && !host.includes(":");
|
|
3264
3266
|
}
|
|
3265
3267
|
|
|
3268
|
+
/** The Ollama cloud check lives in coding-key-prompt.mjs, shared with the terminal. */
|
|
3269
|
+
const _isOllamaCloudUrl = isOllamaCloudUrl;
|
|
3270
|
+
|
|
3266
3271
|
/** Resolve to "ollama" | "openai" | "local". `kind` is the user's Settings choice
|
|
3267
3272
|
* ("ollama" | "openai" | "" = Auto). Only Auto costs a probe. */
|
|
3268
3273
|
async function resolveCodingBackend(kind, baseURL) {
|
|
@@ -3445,7 +3450,9 @@ async function runAgentChatJob(job) {
|
|
|
3445
3450
|
// anything else that endpoint doesn't exist, and on a cloud one we'd be pinging a paid
|
|
3446
3451
|
// API every 4 minutes (which an Auto+cloud config did until #117 was fixed — the old
|
|
3447
3452
|
// gate only skipped an EXPLICIT openai kind).
|
|
3448
|
-
|
|
3453
|
+
// ...and NOT for the Ollama CLOUD (https://ollama.com): that is a paid API, so the
|
|
3454
|
+
// keep-warm ping must not run against it — same rule as the key fetch below.
|
|
3455
|
+
if (codingBackend === "ollama" && !_isOllamaCloudUrl(payload?.codingBaseURL)) {
|
|
3449
3456
|
trackCodingModelForWarmup(payload?.codingBaseURL ?? "", payload?.codingModelId ?? "");
|
|
3450
3457
|
}
|
|
3451
3458
|
|
|
@@ -3598,7 +3605,9 @@ async function runAgentChatJob(job) {
|
|
|
3598
3605
|
// "openai" kind, and python's `coding_api_key or agent_api_key` fallback then
|
|
3599
3606
|
// authenticated a cloud provider with the LOCAL agent key → 401.
|
|
3600
3607
|
let codingApiKey = "";
|
|
3601
|
-
|
|
3608
|
+
// Ollama has no auth — EXCEPT the Ollama cloud (https://ollama.com), which requires a
|
|
3609
|
+
// key like any other paid endpoint. A local/LAN Ollama gets no key (and ignores one).
|
|
3610
|
+
if (codingBackend === "openai" || (codingBackend === "ollama" && _isOllamaCloudUrl(payload?.codingBaseURL))) {
|
|
3602
3611
|
try {
|
|
3603
3612
|
// NAME THE JOB (task #222 phase 2). Two turns run concurrently by default
|
|
3604
3613
|
// (agentConcurrency = 2), and each may have chosen a different key — so the key is
|
package/gu-repl.mjs
CHANGED
|
@@ -133,6 +133,10 @@ import { createHoverController, applyToggleShift } from "./terminal-hover.mjs";
|
|
|
133
133
|
// The required pair, imported rather than re-typed: the startup "no models" message names them,
|
|
134
134
|
// and a second copy of these strings would drift from the check that enforces them.
|
|
135
135
|
import { REQUIRED_AGENT_MODEL, REQUIRED_AGENT_PREFIX, REQUIRED_EMBED_MODEL } from "./ollama-setup.mjs";
|
|
136
|
+
// The brand accent — indigo, the same token the web app paints its wordmark with. It is NOT a
|
|
137
|
+
// plain SGR code: terminals disagree about colour, so accent.mjs picks 24-bit, 256 or the
|
|
138
|
+
// sixteen-colour magenta from what this one actually claims to support.
|
|
139
|
+
import { accent } from "./accent.mjs";
|
|
136
140
|
import {
|
|
137
141
|
createTranscript,
|
|
138
142
|
displayRows,
|
|
@@ -169,7 +173,6 @@ const WS_FILE = join(homedir(), ".gonext", "workspaces.json");
|
|
|
169
173
|
const SESSIONS_DIR = join(homedir(), ".gonext", "sessions");
|
|
170
174
|
|
|
171
175
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
172
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
173
176
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
174
177
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
175
178
|
const white = (s) => `\x1b[37m${s}\x1b[0m`;
|
|
@@ -327,11 +330,11 @@ const renderBanner = (statusRows) => {
|
|
|
327
330
|
if (GUTTER.length + LOGO_W + gap.length + widest <= cols) {
|
|
328
331
|
const n = Math.max(LOGO.length, statusRows.length);
|
|
329
332
|
for (let i = 0; i < n; i++) {
|
|
330
|
-
const left = i < LOGO.length ?
|
|
333
|
+
const left = i < LOGO.length ? accent(LOGO[i].padEnd(LOGO_W)) : " ".repeat(LOGO_W);
|
|
331
334
|
out.push(GUTTER + left + gap + (statusRows[i] ?? ""));
|
|
332
335
|
}
|
|
333
336
|
} else {
|
|
334
|
-
for (const r of LOGO) out.push(GUTTER +
|
|
337
|
+
for (const r of LOGO) out.push(GUTTER + accent(r));
|
|
335
338
|
for (const r of statusRows) out.push(GUTTER + clipVisible(r, cols - 1 - GUTTER.length));
|
|
336
339
|
}
|
|
337
340
|
return out.join("\n");
|
|
@@ -503,7 +506,7 @@ const brightText = (s) => s; // default terminal colour (un-dimmed) — the "pop
|
|
|
503
506
|
// forward reference here is a startup crash, not a lint warning.
|
|
504
507
|
const BULLET_STYLE = {
|
|
505
508
|
fail: { mark: CROSS, markColor: red, textColor: brightText },
|
|
506
|
-
ok: { mark: CHECK, markColor:
|
|
509
|
+
ok: { mark: CHECK, markColor: accent, textColor: dim },
|
|
507
510
|
info: { mark: BULLET, markColor: dim, textColor: dim },
|
|
508
511
|
};
|
|
509
512
|
const classifyBullet = (text) => BULLET_STYLE[bulletKind(text)];
|
|
@@ -536,7 +539,7 @@ const cliVerdict = classifyCliArg(argv, CLI_SUBCOMMANDS);
|
|
|
536
539
|
if (cliVerdict.kind === "unknown") {
|
|
537
540
|
console.error(red(`gu: unknown command "${cliVerdict.typed}"`));
|
|
538
541
|
if (cliVerdict.suggestion) {
|
|
539
|
-
console.error(
|
|
542
|
+
console.error(accent(` did you mean: gu ${cliVerdict.suggestion}`));
|
|
540
543
|
}
|
|
541
544
|
console.error(dim(` commands: ${CLI_SUBCOMMANDS.join(" · ")}`));
|
|
542
545
|
console.error(dim(" gu --help what each one does · gu (no command) opens the agent"));
|
|
@@ -625,7 +628,7 @@ if (!workerKey || !apiBase) {
|
|
|
625
628
|
output: process.stdout,
|
|
626
629
|
});
|
|
627
630
|
const ask0 = (q) =>
|
|
628
|
-
new Promise((res) => rl0.question(
|
|
631
|
+
new Promise((res) => rl0.question(accent(q), (a) => res(String(a || ""))));
|
|
629
632
|
let typed = "";
|
|
630
633
|
// Set when Client Expert setup stops on something the user must do first: the run ends
|
|
631
634
|
// after the instructions instead of continuing into questions that assume a paired,
|
|
@@ -739,7 +742,7 @@ if (!workerKey || !apiBase) {
|
|
|
739
742
|
} else {
|
|
740
743
|
console.error(
|
|
741
744
|
red("gu: no worker key / API base found.") +
|
|
742
|
-
`\nRun ${
|
|
745
|
+
`\nRun ${accent("gu-cli login")} to sign in, or configure manually:` +
|
|
743
746
|
"\n gu-cli set <workerKey> --api-base https://chat-api-v2.gomarsic.cc"
|
|
744
747
|
);
|
|
745
748
|
process.exit(1);
|
|
@@ -820,8 +823,8 @@ function hintRows(cmds, sel) {
|
|
|
820
823
|
cmds.forEach((c, i) => {
|
|
821
824
|
const label = c.usage ?? c.name;
|
|
822
825
|
const alias = c.aliases?.length ? dim(` (${c.aliases.join(", ")})`) : "";
|
|
823
|
-
const body = `${
|
|
824
|
-
rows.push(i === sel ? `${
|
|
826
|
+
const body = `${accent(label)}${alias}${dim(" — " + c.desc)}`;
|
|
827
|
+
rows.push(i === sel ? `${accent("❯")} ${bold(body)}` : ` ${body}`);
|
|
825
828
|
});
|
|
826
829
|
return rows;
|
|
827
830
|
}
|
|
@@ -1329,7 +1332,7 @@ if (IS_TTY) {
|
|
|
1329
1332
|
// the user's typed input lands on the SAME text column (GUTTER+1) as the agent's bullets and
|
|
1330
1333
|
// answer. ">> " is exactly GUTTER-wide, so input aligns; the ">>" right-edge sits at the same
|
|
1331
1334
|
// column the ● / ✓ / ✗ markers hang at (markers right-aligned in the margin, text in one column).
|
|
1332
|
-
const PROMPT =
|
|
1335
|
+
const PROMPT = accent(">> ");
|
|
1333
1336
|
// …and once a question has been SUBMITTED, its marker recedes to grey. Cyan then means exactly
|
|
1334
1337
|
// one thing on screen — "this is the line you are typing" — while the questions already asked
|
|
1335
1338
|
// are history, the same way completed actions dim (see the UI hierarchy in #41/#115).
|
|
@@ -1748,7 +1751,7 @@ const ragSegmentText = () => {
|
|
|
1748
1751
|
// one's. Showing it bright would claim the current prompt has been measured when it has
|
|
1749
1752
|
// not; showing nothing would put a misleading 0 there. Dim says "last known".
|
|
1750
1753
|
if (b.stale) return dim(" · rag ") + dim(`${fmtTokens(b.rag)} ${pct}%·`) + lexSeg;
|
|
1751
|
-
return dim(" · rag ") +
|
|
1754
|
+
return dim(" · rag ") + accent(`${fmtTokens(b.rag)} ${pct}%`) + lexSeg;
|
|
1752
1755
|
}
|
|
1753
1756
|
// A STATE, NOT A MEASUREMENT — and states are verbose-only (user, 2026-09-05: "we need to
|
|
1754
1757
|
// hide rag - not indexed, only show if user go with gu --verbose").
|
|
@@ -1853,7 +1856,7 @@ function announceCliUpdate() {
|
|
|
1853
1856
|
cliNoticeShown = true;
|
|
1854
1857
|
console.log("");
|
|
1855
1858
|
for (const l of lines) {
|
|
1856
|
-
if (l.kind === "head") console.log(
|
|
1859
|
+
if (l.kind === "head") console.log(accent(" ⭯ " + l.text));
|
|
1857
1860
|
else if (l.kind === "cmd") console.log(" " + l.text);
|
|
1858
1861
|
else console.log(dim(" " + l.text));
|
|
1859
1862
|
}
|
|
@@ -1909,7 +1912,7 @@ async function runUpdate() {
|
|
|
1909
1912
|
r.stderr ? ` — ${String(r.stderr).trim().split("\n").slice(-1)[0]}` : ""}`);
|
|
1910
1913
|
}
|
|
1911
1914
|
});
|
|
1912
|
-
console.log(
|
|
1915
|
+
console.log(accent(` ✓ API updated to ${api}\n`));
|
|
1913
1916
|
availableUpdates = { ...availableUpdates, api: "", any: Boolean(availableUpdates.cli) };
|
|
1914
1917
|
} else {
|
|
1915
1918
|
await withPendingStatus(`updating gu to ${cli}`, async () => {
|
|
@@ -1930,7 +1933,7 @@ async function runUpdate() {
|
|
|
1930
1933
|
});
|
|
1931
1934
|
// NOT "updated" — the files changed, THIS process did not. Claiming otherwise is the one
|
|
1932
1935
|
// way this feature could lie to someone.
|
|
1933
|
-
console.log(
|
|
1936
|
+
console.log(accent(` ✓ gu ${cli} installed`) +
|
|
1934
1937
|
dim(` — ${updateRestartNote()}\n`));
|
|
1935
1938
|
availableUpdates = { ...availableUpdates, cli: "", any: Boolean(availableUpdates.api) };
|
|
1936
1939
|
}
|
|
@@ -1978,7 +1981,7 @@ const identityLineText = () =>
|
|
|
1978
1981
|
folder: folderLabel(),
|
|
1979
1982
|
keyUnchosen: keyChoicePending,
|
|
1980
1983
|
},
|
|
1981
|
-
{ orange, cyan, dim, red },
|
|
1984
|
+
{ orange, cyan: accent, dim, red },
|
|
1982
1985
|
);
|
|
1983
1986
|
|
|
1984
1987
|
const tokenLineText = () => {
|
|
@@ -2015,10 +2018,10 @@ const tokenLineText = () => {
|
|
|
2015
2018
|
// `ctx` and `sent`, which do drive decisions, need more.
|
|
2016
2019
|
ragSegmentText() +
|
|
2017
2020
|
dim(" · ") +
|
|
2018
|
-
|
|
2021
|
+
accent(`↓ out ${fmtTokens(lastTurnOutputTokens)}`) +
|
|
2019
2022
|
dim(" · ") +
|
|
2020
2023
|
dim("total ") +
|
|
2021
|
-
|
|
2024
|
+
accent(`↓ ${fmtTokens(Math.max(globalOutputTokens, lastTurnTotalOutput))}`)
|
|
2022
2025
|
);
|
|
2023
2026
|
};
|
|
2024
2027
|
// The newest RAG index file the agent reported writing (task #136), and where it lives.
|
|
@@ -2034,7 +2037,7 @@ const ragLineText = () => {
|
|
|
2034
2037
|
const parts = lastRagFile.split("/").filter(Boolean);
|
|
2035
2038
|
const shortName = parts.slice(-2).join("/") || lastRagFile;
|
|
2036
2039
|
return (
|
|
2037
|
-
dim("rag · wrote ") + (isS3 ?
|
|
2040
|
+
dim("rag · wrote ") + (isS3 ? accent(`s3:…/${shortName}`) : shortName) +
|
|
2038
2041
|
dim(isS3 ? " (cloud)" : " (local)")
|
|
2039
2042
|
);
|
|
2040
2043
|
};
|
|
@@ -2146,7 +2149,7 @@ function frozenZone() {
|
|
|
2146
2149
|
const zone = inputZoneRows({
|
|
2147
2150
|
headControl,
|
|
2148
2151
|
width,
|
|
2149
|
-
prompt: secretInput && secretPrompt ?
|
|
2152
|
+
prompt: secretInput && secretPrompt ? accent(secretPrompt) : inlinePrompt ? accent(inlinePrompt) : PROMPT,
|
|
2150
2153
|
// WHILE A SECRET IS OPEN, THIS ROW IS THE PROMPT — "kimi-k3 API key: ••••" rather than
|
|
2151
2154
|
// ">> ••••". Same row, same masking; what changes is that it now says what it wants. The
|
|
2152
2155
|
// width is measured, not assumed: the label is caller-supplied and every offset in here
|
|
@@ -2199,11 +2202,11 @@ function frozenZone() {
|
|
|
2199
2202
|
agentsRowTarget = null;
|
|
2200
2203
|
if (scrolledBack()) {
|
|
2201
2204
|
const hint = jumpToBottomRow({ below: scrollOffset, width });
|
|
2202
|
-
rows[sep] = clipVisible(
|
|
2205
|
+
rows[sep] = clipVisible(accent(hint.text), width - 1);
|
|
2203
2206
|
} else if (availableUpdates.any) {
|
|
2204
2207
|
const up = updateRow({ ...availableUpdates, width });
|
|
2205
2208
|
if (up) {
|
|
2206
|
-
rows[sep] = clipVisible(dim(up.text.slice(0, up.from)) +
|
|
2209
|
+
rows[sep] = clipVisible(dim(up.text.slice(0, up.from)) + accent(up.text.slice(up.from)), width - 1);
|
|
2207
2210
|
updateRowTarget = { from: up.from, to: up.to };
|
|
2208
2211
|
}
|
|
2209
2212
|
} else if (agentsIndicator(otherAgents)) {
|
|
@@ -2216,7 +2219,7 @@ function frozenZone() {
|
|
|
2216
2219
|
const len = visibleLen(label);
|
|
2217
2220
|
if (len + 4 <= width - 1) {
|
|
2218
2221
|
const from = width - 1 - len;
|
|
2219
|
-
rows[sep] = clipVisible(dim("\u2500".repeat(Math.max(0, from - 1)) + " ") +
|
|
2222
|
+
rows[sep] = clipVisible(dim("\u2500".repeat(Math.max(0, from - 1)) + " ") + accent(label), width - 1);
|
|
2220
2223
|
agentsRowTarget = { from, to: from + len };
|
|
2221
2224
|
}
|
|
2222
2225
|
}
|
|
@@ -2904,7 +2907,7 @@ function paintCopyBadge(row, place, ok = true) {
|
|
|
2904
2907
|
//
|
|
2905
2908
|
// \x1b7/\x1b8 is the ONE save slot the whole REPL shares — the output caret lives in it
|
|
2906
2909
|
// (task #141) — so this moves and returns explicitly instead of borrowing it.
|
|
2907
|
-
ui(`\x1b[${row};${place.col}H` + (ok ?
|
|
2910
|
+
ui(`\x1b[${row};${place.col}H` + (ok ? accent(place.text) : dim(place.text)));
|
|
2908
2911
|
paintBar(); // put the caret back on the input line
|
|
2909
2912
|
if (copyBadgeTimer) clearTimeout(copyBadgeTimer);
|
|
2910
2913
|
copyBadgeTimer = setTimeout(() => {
|
|
@@ -4014,12 +4017,12 @@ async function ensureWorkspace() {
|
|
|
4014
4017
|
wsSync = { enabled: allowSync, name: basename(cwd), path: cwd, run: allowRun };
|
|
4015
4018
|
console.log(
|
|
4016
4019
|
GUTTER +
|
|
4017
|
-
|
|
4018
|
-
(allowRun ?
|
|
4020
|
+
accent(`✓ workspace registered: ${basename(cwd)}`) +
|
|
4021
|
+
(allowRun ? accent(" (run-commands enabled)") : dim(" (run-commands disabled)")) +
|
|
4019
4022
|
(isClientExpert()
|
|
4020
4023
|
? ""
|
|
4021
4024
|
: allowSync
|
|
4022
|
-
?
|
|
4025
|
+
? accent(workspaceSyncLabel({ allowSync, clientExpert: false }))
|
|
4023
4026
|
: dim(workspaceSyncLabel({ allowSync, clientExpert: false })))
|
|
4024
4027
|
);
|
|
4025
4028
|
}
|
|
@@ -4676,7 +4679,7 @@ async function runModelDoctor() {
|
|
|
4676
4679
|
console.log(
|
|
4677
4680
|
"\n" + GUTTER +
|
|
4678
4681
|
(bad.length === 0
|
|
4679
|
-
?
|
|
4682
|
+
? accent("✓ every configured model answered.")
|
|
4680
4683
|
: red(`✗ ${bad.length} of ${results.length} did not answer.`)) +
|
|
4681
4684
|
"\n"
|
|
4682
4685
|
);
|
|
@@ -4944,7 +4947,7 @@ async function reconfigureOllamaServer() {
|
|
|
4944
4947
|
if (g !== null) globalOutputTokens = g;
|
|
4945
4948
|
}
|
|
4946
4949
|
paintBar(); // …and show all of it now, not at the next keystroke
|
|
4947
|
-
console.log(
|
|
4950
|
+
console.log(accent(" ✓ models saved"));
|
|
4948
4951
|
if (agent) {
|
|
4949
4952
|
console.log(dim(` chat ${chat} @ ${agent.normalized.host}`));
|
|
4950
4953
|
console.log(dim(` RAG ${os.REQUIRED_EMBED_MODEL} @ ${agent.normalized.host}`));
|
|
@@ -5221,7 +5224,7 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
5221
5224
|
await saveSessionCoding(resolve(process.cwd()));
|
|
5222
5225
|
console.log(GUTTER + dim("this folder's pick named the old model — cleared it."));
|
|
5223
5226
|
}
|
|
5224
|
-
console.log(
|
|
5227
|
+
console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
|
|
5225
5228
|
}
|
|
5226
5229
|
|
|
5227
5230
|
/** Point a provider at a different URL, keeping its id so saved picks still resolve. */
|
|
@@ -5258,7 +5261,7 @@ async function changeProviderUrl(ps, rows, p) {
|
|
|
5258
5261
|
}
|
|
5259
5262
|
// The KEY still belongs to this row, because the row id did not change — which is the whole
|
|
5260
5263
|
// reason an id is not derived from the URL.
|
|
5261
|
-
console.log(
|
|
5264
|
+
console.log(accent(` ✓ ${p.host} → ${r.host}`) + dim(" · its key still applies\n"));
|
|
5262
5265
|
}
|
|
5263
5266
|
|
|
5264
5267
|
/** Remove a provider, and the keys that would be stranded by its removal. */
|
|
@@ -5314,7 +5317,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
5314
5317
|
console.log(GUTTER + dim("this folder was using it — cleared the pick.\n"));
|
|
5315
5318
|
}
|
|
5316
5319
|
console.log(
|
|
5317
|
-
|
|
5320
|
+
accent(` ✓ removed ${p.host}`) +
|
|
5318
5321
|
(stranded.length ? dim(` · ${deleted} key${deleted === 1 ? "" : "s"} deleted`) : "") +
|
|
5319
5322
|
"\n"
|
|
5320
5323
|
);
|
|
@@ -5622,7 +5625,7 @@ async function addCodingProvider(roleName = "coding") {
|
|
|
5622
5625
|
console.log(GUTTER + dim("run /agent-model to finish, or check the API is reachable.\n"));
|
|
5623
5626
|
return;
|
|
5624
5627
|
}
|
|
5625
|
-
console.log(
|
|
5628
|
+
console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
|
|
5626
5629
|
console.log(dim(` API key ${name || "Unknown"} ····${secret.slice(-4)} · this machine\n`));
|
|
5627
5630
|
// Restart advice, because the banner and the running daemon both read this at startup: a
|
|
5628
5631
|
// machine that keeps talking to the old model after a successful save looks like the save
|
|
@@ -5692,7 +5695,7 @@ async function addCodingProvider(roleName = "coding") {
|
|
|
5692
5695
|
// The key question for this coder is now answered — leaving the flag set would warn about a
|
|
5693
5696
|
// key that was just entered, which reads as the save having failed.
|
|
5694
5697
|
codingKeyMissingKnown = false;
|
|
5695
|
-
console.log(
|
|
5698
|
+
console.log(accent(` ✓ coding model → ${model}`) + dim(` · ${host}`));
|
|
5696
5699
|
console.log(dim(` API key ${name || "Unknown"} ····${secret.slice(-4)} · this folder only\n`));
|
|
5697
5700
|
// A HOSTED PROVIDER NEEDS ITS WINDOW SET (requirement 11). With nothing set gu assumes
|
|
5698
5701
|
// 24,000 prompt tokens and compacts there — on a large hosted model that throws history away
|
|
@@ -5826,7 +5829,7 @@ async function chooseModel() {
|
|
|
5826
5829
|
await saveSessionCoding(resolve(process.cwd()));
|
|
5827
5830
|
const where = showTag ? ` (${tags[idx]})` : "";
|
|
5828
5831
|
console.log(
|
|
5829
|
-
|
|
5832
|
+
accent(` ✓ coding model → ${chosen.model}${where}${chosen.id === def ? " (default)" : ""}\n`)
|
|
5830
5833
|
);
|
|
5831
5834
|
// DOES THIS ONE NEED AN API KEY? (task #181) Reported live on Windows 11 / Client Expert:
|
|
5832
5835
|
// picking kimi-k3 here and asking a question just span — the coder needs a key, none was
|
|
@@ -5870,7 +5873,7 @@ async function chooseModel() {
|
|
|
5870
5873
|
host: ks.provider,
|
|
5871
5874
|
});
|
|
5872
5875
|
console.log(
|
|
5873
|
-
dim(" API key ") +
|
|
5876
|
+
dim(" API key ") + accent(line.text) +
|
|
5874
5877
|
(line.note ? orange(` · ${line.note}`) : "") +
|
|
5875
5878
|
dim(` · ctrl+k to ${line.verb} it`)
|
|
5876
5879
|
);
|
|
@@ -5928,7 +5931,7 @@ async function chooseEmbedModel() {
|
|
|
5928
5931
|
console.log(dim(` Choose the embedder (${PICK_HINT}):`));
|
|
5929
5932
|
const labels = rows.map(
|
|
5930
5933
|
(r) => `${r.model}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}` +
|
|
5931
|
-
(r.current ?
|
|
5934
|
+
(r.current ? accent(" (current)") : "")
|
|
5932
5935
|
);
|
|
5933
5936
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
5934
5937
|
if (idx < 0) {
|
|
@@ -5966,7 +5969,7 @@ async function chooseEmbedModel() {
|
|
|
5966
5969
|
console.log(red(` couldn't save that to your account — still using ${current}.\n`));
|
|
5967
5970
|
return;
|
|
5968
5971
|
}
|
|
5969
|
-
console.log(
|
|
5972
|
+
console.log(accent(` ✓ embedding model → ${chosen}`));
|
|
5970
5973
|
if (plan.stale.length) {
|
|
5971
5974
|
console.log(
|
|
5972
5975
|
dim(` ${plan.stale.length} knowledge base${plan.stale.length === 1 ? "" : "s"} ` +
|
|
@@ -6073,7 +6076,7 @@ async function chooseAgentModel() {
|
|
|
6073
6076
|
// "· an embedding model · embedding model, not for chat".
|
|
6074
6077
|
(!r.note && r.embedder ? dim(" · embedding model, not for chat") : "") +
|
|
6075
6078
|
(r.missing ? dim(" · not on the box any more") : "") +
|
|
6076
|
-
(r.current ?
|
|
6079
|
+
(r.current ? accent(" (current)") : "")
|
|
6077
6080
|
);
|
|
6078
6081
|
});
|
|
6079
6082
|
// APPENDED LAST, after every model, for the reason #223 records: a new entry must never shift
|
|
@@ -6125,7 +6128,7 @@ async function chooseAgentModel() {
|
|
|
6125
6128
|
console.log(GUTTER + dim("`gu api restart` to pick up the current version.\n"));
|
|
6126
6129
|
return;
|
|
6127
6130
|
}
|
|
6128
|
-
console.log(
|
|
6131
|
+
console.log(accent(` ✓ agent model → ${chosen}`));
|
|
6129
6132
|
console.log(dim(" takes effect on your next question.\n"));
|
|
6130
6133
|
}
|
|
6131
6134
|
|
|
@@ -6222,7 +6225,7 @@ async function runContextWindowCommand(line) {
|
|
|
6222
6225
|
console.log(GUTTER + dim(` set by GONEXT_CODING_CONTEXT_WINDOW=${fromEnv} in the environment —`));
|
|
6223
6226
|
console.log(GUTTER + dim(" that wins over anything stored here."));
|
|
6224
6227
|
} else if (stored) {
|
|
6225
|
-
console.log(GUTTER + ` context window: ${
|
|
6228
|
+
console.log(GUTTER + ` context window: ${accent(fmtTokens(stored))}` +
|
|
6226
6229
|
dim(` (compacting above ~${fmtTokens(Math.floor(stored * 2 / 3))})`));
|
|
6227
6230
|
} else {
|
|
6228
6231
|
console.log(GUTTER + dim(" context window: not set — the worker probes the server, falls"));
|
|
@@ -6238,7 +6241,7 @@ async function runContextWindowCommand(line) {
|
|
|
6238
6241
|
if (!arg) return show();
|
|
6239
6242
|
if (arg === "auto" || arg === "clear" || arg === "off") {
|
|
6240
6243
|
await ctl.setCodingContextWindow(0);
|
|
6241
|
-
console.log(GUTTER +
|
|
6244
|
+
console.log(GUTTER + accent("✓ cleared") + dim(" — probing the model server again.\n"));
|
|
6242
6245
|
return;
|
|
6243
6246
|
}
|
|
6244
6247
|
// "1m" / "128k" as well as digits: the number people quote is the round one.
|
|
@@ -6251,7 +6254,7 @@ async function runContextWindowCommand(line) {
|
|
|
6251
6254
|
console.log(GUTTER + dim(" give a token count above 4096 — e.g. 128k, 1m, or 1000000.\n"));
|
|
6252
6255
|
return;
|
|
6253
6256
|
}
|
|
6254
|
-
console.log(GUTTER +
|
|
6257
|
+
console.log(GUTTER + accent(`✓ context window → ${fmtTokens(saved)}`) +
|
|
6255
6258
|
dim(` · compacting above ~${fmtTokens(Math.floor(saved * 2 / 3))}`));
|
|
6256
6259
|
console.log(GUTTER + dim(" takes effect on your next question.\n"));
|
|
6257
6260
|
}
|
|
@@ -6329,7 +6332,7 @@ async function runS3Command(line) {
|
|
|
6329
6332
|
const { ok, rows } = s3.describeS3Test(res);
|
|
6330
6333
|
for (const row of rows) console.log(GUTTER + (ok ? dim(` ${row}`) : ` ${dim(row)}`));
|
|
6331
6334
|
console.log(
|
|
6332
|
-
GUTTER + (ok ?
|
|
6335
|
+
GUTTER + (ok ? accent("✓ S3 is reachable, readable and writable.\n") : red("✗ S3 is not usable yet.\n"))
|
|
6333
6336
|
);
|
|
6334
6337
|
return ok;
|
|
6335
6338
|
};
|
|
@@ -6406,7 +6409,7 @@ async function runS3Command(line) {
|
|
|
6406
6409
|
console.log(GUTTER + dim(" if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
6407
6410
|
return;
|
|
6408
6411
|
}
|
|
6409
|
-
console.log(GUTTER +
|
|
6412
|
+
console.log(GUTTER + accent("✓ saved.") + dim(" now checking it actually works…\n"));
|
|
6410
6413
|
// Tested with what was just SAVED (no body) rather than what was typed: that proves the
|
|
6411
6414
|
// stored config works, which is the thing every later turn will use.
|
|
6412
6415
|
await runTest({});
|
|
@@ -6452,7 +6455,7 @@ async function runProxyCommand(line) {
|
|
|
6452
6455
|
// just said they are no longer using, and off an office network those fail slowly.
|
|
6453
6456
|
resetDispatchers();
|
|
6454
6457
|
console.log(
|
|
6455
|
-
GUTTER + (action === "on" ?
|
|
6458
|
+
GUTTER + (action === "on" ? accent("proxy on") : dim("proxy off")) +
|
|
6456
6459
|
dim(action === "on" ? " — model calls will be routed." : " — model calls go out directly (credentials kept).") + "\n"
|
|
6457
6460
|
);
|
|
6458
6461
|
return;
|
|
@@ -6469,7 +6472,7 @@ async function runProxyCommand(line) {
|
|
|
6469
6472
|
const proxy = await store.loadProxy();
|
|
6470
6473
|
console.log(GUTTER + dim("checking the proxy against your configured model…"));
|
|
6471
6474
|
const res = await cmd.verifyProxy({ proxy, settings, fetchImpl: proxyFetch });
|
|
6472
|
-
console.log(GUTTER + (res.ok ?
|
|
6475
|
+
console.log(GUTTER + (res.ok ? accent("✓ ") : yellow("✗ ")) + dim(res.detail) + "\n");
|
|
6473
6476
|
return;
|
|
6474
6477
|
}
|
|
6475
6478
|
|
|
@@ -6496,14 +6499,14 @@ async function runProxyCommand(line) {
|
|
|
6496
6499
|
const proxy = await store.saveProxy({ enabled: true, host, port, username, password });
|
|
6497
6500
|
resetDispatchers();
|
|
6498
6501
|
const { describeProxy } = await import("./proxy-config.mjs");
|
|
6499
|
-
console.log(GUTTER +
|
|
6502
|
+
console.log(GUTTER + accent("saved") + dim(` — ${describeProxy(proxy)}`));
|
|
6500
6503
|
|
|
6501
6504
|
// VERIFY NOW, while the user still has the password in their head. A proxy that is merely
|
|
6502
6505
|
// stored fails later, mid-turn, as an error the model cannot act on.
|
|
6503
6506
|
console.log(GUTTER + dim("checking it against your configured model…"));
|
|
6504
6507
|
const res = await cmd.verifyProxy({ proxy, settings, fetchImpl: proxyFetch });
|
|
6505
6508
|
if (res.ok) {
|
|
6506
|
-
console.log(GUTTER +
|
|
6509
|
+
console.log(GUTTER + accent("✓ ") + dim(res.detail) + "\n");
|
|
6507
6510
|
} else {
|
|
6508
6511
|
console.log(GUTTER + yellow("✗ ") + dim(res.detail));
|
|
6509
6512
|
// Left ENABLED on purpose: the user asked for it, and the failure may be the model host
|
|
@@ -6555,7 +6558,7 @@ async function chooseServer() {
|
|
|
6555
6558
|
}
|
|
6556
6559
|
const s = servers[idx];
|
|
6557
6560
|
selectedServer = { name: s.name, host: s.host, user: s.user };
|
|
6558
|
-
console.log(
|
|
6561
|
+
console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
|
|
6559
6562
|
await checkServerKeyAuth(selectedServer);
|
|
6560
6563
|
console.log("");
|
|
6561
6564
|
}
|
|
@@ -6709,7 +6712,7 @@ async function stopOtherAgent(jobId, shortName) {
|
|
|
6709
6712
|
if (r?.ok) {
|
|
6710
6713
|
// "asked", not "stopped": this marks the job cancelled, and the worker kills the agent
|
|
6711
6714
|
// when it next sees that. Claiming the process is already gone would be a guess.
|
|
6712
|
-
console.log(GUTTER +
|
|
6715
|
+
console.log(GUTTER + accent("✓ asked it to stop") + dim(" — the worker ends it within a few seconds.\n"));
|
|
6713
6716
|
// Drop it from the cached list NOW so the separator's count stops offering an agent the
|
|
6714
6717
|
// user has just ended; the 20s poll would otherwise leave it there until it catches up.
|
|
6715
6718
|
otherAgents = otherAgents.filter((a) => a.id !== jobId);
|
|
@@ -6776,7 +6779,9 @@ async function coderKeyState(override = "") {
|
|
|
6776
6779
|
const active = override
|
|
6777
6780
|
? choices.find((c) => c.id === override)
|
|
6778
6781
|
: choices.find((c) => c.isDefault);
|
|
6779
|
-
|
|
6782
|
+
// Kind AND url: an Ollama coder at https://ollama.com needs a key; a local one does not.
|
|
6783
|
+
const { codingNeedsKey } = await import("./coding-key-prompt.mjs");
|
|
6784
|
+
const needsKey = codingNeedsKey(active?.kind, active?.url);
|
|
6780
6785
|
return {
|
|
6781
6786
|
needsKey,
|
|
6782
6787
|
model: p.codingKeyModel || active?.model || "the coding model",
|
|
@@ -6904,7 +6909,7 @@ async function promptNewCodingKey(backendId = "", provider = "") {
|
|
|
6904
6909
|
const id = await addCodingKey(name, secret, backendId);
|
|
6905
6910
|
console.log(
|
|
6906
6911
|
id
|
|
6907
|
-
?
|
|
6912
|
+
? accent(` ✓ added ${name || "Unknown"}`) +
|
|
6908
6913
|
(backendId && provider ? dim(` · for ${provider}`) : "") + "\n"
|
|
6909
6914
|
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
6910
6915
|
);
|
|
@@ -6974,7 +6979,7 @@ async function replaceCodingKey() {
|
|
|
6974
6979
|
// would collect something with nowhere to go. The original prompt is exactly right here.
|
|
6975
6980
|
console.log(
|
|
6976
6981
|
"\n" + dim(` ${ks.model} API key `) +
|
|
6977
|
-
(ks.stored ? dim("(replacing ") +
|
|
6982
|
+
(ks.stored ? dim("(replacing ") + accent(maskKey(ks.stored)) + dim(")") : dim("(none stored)")) +
|
|
6978
6983
|
dim(" · Enter alone cancels, \"clear\" removes it\n")
|
|
6979
6984
|
);
|
|
6980
6985
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
@@ -6989,8 +6994,8 @@ async function replaceCodingKey() {
|
|
|
6989
6994
|
console.log(
|
|
6990
6995
|
ok
|
|
6991
6996
|
? clearing
|
|
6992
|
-
?
|
|
6993
|
-
:
|
|
6997
|
+
? accent(` ✓ ${ks.model} API key removed`) + dim(" — coding falls back to your local model\n")
|
|
6998
|
+
: accent(` ✓ ${ks.model} API key saved\n`)
|
|
6994
6999
|
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
6995
7000
|
);
|
|
6996
7001
|
paintBar();
|
|
@@ -7007,15 +7012,15 @@ async function manageCodingKeys(ks, keys) {
|
|
|
7007
7012
|
const current = sessionCodingKeyId;
|
|
7008
7013
|
const rows = keys.map(
|
|
7009
7014
|
(k) =>
|
|
7010
|
-
`${k.name}${dim(` ····${k.tail}`)}${k.id === current ?
|
|
7015
|
+
`${k.name}${dim(` ····${k.tail}`)}${k.id === current ? accent(" (this folder)") : ""}`
|
|
7011
7016
|
);
|
|
7012
7017
|
const ADD = rows.length;
|
|
7013
7018
|
rows.push(dim("add a new key…"));
|
|
7014
7019
|
console.log(
|
|
7015
7020
|
"\n" + GUTTER +
|
|
7016
7021
|
(keys.length > 1
|
|
7017
|
-
? dim(`Which key should ${folderLabel()} use for `) +
|
|
7018
|
-
: dim(`${folderLabel()} uses this key for `) +
|
|
7022
|
+
? dim(`Which key should ${folderLabel()} use for `) + accent(ks.model) + dim("?\n")
|
|
7023
|
+
: dim(`${folderLabel()} uses this key for `) + accent(ks.model) + dim(" — or add another.\n"))
|
|
7019
7024
|
);
|
|
7020
7025
|
console.log(GUTTER + dim("Enter chooses · ctrl+d deletes the highlighted key\n"));
|
|
7021
7026
|
const at = Math.max(0, keys.findIndex((k) => k.id === current));
|
|
@@ -7059,7 +7064,7 @@ async function manageCodingKeys(ks, keys) {
|
|
|
7059
7064
|
keyChoicePending = false; // chosen — the bar's mark comes off
|
|
7060
7065
|
codingKeyMissingKnown = false;
|
|
7061
7066
|
console.log(
|
|
7062
|
-
|
|
7067
|
+
accent(` ✓ ${folderLabel()} → ${keys[idx].name}`) + dim(` ····${keys[idx].tail}\n`)
|
|
7063
7068
|
);
|
|
7064
7069
|
paintBar();
|
|
7065
7070
|
}
|
|
@@ -7110,7 +7115,7 @@ async function deleteCodingKeyFlow(ks, keys, idx) {
|
|
|
7110
7115
|
sessionCodingKeyId = "";
|
|
7111
7116
|
await saveSessionCodingKeyId(resolve(process.cwd()));
|
|
7112
7117
|
}
|
|
7113
|
-
console.log(
|
|
7118
|
+
console.log(accent(` ✓ deleted ${out.name || victim.name}`) +
|
|
7114
7119
|
dim(` ····${victim.tail} · ${out.remaining} key(s) left\n`));
|
|
7115
7120
|
paintBar();
|
|
7116
7121
|
}
|
|
@@ -7138,7 +7143,7 @@ async function promptForCodingKey(override = "") {
|
|
|
7138
7143
|
askSecret,
|
|
7139
7144
|
saveCodingKey,
|
|
7140
7145
|
log: (s) => console.log(s),
|
|
7141
|
-
colors: { dim, cyan, red },
|
|
7146
|
+
colors: { dim, cyan: accent, red },
|
|
7142
7147
|
});
|
|
7143
7148
|
}
|
|
7144
7149
|
|
|
@@ -7225,15 +7230,15 @@ function drawApprovalOptions(redraw) {
|
|
|
7225
7230
|
// is 2 cols so Yes/No align; the \x1b[2A row-up count is unchanged (still 2 rows).
|
|
7226
7231
|
// Both rows are a handful of columns wide, so they survive any resize on ONE physical row
|
|
7227
7232
|
// each and the \x1b[2A count stays exact — which is why a resize can simply redraw here.
|
|
7228
|
-
const yes = approvalSel === 0 ?
|
|
7229
|
-
const no = approvalSel === 1 ?
|
|
7233
|
+
const yes = approvalSel === 0 ? accent("▸ Yes") : dim(" Yes");
|
|
7234
|
+
const no = approvalSel === 1 ? accent("▸ No") : dim(" No");
|
|
7230
7235
|
regionUi((redraw ? "\x1b[2A" : "") + "\x1b[K" + GUTTER + yes + "\n\x1b[K" + GUTTER + no + "\n");
|
|
7231
7236
|
}
|
|
7232
7237
|
function finishApproval(allow) {
|
|
7233
7238
|
if (!approvalActive) return;
|
|
7234
7239
|
approvalActive = false;
|
|
7235
7240
|
// Settle the picker: overwrite the two option lines with a single result line.
|
|
7236
|
-
process.stdout.write("\x1b[2A\x1b[J" + GUTTER + (allow ?
|
|
7241
|
+
process.stdout.write("\x1b[2A\x1b[J" + GUTTER + (allow ? accent("▸ Yes — running it") : red("▸ No — skipped")) + "\n");
|
|
7237
7242
|
const r = approvalResolve;
|
|
7238
7243
|
approvalResolve = null;
|
|
7239
7244
|
if (r) r(!!allow);
|
|
@@ -7398,7 +7403,7 @@ function drawList(redraw) {
|
|
|
7398
7403
|
// multi-select feel like it forgot what you chose.
|
|
7399
7404
|
const box = listMulti ? (listChecked.has(i) ? "[x] " : "[ ] ") : "";
|
|
7400
7405
|
const body = box + listItems[i];
|
|
7401
|
-
return clipVisible(GUTTER + (i === listSel ?
|
|
7406
|
+
return clipVisible(GUTTER + (i === listSel ? accent("▸ " + body) : dim(" " + body)), width - 1);
|
|
7402
7407
|
};
|
|
7403
7408
|
const marker = (text) => clipVisible(GUTTER + dim(text), width - 1);
|
|
7404
7409
|
const rows = [];
|
|
@@ -8283,7 +8288,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8283
8288
|
// figure holds rather than jittering against flowing output.
|
|
8284
8289
|
shownOutputTokens = easeCount(shownOutputTokens, latestOutputTokens);
|
|
8285
8290
|
const outLabel =
|
|
8286
|
-
shownOutputTokens > 0 ? dim(" · ↓ ") +
|
|
8291
|
+
shownOutputTokens > 0 ? dim(" · ↓ ") + accent(fmtTokens(shownOutputTokens)) : "";
|
|
8287
8292
|
// Task #96: the thought line is "clickable" only when the fuller thought has MORE text than
|
|
8288
8293
|
// fits on line 1 (a live command / bare "Thinking" / a thought that already fits has nothing
|
|
8289
8294
|
// to reveal). Line 1 (what's happening now): a live command > the model's current Thought >
|
|
@@ -8388,7 +8393,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8388
8393
|
const clipRow = (r) => clipVisible(r, Math.max(20, process.stdout.columns || 80) - 1);
|
|
8389
8394
|
const contentRows = [];
|
|
8390
8395
|
for (let i = 0; i < LEAD_ROWS; i++) contentRows.push(""); // the separator — see LEAD_ROWS
|
|
8391
|
-
contentRows.push(clipRow(`${markLine(
|
|
8396
|
+
contentRows.push(clipRow(`${markLine(accent(BULLET))}${hovering ? hoverThought(label) : accent(label)}`));
|
|
8392
8397
|
for (const dr of detailRows) contentRows.push(clipRow(dim(dr)));
|
|
8393
8398
|
for (const wl of expLines) contentRows.push(clipRow(GUTTER + dim(wl)));
|
|
8394
8399
|
for (let i = 0; i < WORD_GAP; i++) contentRows.push(""); // see WORD_GAP
|
|
@@ -8623,7 +8628,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8623
8628
|
onContent();
|
|
8624
8629
|
inEditCard = true;
|
|
8625
8630
|
openBullet();
|
|
8626
|
-
process.stdout.write(markLine(
|
|
8631
|
+
process.stdout.write(markLine(accent("⏺")) + bold(line.slice(1).replace(/^\s+/, "")) + "\n");
|
|
8627
8632
|
lastWasBlank = false;
|
|
8628
8633
|
lastContentAt = Date.now();
|
|
8629
8634
|
continue;
|
|
@@ -8634,7 +8639,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8634
8639
|
const m = EDIT_DIFF_RE.exec(line);
|
|
8635
8640
|
if (m) {
|
|
8636
8641
|
// " 19 - old text" → dim line number, red/cyan signed content.
|
|
8637
|
-
const paintDiff = m[3] === "+" ?
|
|
8642
|
+
const paintDiff = m[3] === "+" ? accent : red;
|
|
8638
8643
|
process.stdout.write(`${GUTTER}${m[1]}${dim(m[2])} ${paintDiff(`${m[3]} ${m[4]}`)}\n`);
|
|
8639
8644
|
} else {
|
|
8640
8645
|
process.stdout.write(GUTTER + dim(line) + "\n"); // "└ summary" / "… (+N more)"
|
|
@@ -9349,7 +9354,7 @@ async function main() {
|
|
|
9349
9354
|
servers: await (await import("./model-doctor.mjs")).discoverModelServers(),
|
|
9350
9355
|
});
|
|
9351
9356
|
const statusRows = [
|
|
9352
|
-
bold(
|
|
9357
|
+
bold(accent("GU AGENT")) + dim(` v${REPL_VERSION}`),
|
|
9353
9358
|
// Task #134: a client machine OVERRIDES the account's agent model locally (the
|
|
9354
9359
|
// account value points at the model host's own server). Show what will actually be
|
|
9355
9360
|
// used, not a value this machine can't reach.
|
|
@@ -9383,10 +9388,10 @@ async function main() {
|
|
|
9383
9388
|
...(() => {
|
|
9384
9389
|
const m = modeRow({ clientExpert: isClientExpert(), clientMode: clientModeBanner });
|
|
9385
9390
|
if (!m) return [];
|
|
9386
|
-
const name = m.label === "client expert" ?
|
|
9391
|
+
const name = m.label === "client expert" ? accent(m.label + " ") : m.label + " ";
|
|
9387
9392
|
return [lbl("mode") + name + dim(m.detail)];
|
|
9388
9393
|
})(),
|
|
9389
|
-
sessionTestAuto ?
|
|
9394
|
+
sessionTestAuto ? accent("auto-test on") : dim("auto-test off"),
|
|
9390
9395
|
// Same reason as workspaceSyncLabel: on Client Expert there is no cloud to sync TO, so
|
|
9391
9396
|
// the banner must not advertise one.
|
|
9392
9397
|
lbl("folder") +
|
|
@@ -9534,7 +9539,7 @@ async function main() {
|
|
|
9534
9539
|
askSecret,
|
|
9535
9540
|
saveCodingKey,
|
|
9536
9541
|
log: (s) => console.log(s),
|
|
9537
|
-
colors: { dim, cyan, red },
|
|
9542
|
+
colors: { dim, cyan: accent, red },
|
|
9538
9543
|
});
|
|
9539
9544
|
// Task #190: saving the key is the moment the coder becomes real, and the payload that
|
|
9540
9545
|
// seeded defaultCodingModel above was fetched BEFORE that — it carried no coder at all.
|
|
@@ -9558,7 +9563,7 @@ async function main() {
|
|
|
9558
9563
|
// folder has not chosen, and more than one is stored.
|
|
9559
9564
|
try {
|
|
9560
9565
|
if (!keyRes?.saved) {
|
|
9561
|
-
const { unchosenKeyWarning,
|
|
9566
|
+
const { unchosenKeyWarning, effectiveCoder, codingNeedsKey } = await import(
|
|
9562
9567
|
"./coding-key-prompt.mjs"
|
|
9563
9568
|
);
|
|
9564
9569
|
// THE RULE LIVES IN THE MODULE so it can be tested by calling it. Open-coded here it
|
|
@@ -9571,15 +9576,17 @@ async function main() {
|
|
|
9571
9576
|
// machine an Ollama coder, which needs no key — while the folder was on gpt-5. Reading
|
|
9572
9577
|
// it directly made this check answer about a coder nobody was using, and the mark only
|
|
9573
9578
|
// appeared after /model, which re-probes WITH the override.
|
|
9574
|
-
const kind =
|
|
9579
|
+
const { kind, url } = effectiveCoder({
|
|
9575
9580
|
sessionCodingModel,
|
|
9576
9581
|
choices: probe?.codingChoices,
|
|
9577
9582
|
defaultKind: p?.codingKind,
|
|
9583
|
+
defaultUrl: probe?.codingChoices?.find?.((c) => c?.isDefault)?.url ?? "",
|
|
9578
9584
|
});
|
|
9579
9585
|
const allKeys =
|
|
9580
|
-
kind
|
|
9586
|
+
codingNeedsKey(kind, url) && !sessionCodingKeyId ? await fetchCodingKeys() : null;
|
|
9581
9587
|
const w = unchosenKeyWarning({
|
|
9582
9588
|
codingKind: kind,
|
|
9589
|
+
codingUrl: url,
|
|
9583
9590
|
selectedKeyId: sessionCodingKeyId,
|
|
9584
9591
|
keys: allKeys,
|
|
9585
9592
|
});
|
|
@@ -9892,7 +9899,7 @@ async function main() {
|
|
|
9892
9899
|
// default; this opts in for the session. Ctrl+T expands the thought either way.
|
|
9893
9900
|
hoverMouse = !hoverMouse;
|
|
9894
9901
|
console.log(
|
|
9895
|
-
(hoverMouse ?
|
|
9902
|
+
(hoverMouse ? accent("hover: ON") : dim("hover: OFF")) +
|
|
9896
9903
|
dim(
|
|
9897
9904
|
hoverMouse
|
|
9898
9905
|
? " — mouse hover/click expands the thinking line; scrolling and drag-select are captured while a turn runs.\n"
|
|
@@ -9908,7 +9915,7 @@ async function main() {
|
|
|
9908
9915
|
sessionShowThought = !sessionShowThought;
|
|
9909
9916
|
await saveSessionShowThought(resolve(process.cwd()), sessionShowThought);
|
|
9910
9917
|
console.log(
|
|
9911
|
-
(sessionShowThought ?
|
|
9918
|
+
(sessionShowThought ? accent("thought: ON") : dim("thought: OFF")) +
|
|
9912
9919
|
dim(
|
|
9913
9920
|
sessionShowThought
|
|
9914
9921
|
? " — the model's reasoning prints in grey as it works; click a block (or Ctrl+T) to expand it.\n"
|
|
@@ -9958,7 +9965,7 @@ async function main() {
|
|
|
9958
9965
|
sessionTestAuto = !sessionTestAuto;
|
|
9959
9966
|
await saveSession(cwd, history); // persist the choice for this folder
|
|
9960
9967
|
console.log(
|
|
9961
|
-
(sessionTestAuto ?
|
|
9968
|
+
(sessionTestAuto ? accent("auto-test: ON") : dim("auto-test: OFF")) +
|
|
9962
9969
|
dim(
|
|
9963
9970
|
sessionTestAuto
|
|
9964
9971
|
? " — the agent will verify its work and fix until it passes, for this folder.\n"
|
|
@@ -9973,7 +9980,7 @@ async function main() {
|
|
|
9973
9980
|
await saveSessionSkip(cwd, "skipModelSetupPrompt", false);
|
|
9974
9981
|
await saveSessionSkip(cwd, "skipDaemonPrompt", false);
|
|
9975
9982
|
console.log(
|
|
9976
|
-
|
|
9983
|
+
accent("host mode") +
|
|
9977
9984
|
dim(" — this machine will run turns again. Start the worker with `gu start`, ") +
|
|
9978
9985
|
dim("and `gu doctor` to set up models here.\n")
|
|
9979
9986
|
);
|
|
@@ -9996,7 +10003,7 @@ async function main() {
|
|
|
9996
10003
|
(r) =>
|
|
9997
10004
|
r.label +
|
|
9998
10005
|
dim(` · ${r.note}`) +
|
|
9999
|
-
(r.id === sessionRagStorage ?
|
|
10006
|
+
(r.id === sessionRagStorage ? accent(" (current)") : "")
|
|
10000
10007
|
),
|
|
10001
10008
|
Math.max(0, RAG_STORAGE.findIndex((r) => r.id === sessionRagStorage))
|
|
10002
10009
|
);
|
|
@@ -10017,7 +10024,7 @@ async function main() {
|
|
|
10017
10024
|
// switching to cloud leaves the local shards on disk unread, and switching to local
|
|
10018
10025
|
// leaves the S3 ones untouched — either way the new store is built by the next index.
|
|
10019
10026
|
console.log(
|
|
10020
|
-
|
|
10027
|
+
accent(` ✓ RAG storage → ${row.label}`) +
|
|
10021
10028
|
dim(`\n ${row.note}`) +
|
|
10022
10029
|
dim("\n Existing shards are not moved — the new store fills as this folder is re-indexed.\n")
|
|
10023
10030
|
);
|
|
@@ -10046,7 +10053,7 @@ async function main() {
|
|
|
10046
10053
|
0
|
|
10047
10054
|
);
|
|
10048
10055
|
console.log(
|
|
10049
|
-
GUTTER +
|
|
10056
|
+
GUTTER + accent("prompt sent to the coding model") +
|
|
10050
10057
|
dim(` · ${fmtTokens(total)} tokens in the newest request`) +
|
|
10051
10058
|
// The per-folder maximum (task #84), which used to sit on the bottom bar. It answers
|
|
10052
10059
|
// "have I ever come close to this model's window?" — a question you ask once, not
|
|
@@ -10064,7 +10071,7 @@ async function main() {
|
|
|
10064
10071
|
GUTTER + dim(r.label.padEnd(21)) +
|
|
10065
10072
|
String(fmtTokens(r.tokens)).padStart(7) +
|
|
10066
10073
|
dim(` ${String(r.percent).padStart(3)}% `) +
|
|
10067
|
-
(r.tokens ?
|
|
10074
|
+
(r.tokens ? accent(bar) : "") + (r.note ? dim(` ${r.note}`) : "")
|
|
10068
10075
|
);
|
|
10069
10076
|
}
|
|
10070
10077
|
console.log("");
|
|
@@ -10087,7 +10094,7 @@ async function main() {
|
|
|
10087
10094
|
// The bar takes the FRESHER of the two totals, so the last turn's copy has to be
|
|
10088
10095
|
// zeroed too or the reset wouldn't show up in it.
|
|
10089
10096
|
lastTurnTotalOutput = 0;
|
|
10090
|
-
console.log(GUTTER +
|
|
10097
|
+
console.log(GUTTER + accent("✓ global + workspace output-token totals reset to 0.\n"));
|
|
10091
10098
|
} else {
|
|
10092
10099
|
console.log(GUTTER + dim("Could not reset the output-token totals (API error).\n"));
|
|
10093
10100
|
}
|
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.696",
|
|
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",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"test:bearer-e2e": "bash scripts/bearer-e2e.sh"
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
45
|
+
"accent.mjs",
|
|
45
46
|
"gu-cli.mjs",
|
|
46
47
|
"gu-repl.mjs",
|
|
47
48
|
"terminal-hover.mjs",
|
package/terminal-viewport.mjs
CHANGED
|
@@ -516,13 +516,14 @@ export function dragEventKind(btn, press) {
|
|
|
516
516
|
return (b & 0b11) === 0 ? "press" : null;
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
-
// The selection's colours (task #178). THE LOGO'S
|
|
519
|
+
// The selection's colours (task #178). THE LOGO'S COLOUR, as a BACKGROUND — not as text.
|
|
520
520
|
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
// user's
|
|
524
|
-
//
|
|
525
|
-
//
|
|
521
|
+
// 105 is bright magenta, the sixteen-colour palette's purple, following the accent from cyan to
|
|
522
|
+
// indigo when the brand moved (accent.mjs). Still a PALETTE colour, not the exact #a5b4fc: a
|
|
523
|
+
// background inherits the user's theme this way, where a 256-colour or truecolor background would
|
|
524
|
+
// fight every custom scheme and every light terminal — the same argument that kept syntax colour
|
|
525
|
+
// out of the edit cards. The bright variant rather than 45, because black text on a dark magenta
|
|
526
|
+
// is where this stops being legible.
|
|
526
527
|
//
|
|
527
528
|
// A BACKGROUND, because the cyan FOREGROUND is already spoken for: the ">> " prompt, where it
|
|
528
529
|
// means "this is the line you are typing", and the jump-to-bottom hint. A foreground also
|
|
@@ -530,12 +531,12 @@ export function dragEventKind(btn, press) {
|
|
|
530
531
|
// reserves for "you must not miss this" would be erased by looking at it. A background leaves
|
|
531
532
|
// every foreground meaning intact and reads as a REGION, which is what a selection is.
|
|
532
533
|
//
|
|
533
|
-
// The explicit 30 (black) is not decoration: the prompt and the jump hint are
|
|
534
|
-
//
|
|
534
|
+
// The explicit 30 (black) is not decoration: the prompt and the jump hint are ACCENT TEXT, and
|
|
535
|
+
// accent on accent is invisible. Flattening the foreground inside the selection costs colour
|
|
535
536
|
// meaning for as long as the selection lasts, and is the price of legibility. Reverse video
|
|
536
537
|
// (\x1b[7m) avoids it by letting the terminal swap whatever is there; a named colour was chosen
|
|
537
538
|
// deliberately over it, so the selection looks like gu rather than like the terminal.
|
|
538
|
-
export const SELECTION_SGR = "\x1b[
|
|
539
|
+
export const SELECTION_SGR = "\x1b[105;30m";
|
|
539
540
|
|
|
540
541
|
/**
|
|
541
542
|
* One row with the visible columns [from, to) highlighted.
|