@tiens.nguyen/gu-cli 1.0.835 → 1.0.836

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/gu-repl.mjs 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;