@tiens.nguyen/gu-cli 1.0.693 → 1.0.695
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/gonext_agent_chat.py +3 -3
- package/gu-cli.mjs +27 -4
- package/gu-repl.mjs +84 -81
- package/install-path.mjs +18 -0
- 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/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,6 +12,8 @@
|
|
|
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";
|
|
@@ -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,23 @@ function _isLocalCodingUrl(baseURL) {
|
|
|
3263
3265
|
return !host.includes(".") && !host.includes(":");
|
|
3264
3266
|
}
|
|
3265
3267
|
|
|
3268
|
+
/** TRUE only for the Ollama CLOUD endpoint (https://ollama.com[/...]) — the one Ollama
|
|
3269
|
+
* backend that actually requires an API key. Any other Ollama URL (localhost, a LAN
|
|
3270
|
+
* box, a self-hosted remote) has no auth and gets no key. Mirrors isOllamaCloudUrl in
|
|
3271
|
+
* web/src/lib/coderChoices.mjs — keep the two in step. */
|
|
3272
|
+
function _isOllamaCloudUrl(baseURL) {
|
|
3273
|
+
const v = String(baseURL || "").trim();
|
|
3274
|
+
if (!/^https:\/\//i.test(v)) return false;
|
|
3275
|
+
let host = "";
|
|
3276
|
+
try {
|
|
3277
|
+
host = new URL(v).hostname.toLowerCase().replace(/\.$/, "");
|
|
3278
|
+
} catch {
|
|
3279
|
+
return false;
|
|
3280
|
+
}
|
|
3281
|
+
return host === "ollama.com" || host.endsWith(".ollama.com");
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
|
|
3266
3285
|
/** Resolve to "ollama" | "openai" | "local". `kind` is the user's Settings choice
|
|
3267
3286
|
* ("ollama" | "openai" | "" = Auto). Only Auto costs a probe. */
|
|
3268
3287
|
async function resolveCodingBackend(kind, baseURL) {
|
|
@@ -3445,7 +3464,9 @@ async function runAgentChatJob(job) {
|
|
|
3445
3464
|
// anything else that endpoint doesn't exist, and on a cloud one we'd be pinging a paid
|
|
3446
3465
|
// API every 4 minutes (which an Auto+cloud config did until #117 was fixed — the old
|
|
3447
3466
|
// gate only skipped an EXPLICIT openai kind).
|
|
3448
|
-
|
|
3467
|
+
// ...and NOT for the Ollama CLOUD (https://ollama.com): that is a paid API, so the
|
|
3468
|
+
// keep-warm ping must not run against it — same rule as the key fetch below.
|
|
3469
|
+
if (codingBackend === "ollama" && !_isOllamaCloudUrl(payload?.codingBaseURL)) {
|
|
3449
3470
|
trackCodingModelForWarmup(payload?.codingBaseURL ?? "", payload?.codingModelId ?? "");
|
|
3450
3471
|
}
|
|
3451
3472
|
|
|
@@ -3598,7 +3619,9 @@ async function runAgentChatJob(job) {
|
|
|
3598
3619
|
// "openai" kind, and python's `coding_api_key or agent_api_key` fallback then
|
|
3599
3620
|
// authenticated a cloud provider with the LOCAL agent key → 401.
|
|
3600
3621
|
let codingApiKey = "";
|
|
3601
|
-
|
|
3622
|
+
// Ollama has no auth — EXCEPT the Ollama cloud (https://ollama.com), which requires a
|
|
3623
|
+
// key like any other paid endpoint. A local/LAN Ollama gets no key (and ignores one).
|
|
3624
|
+
if (codingBackend === "openai" || (codingBackend === "ollama" && _isOllamaCloudUrl(payload?.codingBaseURL))) {
|
|
3602
3625
|
try {
|
|
3603
3626
|
// NAME THE JOB (task #222 phase 2). Two turns run concurrently by default
|
|
3604
3627
|
// (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);
|
|
@@ -6904,7 +6907,7 @@ async function promptNewCodingKey(backendId = "", provider = "") {
|
|
|
6904
6907
|
const id = await addCodingKey(name, secret, backendId);
|
|
6905
6908
|
console.log(
|
|
6906
6909
|
id
|
|
6907
|
-
?
|
|
6910
|
+
? accent(` ✓ added ${name || "Unknown"}`) +
|
|
6908
6911
|
(backendId && provider ? dim(` · for ${provider}`) : "") + "\n"
|
|
6909
6912
|
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
6910
6913
|
);
|
|
@@ -6974,7 +6977,7 @@ async function replaceCodingKey() {
|
|
|
6974
6977
|
// would collect something with nowhere to go. The original prompt is exactly right here.
|
|
6975
6978
|
console.log(
|
|
6976
6979
|
"\n" + dim(` ${ks.model} API key `) +
|
|
6977
|
-
(ks.stored ? dim("(replacing ") +
|
|
6980
|
+
(ks.stored ? dim("(replacing ") + accent(maskKey(ks.stored)) + dim(")") : dim("(none stored)")) +
|
|
6978
6981
|
dim(" · Enter alone cancels, \"clear\" removes it\n")
|
|
6979
6982
|
);
|
|
6980
6983
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
@@ -6989,8 +6992,8 @@ async function replaceCodingKey() {
|
|
|
6989
6992
|
console.log(
|
|
6990
6993
|
ok
|
|
6991
6994
|
? clearing
|
|
6992
|
-
?
|
|
6993
|
-
:
|
|
6995
|
+
? accent(` ✓ ${ks.model} API key removed`) + dim(" — coding falls back to your local model\n")
|
|
6996
|
+
: accent(` ✓ ${ks.model} API key saved\n`)
|
|
6994
6997
|
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
6995
6998
|
);
|
|
6996
6999
|
paintBar();
|
|
@@ -7007,15 +7010,15 @@ async function manageCodingKeys(ks, keys) {
|
|
|
7007
7010
|
const current = sessionCodingKeyId;
|
|
7008
7011
|
const rows = keys.map(
|
|
7009
7012
|
(k) =>
|
|
7010
|
-
`${k.name}${dim(` ····${k.tail}`)}${k.id === current ?
|
|
7013
|
+
`${k.name}${dim(` ····${k.tail}`)}${k.id === current ? accent(" (this folder)") : ""}`
|
|
7011
7014
|
);
|
|
7012
7015
|
const ADD = rows.length;
|
|
7013
7016
|
rows.push(dim("add a new key…"));
|
|
7014
7017
|
console.log(
|
|
7015
7018
|
"\n" + GUTTER +
|
|
7016
7019
|
(keys.length > 1
|
|
7017
|
-
? dim(`Which key should ${folderLabel()} use for `) +
|
|
7018
|
-
: dim(`${folderLabel()} uses this key for `) +
|
|
7020
|
+
? dim(`Which key should ${folderLabel()} use for `) + accent(ks.model) + dim("?\n")
|
|
7021
|
+
: dim(`${folderLabel()} uses this key for `) + accent(ks.model) + dim(" — or add another.\n"))
|
|
7019
7022
|
);
|
|
7020
7023
|
console.log(GUTTER + dim("Enter chooses · ctrl+d deletes the highlighted key\n"));
|
|
7021
7024
|
const at = Math.max(0, keys.findIndex((k) => k.id === current));
|
|
@@ -7059,7 +7062,7 @@ async function manageCodingKeys(ks, keys) {
|
|
|
7059
7062
|
keyChoicePending = false; // chosen — the bar's mark comes off
|
|
7060
7063
|
codingKeyMissingKnown = false;
|
|
7061
7064
|
console.log(
|
|
7062
|
-
|
|
7065
|
+
accent(` ✓ ${folderLabel()} → ${keys[idx].name}`) + dim(` ····${keys[idx].tail}\n`)
|
|
7063
7066
|
);
|
|
7064
7067
|
paintBar();
|
|
7065
7068
|
}
|
|
@@ -7110,7 +7113,7 @@ async function deleteCodingKeyFlow(ks, keys, idx) {
|
|
|
7110
7113
|
sessionCodingKeyId = "";
|
|
7111
7114
|
await saveSessionCodingKeyId(resolve(process.cwd()));
|
|
7112
7115
|
}
|
|
7113
|
-
console.log(
|
|
7116
|
+
console.log(accent(` ✓ deleted ${out.name || victim.name}`) +
|
|
7114
7117
|
dim(` ····${victim.tail} · ${out.remaining} key(s) left\n`));
|
|
7115
7118
|
paintBar();
|
|
7116
7119
|
}
|
|
@@ -7138,7 +7141,7 @@ async function promptForCodingKey(override = "") {
|
|
|
7138
7141
|
askSecret,
|
|
7139
7142
|
saveCodingKey,
|
|
7140
7143
|
log: (s) => console.log(s),
|
|
7141
|
-
colors: { dim, cyan, red },
|
|
7144
|
+
colors: { dim, cyan: accent, red },
|
|
7142
7145
|
});
|
|
7143
7146
|
}
|
|
7144
7147
|
|
|
@@ -7225,15 +7228,15 @@ function drawApprovalOptions(redraw) {
|
|
|
7225
7228
|
// is 2 cols so Yes/No align; the \x1b[2A row-up count is unchanged (still 2 rows).
|
|
7226
7229
|
// Both rows are a handful of columns wide, so they survive any resize on ONE physical row
|
|
7227
7230
|
// 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 ?
|
|
7231
|
+
const yes = approvalSel === 0 ? accent("▸ Yes") : dim(" Yes");
|
|
7232
|
+
const no = approvalSel === 1 ? accent("▸ No") : dim(" No");
|
|
7230
7233
|
regionUi((redraw ? "\x1b[2A" : "") + "\x1b[K" + GUTTER + yes + "\n\x1b[K" + GUTTER + no + "\n");
|
|
7231
7234
|
}
|
|
7232
7235
|
function finishApproval(allow) {
|
|
7233
7236
|
if (!approvalActive) return;
|
|
7234
7237
|
approvalActive = false;
|
|
7235
7238
|
// Settle the picker: overwrite the two option lines with a single result line.
|
|
7236
|
-
process.stdout.write("\x1b[2A\x1b[J" + GUTTER + (allow ?
|
|
7239
|
+
process.stdout.write("\x1b[2A\x1b[J" + GUTTER + (allow ? accent("▸ Yes — running it") : red("▸ No — skipped")) + "\n");
|
|
7237
7240
|
const r = approvalResolve;
|
|
7238
7241
|
approvalResolve = null;
|
|
7239
7242
|
if (r) r(!!allow);
|
|
@@ -7398,7 +7401,7 @@ function drawList(redraw) {
|
|
|
7398
7401
|
// multi-select feel like it forgot what you chose.
|
|
7399
7402
|
const box = listMulti ? (listChecked.has(i) ? "[x] " : "[ ] ") : "";
|
|
7400
7403
|
const body = box + listItems[i];
|
|
7401
|
-
return clipVisible(GUTTER + (i === listSel ?
|
|
7404
|
+
return clipVisible(GUTTER + (i === listSel ? accent("▸ " + body) : dim(" " + body)), width - 1);
|
|
7402
7405
|
};
|
|
7403
7406
|
const marker = (text) => clipVisible(GUTTER + dim(text), width - 1);
|
|
7404
7407
|
const rows = [];
|
|
@@ -8283,7 +8286,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8283
8286
|
// figure holds rather than jittering against flowing output.
|
|
8284
8287
|
shownOutputTokens = easeCount(shownOutputTokens, latestOutputTokens);
|
|
8285
8288
|
const outLabel =
|
|
8286
|
-
shownOutputTokens > 0 ? dim(" · ↓ ") +
|
|
8289
|
+
shownOutputTokens > 0 ? dim(" · ↓ ") + accent(fmtTokens(shownOutputTokens)) : "";
|
|
8287
8290
|
// Task #96: the thought line is "clickable" only when the fuller thought has MORE text than
|
|
8288
8291
|
// fits on line 1 (a live command / bare "Thinking" / a thought that already fits has nothing
|
|
8289
8292
|
// to reveal). Line 1 (what's happening now): a live command > the model's current Thought >
|
|
@@ -8388,7 +8391,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8388
8391
|
const clipRow = (r) => clipVisible(r, Math.max(20, process.stdout.columns || 80) - 1);
|
|
8389
8392
|
const contentRows = [];
|
|
8390
8393
|
for (let i = 0; i < LEAD_ROWS; i++) contentRows.push(""); // the separator — see LEAD_ROWS
|
|
8391
|
-
contentRows.push(clipRow(`${markLine(
|
|
8394
|
+
contentRows.push(clipRow(`${markLine(accent(BULLET))}${hovering ? hoverThought(label) : accent(label)}`));
|
|
8392
8395
|
for (const dr of detailRows) contentRows.push(clipRow(dim(dr)));
|
|
8393
8396
|
for (const wl of expLines) contentRows.push(clipRow(GUTTER + dim(wl)));
|
|
8394
8397
|
for (let i = 0; i < WORD_GAP; i++) contentRows.push(""); // see WORD_GAP
|
|
@@ -8623,7 +8626,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8623
8626
|
onContent();
|
|
8624
8627
|
inEditCard = true;
|
|
8625
8628
|
openBullet();
|
|
8626
|
-
process.stdout.write(markLine(
|
|
8629
|
+
process.stdout.write(markLine(accent("⏺")) + bold(line.slice(1).replace(/^\s+/, "")) + "\n");
|
|
8627
8630
|
lastWasBlank = false;
|
|
8628
8631
|
lastContentAt = Date.now();
|
|
8629
8632
|
continue;
|
|
@@ -8634,7 +8637,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8634
8637
|
const m = EDIT_DIFF_RE.exec(line);
|
|
8635
8638
|
if (m) {
|
|
8636
8639
|
// " 19 - old text" → dim line number, red/cyan signed content.
|
|
8637
|
-
const paintDiff = m[3] === "+" ?
|
|
8640
|
+
const paintDiff = m[3] === "+" ? accent : red;
|
|
8638
8641
|
process.stdout.write(`${GUTTER}${m[1]}${dim(m[2])} ${paintDiff(`${m[3]} ${m[4]}`)}\n`);
|
|
8639
8642
|
} else {
|
|
8640
8643
|
process.stdout.write(GUTTER + dim(line) + "\n"); // "└ summary" / "… (+N more)"
|
|
@@ -9349,7 +9352,7 @@ async function main() {
|
|
|
9349
9352
|
servers: await (await import("./model-doctor.mjs")).discoverModelServers(),
|
|
9350
9353
|
});
|
|
9351
9354
|
const statusRows = [
|
|
9352
|
-
bold(
|
|
9355
|
+
bold(accent("GU AGENT")) + dim(` v${REPL_VERSION}`),
|
|
9353
9356
|
// Task #134: a client machine OVERRIDES the account's agent model locally (the
|
|
9354
9357
|
// account value points at the model host's own server). Show what will actually be
|
|
9355
9358
|
// used, not a value this machine can't reach.
|
|
@@ -9383,10 +9386,10 @@ async function main() {
|
|
|
9383
9386
|
...(() => {
|
|
9384
9387
|
const m = modeRow({ clientExpert: isClientExpert(), clientMode: clientModeBanner });
|
|
9385
9388
|
if (!m) return [];
|
|
9386
|
-
const name = m.label === "client expert" ?
|
|
9389
|
+
const name = m.label === "client expert" ? accent(m.label + " ") : m.label + " ";
|
|
9387
9390
|
return [lbl("mode") + name + dim(m.detail)];
|
|
9388
9391
|
})(),
|
|
9389
|
-
sessionTestAuto ?
|
|
9392
|
+
sessionTestAuto ? accent("auto-test on") : dim("auto-test off"),
|
|
9390
9393
|
// Same reason as workspaceSyncLabel: on Client Expert there is no cloud to sync TO, so
|
|
9391
9394
|
// the banner must not advertise one.
|
|
9392
9395
|
lbl("folder") +
|
|
@@ -9534,7 +9537,7 @@ async function main() {
|
|
|
9534
9537
|
askSecret,
|
|
9535
9538
|
saveCodingKey,
|
|
9536
9539
|
log: (s) => console.log(s),
|
|
9537
|
-
colors: { dim, cyan, red },
|
|
9540
|
+
colors: { dim, cyan: accent, red },
|
|
9538
9541
|
});
|
|
9539
9542
|
// Task #190: saving the key is the moment the coder becomes real, and the payload that
|
|
9540
9543
|
// seeded defaultCodingModel above was fetched BEFORE that — it carried no coder at all.
|
|
@@ -9892,7 +9895,7 @@ async function main() {
|
|
|
9892
9895
|
// default; this opts in for the session. Ctrl+T expands the thought either way.
|
|
9893
9896
|
hoverMouse = !hoverMouse;
|
|
9894
9897
|
console.log(
|
|
9895
|
-
(hoverMouse ?
|
|
9898
|
+
(hoverMouse ? accent("hover: ON") : dim("hover: OFF")) +
|
|
9896
9899
|
dim(
|
|
9897
9900
|
hoverMouse
|
|
9898
9901
|
? " — mouse hover/click expands the thinking line; scrolling and drag-select are captured while a turn runs.\n"
|
|
@@ -9908,7 +9911,7 @@ async function main() {
|
|
|
9908
9911
|
sessionShowThought = !sessionShowThought;
|
|
9909
9912
|
await saveSessionShowThought(resolve(process.cwd()), sessionShowThought);
|
|
9910
9913
|
console.log(
|
|
9911
|
-
(sessionShowThought ?
|
|
9914
|
+
(sessionShowThought ? accent("thought: ON") : dim("thought: OFF")) +
|
|
9912
9915
|
dim(
|
|
9913
9916
|
sessionShowThought
|
|
9914
9917
|
? " — the model's reasoning prints in grey as it works; click a block (or Ctrl+T) to expand it.\n"
|
|
@@ -9958,7 +9961,7 @@ async function main() {
|
|
|
9958
9961
|
sessionTestAuto = !sessionTestAuto;
|
|
9959
9962
|
await saveSession(cwd, history); // persist the choice for this folder
|
|
9960
9963
|
console.log(
|
|
9961
|
-
(sessionTestAuto ?
|
|
9964
|
+
(sessionTestAuto ? accent("auto-test: ON") : dim("auto-test: OFF")) +
|
|
9962
9965
|
dim(
|
|
9963
9966
|
sessionTestAuto
|
|
9964
9967
|
? " — the agent will verify its work and fix until it passes, for this folder.\n"
|
|
@@ -9973,7 +9976,7 @@ async function main() {
|
|
|
9973
9976
|
await saveSessionSkip(cwd, "skipModelSetupPrompt", false);
|
|
9974
9977
|
await saveSessionSkip(cwd, "skipDaemonPrompt", false);
|
|
9975
9978
|
console.log(
|
|
9976
|
-
|
|
9979
|
+
accent("host mode") +
|
|
9977
9980
|
dim(" — this machine will run turns again. Start the worker with `gu start`, ") +
|
|
9978
9981
|
dim("and `gu doctor` to set up models here.\n")
|
|
9979
9982
|
);
|
|
@@ -9996,7 +9999,7 @@ async function main() {
|
|
|
9996
9999
|
(r) =>
|
|
9997
10000
|
r.label +
|
|
9998
10001
|
dim(` · ${r.note}`) +
|
|
9999
|
-
(r.id === sessionRagStorage ?
|
|
10002
|
+
(r.id === sessionRagStorage ? accent(" (current)") : "")
|
|
10000
10003
|
),
|
|
10001
10004
|
Math.max(0, RAG_STORAGE.findIndex((r) => r.id === sessionRagStorage))
|
|
10002
10005
|
);
|
|
@@ -10017,7 +10020,7 @@ async function main() {
|
|
|
10017
10020
|
// switching to cloud leaves the local shards on disk unread, and switching to local
|
|
10018
10021
|
// leaves the S3 ones untouched — either way the new store is built by the next index.
|
|
10019
10022
|
console.log(
|
|
10020
|
-
|
|
10023
|
+
accent(` ✓ RAG storage → ${row.label}`) +
|
|
10021
10024
|
dim(`\n ${row.note}`) +
|
|
10022
10025
|
dim("\n Existing shards are not moved — the new store fills as this folder is re-indexed.\n")
|
|
10023
10026
|
);
|
|
@@ -10046,7 +10049,7 @@ async function main() {
|
|
|
10046
10049
|
0
|
|
10047
10050
|
);
|
|
10048
10051
|
console.log(
|
|
10049
|
-
GUTTER +
|
|
10052
|
+
GUTTER + accent("prompt sent to the coding model") +
|
|
10050
10053
|
dim(` · ${fmtTokens(total)} tokens in the newest request`) +
|
|
10051
10054
|
// The per-folder maximum (task #84), which used to sit on the bottom bar. It answers
|
|
10052
10055
|
// "have I ever come close to this model's window?" — a question you ask once, not
|
|
@@ -10064,7 +10067,7 @@ async function main() {
|
|
|
10064
10067
|
GUTTER + dim(r.label.padEnd(21)) +
|
|
10065
10068
|
String(fmtTokens(r.tokens)).padStart(7) +
|
|
10066
10069
|
dim(` ${String(r.percent).padStart(3)}% `) +
|
|
10067
|
-
(r.tokens ?
|
|
10070
|
+
(r.tokens ? accent(bar) : "") + (r.note ? dim(` ${r.note}`) : "")
|
|
10068
10071
|
);
|
|
10069
10072
|
}
|
|
10070
10073
|
console.log("");
|
|
@@ -10087,7 +10090,7 @@ async function main() {
|
|
|
10087
10090
|
// The bar takes the FRESHER of the two totals, so the last turn's copy has to be
|
|
10088
10091
|
// zeroed too or the reset wouldn't show up in it.
|
|
10089
10092
|
lastTurnTotalOutput = 0;
|
|
10090
|
-
console.log(GUTTER +
|
|
10093
|
+
console.log(GUTTER + accent("✓ global + workspace output-token totals reset to 0.\n"));
|
|
10091
10094
|
} else {
|
|
10092
10095
|
console.log(GUTTER + dim("Could not reset the output-token totals (API error).\n"));
|
|
10093
10096
|
}
|
package/install-path.mjs
CHANGED
|
@@ -66,6 +66,15 @@ function main() {
|
|
|
66
66
|
if (process.env.CI) return; // no user shell to help in CI
|
|
67
67
|
const prefix = process.env.npm_config_prefix;
|
|
68
68
|
if (!prefix) return; // not an npm install context
|
|
69
|
+
// A LOCAL install (`npm install @tiens.nguyen/gu-cli` inside a project) must NEVER
|
|
70
|
+
// write to the user's shell rc — `gu` lives in ./node_modules/.bin there, which
|
|
71
|
+
// does not belong on a global PATH. npm sets npm_config_global=true for `-g`;
|
|
72
|
+
// some npm versions omit it, so also bail when the prefix sits inside a
|
|
73
|
+
// project's node_modules tree.
|
|
74
|
+
if (process.env.npm_config_global !== "true") {
|
|
75
|
+
if (resolve(prefix).split("/").includes("node_modules")) return; // local install
|
|
76
|
+
if (process.env.npm_config_global === "false") return; // explicitly local
|
|
77
|
+
}
|
|
69
78
|
const binDir = join(prefix, "bin");
|
|
70
79
|
if (resolve(binDir).startsWith(resolve(tmpdir()))) return; // preflight's throwaway prefix
|
|
71
80
|
|
|
@@ -114,6 +123,15 @@ function rcFilesFor(home) {
|
|
|
114
123
|
if (existsSync(bashrc)) out.push(bashrc);
|
|
115
124
|
} else {
|
|
116
125
|
out.push(bashrc);
|
|
126
|
+
// Interactive bash reads .bashrc, but a LOGIN bash (SSH console, some terminal
|
|
127
|
+
// emulators) reads .bash_profile / .profile instead and NEVER sources .bashrc.
|
|
128
|
+
// Update whichever of those the user already has so `gu` survives an SSH login
|
|
129
|
+
// too — but don't seed them, or bash would start preferring a file we created
|
|
130
|
+
// over the user's real login config.
|
|
131
|
+
const bashProfile = join(home, ".bash_profile");
|
|
132
|
+
const profile = join(home, ".profile");
|
|
133
|
+
if (existsSync(bashProfile)) out.push(bashProfile);
|
|
134
|
+
else if (existsSync(profile)) out.push(profile);
|
|
117
135
|
if (existsSync(zshrc)) out.push(zshrc);
|
|
118
136
|
}
|
|
119
137
|
return out;
|
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.695",
|
|
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.
|