@tiens.nguyen/gu-cli 1.0.783 → 1.0.785

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/step-tokens.mjs CHANGED
@@ -53,3 +53,48 @@ export function readStepTokens(breakdown) {
53
53
  const n = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : 0);
54
54
  return { step, stepIn: n(breakdown.liveStepIn), stepOut: n(breakdown.liveStepOut) };
55
55
  }
56
+
57
+ // ---------------------------------------------------------------------------------------------
58
+ // WHAT THE TURN COST needs two more figures (#323), riding the same breakdown for the same reason:
59
+ // · liveCached / liveCacheReported — the prompt tokens the provider served from its cache, summed
60
+ // over the turn (python's `codeCached`, present ONLY when the provider reported a count);
61
+ // · liveFirstIn — the first step's prompt: nothing is cached on it, so when the provider reports
62
+ // nothing, gu assumes 90% of every LATER step was cached (model-prices.mjs).
63
+ // ---------------------------------------------------------------------------------------------
64
+
65
+ /** The turn's cache figures before any event. */
66
+ export const NO_CACHE = Object.freeze({ cached: 0, cacheReported: false, firstIn: 0, firstStep: 0 });
67
+
68
+ /** Fold one python "tokens" event into the turn's cache figures. */
69
+ export function foldTurnCache(prev, event) {
70
+ let { cached, cacheReported, firstIn, firstStep } = prev ?? NO_CACHE;
71
+ if (!event || typeof event !== "object") return { cached, cacheReported, firstIn, firstStep };
72
+ if (Number.isFinite(event.codeCached) && event.codeCached >= 0) {
73
+ cacheReported = true;
74
+ if (event.codeCached > cached) cached = event.codeCached;
75
+ }
76
+ const step = Number.isFinite(event.step) ? event.step : 0;
77
+ if (step > 0 && (firstStep === 0 || step < firstStep)) {
78
+ firstStep = step;
79
+ firstIn = 0;
80
+ }
81
+ if (step > 0 && step === firstStep && Number.isFinite(event.stepInput) && event.stepInput > firstIn) firstIn = event.stepInput;
82
+ return { cached, cacheReported, firstIn, firstStep };
83
+ }
84
+
85
+ /** The breakdown, carrying the turn's cache figures. */
86
+ export function withTurnCache(breakdown, c) {
87
+ if (!breakdown || typeof breakdown !== "object" || !c) return breakdown;
88
+ return {
89
+ ...breakdown,
90
+ liveFirstIn: c.firstIn,
91
+ ...(c.cacheReported ? { liveCached: c.cached, liveCacheReported: true } : {}),
92
+ };
93
+ }
94
+
95
+ /** What the terminal reads back: { cached, cacheReported, firstIn }, or null from an older worker. */
96
+ export function readTurnCache(breakdown) {
97
+ if (!breakdown || typeof breakdown !== "object" || !("liveFirstIn" in breakdown)) return null;
98
+ const n = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : 0);
99
+ return { cached: n(breakdown.liveCached), cacheReported: breakdown.liveCacheReported === true, firstIn: n(breakdown.liveFirstIn) };
100
+ }
@@ -2852,3 +2852,17 @@ export function noticeRow(items, { width = 80, gutter = " ", sep = " · " }
2852
2852
  });
2853
2853
  return { row, spans };
2854
2854
  }
2855
+
2856
+ /**
2857
+ * THE TURN'S PRICE AT THE END OF THE TOKEN ROW (#323): "↑ sent 500k · ↓ out 4.3k · total ↓ 1.6M | ~$0.33".
2858
+ *
2859
+ * The row is clipped on the RIGHT at the window's width, and the price is the last thing on it — so
2860
+ * on a narrow window (80 columns over SSH) it would be the first thing lost. When the whole row does
2861
+ * not fit, the lifetime "total ↓" goes first (it changes no decision about this turn); the price
2862
+ * stays. `price` = the already-painted " | ~$0.33" segment, or "" when the model has no price.
2863
+ */
2864
+ export function tokenRowWithPrice({ withTotal = "", withoutTotal = "", price = "", room = 80 } = {}) {
2865
+ if (!price) return withTotal;
2866
+ if (visibleLen(withTotal) + visibleLen(price) <= room) return withTotal + price;
2867
+ return withoutTotal + price;
2868
+ }
@@ -674,3 +674,68 @@ export function applySelectionHighlight(rows, sel, sgr = SELECTION_SGR) {
674
674
  return highlightRow(row, from, to, sgr);
675
675
  });
676
676
  }
