@vincemakes/kiso-tui 0.46.1 → 0.47.0
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/dist/ask-panel.d.ts +10 -5
- package/dist/ask-panel.js +109 -54
- package/dist/at-picker.d.ts +38 -68
- package/dist/at-picker.js +50 -141
- package/dist/compositor.d.ts +79 -10
- package/dist/compositor.js +339 -57
- package/dist/context-ledger.d.ts +28 -0
- package/dist/context-ledger.js +62 -0
- package/dist/editor.d.ts +17 -0
- package/dist/editor.js +76 -15
- package/dist/index.d.ts +10 -10
- package/dist/index.js +10 -10
- package/dist/lines.d.ts +11 -0
- package/dist/lines.js +38 -0
- package/dist/notice-meta.d.ts +25 -0
- package/dist/notice-meta.js +83 -0
- package/dist/panel-input.d.ts +2 -2
- package/dist/panel-input.js +70 -60
- package/dist/pick-input.d.ts +23 -3
- package/dist/pick-input.js +9 -29
- package/dist/session-picker.d.ts +37 -22
- package/dist/session-picker.js +270 -57
- package/dist/status.d.ts +78 -75
- package/dist/status.js +182 -108
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -50,20 +50,32 @@ import { MENU_ITEMS, displayWidth } from "./editor.js";
|
|
|
50
50
|
import { leadWidth } from "./width.js"; // W23: the ONE width authority (the editor, #inputRow, and editCol share it)
|
|
51
51
|
// KC3.5: the panel-slot reads come from the DISPATCHERS — one source
|
|
52
52
|
// for four reads, so an ask can never render half as an approval.
|
|
53
|
-
import { panelFrameOf,
|
|
53
|
+
import { panelFrameOf, panelHintOf, panelStatusOf } from "./ask-panel.js";
|
|
54
54
|
import { MOUSE_OFF } from "./editor.js";
|
|
55
55
|
import { pickWindowOf } from "./approval-panel.js";
|
|
56
|
-
import { atPanelRows, bandHeader } from "./at-picker.js";
|
|
56
|
+
import { atPanelRows, bandHeader, bandKeyRow, bandVisible, bandWindow, moreMark } from "./at-picker.js";
|
|
57
57
|
// TUI2-R2 ②: the session picker's rows — the band's third occupant.
|
|
58
|
-
import { sessionPickerRows } from "./session-picker.js";
|
|
59
|
-
import { CAP_PREVIEW, thinkingRow, Container, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent,
|
|
58
|
+
import { RESUME_FILTER_HINT, sessionPickerRows } from "./session-picker.js";
|
|
59
|
+
import { CAP_PREVIEW, thinkingRow, Container, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, cutLine, pendingQueueRows, selectionBar, statusLine, visibleWidth, breathFrame, widthCut, } from "./components.js";
|
|
60
60
|
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary } from "./lines.js";
|
|
61
61
|
import { oneRow } from "@vincemakes/kiso-tui-cells/render";
|
|
62
62
|
import { displayVerb, keysSheetRows } from "./strings.js";
|
|
63
|
+
import { noticeMeta } from "./notice-meta.js";
|
|
64
|
+
import { statusBar } from "./status.js";
|
|
65
|
+
/** Graphite §7.8 — the drawn caret: a block in the terminal's own ink,
|
|
66
|
+
* reverse video on every ground (owner, 2026-09-29 — the 0.44 caret; R1c
|
|
67
|
+
* had painted it gold). */
|
|
68
|
+
const caretOn = () => "\x1b[7m";
|
|
69
|
+
const caretOff = () => "\x1b[27m";
|
|
63
70
|
/** DC-56: does a rendered row carry any visible text once its SGR is
|
|
64
71
|
* stripped? A washed pad row is spaces under a background colour — width
|
|
65
72
|
* without words. */
|
|
66
|
-
|
|
73
|
+
/** A row that says something: not blank, and not a card's half-row pad
|
|
74
|
+
* (Graphite §7.4 — a `▄`/`▀` row is the card's edge, not its head). */
|
|
75
|
+
const saysSomething = (row) => {
|
|
76
|
+
const t = row.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "").trim();
|
|
77
|
+
return t !== "" && !/^[\u2584\u2580]+$/.test(t);
|
|
78
|
+
};
|
|
67
79
|
// R5 — the transcript viewer's PURE projection. The compositor supplies
|
|
68
80
|
// the entries (it holds the cells); the arrangement lives there.
|
|
69
81
|
import { VIEWER_GUTTER, viewerFlat, viewerHint, viewerInit, viewerMove, viewerRows, viewerScroll, viewerTitle, viewerToggle, viewerToggleAll, } from "./transcript.js";
|
|
@@ -86,9 +98,6 @@ const CHROME_ROWS = 4; // box top + input + box bottom + status — the design
|
|
|
86
98
|
* same unbounded height the projection exists to remove; the rest are
|
|
87
99
|
* COUNTED, never dropped silently. */
|
|
88
100
|
const LIVE_ACT_HEADS = 3;
|
|
89
|
-
/** R8 — the command band's window: five rows plus a counter, the same
|
|
90
|
-
* budget the composer's own ceiling can afford above it. */
|
|
91
|
-
const MENU_WINDOW = 5;
|
|
92
101
|
export class Body {
|
|
93
102
|
#opts;
|
|
94
103
|
#cells = [];
|
|
@@ -230,6 +239,17 @@ export class Body {
|
|
|
230
239
|
// the chrome state (the Dock façade)
|
|
231
240
|
#status = "";
|
|
232
241
|
#statusHint = null;
|
|
242
|
+
/** Graphite §8.9 — the status bar's facts, handed over by the CLI and
|
|
243
|
+
* composed at paint time (the drop order spans both sides of the row,
|
|
244
|
+
* and only the paint knows the width and the `ctrl+o` state). Null:
|
|
245
|
+
* the CLI's own status text is the row (setStatus). */
|
|
246
|
+
#bar = null;
|
|
247
|
+
/** Graphite §8.7 — the live row above the composer while a turn runs
|
|
248
|
+
* (working, retrying, compacting), or null. */
|
|
249
|
+
#live = null;
|
|
250
|
+
/** A short confirmation (the clipboard's) on the live row while nothing
|
|
251
|
+
* is live; the next keypress clears it. */
|
|
252
|
+
#flash = null;
|
|
233
253
|
#tail = "";
|
|
234
254
|
// W21: the panel's bound state — the PanelSelect slot occupant (the
|
|
235
255
|
// old ApprovalPrompt's question slot retires with it): while a
|
|
@@ -716,7 +736,103 @@ export class Body {
|
|
|
716
736
|
}
|
|
717
737
|
this.#closeOpenThinking();
|
|
718
738
|
this.#closeOpenText();
|
|
719
|
-
|
|
739
|
+
// Graphite §7.12: a meta row on the terminal; the pipe above keeps
|
|
740
|
+
// the text as written.
|
|
741
|
+
this.#cells.push({ kind: "notice", text, done: true, ...noticeMeta(text) });
|
|
742
|
+
this.#mark();
|
|
743
|
+
}
|
|
744
|
+
/** The tasks round (owner, 2026-10-06) — a notice whose terminal form
|
|
745
|
+
* is one meta row per thing it names (a task's end, a project's trust):
|
|
746
|
+
* each row a label (`TASK` once, `""` under it), the sentence cut to one
|
|
747
|
+
* row, its outcome word marked. A pipe keeps `text`, byte for byte. */
|
|
748
|
+
metaNotice(text, rows) {
|
|
749
|
+
if (!this.#isActive()) {
|
|
750
|
+
this.#closeOpenThinking();
|
|
751
|
+
this.#closeOpenText();
|
|
752
|
+
this.#write(`${text}\n`);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
this.#closeOpenThinking();
|
|
756
|
+
this.#closeOpenText();
|
|
757
|
+
const [first, ...also] = rows;
|
|
758
|
+
if (first === undefined)
|
|
759
|
+
return;
|
|
760
|
+
// ONE cell for the notice, so its rows stay together (D1 puts a blank
|
|
761
|
+
// between two cells)
|
|
762
|
+
this.#cells.push({ kind: "notice", text, done: true, label: first.label, sentence: first.sentence, ...(first.mark !== undefined ? { mark: first.mark } : {}), oneRow: true, ...(also.length > 0 ? { also } : {}) });
|
|
763
|
+
this.#mark();
|
|
764
|
+
}
|
|
765
|
+
/** The last sweep (owner, 2026-10-06) — what `/think` or `/last` brings
|
|
766
|
+
* back: on the terminal ONE cell, a meta row (`label`, `sentence`) and
|
|
767
|
+
* the sections under it; a pipe keeps `text`, byte for byte. */
|
|
768
|
+
recall(text, label, sentence, sections) {
|
|
769
|
+
if (!this.#isActive()) {
|
|
770
|
+
this.#closeOpenThinking();
|
|
771
|
+
this.#closeOpenText();
|
|
772
|
+
this.#write(`${text}\n`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
this.#closeOpenThinking();
|
|
776
|
+
this.#closeOpenText();
|
|
777
|
+
this.#cells.push({ kind: "recall", text, label, sentence, sections, done: true });
|
|
778
|
+
this.#mark();
|
|
779
|
+
}
|
|
780
|
+
/** Graphite R3e (owner, 2026-09-29) — a mode switch: on the terminal
|
|
781
|
+
* `MODE` on a row of its own and `from → to` under it, the new tier
|
|
782
|
+
* bold (full access — the old bypass — in the failure colour, plan in
|
|
783
|
+
* blue) — no explanation:
|
|
784
|
+
* the picker that switched it says what each tier does. A pipe keeps
|
|
785
|
+
* the confirmation it always printed, byte for byte (`text`). */
|
|
786
|
+
modeNotice(text, from, to) {
|
|
787
|
+
if (!this.#isActive()) {
|
|
788
|
+
this.#closeOpenThinking();
|
|
789
|
+
this.#closeOpenText();
|
|
790
|
+
this.#write(`${text}\n`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
this.#closeOpenThinking();
|
|
794
|
+
this.#closeOpenText();
|
|
795
|
+
// the main-sync round: `full access` (the old bypass) keeps the failure colour
|
|
796
|
+
const tone = to === "full access" || to === "bypass" ? "fail" : to === "plan" ? "blue" : "ink";
|
|
797
|
+
this.#cells.push({ kind: "notice", text, done: true, label: "MODE", sentence: `${from} \u2192 ${to}`, mark: { text: to, tone }, stacked: true });
|
|
798
|
+
this.#mark();
|
|
799
|
+
}
|
|
800
|
+
/** The main-sync round (owner, 2026-09-30) — the don't-ask switch, the
|
|
801
|
+
* MODE row's sibling: `DON'T ASK` gold (the person's choice) and
|
|
802
|
+
* `off → on` with the new state bold. It is a switch on top of the tier,
|
|
803
|
+
* not a tier, so it has a row of its own. A pipe keeps #203's words
|
|
804
|
+
* (`don't ask → on`), byte for byte. */
|
|
805
|
+
dontAskNotice(text, on) {
|
|
806
|
+
if (!this.#isActive()) {
|
|
807
|
+
this.#closeOpenThinking();
|
|
808
|
+
this.#closeOpenText();
|
|
809
|
+
this.#write(`${text}\n`);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
this.#closeOpenThinking();
|
|
813
|
+
this.#closeOpenText();
|
|
814
|
+
const to = on ? "on" : "off";
|
|
815
|
+
this.#cells.push({ kind: "notice", text, done: true, label: "DON'T ASK", sentence: `${on ? "off" : "on"} \u2192 ${to}`, mark: { text: to, tone: "ink" }, stacked: true });
|
|
816
|
+
this.#mark();
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Graphite §7.11 — the turn's SEAL. On the terminal a cell of its own,
|
|
820
|
+
* drawn at the current width (widest tier that fits); a pipe gets
|
|
821
|
+
* `pipe` — today's recap line — byte for byte.
|
|
822
|
+
*/
|
|
823
|
+
seal(tiers, pipe) {
|
|
824
|
+
if (!this.#isActive()) {
|
|
825
|
+
this.#closeOpenThinking();
|
|
826
|
+
this.#closeOpenText();
|
|
827
|
+
// byte for byte what `raw(pipe.split("\n"))` wrote before the
|
|
828
|
+
// seal had a cell of its own — the trailing newline included
|
|
829
|
+
for (const line of pipe.split("\n"))
|
|
830
|
+
this.#write(`${line}\n`);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
this.#closeOpenThinking();
|
|
834
|
+
this.#closeOpenText();
|
|
835
|
+
this.#cells.push({ kind: "seal", tiers, done: true });
|
|
720
836
|
this.#mark();
|
|
721
837
|
}
|
|
722
838
|
/** The startup banner (W1): a LIVE cell — the tier re-derives per
|
|
@@ -756,6 +872,33 @@ export class Body {
|
|
|
756
872
|
this.#cells.push({ kind: "raw", lines, done: true, ...(wrap === undefined ? {} : { wrap }) });
|
|
757
873
|
this.#mark();
|
|
758
874
|
}
|
|
875
|
+
/** G3 (R2d) — the person's `!!cmd`: run, shown here, never sent. Off a
|
|
876
|
+
* dock the command and its output are written as they always were. */
|
|
877
|
+
bang(command, output, isError) {
|
|
878
|
+
if (!this.#isActive()) {
|
|
879
|
+
this.#closeOpenThinking();
|
|
880
|
+
this.#closeOpenText();
|
|
881
|
+
this.#write(`$ ${escapeTerminal(command)}\n${escapeTerminal(output)}\n`);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
this.#closeOpenThinking();
|
|
885
|
+
this.#closeOpenText();
|
|
886
|
+
this.#cells.push({ kind: "bang", command, output, isError, done: true });
|
|
887
|
+
this.#mark();
|
|
888
|
+
}
|
|
889
|
+
/** G3 (R2d) — the input's lead: a gold `$ ` while the line starts with
|
|
890
|
+
* `!` (the person's shell, run by `!` / `!!`); the bound prompt
|
|
891
|
+
* otherwise. One line only — a pasted block that begins with `!` is
|
|
892
|
+
* prose (dispatch's rule). */
|
|
893
|
+
#composerLead() {
|
|
894
|
+
const st = this.#inputState();
|
|
895
|
+
const lines = st.lines !== undefined && st.lines.length > 0 ? st.lines : [st.line];
|
|
896
|
+
if (lines.length === 1 && lines[0].startsWith("!")) {
|
|
897
|
+
const p = palette();
|
|
898
|
+
return `${p.gold}$${p.gold === "" ? "" : p.fgEnd} ${this.#inputPrompt}`;
|
|
899
|
+
}
|
|
900
|
+
return this.#inputPrompt;
|
|
901
|
+
}
|
|
759
902
|
/** The last COMPLETE thinking block, for /think. */
|
|
760
903
|
lastThinking() {
|
|
761
904
|
return this.#lastThinking;
|
|
@@ -929,7 +1072,7 @@ export class Body {
|
|
|
929
1072
|
// a light terminal showed "5 folds" over five empty grey rows.
|
|
930
1073
|
const first = rows.findIndex((r) => saysSomething(r));
|
|
931
1074
|
const headAt = first < 0 ? 0 : first;
|
|
932
|
-
out.push({ head: rows[headAt] ?? "", body: rows.slice(headAt + 1) });
|
|
1075
|
+
out.push({ head: rows[headAt] ?? "", body: rows.slice(headAt + 1).filter((r) => saysSomething(r) || r.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "").trim() === "") });
|
|
933
1076
|
}
|
|
934
1077
|
return out;
|
|
935
1078
|
}
|
|
@@ -1246,14 +1389,28 @@ export class Body {
|
|
|
1246
1389
|
* which time the first frame is already painted with the no-ground
|
|
1247
1390
|
* palette. Rather than delay the opening to wait for an answer that
|
|
1248
1391
|
* may never come, the frame is painted at once and repainted when the
|
|
1249
|
-
* answer lands.
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1392
|
+
* answer lands.
|
|
1393
|
+
*
|
|
1394
|
+
* It is NOT a resize, and must not ride one: the settle returns early
|
|
1395
|
+
* when the geometry did not change — which it never does here — so the
|
|
1396
|
+
* old path cleared the held screen and painted nothing, leaving the
|
|
1397
|
+
* no-ground palette up until the first keystroke. Nor is it a reprint:
|
|
1398
|
+
* `3J` would erase the shell's scrollback at every launch, since the
|
|
1399
|
+
* answer arrives at every launch. So: the committed cells' cached rows
|
|
1400
|
+
* are dropped (their bytes carry the old palette; their geometry does
|
|
1401
|
+
* not change, the palette sets no width), the held screen is voided,
|
|
1402
|
+
* and one frame repaints what is on screen. Rows already in the
|
|
1403
|
+
* terminal's scrollback keep the palette they were written in.
|
|
1253
1404
|
*/
|
|
1254
1405
|
onGroundChange() {
|
|
1255
|
-
this.#
|
|
1256
|
-
|
|
1406
|
+
if (!this.#isActive())
|
|
1407
|
+
return;
|
|
1408
|
+
for (let i = 0; i < this.#committed; i += 1)
|
|
1409
|
+
this.#lineCache[i] = null;
|
|
1410
|
+
this.#screen = new Array(Math.max(1, this.#opts.height())).fill(NOT_PAINTED);
|
|
1411
|
+
this.#fullRedraw = true;
|
|
1412
|
+
this.#dirty = true;
|
|
1413
|
+
this.render();
|
|
1257
1414
|
}
|
|
1258
1415
|
/**
|
|
1259
1416
|
* R14 / route B — THE ONE REPAINT A DRAG EARNS IS A REPRINT.
|
|
@@ -1432,6 +1589,26 @@ export class Body {
|
|
|
1432
1589
|
setStatus(text, hint = null) {
|
|
1433
1590
|
this.#status = text;
|
|
1434
1591
|
this.#statusHint = hint;
|
|
1592
|
+
// a caller that paints its own text takes the row
|
|
1593
|
+
this.#bar = null;
|
|
1594
|
+
this.redraw();
|
|
1595
|
+
}
|
|
1596
|
+
/** Graphite §8.9 — hand the status bar its facts. */
|
|
1597
|
+
setBar(bar) {
|
|
1598
|
+
this.#bar = bar;
|
|
1599
|
+
this.redraw();
|
|
1600
|
+
}
|
|
1601
|
+
/** Graphite §8.7 — the live row (already composed for the width the
|
|
1602
|
+
* caller measured; cut here if the terminal narrowed since), or null
|
|
1603
|
+
* when nothing is live. */
|
|
1604
|
+
setLive(row) {
|
|
1605
|
+
this.#live = row;
|
|
1606
|
+
this.redraw();
|
|
1607
|
+
}
|
|
1608
|
+
/** A confirmation that is true for a moment — `copied 212 chars` — on the
|
|
1609
|
+
* live row, until the next keypress. It never displaces a live row. */
|
|
1610
|
+
flash(text) {
|
|
1611
|
+
this.#flash = text;
|
|
1435
1612
|
this.redraw();
|
|
1436
1613
|
}
|
|
1437
1614
|
setTail(tail) {
|
|
@@ -1462,12 +1639,26 @@ export class Body {
|
|
|
1462
1639
|
// block carries its own row (approval-panel.ts pushes it for the
|
|
1463
1640
|
// approval and the pick, ask-panel.ts for the ask), so nothing is
|
|
1464
1641
|
// lost anywhere.
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
//
|
|
1468
|
-
//
|
|
1469
|
-
//
|
|
1470
|
-
//
|
|
1642
|
+
// Graphite P3/P4 (owner, 2026-10-04): a panel leaves the bar where it
|
|
1643
|
+
// is — the session's facts do not stop being true while a panel is up,
|
|
1644
|
+
// and what the panel is doing is said in its band. The panel's own
|
|
1645
|
+
// status line is the form before a bar is bound (the trust question
|
|
1646
|
+
// comes before the session exists): its words, and not the key ladder
|
|
1647
|
+
// §7.8 retired — an empty hint, so the row offers none.
|
|
1648
|
+
if (panel !== null && this.#bar === null)
|
|
1649
|
+
return { status: panelStatusOf(panel), hint: "", expand: null };
|
|
1650
|
+
// Graphite §8.9: the bar, when the CLI handed one over — the steers
|
|
1651
|
+
// are on screen in the band above the input (§8.7), so the bar does
|
|
1652
|
+
// not count them (the main-sync round, owner 2026-09-30: no `+N
|
|
1653
|
+
// steer` on a Graphite bar).
|
|
1654
|
+
if (this.#bar !== null) {
|
|
1655
|
+
const expand = this.#collapsed.length > 0 ? (this.#expandedAll ? "collapse all" : "expand all") : null;
|
|
1656
|
+
return { status: "", hint: undefined, expand, bar: this.#bar };
|
|
1657
|
+
}
|
|
1658
|
+
// ADR-0057: without a bar, while steers wait for the run's next
|
|
1659
|
+
// quiescent boundary, the right hint counts them — the rows carry the
|
|
1660
|
+
// lines. Short on purpose: the running status shares the row, and a
|
|
1661
|
+
// hint that does not fit is dropped whole.
|
|
1471
1662
|
const queued = this.#queueState?.().length ?? 0;
|
|
1472
1663
|
if (queued > 0)
|
|
1473
1664
|
return { status: this.#status, hint: `+${queued} steer`, expand: null };
|
|
@@ -1514,8 +1705,9 @@ export class Body {
|
|
|
1514
1705
|
this.#pickState = state;
|
|
1515
1706
|
}
|
|
1516
1707
|
/** Bind the pending-turn queue — the CLI's live slots (chat.ts):
|
|
1517
|
-
* the
|
|
1518
|
-
* by their rows
|
|
1708
|
+
* the queued rows render in the menu-rows family and the live caps
|
|
1709
|
+
* shrink by their rows (Graphite §8.7). The status bar does not
|
|
1710
|
+
* count them; without a bar the status row's hint still does. */
|
|
1519
1711
|
bindQueue(state) {
|
|
1520
1712
|
this.#queueState = state;
|
|
1521
1713
|
}
|
|
@@ -1532,7 +1724,8 @@ export class Body {
|
|
|
1532
1724
|
// panel rows' edit column; leadWidth is the ONE authority).
|
|
1533
1725
|
// R2: wallL is 0 — the box is retired, so the row starts at column
|
|
1534
1726
|
// one and the frame's marker and this formula share the constant.
|
|
1535
|
-
|
|
1727
|
+
// Graphite P3/P4: a panel leaves the input row to the composer
|
|
1728
|
+
const lead = this.#composerLead();
|
|
1536
1729
|
return 1 + leadWidth(lead) + st.cursor;
|
|
1537
1730
|
}
|
|
1538
1731
|
/** The old dock's redraw — the editor's onRender target: mark + the
|
|
@@ -1553,6 +1746,7 @@ export class Body {
|
|
|
1553
1746
|
return;
|
|
1554
1747
|
this.#dirty = true;
|
|
1555
1748
|
if (fromKey) {
|
|
1749
|
+
this.#flash = null;
|
|
1556
1750
|
this.#scheduleInputFrame();
|
|
1557
1751
|
return;
|
|
1558
1752
|
}
|
|
@@ -1892,7 +2086,7 @@ export class Body {
|
|
|
1892
2086
|
* freezes, #emitScroll is skipped, and the close repaints from
|
|
1893
2087
|
* #lastSkip. */
|
|
1894
2088
|
#noteOverlay() {
|
|
1895
|
-
const sheetUp = this.#sheetState?.()
|
|
2089
|
+
const sheetUp = (this.#sheetState?.() ?? false) !== false;
|
|
1896
2090
|
const viewerUp = this.#viewer !== null;
|
|
1897
2091
|
this.#overlayFrame = sheetUp || this.#sheetWasUp || viewerUp || this.#viewerWasUp;
|
|
1898
2092
|
this.#sheetWasUp = sheetUp;
|
|
@@ -1913,19 +2107,21 @@ export class Body {
|
|
|
1913
2107
|
const capped = Math.max(1, cap);
|
|
1914
2108
|
if (this.#viewer !== null)
|
|
1915
2109
|
return { lines: this.#viewerBand(W).slice(0, capped), panelSpan: null };
|
|
1916
|
-
|
|
1917
|
-
|
|
2110
|
+
// Graphite R3e: the keys sheet, or a sheet of the caller's rows (`/status`)
|
|
2111
|
+
const sheet = this.#sheetState?.() ?? false;
|
|
2112
|
+
if (sheet !== false)
|
|
2113
|
+
return { lines: (sheet === true ? keysSheetRows(W) : sheet(W)).slice(0, capped), panelSpan: null };
|
|
1918
2114
|
const panel = this.#panelState?.() ?? null;
|
|
1919
2115
|
if (panel !== null) {
|
|
1920
2116
|
// W21: the panel's cap is exact, so the force-commit loop never
|
|
1921
2117
|
// fires on it. W22: the queue band sits below the panel — the
|
|
1922
2118
|
// cap shrinks by it.
|
|
1923
|
-
const frame = panelFrameOf(panel, W, capped);
|
|
2119
|
+
const frame = panelFrameOf(panel, W, capped, this.#opts.height());
|
|
1924
2120
|
// B (review of this round): the pick window THIS frame draws. The size
|
|
1925
2121
|
// depends on the budget, so the renderer is the only producer — the
|
|
1926
2122
|
// digit keys are handed this value instead of deriving a second one.
|
|
1927
2123
|
const pick = panel.pick ?? null;
|
|
1928
|
-
return { lines: frame.rows, panelSpan: frame.options, pickWin: pick === null ? null : pickWindowOf(panel.view, pick
|
|
2124
|
+
return { lines: frame.rows, panelSpan: frame.options, pickWin: pick === null ? null : pickWindowOf(panel.view, pick, capped, this.#opts.height()) };
|
|
1929
2125
|
}
|
|
1930
2126
|
return { lines: this.#liveProjection(W, ctx, cap), panelSpan: null };
|
|
1931
2127
|
}
|
|
@@ -2132,6 +2328,18 @@ export class Body {
|
|
|
2132
2328
|
* occupant (the queue is dense, like the menu; each line is its
|
|
2133
2329
|
* own chip with the □ gutter). */
|
|
2134
2330
|
#queueRows(W, H) {
|
|
2331
|
+
// Graphite §8.7: the live row opens the band above the composer; the
|
|
2332
|
+
// queued messages sit under it, next to the input they wait on.
|
|
2333
|
+
const panelUp = (this.#panelState?.() ?? null) !== null;
|
|
2334
|
+
const p = palette();
|
|
2335
|
+
// a blank row above the live row keeps it off the streaming content
|
|
2336
|
+
// (owner, 2026-09-29: the words ran right up to `working`)
|
|
2337
|
+
const live = panelUp ? [] : this.#live !== null ? ["", cutLine(this.#live, W)] : this.#flash !== null ? ["", cutLine(` ${p.dim}${this.#flash}${p.reset}`, W)] : [];
|
|
2338
|
+
return [...live, ...this.#queuedRows(W, H - live.length)];
|
|
2339
|
+
}
|
|
2340
|
+
/** The queued messages' rows; `H` is the height they may share with
|
|
2341
|
+
* the content, the live row's already taken out. */
|
|
2342
|
+
#queuedRows(W, H) {
|
|
2135
2343
|
const queued = this.#queueState?.() ?? [];
|
|
2136
2344
|
if (queued.length === 0)
|
|
2137
2345
|
return [];
|
|
@@ -2153,14 +2361,17 @@ export class Body {
|
|
|
2153
2361
|
// scroll — the scrollback lost the turns entirely (finding #A8b —
|
|
2154
2362
|
// the queued-flood content loss). The band keeps the first H−9
|
|
2155
2363
|
// chips + one "…N more" row (≤ H−8 rows — the content keeps ≥ 4);
|
|
2156
|
-
//
|
|
2157
|
-
//
|
|
2364
|
+
// that row carries the count, so the cap hides nothing without
|
|
2365
|
+
// saying so.
|
|
2366
|
+
// the main-sync round (owner, 2026-09-30): a steer waits for an open
|
|
2367
|
+
// approval — it never answers one — and its row says so
|
|
2368
|
+
const waiting = this.#panelState?.()?.view.flavor === "approval" ? "approval" : "step";
|
|
2158
2369
|
const keep = Math.max(1, H - 9);
|
|
2159
2370
|
if (lines.length <= keep)
|
|
2160
|
-
return pendingQueueRows(lines, W);
|
|
2371
|
+
return pendingQueueRows(lines, W, waiting);
|
|
2161
2372
|
const p = palette();
|
|
2162
2373
|
const hidden = lines.length - keep;
|
|
2163
|
-
return [...pendingQueueRows(lines.slice(0, keep), W),
|
|
2374
|
+
return [...pendingQueueRows(lines.slice(0, keep), W, waiting), ` ${p.dim}\u2026${hidden} more${p.reset}`];
|
|
2164
2375
|
}
|
|
2165
2376
|
/**
|
|
2166
2377
|
* The band ABOVE the box top. Two occupants share it — the slash
|
|
@@ -2185,10 +2396,10 @@ export class Body {
|
|
|
2185
2396
|
// composer, redrawn with the frame.
|
|
2186
2397
|
const pick = this.#pickState?.() ?? null;
|
|
2187
2398
|
if (pick !== null)
|
|
2188
|
-
return sessionPickerRows(pick, W, Date.now());
|
|
2399
|
+
return sessionPickerRows(pick, W, Date.now(), this.#opts.height());
|
|
2189
2400
|
const at = this.#atState?.() ?? null;
|
|
2190
2401
|
if (at !== null)
|
|
2191
|
-
return atPanelRows(at, W);
|
|
2402
|
+
return atPanelRows(at, W, this.#opts.height());
|
|
2192
2403
|
const menu = this.#menuState?.();
|
|
2193
2404
|
if (menu === null || menu === undefined || menu.items.length === 0)
|
|
2194
2405
|
return [];
|
|
@@ -2215,22 +2426,46 @@ export class Body {
|
|
|
2215
2426
|
// - a description is CUT, never folded — a folded row would
|
|
2216
2427
|
// break the window's height, which is the thing being bought.
|
|
2217
2428
|
const items = menu.items;
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
//
|
|
2221
|
-
//
|
|
2222
|
-
//
|
|
2223
|
-
const
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2429
|
+
// Graphite P2 (owner, 2026-10-03): the list takes /resume's shape —
|
|
2430
|
+
// the count in the band, the window (eight rows from a 30-row
|
|
2431
|
+
// terminal, five below) with its more-marks, the typed prefix gold,
|
|
2432
|
+
// and one key row with the counter. The selected row keeps R3a's gold
|
|
2433
|
+
// `›` in column 1 (owner: kept).
|
|
2434
|
+
const total = menu.total ?? items.length;
|
|
2435
|
+
const query = menu.query ?? "";
|
|
2436
|
+
const col = Math.max(menu.nameCol ?? 0, ...items.map((m) => m.name.length - 1));
|
|
2437
|
+
const rows = [bandHeader(`commands \u00b7 ${query === "" ? `${total}` : `${items.length} of ${total} match`}`, W)];
|
|
2438
|
+
const { first, count } = bandWindow(items.length, menu.selected, bandVisible(this.#opts.height()));
|
|
2439
|
+
for (let i = first; i < first + count; i += 1) {
|
|
2226
2440
|
const item = items[i];
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2441
|
+
const name = item.name.slice(1);
|
|
2442
|
+
const selected = i === menu.selected;
|
|
2443
|
+
// the prefix the person typed, gold — the filter is a prefix match
|
|
2444
|
+
// (off a known ground the warn tint stands in, as in the @ band)
|
|
2445
|
+
const typed = Math.min(query.length, name.length);
|
|
2446
|
+
const gold = p.gold !== "" ? p.gold : p.warn;
|
|
2447
|
+
const shownName = `${typed > 0 ? `${p.reset}${p.bold}${gold}${name.slice(0, typed)}${p.reset}${selected ? p.bold : ""}` : ""}${name.slice(typed)}`;
|
|
2448
|
+
// THE FIX (found capturing the real terminal, 2026-10-03): the
|
|
2449
|
+
// description is cut as TEXT, by cells, before it is styled. It was
|
|
2450
|
+
// cut after — by a cutter that counts escape bytes as cells — so
|
|
2451
|
+
// Graphite's 19-byte dim took 19 cells off every row, and a cut that
|
|
2452
|
+
// landed inside the reset left a bare ESC in the output.
|
|
2453
|
+
const room = Math.max(0, W - 2 - col - 2 - 1);
|
|
2454
|
+
const desc = visibleWidth(item.desc) <= room ? item.desc : room <= 1 ? "" : `${widthCut(item.desc, room - 1)}\u2026`;
|
|
2455
|
+
const label = `${shownName}${" ".repeat(Math.max(0, col - name.length))} ${p.reset}${p.dim}${desc}${p.reset}`;
|
|
2456
|
+
const width = col + 2 + visibleWidth(desc);
|
|
2457
|
+
if (selected) {
|
|
2458
|
+
// Graphite §8.2 (R3a): the selected command wears the one selected
|
|
2459
|
+
// row every list has — its `→` is the gold `›` in column 1, the
|
|
2460
|
+
// name from the content edge
|
|
2461
|
+
rows.push(selectionBar(`${p.bold}\u2192${label}`, width + 1, W));
|
|
2462
|
+
}
|
|
2463
|
+
else {
|
|
2464
|
+
const mark = moreMark(i, first, count, items.length);
|
|
2465
|
+
rows.push(`${mark === null ? " " : `${p.dim}${mark}${p.reset}`} ${label}`);
|
|
2466
|
+
}
|
|
2229
2467
|
}
|
|
2230
|
-
|
|
2231
|
-
// list you can see all of, it says nothing the rows do not.
|
|
2232
|
-
if (windowed)
|
|
2233
|
-
rows.push(` ${p.dim}(${menu.selected + 1}/${items.length})${p.reset}`);
|
|
2468
|
+
rows.push(bandKeyRow(["\u2191\u2193 move", "\u23ce completes", "esc"], menu.selected, items.length, W));
|
|
2234
2469
|
return rows;
|
|
2235
2470
|
}
|
|
2236
2471
|
/** ONE input row's bytes — the marker embedded at `embedAt` (the
|
|
@@ -2316,7 +2551,7 @@ export class Body {
|
|
|
2316
2551
|
// the cursor rests past the content — an inverse space,
|
|
2317
2552
|
// taken OUT of the pad (the walk capped content at W, so
|
|
2318
2553
|
// the pad absorbs it and the row still totals W)
|
|
2319
|
-
stripped0 = `${before}
|
|
2554
|
+
stripped0 = `${before}${caretOn()} ${caretOff()}`;
|
|
2320
2555
|
cursorPad = 1;
|
|
2321
2556
|
}
|
|
2322
2557
|
else if (after[0] === "\x1b" || (after.codePointAt(0) >= 0xd800 && after.codePointAt(0) <= 0xdfff)) {
|
|
@@ -2327,7 +2562,7 @@ export class Body {
|
|
|
2327
2562
|
}
|
|
2328
2563
|
else {
|
|
2329
2564
|
const glyph = String.fromCodePoint(after.codePointAt(0));
|
|
2330
|
-
stripped0 = `${before}
|
|
2565
|
+
stripped0 = `${before}${caretOn()}${glyph}${caretOff()}${after.slice(glyph.length)}`;
|
|
2331
2566
|
}
|
|
2332
2567
|
}
|
|
2333
2568
|
// R2 — the box is retired (see boxTop): the composer is two dashed
|
|
@@ -2354,11 +2589,22 @@ export class Body {
|
|
|
2354
2589
|
// the lead — the panel's phase lead when the panel owns the row
|
|
2355
2590
|
// (1-3> / the rule input's "2 Yes, don't ask again for " / the
|
|
2356
2591
|
// amend "feedback (deny): "), the bound prompt otherwise
|
|
2357
|
-
const
|
|
2592
|
+
const p = palette();
|
|
2593
|
+
// Graphite §7.8: the prompt `›` is gold (the edge of a turn); the
|
|
2594
|
+
// bound lead is plain text and the colour is the paint's, so a ground
|
|
2595
|
+
// resolved after the first frame reaches it.
|
|
2596
|
+
// Graphite P3/P4: a panel leaves the input row its own lead — a filter,
|
|
2597
|
+
// a note or an answer is typed there, and a list says its keys on its
|
|
2598
|
+
// key row (DECLARED REVERSAL of `1-4>`, `pick>`, `amend›`)
|
|
2599
|
+
const lead = this.#composerLead().replace("\u203a", `${p.gold}\u203a${p.gold === "" ? "" : p.fgEnd}`);
|
|
2358
2600
|
const leadW = leadWidth(lead);
|
|
2359
2601
|
// a LEGACY one-row provider (the old {line, cursor} shape) keeps
|
|
2360
2602
|
// working: its single line is the composer's single row
|
|
2361
2603
|
let rows = st.lines !== undefined && st.lines.length > 0 ? [...st.lines] : [st.line];
|
|
2604
|
+
// Graphite §7.8 — DECLARED REMOVAL (owner, 2026-09-29): the empty
|
|
2605
|
+
// input carries no placeholder. R1c put the key ladder there; the
|
|
2606
|
+
// row is kept empty — `?` lists the keys, and the empty input is
|
|
2607
|
+
// where later work (follow-up suggestions) will speak.
|
|
2362
2608
|
let cursorRow = Math.min(st.cursorRow ?? 0, rows.length - 1);
|
|
2363
2609
|
const cursorCol = st.cursorCol ?? st.cursor;
|
|
2364
2610
|
// KC1 §5's N_visible, re-applied against the frame's REAL bands:
|
|
@@ -2382,6 +2628,21 @@ export class Body {
|
|
|
2382
2628
|
if (r === cursorRow)
|
|
2383
2629
|
markerCol = 1 + bytes.markerCell;
|
|
2384
2630
|
}
|
|
2631
|
+
// Graphite P1 (owner, 2026-09-30) — DECLARED EXCEPTION to §7.8's empty
|
|
2632
|
+
// input: while the /resume picker is up and nothing is typed, the
|
|
2633
|
+
// input says what typing there does. The input IS the picker's filter,
|
|
2634
|
+
// so the hint names a key the person is about to press; everywhere
|
|
2635
|
+
// else the empty input stays empty. It sits after the drawn cursor,
|
|
2636
|
+
// in the row's pad, so the cursor and the columns do not move.
|
|
2637
|
+
// Graphite P3: the same exception for a pick that filters (/model) —
|
|
2638
|
+
// its input is the filter too, and its hint is the caller's words.
|
|
2639
|
+
const hint = panel === null ? (this.#pickState?.() != null ? RESUME_FILTER_HINT : null) : panelHintOf(panel);
|
|
2640
|
+
if (hint !== null && rows.length === 1 && rows[0] === "") {
|
|
2641
|
+
const pad = / +$/.exec(out[0]);
|
|
2642
|
+
const hintW = visibleWidth(hint);
|
|
2643
|
+
if (pad !== null && pad[0].length > hintW)
|
|
2644
|
+
out[0] = `${out[0].slice(0, pad.index)}${p.dim}${hint}${p.reset}${" ".repeat(pad[0].length - hintW)}`;
|
|
2645
|
+
}
|
|
2385
2646
|
return { rows: out, markerRow: cursorRow, markerCol };
|
|
2386
2647
|
}
|
|
2387
2648
|
/** The full-redraw path (the first frame, the resize repaint) — CUP
|
|
@@ -2544,7 +2805,16 @@ export class Body {
|
|
|
2544
2805
|
// stood here until R14: a resize now reprints from the model, so
|
|
2545
2806
|
// there is no stale fold for a width change to re-index.
|
|
2546
2807
|
const march = all.slice(skip);
|
|
2547
|
-
|
|
2808
|
+
// The sheets round (owner, 2026-10-06): a sheet sits ON the
|
|
2809
|
+
// composer. When the window's top cannot come back down (R13 — a
|
|
2810
|
+
// taller band, the `/` list, pushed rows into the scrollback) the
|
|
2811
|
+
// rows a shorter sheet leaves free go ABOVE it, between the
|
|
2812
|
+
// conversation and the sheet — not between the sheet and the
|
|
2813
|
+
// composer's rail, where they read as the sheet stopping short.
|
|
2814
|
+
const sheetUp = (this.#sheetState?.() ?? false) !== false;
|
|
2815
|
+
const short = contentRows - march.length;
|
|
2816
|
+
const placed = sheetUp && short > 0 && liveLines.length > 0 && liveLines.length <= march.length ? [...march.slice(0, march.length - liveLines.length), ...new Array(short).fill(""), ...march.slice(march.length - liveLines.length)] : march;
|
|
2817
|
+
for (const line of placed.length > contentRows ? placed.slice(placed.length - contentRows) : placed) {
|
|
2548
2818
|
desired[r - 1] = this.#checked(line, W);
|
|
2549
2819
|
r += 1;
|
|
2550
2820
|
}
|
|
@@ -2567,7 +2837,7 @@ export class Body {
|
|
|
2567
2837
|
desired[H - 2 - inputExtra + i - 1] = this.#checked(editor.rows[i], W);
|
|
2568
2838
|
desired[H - 1 - 1] = boxBottom(W);
|
|
2569
2839
|
const statusRow = this.#statusSource();
|
|
2570
|
-
desired[H - 1] = this.#checked(statusLine(statusRow.status, this.#tail, W, statusRow.hint, statusRow.expand), W);
|
|
2840
|
+
desired[H - 1] = this.#checked(statusRow.bar !== undefined ? statusBar(statusRow.bar, W, statusRow.expand) : statusLine(statusRow.status, this.#tail, W, statusRow.hint, statusRow.expand), W);
|
|
2571
2841
|
this.#emitDiff(out, W, H, desired);
|
|
2572
2842
|
// REL-0152-R1: park from where the cursor ACTUALLY is — see
|
|
2573
2843
|
// #cursorRow. It used to be parked from H, which the bottom-up
|
|
@@ -2849,6 +3119,18 @@ export class Dock {
|
|
|
2849
3119
|
setStatus(text, hint) {
|
|
2850
3120
|
compositorRef?.setStatus(text, hint ?? null);
|
|
2851
3121
|
}
|
|
3122
|
+
/** Graphite §8.9 — the status bar's facts. */
|
|
3123
|
+
setBar(bar) {
|
|
3124
|
+
compositorRef?.setBar(bar);
|
|
3125
|
+
}
|
|
3126
|
+
/** Graphite §8.7 — the live row, or null. */
|
|
3127
|
+
setLive(row) {
|
|
3128
|
+
compositorRef?.setLive(row);
|
|
3129
|
+
}
|
|
3130
|
+
/** A short confirmation on the live row, gone at the next keypress. */
|
|
3131
|
+
flash(text) {
|
|
3132
|
+
compositorRef?.flash(text);
|
|
3133
|
+
}
|
|
2852
3134
|
setTail(tail) {
|
|
2853
3135
|
compositorRef?.setTail(tail);
|
|
2854
3136
|
}
|
|
@@ -2919,8 +3201,8 @@ export class Dock {
|
|
|
2919
3201
|
}
|
|
2920
3202
|
compositorRef.bindPick(state);
|
|
2921
3203
|
}
|
|
2922
|
-
/** W22: bind the pending-turn queue — the
|
|
2923
|
-
*
|
|
3204
|
+
/** W22: bind the pending-turn queue — the queued rows (the CLI
|
|
3205
|
+
* binds it from chat(); the editor's pop keys ride
|
|
2924
3206
|
* the LineInput's own bindQueue). */
|
|
2925
3207
|
bindQueue(state) {
|
|
2926
3208
|
if (compositorRef === null) {
|
package/dist/context-ledger.d.ts
CHANGED
|
@@ -69,3 +69,31 @@ export declare function contextRows(ledger: ContextLedger): string[];
|
|
|
69
69
|
* right thing to show is that fact and the one step that produces one.
|
|
70
70
|
* Never an empty bar — an empty bar reads as "measured zero". */
|
|
71
71
|
export declare function contextUnavailableRows(reason: string): string[];
|
|
72
|
+
/** §8.9 — the meter's cells, ONE rule for the status bar's ctx meter and
|
|
73
|
+
* the `/context` sheet (the sheets round, owner 2026-10-06: the two
|
|
74
|
+
* meters cannot disagree). Ten `▆` cells filled to the percentage SHOWN
|
|
75
|
+
* (`0%` is empty; from 1% at least one cell), `ink2` below the soft
|
|
76
|
+
* compaction tier, gold to the hard one, the failure colour past it; the
|
|
77
|
+
* empty cells `track`. "" off a known ground: the colours carry nothing
|
|
78
|
+
* there, and the percentage says it alone. `used`, `soft` and `hard`
|
|
79
|
+
* are shares of the window. */
|
|
80
|
+
export declare const METER_CELLS = 10;
|
|
81
|
+
export declare function meterCells(used: number, soft: number, hard: number): string;
|
|
82
|
+
/**
|
|
83
|
+
* The sheets round (owner, 2026-10-06) — `/context` on a dock is a sheet
|
|
84
|
+
* over the input, the shape `/status` has (§8.16): the band names the
|
|
85
|
+
* total (`context · 2.7k of 200k · 1%`), the bar's meter with when
|
|
86
|
+
* compaction happens beside it, the surfaces as a table — the label, the
|
|
87
|
+
* count right-aligned, the detail dim — the free remainder dim, and the
|
|
88
|
+
* closing row. No blank rows: the printed form spent a cell per row and
|
|
89
|
+
* came out double-spaced. On a pipe `/context` prints `contextRows`,
|
|
90
|
+
* unchanged. `tiers` are the compaction thresholds as shares of the
|
|
91
|
+
* window, or null when the window is not known.
|
|
92
|
+
*/
|
|
93
|
+
export declare function contextSheetRows(ledger: ContextLedger, tiers: {
|
|
94
|
+
readonly soft: number;
|
|
95
|
+
readonly hard: number;
|
|
96
|
+
} | null, W: number): string[];
|
|
97
|
+
/** The sheets round — `/context` before any request, as a sheet: the band
|
|
98
|
+
* says there is no ledger yet, the row says what produces one. */
|
|
99
|
+
export declare function contextUnavailableSheetRows(reason: string, W: number): string[];
|