@tiens.nguyen/gu-cli 1.0.835 → 1.0.837

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/gu-cli.mjs CHANGED
@@ -139,7 +139,7 @@ Usage:
139
139
  gu-cli workspace revert [runId] # undo an agent run's file edits from its backups
140
140
  gu-cli mongo migrate-from-docker # macOS: move a Docker MongoDB to native (Homebrew)
141
141
  gu-cli clear [--scratch|--keep-history] [--yes] # back to a fresh machine (asks first)
142
- gu-cli fix-path # macOS/Linux: put npm's global bin folder on PATH
142
+ gu-cli fix-path [--check] # put npm's global folder on PATH (Windows: check, then ask)
143
143
  # (what the install does, if npm skipped it)
144
144
  gu-cli fix-powershell [--check] # Windows: drop PowerShell's built-in \`gu\` alias
145
145
  # (it is Get-Unique and shadows this CLI)
@@ -1068,8 +1068,39 @@ if (args[0] === "token-usage" || args[0] === "tokens") {
1068
1068
  // prefix worked out from where this file is installed, so there is one implementation.
1069
1069
  if (args[0] === "fix-path") {
1070
1070
  if (process.platform === "win32") {
1071
- console.log(" Nothing to fix here — on Windows npm's folder is on PATH through the registry.");
1072
- console.log(" If `gu` is not found in PowerShell: gu-cli fix-powershell");
1071
+ // #375 (GitHub #156): Windows keeps PATH in the registry — check the SAVED one (what a new
1072
+ // terminal reads), say what to do the way the installer does, and add it only on a yes.
1073
+ const wp = await import("./win-path.mjs");
1074
+ const { fileURLToPath } = await import("node:url");
1075
+ const { dirname, join } = await import("node:path");
1076
+ const { readFileSync } = await import("node:fs");
1077
+ const here = dirname(fileURLToPath(import.meta.url));
1078
+ const dir = wp.windowsPrefixOf(here) || String(process.env.npm_config_prefix || "");
1079
+ if (!dir) {
1080
+ console.log(` gu is not installed globally with npm (${here}) — there is no PATH to fix.`);
1081
+ console.log(" Install it with: irm https://gu.getmarsic.cc/install.ps1 | iex");
1082
+ process.exit(0);
1083
+ }
1084
+ let version = "?";
1085
+ try { version = JSON.parse(readFileSync(join(here, "package.json"), "utf8")).version; } catch { /* shown as ? */ }
1086
+ const saved = wp.readSavedPaths();
1087
+ const status = wp.pathStatus({ dir, sessionPath: process.env.Path || process.env.PATH || "", ...saved });
1088
+ const psHost = Boolean(process.env.PSModulePath) && !process.env.PROMPT; // cmd.exe sets PROMPT
1089
+ let summary = wp.installSummary({ version, location: wp.guLocation(dir), dir, status, powershell: psHost });
1090
+ if (!status.saved && !args.includes("--check") && process.stdin.isTTY) {
1091
+ const { createInterface } = await import("node:readline/promises");
1092
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1093
+ const answer = (await rl.question(` Add ${dir} to your user PATH now? [Y/n] `)).trim().toLowerCase();
1094
+ rl.close();
1095
+ if (answer === "" || answer === "y" || answer === "yes") {
1096
+ const r = wp.addToUserPath(dir);
1097
+ if (r.ok) {
1098
+ console.log(" Added. Open a new terminal and gu is there.");
1099
+ summary = wp.installSummary({ version, location: wp.guLocation(dir), dir, status: { saved: true, session: status.session }, powershell: psHost });
1100
+ } else console.log(` Could not change PATH: ${r.error || "PowerShell refused"}`);
1101
+ }
1102
+ }
1103
+ console.log(wp.formatSummary(summary, { color: Boolean(process.stdout.isTTY) }).replace(/^/gm, " "));
1073
1104
  process.exit(0);
1074
1105
  }
1075
1106
  const { fixPathPrefix } = await import("./install-path.mjs");
package/gu-repl.mjs CHANGED
@@ -140,7 +140,7 @@ import { completedFlush, splitLiveStream } from "./completed-flush.mjs";
140
140
  import { isScaffoldStep, isGroupableStep, stepGroupRows, relativizePaths } from "./step-group.mjs";
141
141
  import { clearLogsNoticeText } from "./clear-logs.mjs";
142
142
  import { allowIn, allowedIn, bridgeCommand, browserReport, bridgeStatus, extensionDir, findDevInstalls, helpInstall, installExtensionFiles, installSteps, localLeftovers, openExtensionsPage, removeLocalFiles, STORE_URL, stopBridge } from "./browser-command.mjs";
143
- import { commandLogFile, killShell, logTail, markAnnounced, readShells, recapQuestion, shellsToRecap, statusText as shellStatusText, renderShellDetails, shellPickerLabel, shellState, shellsNotice } from "./shell-details.mjs";
143
+ import { commandLogFile, killShell, logTail, markAnnounced, readShells, recapQuestion, shellsToRecap, statusText as shellStatusText, renderShellDetails, shellPanelRows, shellPickerLabel, shellState, shellsNotice } from "./shell-details.mjs";
144
144
  import { discoverSkills, matchSkillCommand } from "./skills.mjs";
145
145
  import { appendNew, archivePathFor, archivedWorkspaceKey, markReset, seedArchive } from "./conversation-archive.mjs";
146
146
  import { addAllowRule, listServers as listMcpServers } from "./mcp-config.mjs";
@@ -9668,20 +9668,49 @@ async function showShells() {
9668
9668
  }
9669
9669
  }
9670
9670
  const rec = entries[pick].rec;
9671
- for (;;) {
9672
- const st = printShellDetails(rec);
9673
- if (following) break;
9674
- const actions = [{ id: "refresh", label: "Refresh" }];
9675
- if (st.state === "running") actions.push({ id: "kill", label: "Stop it", note: "kills the command and everything it started" });
9676
- actions.push({ id: "close", label: "Close" });
9677
- const act = await pickFromList(actions.map((a) => a.label + (a.note ? dim(` · ${a.note}`) : "")), 0);
9678
- const id = act < 0 ? "close" : actions[act].id;
9679
- if (id === "refresh") continue;
9680
- if (id === "kill") {
9681
- await killShell(rec);
9682
- console.log(GUTTER + accent("✓ stopped") + dim(" — the agent sees it as killed next turn.\n"));
9683
- }
9684
- break;
9671
+ // During a turn a picker would fight the turn for the keyboard: the details are printed instead.
9672
+ if (following) {
9673
+ printShellDetails(rec);
9674
+ refreshShells();
9675
+ paintBar();
9676
+ return;
9677
+ }
9678
+ // AT THE BOTTOM, ABOVE THE BAR, LIVE; Esc closes (#376, GitHub #157) — Claude Code's Shell
9679
+ // details. Nothing goes into the scrollback: the panel and its choices are one block that the
9680
+ // picker wipes when it closes. Its height is fixed when it opens (what the screen allows, at most
9681
+ // the details plus 15 output lines) and it redraws once a second, so a build's runtime and
9682
+ // newest output move while you watch.
9683
+ shellsViewed.add(String(rec.id));
9684
+ const panel = (rowsWanted) => {
9685
+ const st = shellState(rec);
9686
+ let output = logTail(rec.log, { lines: 40 });
9687
+ let outputFrom = "";
9688
+ if (!output.length) {
9689
+ const lf = commandLogFile(rec.command, rec.workdir);
9690
+ if (lf) {
9691
+ output = logTail(lf, { lines: 40 });
9692
+ outputFrom = lf.startsWith(String(rec.workdir || "\0") + "/") ? lf.slice(String(rec.workdir).length + 1) : lf;
9693
+ }
9694
+ }
9695
+ return shellPanelRows({
9696
+ rec, st, output, outputFrom, width: Math.max(20, process.stdout.columns || 80) - 1, gutter: GUTTER, rows: rowsWanted,
9697
+ footer: "Esc to close",
9698
+ style: { title: (t) => bold(accent(t)), label: bold, running: accent, ok: accent, bad: red, dim },
9699
+ });
9700
+ };
9701
+ const running = shellState(rec).state === "running";
9702
+ const actions = running
9703
+ ? [{ id: "kill", label: "Stop it", note: "kills the command and everything it started" }, { id: "close", label: "Close" }]
9704
+ : [{ id: "close", label: "Close" }];
9705
+ const headerRows = Math.max(8, Math.min(24, viewportHeight() - 3 - actions.length));
9706
+ const act = await pickFromList(
9707
+ actions.map((a) => a.label + (a.note ? dim(` · ${a.note}`) : "")),
9708
+ actions.length - 1, // Close: Enter on its own never stops a build
9709
+ { header: panel, headerRows, refreshMs: 1000 },
9710
+ );
9711
+ if (act >= 0 && actions[act].id === "kill") {
9712
+ await killShell(rec);
9713
+ console.log(GUTTER + accent(`✓ stopped shell ${rec.id}`) + dim(" — the agent sees it as killed next turn.\n"));
9685
9714
  }
9686
9715
  refreshShells();
9687
9716
  paintBar();
@@ -10795,6 +10824,12 @@ let listDeleteRequested = false;
10795
10824
  // /use-presets' { o: "open" } (#312, #322). Null unless a caller asks, like ctrl+d above.
10796
10825
  let listKeys = null;
10797
10826
  let listKeyAction = "";
10827
+ // A PANEL ABOVE THE CHOICES (#376): `listHeader(rows)` returns exactly `listHeaderRows` rows, drawn
10828
+ // at the top of the same in-place block — Shell details sits at the bottom of the screen with its
10829
+ // actions under it, and goes with them (Esc) without leaving a line in the scrollback. A fixed
10830
+ // height, so the once-a-second redraw walks up the same rows it drew.
10831
+ let listHeader = null;
10832
+ let listHeaderRows = 0;
10798
10833
  /** #321: the screen under a picker when it opened (content rows, pad, region height), and its tallest draw. */
10799
10834
  let listOpenedOver = null;
10800
10835
  let listMaxRows = 0;
@@ -10807,7 +10842,7 @@ function drawList(redraw) {
10807
10842
  // provider's /v1/models was taller than the terminal: the screen scrolled, the highlighted row
10808
10843
  // went off the top, and the ESC[nA walk below then counted rows that had moved — "cannot scroll
10809
10844
  // up or down, stuck here". Two rows of margin keep the block clear of the line above it.
10810
- const maxBlock = Math.max(5, viewportHeight() - 2) - (listNote ? 1 : 0);
10845
+ const maxBlock = Math.max(5, viewportHeight() - 2) - (listNote ? 1 : 0) - (listHeader ? listHeaderRows : 0);
10811
10846
  const win = listWindow(listItems.length, listSel, listTop, maxBlock);
10812
10847
  listTop = win.top;
10813
10848
  const row = (i) => {
@@ -10821,6 +10856,11 @@ function drawList(redraw) {
10821
10856
  };
10822
10857
  const marker = (text) => clipVisible(GUTTER + dim(text), width - 1);
10823
10858
  const rows = [];
10859
+ if (listHeader) {
10860
+ let head = [];
10861
+ try { head = listHeader(listHeaderRows) || []; } catch { /* a panel that fails to draw is blank */ }
10862
+ for (let i = 0; i < listHeaderRows; i++) rows.push(clipVisible(head[i] ?? "", width - 1));
10863
+ }
10824
10864
  // Drawn even when empty, so the block keeps a CONSTANT height: every redraw walks up exactly as
10825
10865
  // many rows as it drew, and a block that changed height at the ends would leave that walk out
10826
10866
  // by a row — the bug physicalRows() exists to prevent.
@@ -10863,6 +10903,8 @@ function finishList(index) {
10863
10903
  listMaxRows = 0;
10864
10904
  listRowsDrawn = [];
10865
10905
  listTop = 0;
10906
+ listHeader = null;
10907
+ listHeaderRows = 0;
10866
10908
  const r = listResolve;
10867
10909
  const multi = listMulti;
10868
10910
  const checked = [...listChecked].sort((a, b) => a - b);
@@ -11007,6 +11049,19 @@ function pickFromList(items, initialSel = 0, opts = {}) {
11007
11049
  listPickerActive = true;
11008
11050
  listDeadline = 0;
11009
11051
  listTimeoutIndex = -1;
11052
+ listHeader = typeof opts.header === "function" ? opts.header : null;
11053
+ listHeaderRows = listHeader ? Math.max(0, opts.headerRows | 0) : 0;
11054
+ if (listHeader && opts.refreshMs > 0) {
11055
+ // A live panel (#376): redrawn in place until the picker closes.
11056
+ const live = setInterval(() => {
11057
+ if (listPickerActive && listHeader) drawList(true);
11058
+ }, opts.refreshMs);
11059
+ const done = listResolve;
11060
+ listResolve = (v) => {
11061
+ clearInterval(live);
11062
+ done(v);
11063
+ };
11064
+ }
11010
11065
  if (opts && opts.timeoutMs > 0 && (opts.timeoutIndex ?? -1) >= 0) {
11011
11066
  listDeadline = Date.now() + opts.timeoutMs;
11012
11067
  listTimeoutIndex = opts.timeoutIndex;
package/gu_browser.py CHANGED
@@ -19,7 +19,7 @@ import urllib.request
19
19
  HERE = os.path.dirname(os.path.abspath(__file__))
20
20
  EXT_SRC = os.path.join(HERE, "browser-extension")
21
21
  EXT_FILES = ("manifest.json", "background.js", "actions.js", "popup.html", "popup.js",
22
- "icons/icon16.png", "icons/icon48.png", "icons/icon128.png")
22
+ "icons/icon16.png", "icons/icon32.png", "icons/icon48.png", "icons/icon128.png")
23
23
  BRIDGE = os.path.join(HERE, "browser-bridge.mjs")
24
24
 
25
25