@tiens.nguyen/gonext-cli 1.0.366 → 1.0.367

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/gonext-repl.mjs +57 -37
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -127,6 +127,37 @@ const wrapGutter = (plain, colorFn = (s) => s) =>
127
127
  wrapBlock(plain, GUTTER, process.stdout.columns || 80)
128
128
  .map((l) => (l.length ? GUTTER + colorFn(l) : l))
129
129
  .join("\n");
130
+ // Visible width of a string, ignoring SGR colour escapes (they occupy no cells).
131
+ const vwidth = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "").length;
132
+ // The gonext startup LOGO (task #131): a small block-art right-chevron — the "go / next"
133
+ // play mark — drawn to the left of the status facts. Kept minimal so it renders on any
134
+ // terminal/theme (block elements only, like the ● ✓ ✗ already in use). Rows are plain text;
135
+ // padded + coloured at render time. LOGO_W = the visible column the status text starts after.
136
+ const LOGO = ["█▖", "███▖", "█████▖", "███▘", "█▘"];
137
+ const LOGO_W = 7;
138
+ // Lay the LOGO and the status rows SIDE BY SIDE (logo left, facts right), or STACK them (logo
139
+ // then facts) when the terminal is too narrow to fit both without wrapping — so the banner is
140
+ // never the soft-wrapped jumble the plain "· … · …" lines used to be (task #131). Side-by-side
141
+ // only when GUTTER + logo + gap + the WIDEST status row fits in `cols`; otherwise stacked, with
142
+ // each status row clipped (clipVisible) so nothing spills past the edge. All rows stay within
143
+ // width → the #109 no-wrap invariant holds. `statusRows` are pre-coloured strings.
144
+ const renderBanner = (statusRows) => {
145
+ const cols = process.stdout.columns || 80;
146
+ const gap = " ";
147
+ const widest = Math.max(0, ...statusRows.map(vwidth));
148
+ const out = [];
149
+ if (GUTTER.length + LOGO_W + gap.length + widest <= cols) {
150
+ const n = Math.max(LOGO.length, statusRows.length);
151
+ for (let i = 0; i < n; i++) {
152
+ const left = i < LOGO.length ? cyan(LOGO[i].padEnd(LOGO_W)) : " ".repeat(LOGO_W);
153
+ out.push(GUTTER + left + gap + (statusRows[i] ?? ""));
154
+ }
155
+ } else {
156
+ for (const r of LOGO) out.push(GUTTER + cyan(r));
157
+ for (const r of statusRows) out.push(GUTTER + clipVisible(r, cols - 1 - GUTTER.length));
158
+ }
159
+ return out.join("\n");
160
+ };
130
161
  // The blank SEAM between the dim action log and the bright final answer (grouping, senior-UI
131
162
  // advice): the completed-action bullets are now a TIGHT group with no blanks between them, so
132
163
  // this one gap is what separates the "what I did" scaffolding from the deliverable. A knob:
@@ -770,14 +801,8 @@ async function ensureWorkspace() {
770
801
  );
771
802
  await writeFile(WS_FILE, JSON.stringify({ workspaces: list }, null, 2) + "\n");
772
803
  }
773
- wsSync = { enabled: hit.allowSync !== false, name: hit.name, path: hit.path };
774
- console.log(
775
- wrapGutter(
776
- `workspace: ${hit.name} → ${hit.path} (run ${hit.allowRun ? "enabled" : "disabled"}` +
777
- `${hit.allowSync !== false ? ", cloud sync on" : ""})`,
778
- dim
779
- )
780
- );
804
+ // The workspace facts now live in the startup banner (task #131) — just remember them.
805
+ wsSync = { enabled: hit.allowSync !== false, name: hit.name, path: hit.path, run: !!hit.allowRun };
781
806
  return;
782
807
  }
783
808
  if (!existsSync(cwd) || !statSync(cwd).isDirectory()) return;
@@ -812,7 +837,7 @@ async function ensureWorkspace() {
812
837
  });
813
838
  await mkdir(dirname(WS_FILE), { recursive: true });
814
839
  await writeFile(WS_FILE, JSON.stringify({ workspaces: next }, null, 2) + "\n");
