@vincemakes/kiso-tui 0.16.8 → 0.18.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/compositor.d.ts +35 -0
- package/dist/compositor.js +578 -87
- package/dist/editor.js +3 -0
- package/package.json +2 -2
package/dist/compositor.d.ts
CHANGED
|
@@ -184,6 +184,41 @@ export declare class Body {
|
|
|
184
184
|
isError: boolean;
|
|
185
185
|
};
|
|
186
186
|
} | null;
|
|
187
|
+
/**
|
|
188
|
+
* R4 (C4d) — THE APPEND-ONLY RE-WRAP.
|
|
189
|
+
*
|
|
190
|
+
* The owner's report: resize the window and the reference
|
|
191
|
+
* implementation's text re-wraps to the new width while kiso's does
|
|
192
|
+
* not. It is true, and it is not a bug to be fixed — it is the price
|
|
193
|
+
* of ADR-0046, and the price is worth naming precisely.
|
|
194
|
+
*
|
|
195
|
+
* A terminal can only reflow a SOFT-wrapped line: one long logical
|
|
196
|
+
* line the terminal itself wrapped as the cursor flowed past the last
|
|
197
|
+
* column. Every row kiso commits is either painted by cursor
|
|
198
|
+
* addressing (#emitDiff) or scrolled out by a bare LF (#emitScroll),
|
|
199
|
+
* and frames run with autowrap OFF — so no byte kiso commits can ever
|
|
200
|
+
* carry a continuation flag, and nothing downstream can rejoin rows an
|
|
201
|
+
* application hard-split. That same LF is what makes the transcript
|
|
202
|
+
* the TERMINAL's: it survives kiso's death, a pipe, and tmux. A
|
|
203
|
+
* product whose transcript reflows is a product that repaints its
|
|
204
|
+
* transcript from its own memory, and that transcript dies with it.
|
|
205
|
+
*
|
|
206
|
+
* What kiso can do — and this is all it can do — is APPEND. The
|
|
207
|
+
* committed cells are still in memory; re-render them at the current
|
|
208
|
+
* width and put them at the BOTTOM, where writing is allowed. Nothing
|
|
209
|
+
* above is rewritten, so ADR-0046 holds exactly.
|
|
210
|
+
*
|
|
211
|
+
* Scoped to PROSE. Text is what reads badly at the wrong width — a
|
|
212
|
+
* paragraph folded for 120 columns and read at 60 is the complaint.
|
|
213
|
+
* Tool rows, folds and chips are short, already carry their own
|
|
214
|
+
* width ladders, and re-printing them would duplicate work the folds
|
|
215
|
+
* exist to state once.
|
|
216
|
+
*/
|
|
217
|
+
rewrap(): {
|
|
218
|
+
lines: string[];
|
|
219
|
+
blocks: number;
|
|
220
|
+
skipped: number;
|
|
221
|
+
};
|
|
187
222
|
expandNext(): {
|
|
188
223
|
kind: "toggled";
|
|
189
224
|
} | {
|
package/dist/compositor.js
CHANGED
|
@@ -52,7 +52,7 @@ import { MOUSE_OFF } from "./editor.js";
|
|
|
52
52
|
import { atPanelRows, bandHeader } from "./at-picker.js";
|
|
53
53
|
// TUI2-R2 ②: the session picker's rows — the band's third occupant.
|
|
54
54
|
import { sessionPickerRows } from "./session-picker.js";
|
|
55
|
-
import { Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
|
|
55
|
+
import { ACT_SLOT_ROWS, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, stretchLine, turnFold, visibleWidth, twinkleFrame, } from "./components.js";
|
|
56
56
|
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
|
|
57
57
|
import { displayVerb, keysSheetRows } from "./strings.js";
|
|
58
58
|
/** The cursor marker — an APC private sequence the focus component
|
|
@@ -69,6 +69,11 @@ const NOT_PAINTED = "\u0000never";
|
|
|
69
69
|
* that a single resize still feels immediate. */
|
|
70
70
|
const RESIZE_SETTLE_MS = 80;
|
|
71
71
|
const CHROME_ROWS = 4; // box top + input + box bottom + status — the design §03 chrome (V6-3; the box is W6)
|
|
72
|
+
/** R3i — how many calls in flight the act window shows at once. Beyond
|
|
73
|
+
* it the block would grow with the model's parallelism, which is the
|
|
74
|
+
* same unbounded height the projection exists to remove; the rest are
|
|
75
|
+
* COUNTED, never dropped silently. */
|
|
76
|
+
const LIVE_ACT_HEADS = 3;
|
|
72
77
|
/** W13 / TUI2-R1 (B) — a rolled run's TITLE: the exploration sentence on
|
|
73
78
|
* a mixed run, W13's verb+count on a single-name one. */
|
|
74
79
|
function rolledTitle(cell) {
|
|
@@ -174,7 +179,7 @@ function openSegment(turn, now) {
|
|
|
174
179
|
const last = turn.segments[turn.segments.length - 1];
|
|
175
180
|
if (last !== undefined && last.closedAt === null)
|
|
176
181
|
return last;
|
|
177
|
-
const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), seen: new Map(), folded: false, spilled: false, headCell: null, cells: [] };
|
|
182
|
+
const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), seen: new Map(), thinkingMs: 0, thinkingSince: null, folded: false, spilled: false, headCell: null, foldKey: null, cells: [] };
|
|
178
183
|
turn.segments.push(fresh);
|
|
179
184
|
return fresh;
|
|
180
185
|
}
|
|
@@ -183,8 +188,21 @@ function openSegment(turn, now) {
|
|
|
183
188
|
* keeps a zero-cell segment from ever existing. */
|
|
184
189
|
function closeSegment(turn, now) {
|
|
185
190
|
const last = turn?.segments[turn.segments.length - 1];
|
|
186
|
-
if (last
|
|
187
|
-
|
|
191
|
+
if (last === undefined || last.closedAt !== null)
|
|
192
|
+
return;
|
|
193
|
+
stopThinking(last, now);
|
|
194
|
+
last.closedAt = now;
|
|
195
|
+
}
|
|
196
|
+
/** R3i — the segment's thinking clock stops. It runs from the first
|
|
197
|
+
* thinking delta of a stretch and stops at the first NON-thinking
|
|
198
|
+
* event, the same rule the CLI applies to the turn — so `thought Ns`
|
|
199
|
+
* is thinking time at every scale and never a wall clock wearing the
|
|
200
|
+
* word (the R3g defect, kept closed at the new scale). */
|
|
201
|
+
function stopThinking(seg, now) {
|
|
202
|
+
if (seg === undefined || seg.thinkingSince === null)
|
|
203
|
+
return;
|
|
204
|
+
seg.thinkingMs += Math.max(0, now - seg.thinkingSince);
|
|
205
|
+
seg.thinkingSince = null;
|
|
188
206
|
}
|
|
189
207
|
/** W20 — the whole-table-replace comparison: the live task block only
|
|
190
208
|
* redraws when the items actually changed (the task extension's
|
|
@@ -296,7 +314,17 @@ export class Body {
|
|
|
296
314
|
// rendered row carried the "ctrl+r" affordance; the expand key's
|
|
297
315
|
// cycling pointer walks this list from the newest back.
|
|
298
316
|
#collapsed = [];
|
|
299
|
-
|
|
317
|
+
/** R4 (C1) — the ring walk is by IDENTITY, not by a modular pointer.
|
|
318
|
+
* `#collapsed` is unshifted on every commit that carries the key, so
|
|
319
|
+
* a numeric pointer's target silently CHANGED whenever a new fold
|
|
320
|
+
* landed mid-cycle: the ring was not stable under itself, and the
|
|
321
|
+
* next press opened something other than what the last press
|
|
322
|
+
* implied. This set records what the current cycle has already
|
|
323
|
+
* opened; the walk takes the newest entry not in it, and empties it
|
|
324
|
+
* when every entry has been seen. */
|
|
325
|
+
#opened = new Set();
|
|
326
|
+
/** R4 (C1) — the session's fold counter. Monotonic, never reused. */
|
|
327
|
+
#foldSeq = 0;
|
|
300
328
|
// W14: the turn records — one per userLine, the fold-hold's state
|
|
301
329
|
// machine (ended / hasText / folded) plus the folded-turn line's
|
|
302
330
|
// counts (accumulated at toolStart). The cells carry the record's
|
|
@@ -466,7 +494,20 @@ export class Body {
|
|
|
466
494
|
// the cell belongs to, and a stamp taken first records the
|
|
467
495
|
// PREVIOUS segment (or none at all) — which left the thinking
|
|
468
496
|
// row standing outside the fold it should have led.
|
|
469
|
-
openSegment(this.#turns[this.#turns.length - 1], Date.now());
|
|
497
|
+
const seg = openSegment(this.#turns[this.#turns.length - 1], Date.now());
|
|
498
|
+
// R3i: the stretch's thinking clock starts HERE — at the first
|
|
499
|
+
// delta of this stretch, the same moment the CLI starts the
|
|
500
|
+
// turn's — and stops at the next non-thinking event below.
|
|
501
|
+
if (seg !== null && seg.thinkingSince === null)
|
|
502
|
+
seg.thinkingSince = Date.now();
|
|
503
|
+
// R3i: and the beat starts HERE. Law 1.4 says "a running thought
|
|
504
|
+
// twinkles", and `#armSpinner`'s own predicate has always
|
|
505
|
+
// included an open thinking cell — but the only caller was
|
|
506
|
+
// `toolRunning`, so a stretch that thought and did nothing else
|
|
507
|
+
// never moved at all. The line's seconds are a frame-time
|
|
508
|
+
// derivation, so without the beat they also never ticked: the
|
|
509
|
+
// row read `thinking 0s` for as long as the model thought.
|
|
510
|
+
this.#armSpinner();
|
|
470
511
|
this.#stampSegment();
|
|
471
512
|
}
|
|
472
513
|
this.#mark();
|
|
@@ -474,6 +515,10 @@ export class Body {
|
|
|
474
515
|
thinkingEnd() {
|
|
475
516
|
const last = this.#cells[this.#cells.length - 1];
|
|
476
517
|
if (last !== undefined && last.kind === "thinking" && !last.done) {
|
|
518
|
+
// R3i: every closer — text, a notice, a terminal label, the next
|
|
519
|
+
// turn — routes through here, so the clock cannot keep running
|
|
520
|
+
// past the thing that ended it.
|
|
521
|
+
stopThinking(this.#turns[this.#turns.length - 1]?.segments.at(-1), Date.now());
|
|
477
522
|
last.done = true;
|
|
478
523
|
this.#lastThinking = last.text;
|
|
479
524
|
if (!this.#isActive())
|
|
@@ -527,6 +572,18 @@ export class Body {
|
|
|
527
572
|
// of a target the term has already counted; an ACT-counting
|
|
528
573
|
// tool (a search, a shell command) always bumps, because two
|
|
529
574
|
// searches for the same pattern really are two searches.
|
|
575
|
+
// R3i phase 5 — an ANSWER is words, and words do not fold (law
|
|
576
|
+
// 1.7). `ask_user` closes the open stretch exactly as prose
|
|
577
|
+
// does, and never joins one: absorbed into `1 × ask_user`,
|
|
578
|
+
// what the human said would be gone from the screen — and the
|
|
579
|
+
// one thing a summary must not do is speak for the human.
|
|
580
|
+
if (name === "ask_user") {
|
|
581
|
+
// no stamp: it belongs to NO stretch, so no fold can speak
|
|
582
|
+
// for it — the same standing a block of prose has.
|
|
583
|
+
closeSegment(turn, Date.now());
|
|
584
|
+
this.#mark();
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
530
587
|
const target = foldCountsObjects(name) ? toolTarget(name, input) : null;
|
|
531
588
|
const bump = (rec) => {
|
|
532
589
|
if (target === null)
|
|
@@ -554,6 +611,9 @@ export class Body {
|
|
|
554
611
|
// file read once per segment is one file in each segment's
|
|
555
612
|
// terms and one file in the turn's.
|
|
556
613
|
const seg = openSegment(turn, Date.now());
|
|
614
|
+
// R3i: a tool call is a NON-thinking event — the clock stops,
|
|
615
|
+
// exactly as the CLI's does at the same boundary.
|
|
616
|
+
stopThinking(seg ?? undefined, Date.now());
|
|
557
617
|
if (seg !== null && bump(seg)) {
|
|
558
618
|
if (name === "read_file")
|
|
559
619
|
seg.reads += 1;
|
|
@@ -974,6 +1034,73 @@ export class Body {
|
|
|
974
1034
|
}
|
|
975
1035
|
return -1;
|
|
976
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* R4 (C4d) — THE APPEND-ONLY RE-WRAP.
|
|
1039
|
+
*
|
|
1040
|
+
* The owner's report: resize the window and the reference
|
|
1041
|
+
* implementation's text re-wraps to the new width while kiso's does
|
|
1042
|
+
* not. It is true, and it is not a bug to be fixed — it is the price
|
|
1043
|
+
* of ADR-0046, and the price is worth naming precisely.
|
|
1044
|
+
*
|
|
1045
|
+
* A terminal can only reflow a SOFT-wrapped line: one long logical
|
|
1046
|
+
* line the terminal itself wrapped as the cursor flowed past the last
|
|
1047
|
+
* column. Every row kiso commits is either painted by cursor
|
|
1048
|
+
* addressing (#emitDiff) or scrolled out by a bare LF (#emitScroll),
|
|
1049
|
+
* and frames run with autowrap OFF — so no byte kiso commits can ever
|
|
1050
|
+
* carry a continuation flag, and nothing downstream can rejoin rows an
|
|
1051
|
+
* application hard-split. That same LF is what makes the transcript
|
|
1052
|
+
* the TERMINAL's: it survives kiso's death, a pipe, and tmux. A
|
|
1053
|
+
* product whose transcript reflows is a product that repaints its
|
|
1054
|
+
* transcript from its own memory, and that transcript dies with it.
|
|
1055
|
+
*
|
|
1056
|
+
* What kiso can do — and this is all it can do — is APPEND. The
|
|
1057
|
+
* committed cells are still in memory; re-render them at the current
|
|
1058
|
+
* width and put them at the BOTTOM, where writing is allowed. Nothing
|
|
1059
|
+
* above is rewritten, so ADR-0046 holds exactly.
|
|
1060
|
+
*
|
|
1061
|
+
* Scoped to PROSE. Text is what reads badly at the wrong width — a
|
|
1062
|
+
* paragraph folded for 120 columns and read at 60 is the complaint.
|
|
1063
|
+
* Tool rows, folds and chips are short, already carry their own
|
|
1064
|
+
* width ladders, and re-printing them would duplicate work the folds
|
|
1065
|
+
* exist to state once.
|
|
1066
|
+
*/
|
|
1067
|
+
rewrap() {
|
|
1068
|
+
const W = this.#opts.width();
|
|
1069
|
+
const H = this.#opts.height();
|
|
1070
|
+
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
|
|
1071
|
+
// two screens is the bound: enough to re-read what a resize just
|
|
1072
|
+
// made awkward, short enough that the append is not its own wall
|
|
1073
|
+
// of text. A silent cap would read as "this is all of it".
|
|
1074
|
+
const budget = Math.max(H, 2 * H);
|
|
1075
|
+
const chunks = [];
|
|
1076
|
+
let rows = 0;
|
|
1077
|
+
let blocks = 0;
|
|
1078
|
+
let skipped = 0;
|
|
1079
|
+
for (let i = this.#committed - 1; i >= 0; i -= 1) {
|
|
1080
|
+
const cell = this.#cells[i];
|
|
1081
|
+
if (cell.kind !== "md")
|
|
1082
|
+
continue;
|
|
1083
|
+
blocks += 1;
|
|
1084
|
+
if (rows >= budget) {
|
|
1085
|
+
skipped += 1;
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const lines = cellComponent(cell).render(W, ctx);
|
|
1089
|
+
chunks.unshift(lines);
|
|
1090
|
+
rows += lines.length;
|
|
1091
|
+
}
|
|
1092
|
+
return { lines: chunks.flat(), blocks: blocks - skipped, skipped };
|
|
1093
|
+
}
|
|
1094
|
+
/** R4 (C1) — what the NEXT press will open, by name. The walk is
|
|
1095
|
+
* deterministic (newest unopened first), so this is a promise the
|
|
1096
|
+
* key keeps rather than a guess. */
|
|
1097
|
+
#nextFoldHint() {
|
|
1098
|
+
const pending = this.#collapsed.filter((i) => !this.#opened.has(i));
|
|
1099
|
+
const ring = pending.length > 0 ? pending : this.#collapsed;
|
|
1100
|
+
const idx = ring[0];
|
|
1101
|
+
const key = idx === undefined ? null : (this.#segmentOf(idx)?.foldKey ?? null);
|
|
1102
|
+
return key === null ? "ctrl+r opens the next fold" : `ctrl+r opens fold ${key}`;
|
|
1103
|
+
}
|
|
977
1104
|
expandNext() {
|
|
978
1105
|
for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
|
|
979
1106
|
const cell = this.#cells[i];
|
|
@@ -995,8 +1122,14 @@ export class Body {
|
|
|
995
1122
|
}
|
|
996
1123
|
if (this.#collapsed.length === 0)
|
|
997
1124
|
return { kind: "none" };
|
|
998
|
-
|
|
999
|
-
|
|
1125
|
+
// R4 (C1) — the newest entry this cycle has not opened yet. When
|
|
1126
|
+
// every entry has been seen the cycle restarts, so the walk is
|
|
1127
|
+
// still "newest back" — it is simply immune to the ring growing
|
|
1128
|
+
// underneath it.
|
|
1129
|
+
if (this.#collapsed.every((i) => this.#opened.has(i)))
|
|
1130
|
+
this.#opened.clear();
|
|
1131
|
+
const idx = this.#collapsed.find((i) => !this.#opened.has(i)) ?? this.#collapsed[0];
|
|
1132
|
+
this.#opened.add(idx);
|
|
1000
1133
|
const cell = this.#cells[idx];
|
|
1001
1134
|
// R3b — a folded SEGMENT expands to the work it stands for.
|
|
1002
1135
|
//
|
|
@@ -1075,7 +1208,20 @@ export class Body {
|
|
|
1075
1208
|
// boundaries survive where they carry meaning (the write that
|
|
1076
1209
|
// splits two explore runs); they simply no longer bound what
|
|
1077
1210
|
// the key can reach.
|
|
1078
|
-
|
|
1211
|
+
// DECLARED SUPERSESSION (R3i phase 3) — the expansion covers THIS
|
|
1212
|
+
// STRETCH, and only this stretch.
|
|
1213
|
+
//
|
|
1214
|
+
// R3f widened it to the whole turn, and had to: R3d had made
|
|
1215
|
+
// the fold the TURN's while the expansion still walked one
|
|
1216
|
+
// segment, so a line claiming `read 3 files · edited 1 file`
|
|
1217
|
+
// opened only the reads — work named and then withheld, the one
|
|
1218
|
+
// thing this file's first gate forbids. R3i moves the fold back
|
|
1219
|
+
// to the stretch, so the pairing is exact again: every stretch
|
|
1220
|
+
// has its OWN line and its own key, and each key opens the work
|
|
1221
|
+
// its line named. Keeping the turn walk would break the same
|
|
1222
|
+
// rule from the other side — two lines, each opening
|
|
1223
|
+
// everything, each header describing rows the other also shows.
|
|
1224
|
+
for (const j of seg.cells) {
|
|
1079
1225
|
if (j < idx)
|
|
1080
1226
|
continue;
|
|
1081
1227
|
const c = this.#cells[j];
|
|
@@ -1098,8 +1244,29 @@ export class Body {
|
|
|
1098
1244
|
// the header names what the FOLD said — the turn's terms — so the
|
|
1099
1245
|
// line you pressed and the block it opens agree. It used to name
|
|
1100
1246
|
// segment 1's, which contradicted the fold above it.
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1247
|
+
const t = this.#stretchTerms(seg);
|
|
1248
|
+
const head = foldTerms(t.calls.find(([n]) => n === "read_file")?.[1] ?? 0, t.calls.find(([n]) => n === "edit_file")?.[1] ?? 0, t.calls.filter(([n]) => n !== "read_file" && n !== "edit_file"));
|
|
1249
|
+
// R3i phase 4 — THE FOOTER TELLS THE TRUTH ABOUT THIS PATH.
|
|
1250
|
+
//
|
|
1251
|
+
// The rows come from the rollup's own projection, whose last
|
|
1252
|
+
// row reads `└ ctrl+r collapses` — true where it was written
|
|
1253
|
+
// (the LIVE toggle, which really does close again) and false
|
|
1254
|
+
// here. A committed row is ink: ADR-0046 forbids rewriting
|
|
1255
|
+
// history, so nothing about this block can be taken back. The
|
|
1256
|
+
// next press opens the NEXT fold, and the row now says so.
|
|
1257
|
+
const closing = rows.length > 0 && /ctrl\+r collapses/.test(rows[rows.length - 1] ?? "");
|
|
1258
|
+
const body = closing ? rows.slice(0, -1) : rows;
|
|
1259
|
+
return {
|
|
1260
|
+
kind: "appended",
|
|
1261
|
+
lines: [
|
|
1262
|
+
// R4 (C1): the expansion names the fold it opened, in the
|
|
1263
|
+
// same ordinal the fold row printed — the answer to "which
|
|
1264
|
+
// one did that open", stated rather than inferred.
|
|
1265
|
+
`${p.bold}✦${p.reset} expanded${seg.foldKey === null ? "" : ` ${seg.foldKey}`} · ${escapeTerminal(head.length === 0 ? "thinking" : head.join(" · "))} · ${back}`,
|
|
1266
|
+
...body,
|
|
1267
|
+
` ${p.dim}└ end of expansion · ${this.#nextFoldHint()}${p.reset}`,
|
|
1268
|
+
],
|
|
1269
|
+
};
|
|
1103
1270
|
}
|
|
1104
1271
|
if (cell.kind !== "tool")
|
|
1105
1272
|
return { kind: "none" };
|
|
@@ -1574,6 +1741,239 @@ export class Body {
|
|
|
1574
1741
|
this.render();
|
|
1575
1742
|
}
|
|
1576
1743
|
// ---- the one writer ----
|
|
1744
|
+
/**
|
|
1745
|
+
* R3i phase 2 — THE LIVE PROJECTION.
|
|
1746
|
+
*
|
|
1747
|
+
* One definition, called from the natural path and from inside the
|
|
1748
|
+
* force-commit loop, because two copies of "what the live region
|
|
1749
|
+
* looks like" is two answers to one question.
|
|
1750
|
+
*
|
|
1751
|
+
* The change this phase makes, and the ONLY one: the cells of the
|
|
1752
|
+
* OPEN stretch no longer each hold a row. The stretch is one line —
|
|
1753
|
+
* the same line the settle will keep, in the present tense — plus
|
|
1754
|
+
* the calls actually in flight. A completed call renders nothing;
|
|
1755
|
+
* its count rides the line.
|
|
1756
|
+
*
|
|
1757
|
+
* What it fixes: a 28-call turn used to spend 28 rows of a 30-row
|
|
1758
|
+
* live region, so overflow was the NORM on real turns rather than
|
|
1759
|
+
* the edge — and a turn that overflows may not fold (R3f: a line
|
|
1760
|
+
* cannot claim rows already in the scrollback), which is why the
|
|
1761
|
+
* fold missed exactly the turns it exists for. The block's height
|
|
1762
|
+
* no longer depends on the call count at all.
|
|
1763
|
+
*
|
|
1764
|
+
* What it does NOT change: nothing about what commits or when. The
|
|
1765
|
+
* hold is untouched, the force-commit cap is untouched, and the
|
|
1766
|
+
* settle still produces the same fold it did before. That is the
|
|
1767
|
+
* charter's line between this phase and the next.
|
|
1768
|
+
*/
|
|
1769
|
+
#liveProjection(W, ctx, cap) {
|
|
1770
|
+
const rows = this.#project(W, ctx, ACT_SLOT_ROWS);
|
|
1771
|
+
if (cap === undefined || rows.length <= cap)
|
|
1772
|
+
return rows;
|
|
1773
|
+
// R4 — the slot gives way BEFORE any cell is force-committed.
|
|
1774
|
+
// A standing slot that could overflow the content cap would make
|
|
1775
|
+
// the force-commit loop push REAL cells into the scrollback to
|
|
1776
|
+
// relieve rows that are, at the bottom of the slot, blank padding.
|
|
1777
|
+
// So the slot shrinks first, in the pinned order slotPad already
|
|
1778
|
+
// implements (the pad rows are last, so they go first, then the
|
|
1779
|
+
// tail, then the heads beyond the first) and the floor is one row.
|
|
1780
|
+
return this.#project(W, ctx, Math.max(1, ACT_SLOT_ROWS - (rows.length - cap)));
|
|
1781
|
+
}
|
|
1782
|
+
/** R4 — one pass of the live projection at a given slot budget. */
|
|
1783
|
+
#project(W, ctx, budget) {
|
|
1784
|
+
const out = [];
|
|
1785
|
+
const focus = this.#focusIndex();
|
|
1786
|
+
const turn = this.#turns[this.#turns.length - 1];
|
|
1787
|
+
const open = turn !== undefined && !turn.ended ? (turn.segments[turn.segments.length - 1] ?? null) : null;
|
|
1788
|
+
const openSeg = open !== null && open.closedAt === null ? open : null;
|
|
1789
|
+
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1790
|
+
let stretchDrawn = false;
|
|
1791
|
+
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1792
|
+
const cell = this.#cells[i];
|
|
1793
|
+
const inOpen = openSeg !== null && openSeg.cells.includes(i);
|
|
1794
|
+
if (inOpen) {
|
|
1795
|
+
// R4 — the open stretch is ONE contiguous block: its line
|
|
1796
|
+
// plus the standing act slot, spaced once, at the segment's
|
|
1797
|
+
// first live cell. Every other cell of the segment draws
|
|
1798
|
+
// nothing; its work is counted on the line and its output,
|
|
1799
|
+
// if it is the current thing, is in the slot.
|
|
1800
|
+
//
|
|
1801
|
+
// R3i drew the line here and then let each cell decide for
|
|
1802
|
+
// itself whether it still had rows — which is why the
|
|
1803
|
+
// region's height moved between every pair of calls.
|
|
1804
|
+
if (stretchDrawn)
|
|
1805
|
+
continue;
|
|
1806
|
+
stretchDrawn = true;
|
|
1807
|
+
const rows = [
|
|
1808
|
+
...stretchLine({ ...this.#stretchTerms(openSeg), liveNames: this.#liveNames(openSeg), phase: this.#stretchPhase(openSeg), mark: twinkleFrame(this.#spinnerI) }, W),
|
|
1809
|
+
...this.#actSlot(openSeg, W, ctx, budget, focus),
|
|
1810
|
+
];
|
|
1811
|
+
out.push(...this.#space(i, prev, rows));
|
|
1812
|
+
prev = rows;
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
const rows = cellComponent(cell).render(W, ctx);
|
|
1816
|
+
// the head row carries the affordance; the tint lands on it and
|
|
1817
|
+
// nowhere else, which is what makes "exactly one" structural
|
|
1818
|
+
if (i === focus && rows.length > 0)
|
|
1819
|
+
rows[0] = focusToken(rows[0], W);
|
|
1820
|
+
out.push(...this.#space(i, prev, rows));
|
|
1821
|
+
prev = rows;
|
|
1822
|
+
}
|
|
1823
|
+
return out;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* R4 — the standing act slot's rows. EXACTLY `budget` rows in every
|
|
1827
|
+
* phase, so the live region's height changes twice per stretch (once
|
|
1828
|
+
* when it opens, once when it folds) instead of twice per call.
|
|
1829
|
+
*
|
|
1830
|
+
* The phases, in the order they are tested:
|
|
1831
|
+
* - an EXPANDED live cell outranks the slot (W15 — "the user asked
|
|
1832
|
+
* for it"): it renders in full, variable height. This is also
|
|
1833
|
+
* DC-28's cure: mid-stretch `ctrl+r` had a target it toggled and
|
|
1834
|
+
* never drew, so the press did nothing visible now and changed a
|
|
1835
|
+
* later expansion's shape;
|
|
1836
|
+
* - CALLS IN FLIGHT: one head row each within the budget, the tail
|
|
1837
|
+
* of the LAST head shown filling what is left, and the overflow
|
|
1838
|
+
* row inside the slot. The tail belongs to the last head by
|
|
1839
|
+
* construction — never call N's output under call N+1's header;
|
|
1840
|
+
* - the GAP between two calls: the call that just finished keeps its
|
|
1841
|
+
* settled head and its tail. This is the frame R3i collapsed, and
|
|
1842
|
+
* collapsing it is most of the jump;
|
|
1843
|
+
* - THINKING, before any call: the thinking's own tail (R3i ruling
|
|
1844
|
+
* 5, wired at last).
|
|
1845
|
+
*/
|
|
1846
|
+
#actSlot(seg, W, ctx, budget, focus) {
|
|
1847
|
+
const tint = (i, rows) => {
|
|
1848
|
+
if (i === focus && rows.length > 0)
|
|
1849
|
+
rows[0] = focusToken(rows[0], W);
|
|
1850
|
+
return rows;
|
|
1851
|
+
};
|
|
1852
|
+
const live = seg.cells.filter((i) => i >= this.#committed);
|
|
1853
|
+
const tools = [];
|
|
1854
|
+
for (const i of live)
|
|
1855
|
+
if (this.#cells[i]?.kind === "tool")
|
|
1856
|
+
tools.push(i);
|
|
1857
|
+
const toolAt = (i) => this.#cells[i];
|
|
1858
|
+
// An APPROVAL and an EXPANSION both outrank the slot, for the same
|
|
1859
|
+
// reason: their height is the human's business, not the renderer's.
|
|
1860
|
+
// W21 gives a pending approval the live region wholesale — its
|
|
1861
|
+
// diff is the thing being decided about, and a diff clamped to
|
|
1862
|
+
// four rows is a decision made on partial evidence. W15 gives an
|
|
1863
|
+
// expanded cell its full body — "the user asked for it". The slot
|
|
1864
|
+
// exists to stop the height moving ON ITS OWN; a height a human
|
|
1865
|
+
// asked for is not the oscillation it was built against.
|
|
1866
|
+
//
|
|
1867
|
+
// (The approval half is a regression this round caused and its
|
|
1868
|
+
// gate caught: the first draft treated a pending approval as a
|
|
1869
|
+
// call in flight, so `⏸ edit x.ts` lost its diff tail and the
|
|
1870
|
+
// `ctrl+r to expand` note with it.)
|
|
1871
|
+
const owned = tools.filter((i) => toolAt(i).expanded || toolAt(i).state === "approval");
|
|
1872
|
+
if (owned.length > 0) {
|
|
1873
|
+
// In CELL ORDER, so the frame reads the way the work happened:
|
|
1874
|
+
// an owned cell in full, every OTHER call still in flight
|
|
1875
|
+
// keeping its head row. An approval pausing one call must never
|
|
1876
|
+
// hide the others — the v2d parallel-frame gate caught exactly
|
|
1877
|
+
// that: with the shell running and asky_read at its panel, the
|
|
1878
|
+
// first draft returned the panel alone and the running shell's
|
|
1879
|
+
// `● shell sleep 1; echo hi · 1s` row vanished from the screen.
|
|
1880
|
+
const shown = tools.filter((i) => owned.includes(i) || !toolAt(i).done);
|
|
1881
|
+
const out = [];
|
|
1882
|
+
let heads = 0;
|
|
1883
|
+
for (const i of shown) {
|
|
1884
|
+
const rows = tint(i, cellComponent(this.#cells[i]).render(W, ctx));
|
|
1885
|
+
if (owned.includes(i)) {
|
|
1886
|
+
out.push(...rows);
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
if (heads >= LIVE_ACT_HEADS)
|
|
1890
|
+
continue;
|
|
1891
|
+
heads += 1;
|
|
1892
|
+
out.push(rows[0] ?? "");
|
|
1893
|
+
}
|
|
1894
|
+
const hidden = shown.length - owned.length - heads;
|
|
1895
|
+
if (hidden > 0)
|
|
1896
|
+
out.push(moreRunningRow(hidden, W));
|
|
1897
|
+
return out;
|
|
1898
|
+
}
|
|
1899
|
+
const flight = tools.filter((i) => !toolAt(i).done);
|
|
1900
|
+
if (flight.length > 0) {
|
|
1901
|
+
// the commonest frame — exactly one call, the full budget — is
|
|
1902
|
+
// the W8 block verbatim, which is what 0.17.0 already drew.
|
|
1903
|
+
if (flight.length === 1 && budget >= ACT_SLOT_ROWS)
|
|
1904
|
+
return slotPad(tint(flight[0], cellComponent(this.#cells[flight[0]]).render(W, ctx)), budget);
|
|
1905
|
+
const heads = flight.slice(0, Math.max(1, Math.min(flight.length, budget - 1, LIVE_ACT_HEADS)));
|
|
1906
|
+
const hidden = flight.length - heads.length;
|
|
1907
|
+
const rows = [];
|
|
1908
|
+
for (const i of heads)
|
|
1909
|
+
rows.push(tint(i, cellComponent(this.#cells[i]).render(W, ctx))[0] ?? "");
|
|
1910
|
+
const rest = budget - rows.length - (hidden > 0 ? 1 : 0);
|
|
1911
|
+
if (rest > 0)
|
|
1912
|
+
rows.push(...slotTail(toolAt(heads[heads.length - 1]).resultText, W, rest));
|
|
1913
|
+
if (hidden > 0)
|
|
1914
|
+
rows.push(moreRunningRow(hidden, W));
|
|
1915
|
+
return slotPad(rows, budget);
|
|
1916
|
+
}
|
|
1917
|
+
const settled = tools.length > 0 ? tools[tools.length - 1] : null;
|
|
1918
|
+
if (settled !== null) {
|
|
1919
|
+
const head = tint(settled, cellComponent(this.#cells[settled]).render(W, ctx))[0] ?? "";
|
|
1920
|
+
return slotPad([head, ...slotTail(toolAt(settled).resultText, W, budget - 1)], budget);
|
|
1921
|
+
}
|
|
1922
|
+
const think = [...live].reverse().find((i) => this.#cells[i]?.kind === "thinking");
|
|
1923
|
+
return slotPad(think === undefined ? [] : slotTail(this.#cells[think].text, W, budget), budget);
|
|
1924
|
+
}
|
|
1925
|
+
/** R4 — the tool names with a call still IN FLIGHT in this segment.
|
|
1926
|
+
* The stretch line's tense is per term, so a finished shell reads
|
|
1927
|
+
* `ran 1 shell command` while a read is still running. */
|
|
1928
|
+
#liveNames(seg) {
|
|
1929
|
+
const names = new Set();
|
|
1930
|
+
for (const i of seg.cells) {
|
|
1931
|
+
const c = this.#cells[i];
|
|
1932
|
+
if (c !== undefined && c.kind === "tool" && !c.done)
|
|
1933
|
+
names.add(c.name);
|
|
1934
|
+
}
|
|
1935
|
+
return [...names];
|
|
1936
|
+
}
|
|
1937
|
+
/** R3i — the open stretch's phase. It is THINKING while a thinking
|
|
1938
|
+
* cell of it is still open and no call has started; otherwise it is
|
|
1939
|
+
* ACTING. The tense follows the phase, and the phase is what the
|
|
1940
|
+
* human is watching happen. */
|
|
1941
|
+
#stretchPhase(seg) {
|
|
1942
|
+
return seg.thinkingSince !== null && seg.reads === 0 && seg.edits === 0 && seg.others.size === 0 ? "thinking" : "acting";
|
|
1943
|
+
}
|
|
1944
|
+
/** R3i — the open stretch's terms, in the shape the line renders. */
|
|
1945
|
+
#stretchTerms(seg) {
|
|
1946
|
+
const ms = seg.thinkingMs + (seg.thinkingSince === null ? 0 : Math.max(0, Date.now() - seg.thinkingSince));
|
|
1947
|
+
// R3i: a call in TROUBLE does not contribute to the work terms.
|
|
1948
|
+
// The counts are taken at toolStart, before the outcome is known,
|
|
1949
|
+
// so a denied write would otherwise fold as `wrote 1 file` beside
|
|
1950
|
+
// the clause admitting it was refused — the line saying, in one
|
|
1951
|
+
// breath, that the file was written and that it was not. The
|
|
1952
|
+
// trouble clause is where those calls are counted.
|
|
1953
|
+
const bad = new Map();
|
|
1954
|
+
for (const j of seg.cells) {
|
|
1955
|
+
const c = this.#cells[j];
|
|
1956
|
+
if (c === undefined || c.kind !== "tool" || !this.#cellInTrouble(j))
|
|
1957
|
+
continue;
|
|
1958
|
+
bad.set(c.name, (bad.get(c.name) ?? 0) + 1);
|
|
1959
|
+
}
|
|
1960
|
+
const net = (name, n) => Math.max(0, n - (bad.get(name) ?? 0));
|
|
1961
|
+
const calls = [];
|
|
1962
|
+
if (net("read_file", seg.reads) > 0)
|
|
1963
|
+
calls.push(["read_file", net("read_file", seg.reads)]);
|
|
1964
|
+
if (net("edit_file", seg.edits) > 0)
|
|
1965
|
+
calls.push(["edit_file", net("edit_file", seg.edits)]);
|
|
1966
|
+
for (const [name, n] of seg.others)
|
|
1967
|
+
if (net(name, n) > 0)
|
|
1968
|
+
calls.push([name, net(name, n)]);
|
|
1969
|
+
const targets = [];
|
|
1970
|
+
for (const j of seg.cells) {
|
|
1971
|
+
const c = this.#cells[j];
|
|
1972
|
+
if (c !== undefined && c.kind === "tool")
|
|
1973
|
+
targets.push(toolTarget(c.name, JSON.parse(c.inputFull)));
|
|
1974
|
+
}
|
|
1975
|
+
return { thoughtSeconds: Math.round(ms / 1000), calls, targets, trouble: this.#segmentTroubleTerms(seg) };
|
|
1976
|
+
}
|
|
1577
1977
|
/** The live region's scalar — the unit tests assert the cap directly
|
|
1578
1978
|
* (the e2e gate pins the screen consequence). W11: the formula's
|
|
1579
1979
|
* blanks are join artifacts — the count includes them (they are real
|
|
@@ -1603,16 +2003,25 @@ export class Body {
|
|
|
1603
2003
|
inputExtra +
|
|
1604
2004
|
queueRows.length);
|
|
1605
2005
|
}
|
|
2006
|
+
// DC-27 — the scalar measures the PROJECTION, not a second render
|
|
2007
|
+
// of its own. This loop used to walk every live cell and render it
|
|
2008
|
+
// in full: no open-segment collapse, no flight rule, no act-slot
|
|
2009
|
+
// budget. After R3i that described a screen the compositor had
|
|
2010
|
+
// stopped drawing — for an open stretch with five finished calls
|
|
2011
|
+
// it counted five four-row blocks that were not there. Nothing
|
|
2012
|
+
// broke, because the force-commit loop measures liveLines.length
|
|
2013
|
+
// and the over-count is conservative; but the cap and geometry
|
|
2014
|
+
// gates were asserting a property of a function nothing paints
|
|
2015
|
+
// from, so a real regression in the region's height could not
|
|
2016
|
+
// have moved them. The rule this file already states for the
|
|
2017
|
+
// sheet ("the scalar must say so, or the cap arithmetic disagrees
|
|
2018
|
+
// with the screen") is the same rule here.
|
|
1606
2019
|
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
|
|
1607
2020
|
const W = this.#opts.width();
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
lines += this.#space(i, prev, rows).length;
|
|
1613
|
-
prev = rows;
|
|
1614
|
-
}
|
|
1615
|
-
return lines + CHROME_ROWS + inputExtra + this.#menuRows(W).length + queueRows.length;
|
|
2021
|
+
// the SAME content cap the force-commit loop applies, so the
|
|
2022
|
+
// scalar sees the same slot budget the screen gets.
|
|
2023
|
+
const rows = this.#liveProjection(W, ctx, this.#opts.height() - 4 - inputExtra - queueRows.length);
|
|
2024
|
+
return rows.length + CHROME_ROWS + inputExtra + this.#menuRows(W).length + queueRows.length;
|
|
1616
2025
|
}
|
|
1617
2026
|
/** The lines committed THIS frame — the writes land in the frame's
|
|
1618
2027
|
* committed section (the rows just above the live region). */
|
|
@@ -1721,18 +2130,7 @@ export class Body {
|
|
|
1721
2130
|
// by construction), so the marker can never point at a cell the
|
|
1722
2131
|
// key would not take — which is the only way a focus marker is
|
|
1723
2132
|
// worth having.
|
|
1724
|
-
|
|
1725
|
-
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1726
|
-
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1727
|
-
const cell = this.#cells[i];
|
|
1728
|
-
const rows = cellComponent(cell).render(W, ctx);
|
|
1729
|
-
// the head row carries the affordance; the tint lands on it and
|
|
1730
|
-
// nowhere else, which is what makes "exactly one" structural
|
|
1731
|
-
if (i === focus && rows.length > 0)
|
|
1732
|
-
rows[0] = focusToken(rows[0], W);
|
|
1733
|
-
liveLines.push(...this.#space(i, prev, rows));
|
|
1734
|
-
prev = rows;
|
|
1735
|
-
}
|
|
2133
|
+
liveLines = this.#liveProjection(W, ctx, H - 4 - inputExtra - queueRows.length);
|
|
1736
2134
|
}
|
|
1737
2135
|
// 3. the FORCE commits — the live region's hard cap H−1: overflow
|
|
1738
2136
|
// commits the oldest live cell UNCONDITIONALLY (the one sharp
|
|
@@ -1751,21 +2149,9 @@ export class Body {
|
|
|
1751
2149
|
// visible above it.
|
|
1752
2150
|
this.#markSpilled(this.#committed);
|
|
1753
2151
|
this.#commitCell(this.#committed, W, ctx);
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
// pointed at may have just left the live region
|
|
1758
|
-
const focus = this.#focusIndex();
|
|
1759
|
-
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1760
|
-
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1761
|
-
const cell = this.#cells[i];
|
|
1762
|
-
const rows = cellComponent(cell).render(W, ctx);
|
|
1763
|
-
if (i === focus && rows.length > 0)
|
|
1764
|
-
rows[0] = focusToken(rows[0], W);
|
|
1765
|
-
liveLines.push(...this.#space(i, prev, rows));
|
|
1766
|
-
prev = rows;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
2152
|
+
// TUI2-R2 ⑤: the focus re-derives after a commit — the cell it
|
|
2153
|
+
// pointed at may have just left the live region.
|
|
2154
|
+
liveLines = this.#liveProjection(W, ctx, H - 4 - inputExtra - queueRows.length);
|
|
1769
2155
|
}
|
|
1770
2156
|
// 4. the geometry — the live region's first row:
|
|
1771
2157
|
// liveTop = min(totalCommitted, H - liveRows) + 1 — the screen
|
|
@@ -1905,7 +2291,29 @@ export class Body {
|
|
|
1905
2291
|
#space(i, prev, rows) {
|
|
1906
2292
|
if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
|
|
1907
2293
|
return rows;
|
|
1908
|
-
return bodySpacing(prev, rows);
|
|
2294
|
+
return bodySpacing(this.#lastDrawn(i, prev), rows);
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* R3i — the previous DRAWN sibling, not the previous cell.
|
|
2298
|
+
*
|
|
2299
|
+
* The spacing formula reads what stood above; a cell that rendered
|
|
2300
|
+
* nothing did not stand above anything. Since R3d whole families of
|
|
2301
|
+
* cells render `[]` — the members a fold speaks for — and the
|
|
2302
|
+
* formula was reading that empty array as "a zero-row sibling", so a
|
|
2303
|
+
* multi-row block following a fold lost the blank that belongs above
|
|
2304
|
+
* it. The defect predates this round (any folded turn followed by a
|
|
2305
|
+
* raw block has it); R3i's projection is what finally put a test on
|
|
2306
|
+
* the path.
|
|
2307
|
+
*/
|
|
2308
|
+
#lastDrawn(i, prev) {
|
|
2309
|
+
if (prev !== null && prev.length > 0)
|
|
2310
|
+
return prev;
|
|
2311
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
2312
|
+
const cached = this.#lineCache[j];
|
|
2313
|
+
if (cached !== null && cached !== undefined && cached.length > 0)
|
|
2314
|
+
return cached;
|
|
2315
|
+
}
|
|
2316
|
+
return prev;
|
|
1909
2317
|
}
|
|
1910
2318
|
/** Commit the cell at index i: render + cache its lines (immutable —
|
|
1911
2319
|
* the force-committed form freezes at the current render), advance
|
|
@@ -1962,6 +2370,67 @@ export class Body {
|
|
|
1962
2370
|
* denial keeps all twenty rows. The alternative is a screen that says
|
|
1963
2371
|
* `✦ thought 3s · 20 reads` while a write was refused inside it.
|
|
1964
2372
|
*/
|
|
2373
|
+
/**
|
|
2374
|
+
* R3i — the trouble the stretch met, as the line's own terms.
|
|
2375
|
+
*
|
|
2376
|
+
* Law 1.3: an outcome is stated in WORDS, "the only form that
|
|
2377
|
+
* survives a pipe". So the kind is a different word, never a
|
|
2378
|
+
* different colour — `2 failed`, `1 denied`, `1 interrupted` — and
|
|
2379
|
+
* the failure's identity rides with it. In this phase the terms are
|
|
2380
|
+
* only DRAWN (the live line names trouble the moment it happens);
|
|
2381
|
+
* whether trouble still blocks the fold is the next phase's ruling.
|
|
2382
|
+
*/
|
|
2383
|
+
/** R3i — is this cell one the fold must not count as work done? */
|
|
2384
|
+
#cellInTrouble(i) {
|
|
2385
|
+
const c = this.#cells[i];
|
|
2386
|
+
if (c === undefined || c.kind !== "tool")
|
|
2387
|
+
return false;
|
|
2388
|
+
return c.isError || c.reason !== null || c.verdict?.decision === "denied";
|
|
2389
|
+
}
|
|
2390
|
+
#segmentTroubleTerms(seg) {
|
|
2391
|
+
let failed = 0;
|
|
2392
|
+
let denied = 0;
|
|
2393
|
+
let interrupted = 0;
|
|
2394
|
+
let what = "";
|
|
2395
|
+
for (const j of seg.cells) {
|
|
2396
|
+
const c = this.#cells[j];
|
|
2397
|
+
if (c === undefined || c.kind !== "tool")
|
|
2398
|
+
continue;
|
|
2399
|
+
// WHICH call, and WHY. The target alone answers the first and
|
|
2400
|
+
// not the second, and for a policy denial the second is the
|
|
2401
|
+
// whole point: `sub/out.txt` does not tell a human that plan
|
|
2402
|
+
// mode is read-only, and that sentence is the one they act
|
|
2403
|
+
// on. Law 1.3's own words — an outcome is stated in words —
|
|
2404
|
+
// and the ladder cuts this clause last, so it degrades to the
|
|
2405
|
+
// target before it disappears.
|
|
2406
|
+
const named = () => {
|
|
2407
|
+
const t = toolTarget(c.name, JSON.parse(c.inputFull));
|
|
2408
|
+
const why = c.verdict?.reason ?? c.reason;
|
|
2409
|
+
return why === null || why === undefined || why === "" || why === "interrupted" ? t : `${t} (${why})`;
|
|
2410
|
+
};
|
|
2411
|
+
if (c.verdict?.decision === "denied" || (c.reason !== null && c.reason !== "interrupted" && /denied/i.test(c.reason))) {
|
|
2412
|
+
denied += 1;
|
|
2413
|
+
if (what === "")
|
|
2414
|
+
what = named();
|
|
2415
|
+
}
|
|
2416
|
+
else if (c.reason === "interrupted") {
|
|
2417
|
+
interrupted += 1;
|
|
2418
|
+
}
|
|
2419
|
+
else if (c.isError || c.reason !== null) {
|
|
2420
|
+
failed += 1;
|
|
2421
|
+
if (what === "")
|
|
2422
|
+
what = named();
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
const out = [];
|
|
2426
|
+
if (failed > 0)
|
|
2427
|
+
out.push(["failed", failed, what]);
|
|
2428
|
+
if (denied > 0)
|
|
2429
|
+
out.push(["denied", denied, what]);
|
|
2430
|
+
if (interrupted > 0)
|
|
2431
|
+
out.push(["interrupted", interrupted, ""]);
|
|
2432
|
+
return out;
|
|
2433
|
+
}
|
|
1965
2434
|
#segmentHasTrouble(seg) {
|
|
1966
2435
|
// R3g (fable, 2026-08-28): a DENIED call is the case this rule
|
|
1967
2436
|
// exists for, and it was the one case the predicate could not
|
|
@@ -2030,6 +2499,11 @@ export class Body {
|
|
|
2030
2499
|
const cell = this.#cells[i];
|
|
2031
2500
|
if (cell.kind !== "thinking" && cell.kind !== "tool")
|
|
2032
2501
|
return false;
|
|
2502
|
+
// R3i phase 5: an answered question is WORDS (law 1.7). It commits
|
|
2503
|
+
// when it is done, like prose, and is never held for a fold that
|
|
2504
|
+
// is not going to speak for it.
|
|
2505
|
+
if (cell.kind === "tool" && cell.name === "ask_user")
|
|
2506
|
+
return false;
|
|
2033
2507
|
const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
2034
2508
|
if (turn === undefined || turn !== this.#turns[this.#turns.length - 1])
|
|
2035
2509
|
return false;
|
|
@@ -2052,15 +2526,27 @@ export class Body {
|
|
|
2052
2526
|
// The quiet turn is the same rule seen from one side: its single
|
|
2053
2527
|
// segment never closes until the settle, so it holds exactly as
|
|
2054
2528
|
// it always did.
|
|
2055
|
-
//
|
|
2056
|
-
//
|
|
2057
|
-
//
|
|
2058
|
-
//
|
|
2529
|
+
// DECLARED SUPERSESSION (R3i phase 3, owner-ruled) — THE HOLD IS
|
|
2530
|
+
// THE SEGMENT'S AGAIN.
|
|
2531
|
+
//
|
|
2532
|
+
// R3d had made it the TURN's, so a turn's whole work waited on the
|
|
2533
|
+
// settle and every one of its counts then landed on ONE line above
|
|
2534
|
+
// all of its prose. The shape the owner asked for is one summary
|
|
2535
|
+
// per stretch, standing with the prose that stretch led to — which
|
|
2536
|
+
// requires a stretch to commit when its own text arrives.
|
|
2537
|
+
//
|
|
2538
|
+
// R3d's stated reason for leaving the segment was R3b's disease (a
|
|
2539
|
+
// chatty model turning every call into its own `✦ thought 2s ·
|
|
2540
|
+
// 1 read` row); the cures are the two rules R3b never had — a fold
|
|
2541
|
+
// must absorb at least two rows, and a stretch of exactly one call
|
|
2542
|
+
// names its TARGET rather than its count.
|
|
2543
|
+
//
|
|
2544
|
+
// The force-commit path still overrides this (a stretch too big for
|
|
2059
2545
|
// the screen spills and renders normally); that is the honest
|
|
2060
2546
|
// degradation, marked `spilled`.
|
|
2061
2547
|
const seg = this.#segmentOf(i);
|
|
2062
2548
|
if (seg !== null)
|
|
2063
|
-
return
|
|
2549
|
+
return seg.closedAt === null;
|
|
2064
2550
|
// no segment (the pipe path's shape) — W14's original test, kept
|
|
2065
2551
|
// so a cell that never got a segment behaves as it used to.
|
|
2066
2552
|
if (!turn.ended && !turn.hasText)
|
|
@@ -2154,45 +2640,50 @@ export class Body {
|
|
|
2154
2640
|
// The quiet turn keeps its fold because there IS no recap line
|
|
2155
2641
|
// to carry it: a turn with no text is the fold, and W14's gates
|
|
2156
2642
|
// pin that shape.
|
|
2157
|
-
|
|
2158
|
-
|
|
2643
|
+
// DECLARED SUPERSESSION (R3i phase 3, owner-ruled) — THE FOLD IS
|
|
2644
|
+
// THE SEGMENT'S, AND TROUBLE DOES NOT STOP IT.
|
|
2645
|
+
//
|
|
2646
|
+
// ① the SEGMENT. R3d folded the turn; the owner's shape is one
|
|
2647
|
+
// summary per stretch of work, standing with the prose that
|
|
2648
|
+
// stretch led to. R3b's disease is answered by the two rules
|
|
2649
|
+
// below rather than by leaving the segment: a fold must
|
|
2650
|
+
// absorb at least TWO rows, and a stretch of exactly one
|
|
2651
|
+
// call names its target (see stretchTerms).
|
|
2652
|
+
//
|
|
2653
|
+
// ② TROUBLE. R3b refused to fold any run holding a failure,
|
|
2654
|
+
// and R3g extended that to interrupts. Law 1.3 governs
|
|
2655
|
+
// marks versus WORDS and never granted a failure a
|
|
2656
|
+
// permanent row; law 1.7 says "Work folds, words do not".
|
|
2657
|
+
// So the work folds and the outcome words ride the line —
|
|
2658
|
+
// `1 denied: .env` — and the human sees, without pressing
|
|
2659
|
+
// anything, that trouble happened, on which call, and what
|
|
2660
|
+
// happened. The stderr is behind the key, because it is
|
|
2661
|
+
// detail, not outcome. The cost R3b priced as rare measured
|
|
2662
|
+
// at 2 failures in 28 calls in the 0.16.7 dogfood, with
|
|
2663
|
+
// zero folds as the result.
|
|
2664
|
+
if (turn !== undefined && seg !== null && seg.closedAt !== null && !seg.spilled && seg.cells.length >= 2) {
|
|
2665
|
+
if (!seg.folded) {
|
|
2159
2666
|
seg.folded = true;
|
|
2160
2667
|
seg.headCell = i;
|
|
2161
2668
|
turn.folded = true;
|
|
2162
|
-
//
|
|
2163
|
-
//
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
// (
|
|
2169
|
-
//
|
|
2170
|
-
const quiet = turn.ended && !turn.hasText;
|
|
2171
|
-
// R3g (fable, 2026-08-28) — DECLARED SUPERSESSION: both
|
|
2172
|
-
// branches read the SAME number now, `thoughtSeconds`,
|
|
2173
|
-
// the measure the kernel took and handed to endTurn.
|
|
2174
|
-
// The non-quiet branch used to re-derive a wall clock
|
|
2175
|
-
// from the segment's opening and print it under the word
|
|
2176
|
-
// "thought" — a different quantity wearing the same
|
|
2177
|
-
// label: a turn that thought 1s and then ran a 40s shell
|
|
2178
|
-
// said "thought 41s". The fold only ever renders after
|
|
2179
|
-
// endTurn (the gate below requires `turn.ended`), so the
|
|
2180
|
-
// honest number is always available by the time it runs.
|
|
2669
|
+
// R4 (C1): the ordinal is assigned HERE, before the row
|
|
2670
|
+
// is rendered, because the settled line always carries
|
|
2671
|
+
// the key — so the number is known without guessing
|
|
2672
|
+
// whether the row will earn an affordance.
|
|
2673
|
+
this.#foldSeq += 1;
|
|
2674
|
+
seg.foldKey = this.#foldSeq;
|
|
2675
|
+
// DECLARED SUPERSESSION (R3i phase 3) — A9 NARROWS: the
|
|
2676
|
+
// fold carries WORK, never the human's words.
|
|
2181
2677
|
//
|
|
2182
|
-
//
|
|
2183
|
-
//
|
|
2184
|
-
//
|
|
2185
|
-
//
|
|
2186
|
-
//
|
|
2187
|
-
//
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
thoughtSeconds: seconds,
|
|
2192
|
-
reads: turn.reads,
|
|
2193
|
-
edits: turn.edits,
|
|
2194
|
-
others: [...turn.others],
|
|
2195
|
-
}, W);
|
|
2678
|
+
// A9 put the user's words on the fold as the SGR-7 chip.
|
|
2679
|
+
// Measured under R3i, that prints them twice: the chip
|
|
2680
|
+
// BAND commits on the frame it is pushed (it always has —
|
|
2681
|
+
// `#held` exempts non-thinking/tool cells), so a quiet
|
|
2682
|
+
// turn showed ` x ` on its own row and ` x ` again inside
|
|
2683
|
+
// the fold directly beneath it. The band is the record of
|
|
2684
|
+
// what was asked; this line is the record of what was
|
|
2685
|
+
// done. One fact, one row, each.
|
|
2686
|
+
return stretchLine({ ...this.#stretchTerms(seg), phase: "settled", foldKey: seg.foldKey }, W);
|
|
2196
2687
|
}
|
|
2197
2688
|
return [];
|
|
2198
2689
|
}
|
package/dist/editor.js
CHANGED
|
@@ -61,6 +61,9 @@ export const MENU_ITEMS = [
|
|
|
61
61
|
{ name: "/resume", desc: "switch to another session; /resume <id> goes directly" },
|
|
62
62
|
{ name: "/think", desc: "show the last full thinking block" },
|
|
63
63
|
{ name: "/last", desc: "show the most recent tool call's input and output" },
|
|
64
|
+
// R4 (C4d): the committed transcript belongs to the terminal and can
|
|
65
|
+
// never be re-wrapped in place (ADR-0046); this appends it re-folded.
|
|
66
|
+
{ name: "/rewrap", desc: "re-print the recent prose at the current width" },
|
|
64
67
|
{ name: "/status", desc: "show session id, event count, and context estimate" },
|
|
65
68
|
// TUI2-R1 (E): the rent-ledger attribution — where the context went
|
|
66
69
|
{ name: "/context", desc: "show where the context went — the last request's rent ledger" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "kiso tui — the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.18.0"
|
|
39
39
|
}
|
|
40
40
|
}
|