@vincemakes/kiso-tui 0.7.0 → 0.9.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 +95 -0
- package/dist/ask-panel.js +276 -0
- package/dist/at-picker.d.ts +4 -0
- package/dist/at-picker.js +41 -8
- package/dist/compositor.d.ts +21 -2
- package/dist/compositor.js +232 -35
- package/dist/context-ledger.d.ts +66 -0
- package/dist/context-ledger.js +87 -0
- package/dist/editor.d.ts +4 -1
- package/dist/editor.js +118 -6
- package/dist/index.d.ts +5 -2
- package/dist/index.js +14 -2
- package/dist/status.d.ts +32 -2
- package/dist/status.js +24 -3
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -45,10 +45,13 @@
|
|
|
45
45
|
import { truncateDiff } from "./diff.js";
|
|
46
46
|
import { displayWidth } from "./editor.js";
|
|
47
47
|
import { leadWidth } from "./width.js"; // W23: the ONE width authority (the editor, #inputRow, and editCol share it)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
import {
|
|
48
|
+
// KC3.5: the panel-slot reads come from the DISPATCHERS — one source
|
|
49
|
+
// for four reads, so an ask can never render half as an approval.
|
|
50
|
+
import { panelAffordanceOf, panelLeadOf, panelRowsOf, panelStatusOf } from "./ask-panel.js";
|
|
51
|
+
import { atPanelRows, bandHeader } from "./at-picker.js";
|
|
52
|
+
import { Container, ROLLUP_NOUN, SPINNER, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
|
|
51
53
|
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
|
|
54
|
+
import { keysSheetRows } from "./strings.js";
|
|
52
55
|
/** The cursor marker — an APC private sequence the focus component
|
|
53
56
|
* embeds at the edit position; the compositor strips it and moves
|
|
54
57
|
* relatively (it never reaches the terminal). */
|
|
@@ -116,6 +119,15 @@ export class Body {
|
|
|
116
119
|
// panel is up it replaces the live region, owns the input lead, and
|
|
117
120
|
// derives the status row (the CLI's painting status yields).
|
|
118
121
|
#panelState = null;
|
|
122
|
+
/** TUI2-R1 (D): the keys sheet's slot read — the editor's boolean.
|
|
123
|
+
* Unbound, the sheet cannot render and every frame is byte-identical
|
|
124
|
+
* to before the round. */
|
|
125
|
+
#sheetState = null;
|
|
126
|
+
/** TUI2-R1.5 7(a): the sheet's previous up/down state — a transition
|
|
127
|
+
* in either direction takes the full-redraw path. */
|
|
128
|
+
#sheetWasUp = false;
|
|
129
|
+
/** This frame is an overlay open or close — it must not scroll. */
|
|
130
|
+
#overlayFrame = false;
|
|
119
131
|
#inputState = () => ({ line: "", cursor: 0 });
|
|
120
132
|
#inputPrompt = "";
|
|
121
133
|
#menuState = null;
|
|
@@ -151,6 +163,7 @@ export class Body {
|
|
|
151
163
|
if (dockBindings.at !== null)
|
|
152
164
|
this.#atState = dockBindings.at;
|
|
153
165
|
this.#panelState = dockBindings.panel;
|
|
166
|
+
this.#sheetState = dockBindings.sheet;
|
|
154
167
|
if (dockBindings.queue !== null)
|
|
155
168
|
this.#queueState = dockBindings.queue;
|
|
156
169
|
}
|
|
@@ -210,6 +223,19 @@ export class Body {
|
|
|
210
223
|
this.#write(`→ ${escapeTerminal(name)}(${escapeTerminal(JSON.stringify(input).slice(0, 200))})\n`);
|
|
211
224
|
return;
|
|
212
225
|
}
|
|
226
|
+
// TUI2-R1.5 ① (VD-1): the tool's start CLOSES an open text block —
|
|
227
|
+
// the inactive path above has always done this; the active path
|
|
228
|
+
// forgot, and the consequence was structural. textAppend only ever
|
|
229
|
+
// grows the LAST cell, so a text block with a tool cell after it can
|
|
230
|
+
// never receive another byte: it is finished in fact while its
|
|
231
|
+
// `done` flag says otherwise. The commit loop takes leading DONE
|
|
232
|
+
// cells, so that one stale flag parked the whole rest of the turn
|
|
233
|
+
// behind it — every tool cell then reached the screen through the
|
|
234
|
+
// FORCE-commit path, which by design bypasses the fold-hold. That is
|
|
235
|
+
// why the walkthrough saw nine individual rows: not a fold that
|
|
236
|
+
// declined to form, a fold that was never consulted.
|
|
237
|
+
this.#closeOpenThinking();
|
|
238
|
+
this.#closeOpenText();
|
|
213
239
|
// W12: the cell carries the delegate's child roles from the FULL
|
|
214
240
|
// input — the display summary is sliced at 60 chars (unparseable);
|
|
215
241
|
// the roles are the only running-state data the parent holds (there
|
|
@@ -280,6 +306,30 @@ export class Body {
|
|
|
280
306
|
}
|
|
281
307
|
this.#mark();
|
|
282
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* TUI2-R1 (C) — the RUNNING call's observed output.
|
|
311
|
+
*
|
|
312
|
+
* The CLI tails the shell tool's progress sidecar and hands what it
|
|
313
|
+
* read to the cell. Deliberately narrow: only a cell that is still
|
|
314
|
+
* RUNNING accepts it, so an observation can never overwrite a real
|
|
315
|
+
* result, and an unchanged read costs no frame at all (a poller
|
|
316
|
+
* fires far more often than the output changes).
|
|
317
|
+
*
|
|
318
|
+
* This adds no event and no durable state. The text lands in the
|
|
319
|
+
* cell's live rendering and is replaced wholesale by the tool's own
|
|
320
|
+
* result at settle — which is the only text anything else ever reads.
|
|
321
|
+
*/
|
|
322
|
+
toolProgress(callId, text) {
|
|
323
|
+
if (!this.#isActive())
|
|
324
|
+
return; // the pipe path has no live region
|
|
325
|
+
const cell = this.#toolCell(callId);
|
|
326
|
+
if (cell === null || cell.kind !== "tool" || cell.state !== "running" || cell.done)
|
|
327
|
+
return;
|
|
328
|
+
if (cell.resultText === text)
|
|
329
|
+
return;
|
|
330
|
+
cell.resultText = text;
|
|
331
|
+
this.#mark();
|
|
332
|
+
}
|
|
283
333
|
toolSucceeded(callId) {
|
|
284
334
|
if (!this.#isActive())
|
|
285
335
|
this.#write(" ok\n");
|
|
@@ -490,7 +540,7 @@ export class Body {
|
|
|
490
540
|
this.#cells.push({ kind: "banner", version, extensionsText, resume, done: true });
|
|
491
541
|
this.#mark();
|
|
492
542
|
}
|
|
493
|
-
raw(lines) {
|
|
543
|
+
raw(lines, wrap) {
|
|
494
544
|
if (!this.#isActive()) {
|
|
495
545
|
this.#closeOpenThinking();
|
|
496
546
|
this.#closeOpenText();
|
|
@@ -500,7 +550,7 @@ export class Body {
|
|
|
500
550
|
}
|
|
501
551
|
this.#closeOpenThinking();
|
|
502
552
|
this.#closeOpenText();
|
|
503
|
-
this.#cells.push({ kind: "raw", lines, done: true });
|
|
553
|
+
this.#cells.push({ kind: "raw", lines, done: true, ...(wrap === undefined ? {} : { wrap }) });
|
|
504
554
|
this.#mark();
|
|
505
555
|
}
|
|
506
556
|
/** The last COMPLETE thinking block, for /think. */
|
|
@@ -553,8 +603,16 @@ export class Body {
|
|
|
553
603
|
// land as NEW content, history is never rewritten, ADR-0046).
|
|
554
604
|
const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
|
|
555
605
|
const p = palette();
|
|
606
|
+
const back = `${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`;
|
|
607
|
+
// TUI2-R1 (B): an EXPLORATION head lists per TOOL — the counts
|
|
608
|
+
// the row showed, then one row per tool with its subjects. The
|
|
609
|
+
// header keeps W15's shape; only the subject changes.
|
|
610
|
+
if (cell.rolled.parts !== undefined) {
|
|
611
|
+
const header = `${p.bold}▞${p.reset} expanded · ${escapeTerminal(`explored ${exploreCounts(cell.rolled.parts)}`)} · ${back}`;
|
|
612
|
+
return { kind: "appended", lines: [header, ...exploreRows(cell.rolled.parts, this.#opts.width())] };
|
|
613
|
+
}
|
|
556
614
|
const noun = ROLLUP_NOUN[cell.name] ?? "calls";
|
|
557
|
-
const header = `${p.bold}▞${p.reset} expanded · ${escapeTerminal(`${cell.name.replace("_file", "")} ${cell.rolled.count} ${noun}`)} · ${
|
|
615
|
+
const header = `${p.bold}▞${p.reset} expanded · ${escapeTerminal(`${cell.name.replace("_file", "")} ${cell.rolled.count} ${noun}`)} · ${back}`;
|
|
558
616
|
return {
|
|
559
617
|
kind: "appended",
|
|
560
618
|
lines: [header, ...cell.rolled.targets.map((t) => ` ${p.dim}└ ${escapeTerminal(t)}${p.reset}`)],
|
|
@@ -657,7 +715,7 @@ export class Body {
|
|
|
657
715
|
#statusSource() {
|
|
658
716
|
const panel = this.#panelState?.() ?? null;
|
|
659
717
|
if (panel !== null)
|
|
660
|
-
return { status:
|
|
718
|
+
return { status: panelStatusOf(panel), hint: panelAffordanceOf(panel) };
|
|
661
719
|
// W22: while turns wait in the queue, the right hint shows the
|
|
662
720
|
// count — the chips below carry the lines themselves.
|
|
663
721
|
const queued = this.#queueState?.().length ?? 0;
|
|
@@ -678,6 +736,11 @@ export class Body {
|
|
|
678
736
|
}
|
|
679
737
|
/** Bind the editor's slash-command menu state — the MenuSelect slot
|
|
680
738
|
* occupant (the menu replaces the editor's view while open). */
|
|
739
|
+
/** TUI2-R1 (D): bind the editor's keys-sheet flag. */
|
|
740
|
+
bindSheet(state) {
|
|
741
|
+
this.#sheetState = state;
|
|
742
|
+
this.#mark();
|
|
743
|
+
}
|
|
681
744
|
bindMenu(state) {
|
|
682
745
|
this.#menuState = state;
|
|
683
746
|
}
|
|
@@ -704,7 +767,7 @@ export class Body {
|
|
|
704
767
|
// + 1 — the SAME formula the marker embeds at (the panel lead when
|
|
705
768
|
// the panel owns the row; the old prompt-only math desynced the
|
|
706
769
|
// panel rows' edit column; leadWidth is the ONE authority)
|
|
707
|
-
const lead = panel !== null ?
|
|
770
|
+
const lead = panel !== null ? panelLeadOf(panel) : this.#inputPrompt;
|
|
708
771
|
return 3 + leadWidth(lead) + st.cursor;
|
|
709
772
|
}
|
|
710
773
|
/** The old dock's redraw — the editor's onRender target: mark + the
|
|
@@ -770,15 +833,25 @@ export class Body {
|
|
|
770
833
|
* screen rows), threaded against the previous sibling's OWN rows. */
|
|
771
834
|
liveCount() {
|
|
772
835
|
const panel = this.#panelState?.() ?? null;
|
|
836
|
+
const sheet = this.#sheetState?.() === true;
|
|
773
837
|
const queueRows = this.#queueRows(this.#opts.width(), this.#opts.height());
|
|
774
838
|
// KC1 §6: the composer's extra rows are chrome too — the scalar
|
|
775
839
|
// counts them exactly like the menu/queue bands (N = 1 ⇒ +0)
|
|
776
840
|
const inputExtra = this.#inputRows(this.#opts.width(), this.#opts.height(), this.#menuRows(this.#opts.width()).length, queueRows.length).rows.length - 1;
|
|
841
|
+
// TUI2-R1 (D): the sheet occupies the live region, exactly like the
|
|
842
|
+
// panel — the scalar must say so, or the cap arithmetic disagrees
|
|
843
|
+
// with the screen.
|
|
844
|
+
if (sheet) {
|
|
845
|
+
return (keysSheetRows(this.#opts.width()).slice(0, Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length)).length +
|
|
846
|
+
CHROME_ROWS +
|
|
847
|
+
inputExtra +
|
|
848
|
+
queueRows.length);
|
|
849
|
+
}
|
|
777
850
|
if (panel !== null) {
|
|
778
851
|
// W21: the panel's own rows (the cap is exact — the scalar
|
|
779
852
|
// reflects the screen). W22: the queue chips occupy their
|
|
780
853
|
// own band — the panel's cap shrinks by their rows.
|
|
781
|
-
return (
|
|
854
|
+
return (panelRowsOf(panel, this.#opts.width(), Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length)).length +
|
|
782
855
|
CHROME_ROWS +
|
|
783
856
|
inputExtra +
|
|
784
857
|
queueRows.length);
|
|
@@ -859,13 +932,32 @@ export class Body {
|
|
|
859
932
|
const chromeRows = CHROME_ROWS + inputExtra + menuRows.length + queueRows.length;
|
|
860
933
|
let liveLines = [];
|
|
861
934
|
const panel = this.#panelState?.() ?? null;
|
|
862
|
-
|
|
935
|
+
// TUI2-R1.5 ⑦(a) (VD-8): the sheet is an OVERLAY, and the frame it
|
|
936
|
+
// opens on — and the one it closes on — take the full-redraw path.
|
|
937
|
+
// The sheet REPLACES the live region, so on an idle composer (where
|
|
938
|
+
// the live region is empty) opening it GROWS the model by its own
|
|
939
|
+
// height; the frame's skip grows with it and the difference is paid
|
|
940
|
+
// in real LFs — rows scrolled permanently into the terminal's
|
|
941
|
+
// scrollback, which closing cannot undo, because the scrollback is
|
|
942
|
+
// not ours to rewrite. Measured: three rows per open on a full
|
|
943
|
+
// screen. The overlay below displaces content on screen instead.
|
|
944
|
+
const sheetUp = this.#sheetState?.() === true;
|
|
945
|
+
this.#overlayFrame = sheetUp || this.#sheetWasUp;
|
|
946
|
+
this.#sheetWasUp = sheetUp;
|
|
947
|
+
if (sheetUp) {
|
|
948
|
+
// TUI2-R1 (D): the sheet REPLACES the live region — the same
|
|
949
|
+
// slot the panel uses, for the same reason (it is what the
|
|
950
|
+
// human is reading right now). It cannot coexist with a panel:
|
|
951
|
+
// the editor only opens it from an idle composer.
|
|
952
|
+
liveLines = keysSheetRows(W).slice(0, Math.max(1, H - 4 - inputExtra - queueRows.length));
|
|
953
|
+
}
|
|
954
|
+
else if (panel !== null) {
|
|
863
955
|
// W21: the panel REPLACES the running tool's live window — the
|
|
864
956
|
// bounded block, capped at H−4 (the panel IS the live region;
|
|
865
957
|
// the W11 blank would separate it from the frozen content).
|
|
866
958
|
// The cap is exact, so the force-commit loop never fires. W22:
|
|
867
959
|
// the queue band sits below the panel — the cap shrinks by it.
|
|
868
|
-
liveLines =
|
|
960
|
+
liveLines = panelRowsOf(panel, W, Math.max(1, H - 4 - inputExtra - queueRows.length));
|
|
869
961
|
}
|
|
870
962
|
else {
|
|
871
963
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
@@ -938,14 +1030,17 @@ export class Body {
|
|
|
938
1030
|
#commitCell(i, W, ctx) {
|
|
939
1031
|
const cell = this.#cells[i];
|
|
940
1032
|
const lines = this.#foldOrRollup(cell, i, W, ctx);
|
|
941
|
-
// W15: a tool cell whose
|
|
942
|
-
// affordance
|
|
943
|
-
//
|
|
944
|
-
//
|
|
1033
|
+
// W15: a tool cell whose committed rows carried the "ctrl+r"
|
|
1034
|
+
// affordance joins the expand history — the detection is the
|
|
1035
|
+
// renderer's OWN output, so the read's "/last"-only cut note never
|
|
1036
|
+
// lands here. TUI2-R1 (A/B): the affordance is no longer only the
|
|
1037
|
+
// renderer cut's "└ … ctrl+r" — the self-naming head suffix and
|
|
1038
|
+
// the exploration row carry it on the HEAD row, and a promise the
|
|
1039
|
+
// key does not answer would be the one thing worse than silence.
|
|
945
1040
|
// unshift: the cells commit oldest-first, so the NEWEST cut lands
|
|
946
1041
|
// at the front — the expand pointer's "newest back" walk starts
|
|
947
1042
|
// where the user's last key press would aim.
|
|
948
|
-
if (cell.kind === "tool" &&
|
|
1043
|
+
if (cell.kind === "tool" && lines.some((l) => l.includes("ctrl+r")))
|
|
949
1044
|
this.#collapsed.unshift(i);
|
|
950
1045
|
this.#lineCache[i] = lines;
|
|
951
1046
|
const placed = bodySpacing(i > 0 ? this.#lineCache[i - 1] : null, lines);
|
|
@@ -965,7 +1060,51 @@ export class Body {
|
|
|
965
1060
|
const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
966
1061
|
if (turn === undefined || turn !== this.#turns[this.#turns.length - 1])
|
|
967
1062
|
return false;
|
|
968
|
-
|
|
1063
|
+
if (!turn.ended && !turn.hasText)
|
|
1064
|
+
return true;
|
|
1065
|
+
// the turn's END releases every hold — the settle is where the run
|
|
1066
|
+
// is decided, and a held cell at settle would never commit at all.
|
|
1067
|
+
if (turn.ended)
|
|
1068
|
+
return false;
|
|
1069
|
+
return this.#growingRun(i);
|
|
1070
|
+
}
|
|
1071
|
+
/** TUI2-R1.5 ① (VD-1) — the explore-run hold. W14's hold covers the
|
|
1072
|
+
* QUIET turn only, and the model's own narration ("let me look at the
|
|
1073
|
+
* parser area") sets hasText before the first read even starts: from
|
|
1074
|
+
* there each completion committed in its OWN frame, the head committed
|
|
1075
|
+
* alone, and `members.every(done)` — the fold's gate — could never be
|
|
1076
|
+
* true again. Every real session therefore degraded to one row per
|
|
1077
|
+
* call while the unit suite, which feeds the burst synchronously,
|
|
1078
|
+
* stayed green (the walkthrough's frame s1-06).
|
|
1079
|
+
*
|
|
1080
|
+
* The hold is the smallest honest fix: a DONE explore cell whose run
|
|
1081
|
+
* can still GROW does not commit yet — its committed form is not
|
|
1082
|
+
* decided until the run is closed. The run closes at the first
|
|
1083
|
+
* non-explore cell (the model's next word, an edit, a shell) or at the
|
|
1084
|
+
* turn's end, and the whole run then commits in ONE frame, which is
|
|
1085
|
+
* exactly the shape the fold was written for.
|
|
1086
|
+
*
|
|
1087
|
+
* The force-commit path never consults this (see #held's callers): the
|
|
1088
|
+
* screen's hard cap still wins, so the screen never sticks — a run
|
|
1089
|
+
* under real screen pressure degrades mid-turn, and the rows it
|
|
1090
|
+
* already froze stay frozen (history is never rewritten, ADR-0046). */
|
|
1091
|
+
#growingRun(i) {
|
|
1092
|
+
const cell = this.#cells[i];
|
|
1093
|
+
if (cell.kind !== "tool" || !isExploreTool(cell.name))
|
|
1094
|
+
return false;
|
|
1095
|
+
// the run is still growing while NOTHING but explore cells follow —
|
|
1096
|
+
// the turn-less noise cells (permission raws, ⚠ notices) are
|
|
1097
|
+
// transparent here for the same reason the run scan sees through
|
|
1098
|
+
// them: the streaming execution interleaves them between the calls.
|
|
1099
|
+
for (let j = i + 1; j < this.#cells.length; j += 1) {
|
|
1100
|
+
const next = this.#cells[j];
|
|
1101
|
+
if (next.kind === "raw" || next.kind === "notice")
|
|
1102
|
+
continue;
|
|
1103
|
+
if (next.kind === "tool" && isExploreTool(next.name))
|
|
1104
|
+
continue;
|
|
1105
|
+
return false; // a non-explore cell closed the run — commit now
|
|
1106
|
+
}
|
|
1107
|
+
return true;
|
|
969
1108
|
}
|
|
970
1109
|
/** W14/W13 — the release-time decision at a commit, BEFORE the cell's
|
|
971
1110
|
* own render: the folded-turn fold first (a QUIET turn — ended, no
|
|
@@ -994,26 +1133,38 @@ export class Body {
|
|
|
994
1133
|
return [];
|
|
995
1134
|
}
|
|
996
1135
|
}
|
|
997
|
-
if (cell.kind !== "tool" ||
|
|
1136
|
+
if (cell.kind !== "tool" || !isExploreTool(cell.name))
|
|
998
1137
|
return cellComponent(cell).render(W, ctx);
|
|
999
|
-
//
|
|
1138
|
+
// TUI2-R1 (B): the run is over the READ-ONLY SET, not one name —
|
|
1139
|
+
// a model exploring mixes read/list/search, and the same-name scan
|
|
1140
|
+
// split every real burst into fragments. Writes, edits, shells and
|
|
1141
|
+
// extension tools still break the run at the first one.
|
|
1142
|
+
// the maximal read-only run around i — forward/backward scans over
|
|
1000
1143
|
// the cells. The turn-less noise cells (the permission raws, the ⚠
|
|
1001
1144
|
// notices) are TRANSPARENT: the streaming execution (loop.ts launch)
|
|
1002
1145
|
// interleaves them BETWEEN the calls of one burst, so the run must
|
|
1003
1146
|
// see through them. It never crosses a user/text/thinking cell —
|
|
1004
1147
|
// those separate turns and contexts.
|
|
1148
|
+
// TUI2-R1.5 ① (VD-1): the backward scan stops at the cells this
|
|
1149
|
+
// FRAME is committing. A cell committed in an earlier frame is
|
|
1150
|
+
// frozen — its rows are on the screen and in the scrollback — so it
|
|
1151
|
+
// can never become the head of a rollup now, and a run that
|
|
1152
|
+
// force-committed its first rows mid-turn must not have the rest
|
|
1153
|
+
// silently absorbed into a summary that was computed without them.
|
|
1154
|
+
// The degraded head keeps its individual row; the rest of the run
|
|
1155
|
+
// rolls on its own.
|
|
1005
1156
|
let s = i;
|
|
1006
1157
|
let head = i;
|
|
1007
|
-
while (s >
|
|
1158
|
+
while (s > this.#committedAtFrameStart) {
|
|
1008
1159
|
const prev = this.#cells[s - 1];
|
|
1009
1160
|
if (prev.kind === "raw" || prev.kind === "notice") {
|
|
1010
1161
|
s -= 1;
|
|
1011
1162
|
continue;
|
|
1012
1163
|
}
|
|
1013
|
-
if (prev.kind !== "tool" || prev.name
|
|
1164
|
+
if (prev.kind !== "tool" || !isExploreTool(prev.name))
|
|
1014
1165
|
break;
|
|
1015
1166
|
s -= 1;
|
|
1016
|
-
head = s; // a
|
|
1167
|
+
head = s; // a read-only tool precedes — it is the group's head
|
|
1017
1168
|
}
|
|
1018
1169
|
let e = i;
|
|
1019
1170
|
while (e + 1 < this.#cells.length) {
|
|
@@ -1022,7 +1173,7 @@ export class Body {
|
|
|
1022
1173
|
e += 1;
|
|
1023
1174
|
continue;
|
|
1024
1175
|
}
|
|
1025
|
-
if (next.kind !== "tool" || next.name
|
|
1176
|
+
if (next.kind !== "tool" || !isExploreTool(next.name))
|
|
1026
1177
|
break;
|
|
1027
1178
|
e += 1;
|
|
1028
1179
|
}
|
|
@@ -1040,13 +1191,18 @@ export class Body {
|
|
|
1040
1191
|
this.#rolledHeads.add(head);
|
|
1041
1192
|
let total = 0;
|
|
1042
1193
|
const targets = [];
|
|
1194
|
+
// TUI2-R1 (B): the per-tool parts, in first-call order — the
|
|
1195
|
+
// exploration row's counts and its expanded list both read them.
|
|
1196
|
+
// A search's subject is the PATTERN it looked for (quoted); a
|
|
1197
|
+
// read's or a list's is the path it named.
|
|
1198
|
+
const parts = [];
|
|
1043
1199
|
for (const m of members) {
|
|
1044
1200
|
// the lines count, excluding the tool's OWN truncation note
|
|
1045
1201
|
// (read_file's "… N more lines") — the per-cell meta's rule
|
|
1046
1202
|
const noteAt = m.resultText.lastIndexOf("\n… ");
|
|
1047
1203
|
const shown = noteAt >= 0 ? m.resultText.slice(0, noteAt) : m.resultText;
|
|
1048
|
-
const
|
|
1049
|
-
total +=
|
|
1204
|
+
const rows = shown.split("\n");
|
|
1205
|
+
total += rows[rows.length - 1] === "" ? rows.length - 1 : rows.length;
|
|
1050
1206
|
let input = {};
|
|
1051
1207
|
try {
|
|
1052
1208
|
input = JSON.parse(m.inputFull);
|
|
@@ -1057,11 +1213,19 @@ export class Body {
|
|
|
1057
1213
|
}
|
|
1058
1214
|
const target = toolTarget(m.name, input);
|
|
1059
1215
|
targets.push(target.split("/").pop() ?? target);
|
|
1216
|
+
const subject = m.name === "search_text" ? `"${String(input.pattern ?? "")}"` : target;
|
|
1217
|
+
const part = parts.find((x) => x.name === m.name);
|
|
1218
|
+
if (part === undefined)
|
|
1219
|
+
parts.push({ name: m.name, subjects: [subject] });
|
|
1220
|
+
else
|
|
1221
|
+
part.subjects.push(subject);
|
|
1060
1222
|
}
|
|
1061
1223
|
const first = members[0];
|
|
1062
1224
|
const last = members[members.length - 1];
|
|
1063
1225
|
const elapsed = first.startedAt !== null && last.doneAt !== null ? ((last.doneAt - first.startedAt) / 1000).toFixed(1) : "?";
|
|
1064
|
-
|
|
1226
|
+
// TUI2-R1 (B): `parts` rides ONLY a mixed run — a single-name
|
|
1227
|
+
// run keeps W13's row, byte for byte (the generalization adds).
|
|
1228
|
+
cell.rolled = { count: members.length, lines: total, elapsed, targets, ...(parts.length > 1 ? { parts } : {}) };
|
|
1065
1229
|
return cellComponent(cell).render(W, ctx);
|
|
1066
1230
|
}
|
|
1067
1231
|
// a MEMBER of an already-rolled run → [] (its rows live in the
|
|
@@ -1132,7 +1296,11 @@ export class Body {
|
|
|
1132
1296
|
if (menu === null || menu === undefined || menu.items.length === 0)
|
|
1133
1297
|
return [];
|
|
1134
1298
|
const p = palette();
|
|
1135
|
-
|
|
1299
|
+
// TUI2-R1.5 ⑦(b) (VD-8): the band NAMES itself, the same way the @
|
|
1300
|
+
// picker's does. Both render frameless directly above the composer,
|
|
1301
|
+
// so with scrollback behind them there was nothing to say where the
|
|
1302
|
+
// surface began — the rows read as more history.
|
|
1303
|
+
const rows = [bandHeader("commands", W)];
|
|
1136
1304
|
for (let i = 0; i < menu.items.length; i += 1) {
|
|
1137
1305
|
const item = menu.items[i];
|
|
1138
1306
|
const text = i === menu.selected
|
|
@@ -1224,7 +1392,7 @@ export class Body {
|
|
|
1224
1392
|
// the lead — the panel's phase lead when the panel owns the row
|
|
1225
1393
|
// (1-3> / the rule input's "2 Yes, don't ask again for " / the
|
|
1226
1394
|
// amend "feedback (deny): "), the bound prompt otherwise
|
|
1227
|
-
const lead = panel !== null ?
|
|
1395
|
+
const lead = panel !== null ? panelLeadOf(panel) : this.#inputPrompt;
|
|
1228
1396
|
const leadW = leadWidth(lead);
|
|
1229
1397
|
// a LEGACY one-row provider (the old {line, cursor} shape) keeps
|
|
1230
1398
|
// working: its single line is the composer's single row
|
|
@@ -1266,6 +1434,7 @@ export class Body {
|
|
|
1266
1434
|
* EVERY row is idempotent: N consecutive resizes end with the same
|
|
1267
1435
|
* screen as a single jump to the same size. */
|
|
1268
1436
|
#drawFull(out, W, H, liveTop, liveLines, queueRows, menuRows, editor) {
|
|
1437
|
+
const overlay = this.#overlayFrame;
|
|
1269
1438
|
const inputExtra = editor.rows.length - 1; // KC1: the composer's rows above the retired single input row
|
|
1270
1439
|
const committed = this.#committedLinesThisFrame;
|
|
1271
1440
|
// 0. the FROZEN rows — the re-folded committed content (re-flowed
|
|
@@ -1293,7 +1462,15 @@ export class Body {
|
|
|
1293
1462
|
// window (the committed share + the live + the chrome), r
|
|
1294
1463
|
// monotone, every row 1..H re-painted (the V6-1 every-row rule).
|
|
1295
1464
|
const all = [...frozen, ...committed, ...liveLines];
|
|
1296
|
-
|
|
1465
|
+
// TUI2-R1.5 7(a) (VD-8): while the sheet is up the window does NOT
|
|
1466
|
+
// move. skip is frozen at its pre-open value and #lastSkip is left
|
|
1467
|
+
// alone, so no LF is emitted and nothing enters the scrollback; the
|
|
1468
|
+
// march below is clamped to the window instead, which makes the
|
|
1469
|
+
// sheet displace content ON SCREEN. Closing takes the full-redraw
|
|
1470
|
+
// path with the same #lastSkip and every displaced row comes back.
|
|
1471
|
+
const skip = overlay
|
|
1472
|
+
? this.#lastSkip
|
|
1473
|
+
: Math.max(0, all.length + CHROME_ROWS + inputExtra + queueRows.length + menuRows.length - H);
|
|
1297
1474
|
// A8b (the shrink-trigger's completion): the rows that LEAVE the
|
|
1298
1475
|
// window scroll into the terminal's scrollback — the LF mechanism
|
|
1299
1476
|
// (the steady path's own). Only the rows the paint re-covers (the
|
|
@@ -1305,7 +1482,7 @@ export class Body {
|
|
|
1305
1482
|
// shrink EVERY frame) loses the scrolled-away turns from the
|
|
1306
1483
|
// terminal's scrollback entirely (finding #A8b — the queued-flood
|
|
1307
1484
|
// content loss).
|
|
1308
|
-
if (skip > 0) {
|
|
1485
|
+
if (skip > 0 && !overlay) {
|
|
1309
1486
|
const leaving = Math.max(0, skip - this.#lastSkip);
|
|
1310
1487
|
// A8b (the fresh leaving share): a leaving row whose old-screen
|
|
1311
1488
|
// copy is stale — the committed-this-frame lines (their old rows
|
|
@@ -1329,9 +1506,16 @@ export class Body {
|
|
|
1329
1506
|
for (let i = 0; i < skip; i += 1)
|
|
1330
1507
|
out.push("\n");
|
|
1331
1508
|
}
|
|
1332
|
-
|
|
1509
|
+
if (!overlay)
|
|
1510
|
+
this.#lastSkip = skip;
|
|
1333
1511
|
let r = 1;
|
|
1334
|
-
|
|
1512
|
+
// the window's content rows: everything above the chrome. With the
|
|
1513
|
+
// overlay up `all` can exceed it, and the rows that give way are the
|
|
1514
|
+
// OLDEST on screen — they are still in the model and come back on
|
|
1515
|
+
// the close.
|
|
1516
|
+
const contentRows = Math.max(0, H - CHROME_ROWS - inputExtra - queueRows.length - menuRows.length);
|
|
1517
|
+
const march = all.slice(skip);
|
|
1518
|
+
for (const line of march.length > contentRows ? march.slice(march.length - contentRows) : march) {
|
|
1335
1519
|
out.push(`\x1b[${r};1H\x1b[0K${this.#checked(line, W)}`);
|
|
1336
1520
|
r += 1;
|
|
1337
1521
|
}
|
|
@@ -1382,8 +1566,12 @@ export class Body {
|
|
|
1382
1566
|
// frozenCount, so the lines at [frozenCount..skip−1] are the fresh
|
|
1383
1567
|
// leaving share (their old-screen copies are stale).
|
|
1384
1568
|
const frozenCount = this.#committedLines - committed.length;
|
|
1385
|
-
|
|
1386
|
-
|
|
1569
|
+
// TUI2-R1.5 7(a): an overlay frame never moves the window (see
|
|
1570
|
+
// #drawFull) — the sheet's rows displace content on screen instead
|
|
1571
|
+
// of pushing it into the scrollback.
|
|
1572
|
+
const overlay = this.#overlayFrame;
|
|
1573
|
+
const skip = overlay ? this.#lastSkip : Math.max(0, this.#committedLines + liveLines.length + CHROME_ROWS + inputExtra + queueRows.length + menuRows.length - H);
|
|
1574
|
+
const leaving = overlay ? 0 : Math.max(0, skip - this.#lastSkip);
|
|
1387
1575
|
// the jump to the bottom row H, then N real LFs scroll the screen
|
|
1388
1576
|
// exactly N rows — ONE per committed line (the bookkeeping; the
|
|
1389
1577
|
// stale 1B anchor jumped to H−1 and the N LFs scrolled only N−1 —
|
|
@@ -1603,6 +1791,15 @@ export class Dock {
|
|
|
1603
1791
|
}
|
|
1604
1792
|
compositorRef.bindApproval(state);
|
|
1605
1793
|
}
|
|
1794
|
+
/** TUI2-R1 (D): bind the editor's keys-sheet flag — the slot read for
|
|
1795
|
+
* the ? overlay (the menu/picker binding pattern). */
|
|
1796
|
+
bindSheet(state) {
|
|
1797
|
+
if (compositorRef === null) {
|
|
1798
|
+
dockBindings.sheet = state;
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
compositorRef.bindSheet(state);
|
|
1802
|
+
}
|
|
1606
1803
|
bindInput(state, prompt) {
|
|
1607
1804
|
if (compositorRef === null) {
|
|
1608
1805
|
dockBindings.state = state; // the live buffer — order-agnostic
|
|
@@ -1653,4 +1850,4 @@ let compositorRef = null;
|
|
|
1653
1850
|
* — the old snapshot froze `menu` at bindInput time and the slash-
|
|
1654
1851
|
* command menu silently never bound in the real CLI (the e2e gates
|
|
1655
1852
|
* bind the Body directly and could not see it). */
|
|
1656
|
-
const dockBindings = { state: null, prompt: "", menu: null, at: null, panel: null, queue: null };
|
|
1853
|
+
const dockBindings = { state: null, prompt: "", menu: null, at: null, panel: null, sheet: null, queue: null };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI2-R1 (E) — /context's attribution rows.
|
|
3
|
+
*
|
|
4
|
+
* The question "where did my context go?" has had an answer since E3:
|
|
5
|
+
* the trace sidecar's rent ledger records, per request, exactly what
|
|
6
|
+
* each static surface costs, and the context manifest records what the
|
|
7
|
+
* conversation costs. Until now that answer was only readable by
|
|
8
|
+
* someone willing to parse JSONL.
|
|
9
|
+
*
|
|
10
|
+
* This module is the presentation half and nothing else — a pure
|
|
11
|
+
* function from counts to rows, with no idea where the counts came
|
|
12
|
+
* from. That matters for the purity gate: the trace surface is an
|
|
13
|
+
* OBSERVATION surface (ADR-0051 §6), correctness never reads it, and
|
|
14
|
+
* keeping the reader in the CLI and the renderer here means this module
|
|
15
|
+
* cannot accidentally become a second correctness path.
|
|
16
|
+
*
|
|
17
|
+
* Every number is a count the ledger already carries. Nothing here
|
|
18
|
+
* estimates, projects, or predicts.
|
|
19
|
+
*/
|
|
20
|
+
/** The counts one request's ledger yields, already grouped by surface.
|
|
21
|
+
* Estimated tokens throughout (the rent ledger's own chars/4 convention
|
|
22
|
+
* — R6), because that is the unit the ledger records. */
|
|
23
|
+
export interface ContextLedger {
|
|
24
|
+
/** The model's context window, as the session is configured. */
|
|
25
|
+
readonly window: number;
|
|
26
|
+
/** system:base + every system:ext:* append EXCEPT skills. */
|
|
27
|
+
readonly systemPrompt: number;
|
|
28
|
+
/** system:base alone — the detail behind the row. */
|
|
29
|
+
readonly systemBase: number;
|
|
30
|
+
/** how many extensions appended (the detail's count). */
|
|
31
|
+
readonly appends: number;
|
|
32
|
+
/** the sum of the tool:* lines. */
|
|
33
|
+
readonly toolTable: number;
|
|
34
|
+
readonly tools: number;
|
|
35
|
+
/** system:ext:skills — broken out because it is an INDEX of content
|
|
36
|
+
* rather than an instruction, and it grows with the workspace rather
|
|
37
|
+
* than with the build. 0 when the extension is not loaded. */
|
|
38
|
+
readonly skillsIndex: number;
|
|
39
|
+
/** how many skills the index lists — 0 when the caller cannot know
|
|
40
|
+
* (the rent ledger records surfaces, never their contents). */
|
|
41
|
+
readonly skills: number;
|
|
42
|
+
/** the per-request skeleton (the `envelope` rent line). */
|
|
43
|
+
readonly envelope: number;
|
|
44
|
+
/** the context manifest's turn segments — the conversation itself. */
|
|
45
|
+
readonly messages: number;
|
|
46
|
+
readonly turns: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The rows: the header, the bar, one row per surface that EXISTS, and
|
|
50
|
+
* the free remainder.
|
|
51
|
+
*
|
|
52
|
+
* An absent surface is an absent row — the rent ledger's own R9 rule
|
|
53
|
+
* ("not paid = no rent"), carried into the display: a session with no
|
|
54
|
+
* skills extension should not read a "skills index 0" row, because the
|
|
55
|
+
* zero would look like a measurement rather than an absence.
|
|
56
|
+
*
|
|
57
|
+
* The columns are fixed so the numbers line up as a column of numbers;
|
|
58
|
+
* the detail text rides after them, dim, and is cut by the caller's
|
|
59
|
+
* width if it must be.
|
|
60
|
+
*/
|
|
61
|
+
export declare function contextRows(ledger: ContextLedger): string[];
|
|
62
|
+
/** TUI2-R1 (E) — the honest fallback. The ledger is written PER REQUEST:
|
|
63
|
+
* a session that has not called the model yet has no sidecar, and the
|
|
64
|
+
* right thing to show is that fact and the one step that produces one.
|
|
65
|
+
* Never an empty bar — an empty bar reads as "measured zero". */
|
|
66
|
+
export declare function contextUnavailableRows(reason: string): string[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI2-R1 (E) — /context's attribution rows.
|
|
3
|
+
*
|
|
4
|
+
* The question "where did my context go?" has had an answer since E3:
|
|
5
|
+
* the trace sidecar's rent ledger records, per request, exactly what
|
|
6
|
+
* each static surface costs, and the context manifest records what the
|
|
7
|
+
* conversation costs. Until now that answer was only readable by
|
|
8
|
+
* someone willing to parse JSONL.
|
|
9
|
+
*
|
|
10
|
+
* This module is the presentation half and nothing else — a pure
|
|
11
|
+
* function from counts to rows, with no idea where the counts came
|
|
12
|
+
* from. That matters for the purity gate: the trace surface is an
|
|
13
|
+
* OBSERVATION surface (ADR-0051 §6), correctness never reads it, and
|
|
14
|
+
* keeping the reader in the CLI and the renderer here means this module
|
|
15
|
+
* cannot accidentally become a second correctness path.
|
|
16
|
+
*
|
|
17
|
+
* Every number is a count the ledger already carries. Nothing here
|
|
18
|
+
* estimates, projects, or predicts.
|
|
19
|
+
*/
|
|
20
|
+
import { palette } from "./render.js";
|
|
21
|
+
const BAR_CELLS = 12;
|
|
22
|
+
/** k-units for the ledger's columns: 25700 → 25.7k, 300 → 300, 11 → 11.
|
|
23
|
+
*
|
|
24
|
+
* TUI2-R1.5 ⑤ (VD-15): the floor was 100, which put `11`, `0.3k` and
|
|
25
|
+
* `25.7k` in one right-aligned column — two unit systems stacked, and
|
|
26
|
+
* the reader has to switch between them row by row to compare. The
|
|
27
|
+
* repo already had a k-formatter with a 1000 floor (render.ts's kUnit,
|
|
28
|
+
* which the status row and every settled card use); this now agrees
|
|
29
|
+
* with it, so /context speaks the same number language as the rest of
|
|
30
|
+
* the product. It still differs from kUnit in never having a null to
|
|
31
|
+
* report — every ledger figure is a measured count. */
|
|
32
|
+
function k(n) {
|
|
33
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k` : String(Math.round(n));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The rows: the header, the bar, one row per surface that EXISTS, and
|
|
37
|
+
* the free remainder.
|
|
38
|
+
*
|
|
39
|
+
* An absent surface is an absent row — the rent ledger's own R9 rule
|
|
40
|
+
* ("not paid = no rent"), carried into the display: a session with no
|
|
41
|
+
* skills extension should not read a "skills index 0" row, because the
|
|
42
|
+
* zero would look like a measurement rather than an absence.
|
|
43
|
+
*
|
|
44
|
+
* The columns are fixed so the numbers line up as a column of numbers;
|
|
45
|
+
* the detail text rides after them, dim, and is cut by the caller's
|
|
46
|
+
* width if it must be.
|
|
47
|
+
*/
|
|
48
|
+
export function contextRows(ledger) {
|
|
49
|
+
const p = palette();
|
|
50
|
+
const used = ledger.systemPrompt + ledger.toolTable + ledger.skillsIndex + ledger.envelope + ledger.messages;
|
|
51
|
+
const free = Math.max(0, ledger.window - used);
|
|
52
|
+
const ratio = ledger.window > 0 ? Math.min(1, used / ledger.window) : 1;
|
|
53
|
+
const filled = Math.max(0, Math.min(BAR_CELLS, Math.round(ratio * BAR_CELLS)));
|
|
54
|
+
const rows = [
|
|
55
|
+
`${p.bold}context — ${k(used)} / ${k(ledger.window)} tokens (${Math.round(ratio * 100)}%)${p.reset}`,
|
|
56
|
+
`${p.bold}${"▰".repeat(filled)}${p.reset}${p.dim}${"▱".repeat(BAR_CELLS - filled)}${p.reset}`,
|
|
57
|
+
];
|
|
58
|
+
/** One surface row: the label at 14 columns, the count right-aligned
|
|
59
|
+
* at 5, then the dim detail. */
|
|
60
|
+
const row = (label, value, detail) => ` ${p.bold}▰${p.reset} ${label.padEnd(14)}${p.bold}${k(value).padStart(5)}${p.reset}${detail === "" ? "" : ` ${p.dim}${detail}${p.reset}`}`;
|
|
61
|
+
if (ledger.systemPrompt > 0) {
|
|
62
|
+
rows.push(row("system prompt", ledger.systemPrompt, `(base ${k(ledger.systemBase)}${ledger.appends > 0 ? ` + ${ledger.appends} extension append${ledger.appends === 1 ? "" : "s"}` : ""})`));
|
|
63
|
+
}
|
|
64
|
+
if (ledger.toolTable > 0)
|
|
65
|
+
rows.push(row("tool table", ledger.toolTable, `${ledger.tools} tool${ledger.tools === 1 ? "" : "s"}`));
|
|
66
|
+
if (ledger.skillsIndex > 0) {
|
|
67
|
+
// the skill COUNT is not in the ledger (rent records surfaces, not
|
|
68
|
+
// their contents) — a caller that knows it passes it, and a caller
|
|
69
|
+
// that does not gets the honest half of the sentence rather than a
|
|
70
|
+
// fabricated number.
|
|
71
|
+
rows.push(row("skills index", ledger.skillsIndex, `${ledger.skills > 0 ? `${ledger.skills} skill${ledger.skills === 1 ? "" : "s"}, ` : ""}tier-1 lines only`));
|
|
72
|
+
}
|
|
73
|
+
if (ledger.envelope > 0)
|
|
74
|
+
rows.push(row("envelope", ledger.envelope, ""));
|
|
75
|
+
if (ledger.messages > 0)
|
|
76
|
+
rows.push(row("messages", ledger.messages, `${ledger.turns} turn${ledger.turns === 1 ? "" : "s"}`));
|
|
77
|
+
rows.push(` ${p.dim}▱ ${"free".padEnd(14)}${k(free).padStart(5)}${p.reset}`);
|
|
78
|
+
return rows;
|
|
79
|
+
}
|
|
80
|
+
/** TUI2-R1 (E) — the honest fallback. The ledger is written PER REQUEST:
|
|
81
|
+
* a session that has not called the model yet has no sidecar, and the
|
|
82
|
+
* right thing to show is that fact and the one step that produces one.
|
|
83
|
+
* Never an empty bar — an empty bar reads as "measured zero". */
|
|
84
|
+
export function contextUnavailableRows(reason) {
|
|
85
|
+
const p = palette();
|
|
86
|
+
return [`${p.bold}context — no ledger yet${p.reset}`, ` ${p.dim}${reason}${p.reset}`];
|
|
87
|
+
}
|