@tiens.nguyen/gu-cli 1.0.779 → 1.0.780
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 +42 -67
- package/package.json +1 -1
- package/terminal-layout.mjs +38 -0
package/gu-repl.mjs
CHANGED
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
maskKey,
|
|
75
75
|
otherAgentRows,
|
|
76
76
|
updateRow,
|
|
77
|
+
noticeRow,
|
|
77
78
|
updateNotice,
|
|
78
79
|
answerBlock,
|
|
79
80
|
updateRestartNote,
|
|
@@ -2604,6 +2605,28 @@ function syncBarPinning() {
|
|
|
2604
2605
|
if (!canPinBar()) unpinBar();
|
|
2605
2606
|
else pinBar();
|
|
2606
2607
|
}
|
|
2608
|
+
/**
|
|
2609
|
+
* THE NOTICE ROW ABOVE THE SEPARATOR (#318) — or null when there is nothing to say.
|
|
2610
|
+
* Priority, highest first: where you are (jump to bottom), what you can act on with a click (an
|
|
2611
|
+
* update), a standing fact about the licence (the gu token notice — yellow only when urgent), and
|
|
2612
|
+
* other agents. Dropped whole, lowest first, when they do not all fit.
|
|
2613
|
+
*/
|
|
2614
|
+
function zoneNotice(width) {
|
|
2615
|
+
const items = [];
|
|
2616
|
+
if (scrolledBack()) {
|
|
2617
|
+
const n = scrollOffset;
|
|
2618
|
+
items.push({ key: "jump", clickable: true, paint: accent, text: `↓ jump to bottom (click) · ${n} line${n === 1 ? "" : "s"} below` });
|
|
2619
|
+
}
|
|
2620
|
+
if (availableUpdates.any) {
|
|
2621
|
+
const up = updateRow({ ...availableUpdates, width: 400 });
|
|
2622
|
+
if (up) items.push({ key: "update", clickable: true, paint: accent, text: up.text.slice(up.from) });
|
|
2623
|
+
}
|
|
2624
|
+
if (tokenNotice?.text) items.push({ key: "token", paint: tokenNotice.urgent ? yellow : dim, text: tokenNotice.text });
|
|
2625
|
+
const agents = agentsIndicator(otherAgents);
|
|
2626
|
+
if (agents) items.push({ key: "agents", clickable: true, paint: accent, text: `${agents} running (click)` });
|
|
2627
|
+
return noticeRow(items, { width, gutter: GUTTER });
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2607
2630
|
/**
|
|
2608
2631
|
* The whole frozen zone, top to bottom: a separator, a blank row, the INPUT LINE, then the
|
|
2609
2632
|
* bar's rule, token and mode rows. The blank is what lifts the separator — and the control it
|
|
@@ -2624,17 +2647,12 @@ function frozenZone() {
|
|
|
2624
2647
|
// #307: a question held until its turn prints anything sits on top of the status block — where
|
|
2625
2648
|
// it will land once committed, so nothing moves when it does.
|
|
2626
2649
|
const status = footerActive ? [] : [...pendingEchoRows(), ...liveStatusRows];
|
|
2627
|
-
//
|
|
2628
|
-
//
|
|
2629
|
-
//
|
|
2630
|
-
|
|
2631
|
-
// The token notice counts only when it FITS — the same test its write below uses. With
|
|
2632
|
-
// Boolean(tokenNotice) alone, a narrow terminal lifted a blank row for a line it never drew.
|
|
2633
|
-
const tokenFits = Boolean(tokenNotice) && visibleLen(tokenNotice.text) + 4 <= width - 1;
|
|
2634
|
-
const headControl =
|
|
2635
|
-
scrolledBack() || availableUpdates.any || tokenFits || Boolean(agentsIndicator(otherAgents));
|
|
2650
|
+
// THE SEPARATOR CARRIES NOTHING (#318). What used to ride on it — jump-to-bottom, the update
|
|
2651
|
+
// control, the gu token notice, the agents count — is on its own NOTICE ROW above it; the head
|
|
2652
|
+
// is just the rule, with no blank to lift a control off the typed line.
|
|
2653
|
+
const notice = zoneNotice(width);
|
|
2636
2654
|
const zone = inputZoneRows({
|
|
2637
|
-
headControl,
|
|
2655
|
+
headControl: false,
|
|
2638
2656
|
width,
|
|
2639
2657
|
// #258: typing during a turn is not sent — the row says so (an ask() label still comes first).
|
|
2640
2658
|
prompt: secretInput && secretPrompt
|
|
@@ -2679,61 +2697,14 @@ function frozenZone() {
|
|
|
2679
2697
|
selection: secretInput ? null : inputSelection,
|
|
2680
2698
|
highlight: highlightRow,
|
|
2681
2699
|
});
|
|
2682
|
-
const rows = status.concat(zone.rows, askHintRows(width), footerRows());
|
|
2683
|
-
//
|
|
2684
|
-
//
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
// right now; an update is neither, and will still be there when you come back.
|
|
2691
|
-
// · otherwise, and only when something is genuinely newer → the update control (task #181).
|
|
2692
|
-
//
|
|
2693
|
-
// Both are stored with their COLUMN span, because the row is hit-tested by column now — one
|
|
2694
|
-
// row, two possible targets, and "anywhere on this row" would make the wrong one fire.
|
|
2695
|
-
updateRowTarget = null;
|
|
2696
|
-
agentsRowTarget = null;
|
|
2697
|
-
if (scrolledBack()) {
|
|
2698
|
-
const hint = jumpToBottomRow({ below: scrollOffset, width });
|
|
2699
|
-
rows[sep] = clipVisible(accent(hint.text), width - 1);
|
|
2700
|
-
} else if (availableUpdates.any) {
|
|
2701
|
-
const up = updateRow({ ...availableUpdates, width });
|
|
2702
|
-
if (up) {
|
|
2703
|
-
rows[sep] = clipVisible(dim(up.text.slice(0, up.from)) + accent(up.text.slice(up.from)), width - 1);
|
|
2704
|
-
updateRowTarget = { from: up.from, to: up.to };
|
|
2705
|
-
}
|
|
2706
|
-
} else if (tokenFits) {
|
|
2707
|
-
// The gu token's one line (#232), right-aligned like the controls around it. BELOW the update
|
|
2708
|
-
// control, because an update is actionable with a click and this is a standing fact; ABOVE the
|
|
2709
|
-
// agents count, because it needs the user and that does not. Not clickable. Too narrow to say
|
|
2710
|
-
// it whole → the condition above lets the agents count (or nothing) have the row instead.
|
|
2711
|
-
const len = visibleLen(tokenNotice.text);
|
|
2712
|
-
const from = width - 1 - len;
|
|
2713
|
-
// Yellow only when the user must act (plan ended, over the computer limit); an old token is
|
|
2714
|
-
// routine and reads in the rule's own dim grey (#292 — the yellow line "seems so serious").
|
|
2715
|
-
const paint = tokenNotice.urgent ? yellow : dim;
|
|
2716
|
-
rows[sep] = clipVisible(dim("\u2500".repeat(Math.max(0, from - 1)) + " ") + paint(tokenNotice.text), width - 1);
|
|
2717
|
-
} else if (agentsIndicator(otherAgents)) {
|
|
2718
|
-
// …and when there is no update to offer, the same row carries the agents count (task #195).
|
|
2719
|
-
// THREE THINGS CANNOT SHARE ONE ROW, so they are ranked by how time-critical they are:
|
|
2720
|
-
// the jump hint is about where you are RIGHT NOW, an update is a standing fact, and other
|
|
2721
|
-
// agents change on their own. Update wins over agents because it is rarer and actionable;
|
|
2722
|
-
// agents are always one click away through the same row once it is free.
|
|
2723
|
-
const label = `${agentsIndicator(otherAgents)} running (click)`;
|
|
2724
|
-
const len = visibleLen(label);
|
|
2725
|
-
if (len + 4 <= width - 1) {
|
|
2726
|
-
const from = width - 1 - len;
|
|
2727
|
-
rows[sep] = clipVisible(dim("\u2500".repeat(Math.max(0, from - 1)) + " ") + accent(label), width - 1);
|
|
2728
|
-
agentsRowTarget = { from, to: from + len };
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
// Spread, not a hand-written pick: this used to list the fields it forwarded, and adding
|
|
2732
|
-
// rowCount/caretRow to inputZoneRows left them silently undefined here — the caret would have
|
|
2733
|
-
// parked on the first row of a wrapped input forever.
|
|
2734
|
-
// statusRows so paintBar can offset the input row and the separator's absolute position;
|
|
2735
|
-
// without it every click and caret park would be `status.length` rows out.
|
|
2736
|
-
return { ...zone, rows, statusRows: sep };
|
|
2700
|
+
const rows = status.concat(notice ? [notice.row] : [], zone.rows, askHintRows(width), footerRows());
|
|
2701
|
+
// The separator is `status.length (+1 for the notice row)` rows down — everything that writes to
|
|
2702
|
+
// or hit-tests a row by index counts from here.
|
|
2703
|
+
const sep = status.length + (notice ? 1 : 0);
|
|
2704
|
+
updateRowTarget = notice?.spans.update ?? null;
|
|
2705
|
+
agentsRowTarget = notice?.spans.agents ?? null;
|
|
2706
|
+
jumpSpan = notice?.spans.jump ?? null;
|
|
2707
|
+
return { ...zone, rows, statusRows: sep, noticeRow: Boolean(notice) };
|
|
2737
2708
|
}
|
|
2738
2709
|
/**
|
|
2739
2710
|
* Put the output position back at column 0 of its own row — without printing anything.
|
|
@@ -2773,8 +2744,10 @@ function paintBar() {
|
|
|
2773
2744
|
// The rows exactly as painted, so a double-click reads the same characters the user is
|
|
2774
2745
|
// looking at rather than re-deriving them from rl.line at a width this may not be.
|
|
2775
2746
|
inputRowTexts = zone.rows.slice((zone.statusRows ?? 0) + head);
|
|
2776
|
-
|
|
2777
|
-
|
|
2747
|
+
// THE NOTICE ROW is the one directly above the separator (#318) — never the separator itself.
|
|
2748
|
+
const noticeAbs = zone.noticeRow ? sepAbs - 1 : 0;
|
|
2749
|
+
jumpRowTarget = scrolledBack() && jumpSpan ? { row: noticeAbs, ...jumpSpan } : null;
|
|
2750
|
+
updateRowAbs = updateRowTarget || agentsRowTarget ? noticeAbs : 0;
|
|
2778
2751
|
// preserveCaret:false — the save slot holds the OUTPUT position (see regionUi), and the caret
|
|
2779
2752
|
// is parked on the input line right after, so there is nothing to preserve here.
|
|
2780
2753
|
ui(
|
|
@@ -2928,6 +2901,8 @@ let liveStatusRows = [];
|
|
|
2928
2901
|
// Where the "jump to bottom" line currently is (1-based viewport row + column span), so a click
|
|
2929
2902
|
// can hit it. Null while the view is live.
|
|
2930
2903
|
let jumpRowTarget = null;
|
|
2904
|
+
/** The jump-to-bottom hint's column span on the notice row (#318), set by frozenZone. */
|
|
2905
|
+
let jumpSpan = null;
|
|
2931
2906
|
// The update control's column span on the separator row, and that row's absolute position.
|
|
2932
2907
|
// Column-scoped (task #181): the row is shared with the jump-to-bottom hint, so a click has to
|
|
2933
2908
|
// land ON the label rather than anywhere along it.
|
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.780",
|
|
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",
|
package/terminal-layout.mjs
CHANGED
|
@@ -2814,3 +2814,41 @@ export function hintRows(lines, { width, gutter = " ", paint = (s) => s } = {}
|
|
|
2814
2814
|
}
|
|
2815
2815
|
return out.map((r) => gutter + paint(r));
|
|
2816
2816
|
}
|
|
2817
|
+
|
|
2818
|
+
/**
|
|
2819
|
+
* THE NOTICE ROW — its own row, directly ABOVE the separator (#318).
|
|
2820
|
+
*
|
|
2821
|
+
* The rule above ">>" is only a separator (the user, angrily: "the line is for separating the
|
|
2822
|
+
* user's input, not for that"). Four things used to ride on it, one at a time — the jump-to-bottom
|
|
2823
|
+
* hint, the update control, the gu token notice, the agents count — because the row looked free.
|
|
2824
|
+
* Now they share this row instead, right-aligned, joined with " · ", in PRIORITY order (the first
|
|
2825
|
+
* item is the most important). What does not fit is dropped WHOLE, lowest priority first — never
|
|
2826
|
+
* cut mid-word. No items → null, and the zone simply has no such row.
|
|
2827
|
+
*
|
|
2828
|
+
* items: [{ key, text, paint?, clickable? }] → { row, spans: { [key]: { from, to } } } | null
|
|
2829
|
+
* `spans` are 0-based columns of the CLICKABLE items, gutter included — what a click is tested on.
|
|
2830
|
+
*/
|
|
2831
|
+
export function noticeRow(items, { width = 80, gutter = " ", sep = " · " } = {}) {
|
|
2832
|
+
const w = Math.max(20, width | 0);
|
|
2833
|
+
const room = w - 1 - visibleLen(gutter);
|
|
2834
|
+
const list = (Array.isArray(items) ? items : []).filter((i) => i && i.text);
|
|
2835
|
+
let kept = list;
|
|
2836
|
+
const joinedLen = (xs) => xs.reduce((n, x, i) => n + visibleLen(x.text) + (i ? sep.length : 0), 0);
|
|
2837
|
+
while (kept.length && joinedLen(kept) > room) kept = kept.slice(0, -1);
|
|
2838
|
+
if (!kept.length) return null;
|
|
2839
|
+
const start = w - 1 - joinedLen(kept);
|
|
2840
|
+
let row = " ".repeat(start);
|
|
2841
|
+
let col = start;
|
|
2842
|
+
const spans = {};
|
|
2843
|
+
kept.forEach((x, i) => {
|
|
2844
|
+
if (i) {
|
|
2845
|
+
row += sep;
|
|
2846
|
+
col += sep.length;
|
|
2847
|
+
}
|
|
2848
|
+
const len = visibleLen(x.text);
|
|
2849
|
+
if (x.clickable) spans[x.key] = { from: col, to: col + len };
|
|
2850
|
+
row += (x.paint ?? ((s) => s))(x.text);
|
|
2851
|
+
col += len;
|
|
2852
|
+
});
|
|
2853
|
+
return { row, spans };
|
|
2854
|
+
}
|