815
- wsSync = { enabled: allowSync, name: basename(cwd), path: cwd };
840
+ wsSync = { enabled: allowSync, name: basename(cwd), path: cwd, run: allowRun };
816
841
  console.log(
817
842
  GUTTER +
818
843
  green(`✓ workspace registered: ${basename(cwd)}`) +
@@ -2442,23 +2467,9 @@ async function runAgentTurn(history) {
2442
2467
 
2443
2468
  // ---------- REPL ----------
2444
2469
  async function main() {
2445
- // Wrap the header so a narrow window doesn't soft-wrap "key wk_…" to column 0; keep the
2446
- // "GoNext agent REPL" label cyan on row 0, dim the rest (and any wrapped continuation rows).
2447
- {
2448
- const LABEL = "GoNext agent REPL";
2449
- const header = `${LABEL} v${REPL_VERSION} · API ${apiBase} · key ${workerKey.slice(0, 8)}…`;
2450
- console.log(
2451
- wrapBlock(header, GUTTER, process.stdout.columns || 80)
2452
- .map((l, i) =>
2453
- !l.length
2454
- ? l
2455
- : i === 0 && l.startsWith(LABEL)
2456
- ? GUTTER + cyan(LABEL) + dim(l.slice(LABEL.length))
2457
- : GUTTER + dim(l)
2458
- )
2459
- .join("\n")
2460
- );
2461
- }
2470
+ // The startup banner (logo + status facts) is printed AFTER the payload fetch below, once the
2471
+ // model/coder/RAG are known see renderBanner (task #131). Nothing prints here so the banner
2472
+ // lands as one clean block.
2462
2473
  // Task #127, Phase 2: the REPL enqueues jobs that the polling DAEMON executes, so ensure
2463
2474
  // one is running — no GoTerminal needed. Idempotent + BC4-safe: startDaemon() no-ops when
2464
2475
  // a daemon (ours, GoTerminal's, or a manual one) is already alive. Best-effort; a failure
@@ -2504,17 +2515,26 @@ async function main() {
2504
2515
  sessionRagMode = local ? "local" : "cloud";
2505
2516
  await saveSessionRagMode(resolve(process.cwd()), sessionRagMode);
2506
2517
  }
2507
- console.log(
2508
- wrapGutter(
2509
- `agent model: ${p.agentModelId || probe?.modelKey || "?"}` +
2510
- (p.codingModelId ? ` · coder: ${p.codingModelId}` : "") +
2511
- (p.ragEnabled ? ` · RAG ${sessionRagMode === "cloud" ? "cloud" : "local"}` : "") +
2512
- ` · auto-test: ${sessionTestAuto ? "on" : "off"}` +
2513
- (selectedServer ? ` · deploy: ${selectedServer.name} (${selectedServer.user}@${selectedServer.host})` : "") +
2514
- (sessionTestAuto ? "" : " (/test-auto to enable)"),
2515
- dim
2516
- )
2517
- );
2518
+ // Startup banner (task #131): the gonext logo on the left, compact status facts on the
2519
+ // right (label dim, value bright) — one fact per short line instead of the old wrapped
2520
+ // … · …" wall. renderBanner lays them side by side, or stacks on a narrow terminal.
2521
+ {
2522
+ const lbl = (s) => dim(s.padEnd(7)); // aligned dim labels, values start at col 7
2523
+ const ws = wsSync?.name || basename(resolve(process.cwd()));
2524
+ const statusRows = [
2525
+ bold(cyan("GONEXT AGENT")) + dim(` v${REPL_VERSION}`),
2526
+ lbl("model") + (p.agentModelId || probe?.modelKey || "?"),
2527
+ lbl("coder") + (p.codingModelId || dim("—")),
2528
+ lbl("RAG") +
2529
+ (p.ragEnabled ? (sessionRagMode === "cloud" ? "cloud" : "local") : dim("off")) +
2530
+ dim(" auto-test ") + (sessionTestAuto ? "on" : dim("off")),
2531
+ lbl("folder") + ws + dim(wsSync ? ` (${wsSync.run ? "run" : "read-only"}${wsSync.enabled ? " · sync" : ""})` : ""),
2532
+ ];
2533
+ if (selectedServer)
2534
+ statusRows.push(lbl("deploy") + `${selectedServer.name} ${dim(`(${selectedServer.user}@${selectedServer.host})`)}`);
2535
+ statusRows.push(dim(`api ${apiBase.replace(/^https?:\/\//, "")} · key ${workerKey.slice(0, 8)}…`));
2536
+ console.log(renderBanner(statusRows));
2537
+ }
2518
2538
  // Task #127, Phase 3: if the base chat model server isn't answering, offer to set it up
2519
2539
  // (download + start mlx_lm.server). BC3: only runs when NOTHING answers — an already-up
2520
2540
  // model is left untouched, and this is skipped entirely. Interactive TTYs only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-cli",
3
- "version": "1.0.366",
3
+ "version": "1.0.367",
4
4
  "description": "GoNext CLI — the gonext 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",