677
+
678
+ /**
679
+ * WHEN THE FROZEN ZONE CHANGES HEIGHT (#321) — move the screen the way a terminal would, instead of
680
+ * repainting it from the transcript.
681
+ *
682
+ * Reported: after an update, the banner and the update notice appeared again and again in the
683
+ * scrollback, and single lines ("auto-test off", "1 skill · 1 MCP server") were doubled. The cause
684
+ * was the repaint that followed every height change of the bottom zone (a picker, the notice row,
685
+ * the status block coming and going):
686
+ * · the zone SHRANK → the region grew → the repaint filled the new rows at the TOP with older
687
+ * transcript lines — lines the terminal had ALREADY scrolled into its scrollback. The next
688
+ * output scrolled them in again: one more copy per shrink;
689
+ * · the zone GREW → the repaint painted over the top rows, which never reached the scrollback.
690
+ *
691
+ * Now:
692
+ * · GROW by k: the k rows the zone takes come out of the blank rows below the content first; for
693
+ * the rest, the region is scrolled up (real newlines), so the top lines go into the scrollback
694
+ * ONCE, the normal way;
695
+ * · SHRINK by k: the content stays where it is and the k freed rows are cleared; they are
696
+ * remembered as blank rows below the content ("pad"), which new output fills first.
697
+ * gu's model of the screen is the transcript's rows plus `pad` blank rows, bottom-aligned once it
698
+ * fills the region and top-aligned before (viewportSlice), so it matches without a repaint.
699
+ *
700
+ * → { scrollUp, clearRows, pad }: lines to scroll the OLD region up by, rows to clear below the old
701
+ * region (the freed rows), and the new pad.
702
+ */
703
+ export function zoneResizePlan({ contentRows = 0, pad = 0, oldHeight, newHeight }) {
704
+ const c = Math.max(0, contentRows | 0);
705
+ const p = Math.max(0, pad | 0);
706
+ const oldH = Math.max(1, oldHeight | 0);
707
+ const newH = Math.max(1, newHeight | 0);
708
+ const full = c + p >= oldH; // bottom-aligned (the region is full) vs top-aligned
709
+ if (newH >= oldH) {
710
+ const k = newH - oldH;
711
+ return { scrollUp: 0, clearRows: k, pad: full ? p + k : p };
712
+ }
713
+ const k = oldH - newH;
714
+ const blanks = full ? p : oldH - c; // rows at the bottom holding nothing
715
+ const fromBlank = Math.min(blanks, k);
716
+ const scrollUp = k - fromBlank;
717
+ if (scrollUp > 0) return { scrollUp, clearRows: 0, pad: 0 };
718
+ return { scrollUp: 0, clearRows: 0, pad: full ? p - fromBlank : Math.max(0, Math.min(p, newH - c)) };
719
+ }
720
+
721
+ /**
722
+ * A BLOCK DRAWN BELOW THE CONTENT AND WIPED AGAIN — a picker (#321). Its rows are not transcript
723
+ * content, but drawing them can scroll the region: whatever scrolled away is in the terminal's
724
+ * scrollback now. Wiping the block leaves the content shifted up by that much with blank rows under
725
+ * it — recorded as pad, so no later repaint brings the scrolled-away lines back (each one would
726
+ * land in the scrollback a second time).
727
+ * contentRows the transcript's rows when the block was first drawn (its last is the cursor row)
728
+ * pad the blank rows below the content then (gu's pad)
729
+ * height the region's rows
730
+ * blockRows the most physical rows the block drew (it ends with a newline)
731
+ * → the pad after the wipe.
732
+ */
733
+ export function blockClosedPad({ contentRows = 0, pad = 0, height, blockRows = 0 }) {
734
+ const c = Math.max(0, contentRows | 0);
735
+ const p = Math.max(0, pad | 0);
736
+ const h = Math.max(1, height | 0);
737
+ const full = c + p >= h;
738
+ const blanks = full ? p : h - c; // rows below the cursor row's region that hold nothing
739
+ const scrolled = Math.max(0, (blockRows | 0) - blanks);
740
+ return full || scrolled > 0 ? blanks + scrolled : p;
741
+ }
package/turn-stop.mjs CHANGED
@@ -55,6 +55,23 @@ export function replaceStoppedQuestion(history, oldTask, newTask) {
55
55
  return true;
56
56
  }
57
57
 
58
+ /**
59
+ * Has the user LET GO of the restored question? (task #247 meets #302)
60
+ *
61
+ * The line going EMPTY is "never mind", however it was emptied — Ctrl+C, Backspace, Ctrl+U, a
62
+ * picker taking the line. Only Ctrl+C used to count, so erasing the question and typing "where
63
+ * were we?" was taken for an EDIT of it: the message replaced the stopped question in history and
64
+ * the model was asked where we were with nothing before it — the report #247 began with.
65
+ * What is typed after that is a new message; the stopped question stays, tagged "still stands".
66
+ *
67
+ * Enter is not an emptying: readline has already cleared the line it is submitting by the time
68
+ * the keypress is seen.
69
+ */
70
+ export function restoredQuestionLetGo(line, key) {
71
+ if (key && (key.name === "return" || key.name === "enter")) return false;
72
+ return String(line ?? "") === "";
73
+ }
74
+
58
75
  /** Short relative names for the stop note: the last two path parts. */
59
76
  function shortPath(p) {
60
77
  const parts = String(p ?? "").split(/[\\/]/).filter(Boolean);