@vincemakes/kiso-tui 0.19.0 → 0.20.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 +33 -2
- package/dist/ask-panel.js +89 -7
- package/dist/compositor.js +297 -39
- package/package.json +2 -2
package/dist/ask-panel.d.ts
CHANGED
|
@@ -73,8 +73,39 @@ export declare function askDescriptionStop(q: AskQuestion, W: number): number;
|
|
|
73
73
|
* type-your-own line last. R2: the shape is the RULE's — rule,
|
|
74
74
|
* title, header, body, affordance, rule. */
|
|
75
75
|
export declare function askBlockRows(view: PanelView, state: AskRuntime, W: number, maxRows: number): string[];
|
|
76
|
-
/**
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* R6/D2 — the affordance names the FINISHER, and it names the real keys.
|
|
78
|
+
*
|
|
79
|
+
* Three defects lived in the one line this replaces:
|
|
80
|
+
* - it never mentioned ENTER, on the one panel shape where enter is the
|
|
81
|
+
* only way to finish. The owner could not find the finisher because
|
|
82
|
+
* the screen did not name it;
|
|
83
|
+
* - it said the same words in both modes, while a digit means two
|
|
84
|
+
* different things across them (take vs mark);
|
|
85
|
+
* - it hardcoded "1-4" whatever the option count was — the same defect
|
|
86
|
+
* REL-0152-D3 fixed in the input LEAD one function up, and missed
|
|
87
|
+
* here.
|
|
88
|
+
*
|
|
89
|
+
* The clauses are returned whole; `askAffordanceFit` below is what a
|
|
90
|
+
* width-bound caller uses.
|
|
91
|
+
*/
|
|
92
|
+
export declare function askAffordance(state: AskRuntime, q?: AskQuestion): string;
|
|
93
|
+
/**
|
|
94
|
+
* The affordance at a width, by the DC-2 clause ladder.
|
|
95
|
+
*
|
|
96
|
+
* The row this serves used to be pushed UNCUT into the panel block
|
|
97
|
+
* (`askBlockRows`), unlike the approval's cutLine'd row — and panel rows
|
|
98
|
+
* go through `#checked`, which THROWS on any row wider than W
|
|
99
|
+
* (invariant ①). A multi-question ask on question 2 at W ≤ 41, or any
|
|
100
|
+
* ask at W ≤ 32, killed the renderer. So the ladder is not polish; it is
|
|
101
|
+
* the fix for a crash.
|
|
102
|
+
*
|
|
103
|
+
* The order gives way from the least load-bearing inward, and the LAST
|
|
104
|
+
* standing pair in both modes is the way through and the way out. In
|
|
105
|
+
* multi, `⏎ sends` is the fact whose absence caused this whole finding:
|
|
106
|
+
* it gives way never.
|
|
107
|
+
*/
|
|
108
|
+
export declare function askAffordanceFit(state: AskRuntime, q: AskQuestion | undefined, W: number): string;
|
|
78
109
|
/** The status row's left text — the ask's own line, with the walk. */
|
|
79
110
|
export declare function askStatus(view: PanelView, state: AskRuntime): string;
|
|
80
111
|
/** The input row's lead: the digit lead while picking, the typing lead
|
package/dist/ask-panel.js
CHANGED
|
@@ -149,7 +149,27 @@ export function askKey(spec, state, key) {
|
|
|
149
149
|
// row's neighbours answer with.
|
|
150
150
|
if (state.cursor === customRow(q))
|
|
151
151
|
return { state: { ...state, phase: "custom" } };
|
|
152
|
-
|
|
152
|
+
if (answered(state, state.qIndex))
|
|
153
|
+
return advance(spec, state);
|
|
154
|
+
// R6/D2 — ENTER TAKES THE BAR'S ROW.
|
|
155
|
+
//
|
|
156
|
+
// It used to return `{ state }` here: the cursor is not a pick, so
|
|
157
|
+
// bar-on-option-1 + enter did NOTHING, in either mode, with no
|
|
158
|
+
// message. The approval panel ruled the opposite in TUI2-R3v2 ①
|
|
159
|
+
// ("the panel used to ignore the key every human presses first"),
|
|
160
|
+
// so the product had two selection models under one identical
|
|
161
|
+
// bar — and the owner's dogfood is that trap verbatim: bar to
|
|
162
|
+
// option 1, enter (silence), "do I really have to type 1?", digit
|
|
163
|
+
// (a mark appears), enter (it commits).
|
|
164
|
+
//
|
|
165
|
+
// Single-select takes the row and advances, which is what the bar
|
|
166
|
+
// has always promised. Multi MARKS the row and stays: the press
|
|
167
|
+
// becomes visible (◯ → ◉), which teaches the mode in one frame,
|
|
168
|
+
// and the second enter sends the set. A press that "did nothing"
|
|
169
|
+
// is the defect; a press that does the smallest true thing is the
|
|
170
|
+
// fix.
|
|
171
|
+
const taken = toggle(state, state.cursor, multi);
|
|
172
|
+
return multi ? { state: taken } : advance(spec, taken);
|
|
153
173
|
}
|
|
154
174
|
// SPACE selects at the cursor and NEVER commits — in either mode. It
|
|
155
175
|
// used to answer-and-advance a single-select question, which made a
|
|
@@ -270,7 +290,10 @@ export function askBlockRows(view, state, W, maxRows) {
|
|
|
270
290
|
// here), but the multi-select gesture it carried is INFORMATION and
|
|
271
291
|
// rides the header instead — dropping it would have been a regression
|
|
272
292
|
// wearing a restyle's clothes.
|
|
273
|
-
|
|
293
|
+
// R6/D2: the header names the MODE; the keys live on the keys row,
|
|
294
|
+
// once. It used to name `space toggles` there and nowhere say what
|
|
295
|
+
// finishes the set.
|
|
296
|
+
const gesture = multi ? `${p.dim} · pick any${p.reset}` : "";
|
|
274
297
|
rows.push(` ${cutLine(`${p.dim}${header}${p.reset}${gesture}`, Math.max(1, W - 2))}`);
|
|
275
298
|
rows.push("");
|
|
276
299
|
const picks = state.picks[state.qIndex] ?? [];
|
|
@@ -311,7 +334,9 @@ export function askBlockRows(view, state, W, maxRows) {
|
|
|
311
334
|
else {
|
|
312
335
|
rows.push(...body);
|
|
313
336
|
}
|
|
314
|
-
|
|
337
|
+
// R6/D2: FITTED, and the two-space indent is inside the budget —
|
|
338
|
+
// the uncut push here is what made a narrow ask throw.
|
|
339
|
+
rows.push(` ${p.dim}${askAffordanceFit(state, q, Math.max(1, W - 2))}${p.reset}`);
|
|
315
340
|
// R2, shared with the approval panel: the block closes with the SAME
|
|
316
341
|
// dashed rule it opened with, and the same one the composer uses.
|
|
317
342
|
// TUI2-R1.5 ⑪ had already replaced a two-cell `\u2514 ` stub with a
|
|
@@ -320,11 +345,68 @@ export function askBlockRows(view, state, W, maxRows) {
|
|
|
320
345
|
rows.push(`${p.dim}${"\u2500".repeat(Math.max(0, W))}${p.reset}`);
|
|
321
346
|
return rows;
|
|
322
347
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
348
|
+
/**
|
|
349
|
+
* R6/D2 — the affordance names the FINISHER, and it names the real keys.
|
|
350
|
+
*
|
|
351
|
+
* Three defects lived in the one line this replaces:
|
|
352
|
+
* - it never mentioned ENTER, on the one panel shape where enter is the
|
|
353
|
+
* only way to finish. The owner could not find the finisher because
|
|
354
|
+
* the screen did not name it;
|
|
355
|
+
* - it said the same words in both modes, while a digit means two
|
|
356
|
+
* different things across them (take vs mark);
|
|
357
|
+
* - it hardcoded "1-4" whatever the option count was — the same defect
|
|
358
|
+
* REL-0152-D3 fixed in the input LEAD one function up, and missed
|
|
359
|
+
* here.
|
|
360
|
+
*
|
|
361
|
+
* The clauses are returned whole; `askAffordanceFit` below is what a
|
|
362
|
+
* width-bound caller uses.
|
|
363
|
+
*/
|
|
364
|
+
export function askAffordance(state, q) {
|
|
325
365
|
if (state.phase === "custom")
|
|
326
366
|
return "enter answers · esc backs out";
|
|
327
|
-
|
|
367
|
+
const n = q?.options.length ?? 4;
|
|
368
|
+
const multi = q?.multiSelect === true;
|
|
369
|
+
const back = state.qIndex > 0 ? ["← back"] : [];
|
|
370
|
+
return (multi
|
|
371
|
+
? ["↑↓ move", `space or 1-${n} marks`, "⏎ sends the set", "t types", ...back, "esc declines"]
|
|
372
|
+
: ["↑↓ move", "⏎ confirms", `1-${n} instant`, "t types", ...back, "esc declines"]).join(" · ");
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The affordance at a width, by the DC-2 clause ladder.
|
|
376
|
+
*
|
|
377
|
+
* The row this serves used to be pushed UNCUT into the panel block
|
|
378
|
+
* (`askBlockRows`), unlike the approval's cutLine'd row — and panel rows
|
|
379
|
+
* go through `#checked`, which THROWS on any row wider than W
|
|
380
|
+
* (invariant ①). A multi-question ask on question 2 at W ≤ 41, or any
|
|
381
|
+
* ask at W ≤ 32, killed the renderer. So the ladder is not polish; it is
|
|
382
|
+
* the fix for a crash.
|
|
383
|
+
*
|
|
384
|
+
* The order gives way from the least load-bearing inward, and the LAST
|
|
385
|
+
* standing pair in both modes is the way through and the way out. In
|
|
386
|
+
* multi, `⏎ sends` is the fact whose absence caused this whole finding:
|
|
387
|
+
* it gives way never.
|
|
388
|
+
*/
|
|
389
|
+
export function askAffordanceFit(state, q, W) {
|
|
390
|
+
if (state.phase === "custom")
|
|
391
|
+
return cutLine("enter answers · esc backs out", W);
|
|
392
|
+
const n = q?.options.length ?? 4;
|
|
393
|
+
const multi = q?.multiSelect === true;
|
|
394
|
+
const back = state.qIndex > 0 ? ["← back"] : [];
|
|
395
|
+
const act = multi ? `space or 1-${n} marks` : `1-${n} instant`;
|
|
396
|
+
const go = multi ? "⏎ sends the set" : "⏎ confirms";
|
|
397
|
+
const tiers = [
|
|
398
|
+
["↑↓ move", act, go, "t types", ...back, "esc declines"],
|
|
399
|
+
["↑↓ move", act, go, ...back, "esc declines"],
|
|
400
|
+
[act, go, ...back, "esc declines"],
|
|
401
|
+
[multi ? "space marks" : `1-${n} picks`, multi ? "⏎ sends" : "⏎ confirms", "esc declines"],
|
|
402
|
+
[multi ? "⏎ sends" : "⏎ confirms", "esc declines"],
|
|
403
|
+
];
|
|
404
|
+
for (const tier of tiers) {
|
|
405
|
+
const row = tier.join(" · ");
|
|
406
|
+
if (visibleWidth(row) <= W)
|
|
407
|
+
return row;
|
|
408
|
+
}
|
|
409
|
+
return cutLine(tiers[tiers.length - 1].join(" · "), W);
|
|
328
410
|
}
|
|
329
411
|
/** The status row's left text — the ask's own line, with the walk. */
|
|
330
412
|
export function askStatus(view, state) {
|
|
@@ -380,7 +462,7 @@ export function panelAffordance(view, phase, cursor, ask, pick, safer) {
|
|
|
380
462
|
if (view.pick !== undefined && pick !== undefined)
|
|
381
463
|
return pickAffordance(pick);
|
|
382
464
|
if (view.ask !== undefined && ask !== undefined)
|
|
383
|
-
return askAffordance(ask);
|
|
465
|
+
return askAffordance(ask, view.ask.questions[ask.qIndex]);
|
|
384
466
|
return basePanelAffordance(view, phase, cursor, safer);
|
|
385
467
|
}
|
|
386
468
|
/** The whole panel state in one call — the compositor's four reads share
|
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 { ACT_SLOT_ROWS, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, cutLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, stretchLine, turnFold, visibleWidth,
|
|
55
|
+
import { ACT_SLOT_ROWS, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, cutLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, stretchLine, turnFold, visibleWidth, breathFrame, } 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
|
// R5 — the transcript viewer's PURE projection. The compositor supplies
|
|
@@ -476,7 +476,7 @@ export class Body {
|
|
|
476
476
|
// W14: the turn boundary — the record the fold-hold's release
|
|
477
477
|
// state machine reads; the cell carries the record's index. A9:
|
|
478
478
|
// the user's own words ride the record — the fold's leading chip.
|
|
479
|
-
this.#turns.push({ ended: false, hasText: false, thoughtSeconds: 0, reads: 0, edits: 0, others: new Map(), seen: new Map(), words: text, folded: false, segments: [] });
|
|
479
|
+
this.#turns.push({ ended: false, hasText: false, begun: false, thoughtSeconds: 0, reads: 0, edits: 0, others: new Map(), seen: new Map(), words: text, folded: false, segments: [] });
|
|
480
480
|
this.#cells.push({ kind: "user", text, done: true, turn: this.#turns.length - 1 });
|
|
481
481
|
this.#mark();
|
|
482
482
|
}
|
|
@@ -491,21 +491,35 @@ export class Body {
|
|
|
491
491
|
}
|
|
492
492
|
else {
|
|
493
493
|
this.#cells.push({ kind: "thinking", text, done: false, turn: this.#turns.length - 1 });
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
// second one's clock starts here rather than at a tool call it
|
|
497
|
-
// may never make.
|
|
494
|
+
// DECLARED SUPERSESSION (R7, owner-ruled 2026-08-31) — THINKING
|
|
495
|
+
// IS WORDS, NOT WORK.
|
|
498
496
|
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
497
|
+
// R3b made thinking open a segment, on the reading that it is
|
|
498
|
+
// work like a tool call. Four rounds of consequences followed
|
|
499
|
+
// from that one classification: folded away with the calls, it
|
|
500
|
+
// became unreachable, and R4's printed ordinal, R5's viewer,
|
|
501
|
+
// R6's subject index and a look-back viewport were each built
|
|
502
|
+
// to hand it back. The owner's ruling is to stop hiding it —
|
|
503
|
+
// and then none of those mechanisms is answering a question
|
|
504
|
+
// anyone still asks.
|
|
505
|
+
//
|
|
506
|
+
// So thinking CLOSES the open segment, exactly as text does
|
|
507
|
+
// (see textAppend): a segment is what sits between two of
|
|
508
|
+
// these. It must close rather than merely not-open, because
|
|
509
|
+
// `#committed` is a PREFIX count — a thinking cell cannot
|
|
510
|
+
// commit past a held call, so think → call → think would
|
|
511
|
+
// otherwise flush at the segment's close with the second
|
|
512
|
+
// thought printing BELOW the fold that contains the later
|
|
513
|
+
// call.
|
|
514
|
+
//
|
|
515
|
+
// Consequence, and it is wanted: the segment's thinking clock
|
|
516
|
+
// never starts, so `thought Ns` drops off every fold line by
|
|
517
|
+
// R3h's own zero-term rule. The line stops claiming a fact the
|
|
518
|
+
// paragraph above it already states in full.
|
|
519
|
+
const t0 = this.#turns[this.#turns.length - 1];
|
|
520
|
+
closeSegment(t0, Date.now());
|
|
521
|
+
if (t0 !== undefined)
|
|
522
|
+
t0.begun = true; // R6/D1: the block allocates here
|
|
509
523
|
// R3i: and the beat starts HERE. Law 1.4 says "a running thought
|
|
510
524
|
// twinkles", and `#armSpinner`'s own predicate has always
|
|
511
525
|
// included an open thinking cell — but the only caller was
|
|
@@ -514,7 +528,6 @@ export class Body {
|
|
|
514
528
|
// derivation, so without the beat they also never ticked: the
|
|
515
529
|
// row read `thinking 0s` for as long as the model thought.
|
|
516
530
|
this.#armSpinner();
|
|
517
|
-
this.#stampSegment();
|
|
518
531
|
}
|
|
519
532
|
this.#mark();
|
|
520
533
|
}
|
|
@@ -630,6 +643,9 @@ export class Body {
|
|
|
630
643
|
}
|
|
631
644
|
}
|
|
632
645
|
this.#stampSegment();
|
|
646
|
+
const t1 = this.#turns[this.#turns.length - 1];
|
|
647
|
+
if (t1 !== undefined)
|
|
648
|
+
t1.begun = true; // R6/D1: the block allocates here
|
|
633
649
|
this.#mark();
|
|
634
650
|
}
|
|
635
651
|
toolApproval(callId, diff) {
|
|
@@ -1934,8 +1950,10 @@ export class Body {
|
|
|
1934
1950
|
const openSeg = open !== null && open.closedAt === null ? open : null;
|
|
1935
1951
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1936
1952
|
let stretchDrawn = false;
|
|
1953
|
+
let lastIdx = this.#committed;
|
|
1937
1954
|
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1938
1955
|
const cell = this.#cells[i];
|
|
1956
|
+
lastIdx = i;
|
|
1939
1957
|
const inOpen = openSeg !== null && openSeg.cells.includes(i);
|
|
1940
1958
|
if (inOpen) {
|
|
1941
1959
|
// R4 — the open stretch is ONE contiguous block: its line
|
|
@@ -1950,11 +1968,35 @@ export class Body {
|
|
|
1950
1968
|
if (stretchDrawn)
|
|
1951
1969
|
continue;
|
|
1952
1970
|
stretchDrawn = true;
|
|
1971
|
+
// R7a — a ONE-CELL stretch draws NO line.
|
|
1972
|
+
//
|
|
1973
|
+
// It said `running 1 shell command` directly above a row
|
|
1974
|
+
// reading `● shell npm run check`: the same fact twice, and
|
|
1975
|
+
// fable's R4 review had already named the duplication. R7
|
|
1976
|
+
// then made it a SWALLOW as well — a one-cell segment does
|
|
1977
|
+
// not fold, so that line has no committed counterpart and
|
|
1978
|
+
// vanished at the settle, taking a row off the screen. The
|
|
1979
|
+
// call's own head row is the line; a summary of one thing
|
|
1980
|
+
// is the thing.
|
|
1981
|
+
const single = openSeg.cells.filter((j) => this.#cells[j]?.kind === "tool").length <= 1;
|
|
1953
1982
|
const rows = [
|
|
1954
|
-
...
|
|
1983
|
+
...(single
|
|
1984
|
+
? []
|
|
1985
|
+
: stretchLine({ ...this.#stretchTerms(openSeg), liveNames: this.#liveNames(openSeg), phase: this.#stretchPhase(openSeg), ...(this.#inFlight(openSeg) ? { mark: breathFrame(this.#spinnerI) } : {}) }, W)),
|
|
1955
1986
|
...this.#actSlot(openSeg, W, ctx, budget, focus),
|
|
1956
1987
|
];
|
|
1957
|
-
|
|
1988
|
+
// R7a — the block TAKES the W11 blank, like everything else.
|
|
1989
|
+
//
|
|
1990
|
+
// CORRECTION of my own R6/D1 change, which removed it. The
|
|
1991
|
+
// reasoning then was that the committed fold is a single row
|
|
1992
|
+
// and single rows take no blank — but `bodySpacing` gives a
|
|
1993
|
+
// blank after any MULTI-row sibling, and the thinking block
|
|
1994
|
+
// above the work is exactly that. So the committed side had
|
|
1995
|
+
// one and the live side did not, and a blank row APPEARED at
|
|
1996
|
+
// every settle, shoving everything below it down. The owner
|
|
1997
|
+
// saw it and said the blank is the correct form; it is, and
|
|
1998
|
+
// the fix is to have it on both sides rather than neither.
|
|
1999
|
+
out.push(...this.#blockSpace(i, prev, rows));
|
|
1958
2000
|
prev = rows;
|
|
1959
2001
|
continue;
|
|
1960
2002
|
}
|
|
@@ -1966,8 +2008,52 @@ export class Body {
|
|
|
1966
2008
|
out.push(...this.#space(i, prev, rows));
|
|
1967
2009
|
prev = rows;
|
|
1968
2010
|
}
|
|
2011
|
+
// R6/D1 — THE BLOCK STANDS FOR THE TURN, not for the stretch.
|
|
2012
|
+
//
|
|
2013
|
+
// R4 made the block's height constant WITHIN a stretch. Between
|
|
2014
|
+
// two stretches it was released and rebuilt, so the live region
|
|
2015
|
+
// breathed by its whole height twice per stretch — and the live
|
|
2016
|
+
// region is anchored to the bottom (liveTop below), so every
|
|
2017
|
+
// committed row on screen moved with it. The owner's report, and
|
|
2018
|
+
// their own formulation of the cure: "once something is at a line
|
|
2019
|
+
// it should not jump up or down — hold the absolute position and
|
|
2020
|
+
// update the content there."
|
|
2021
|
+
//
|
|
2022
|
+
// So when the turn has begun work and no open stretch drew the
|
|
2023
|
+
// block above, it is drawn HERE, after the live cells, with its
|
|
2024
|
+
// top row swapped to the slot's own pad: the closed stretch's
|
|
2025
|
+
// terms are already the committed fold row further up, and
|
|
2026
|
+
// printing them again one screen apart is the duplication A9's
|
|
2027
|
+
// narrowing forbids.
|
|
2028
|
+
//
|
|
2029
|
+
// Projection-only. Commit order, commit timing and every
|
|
2030
|
+
// committed byte are untouched — which is why the transcript this
|
|
2031
|
+
// leaves behind cannot regress: it cannot differ.
|
|
2032
|
+
if (!stretchDrawn && turn !== undefined && !turn.ended && turn.begun) {
|
|
2033
|
+
// R7a: the top row is BLANK, not a `│`. It stands where the
|
|
2034
|
+
// stretch line stands while a stretch is open, and between two
|
|
2035
|
+
// stretches there is no line to draw — a bare gutter there is
|
|
2036
|
+
// a mark on a row with nothing to mark (law 1.3), and it is
|
|
2037
|
+
// the "long vertical line" the owner saw under a finished
|
|
2038
|
+
// turn. The ROW is what holds the height; the glyph never was.
|
|
2039
|
+
const rows = ["", ...this.#actSlot(null, W, ctx, budget, focus)];
|
|
2040
|
+
out.push(...this.#blockSpace(lastIdx, prev, rows)); // R7a: the same blank
|
|
2041
|
+
}
|
|
1969
2042
|
return out;
|
|
1970
2043
|
}
|
|
2044
|
+
/** R6/D1 — the turn's most recent CLOSED segment: what the block
|
|
2045
|
+
* shows in the gap between two stretches. */
|
|
2046
|
+
#lastClosedSegment() {
|
|
2047
|
+
const turn = this.#turns[this.#turns.length - 1];
|
|
2048
|
+
if (turn === undefined)
|
|
2049
|
+
return null;
|
|
2050
|
+
for (let i = turn.segments.length - 1; i >= 0; i -= 1) {
|
|
2051
|
+
const seg = turn.segments[i];
|
|
2052
|
+
if (seg.cells.some((j) => j >= this.#committed))
|
|
2053
|
+
return seg;
|
|
2054
|
+
}
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
1971
2057
|
/**
|
|
1972
2058
|
* R4 — the standing act slot's rows. EXACTLY `budget` rows in every
|
|
1973
2059
|
* phase, so the live region's height changes twice per stretch (once
|
|
@@ -1995,7 +2081,16 @@ export class Body {
|
|
|
1995
2081
|
rows[0] = focusToken(rows[0], W);
|
|
1996
2082
|
return rows;
|
|
1997
2083
|
};
|
|
1998
|
-
|
|
2084
|
+
// R6/D1: with no open stretch (the gap BETWEEN two of them) the
|
|
2085
|
+
// slot looks at the turn's last closed segment instead — the call
|
|
2086
|
+
// that just finished keeps its head and its tail, which is R4 B's
|
|
2087
|
+
// rule extended across the boundary. Cells outlive their commit,
|
|
2088
|
+
// so these are live repaints of live rows, never a rewrite of a
|
|
2089
|
+
// committed one.
|
|
2090
|
+
const src = seg ?? this.#lastClosedSegment();
|
|
2091
|
+
if (src === null)
|
|
2092
|
+
return [];
|
|
2093
|
+
const live = src.cells.filter((i) => i >= this.#committed);
|
|
1999
2094
|
const tools = [];
|
|
2000
2095
|
for (const i of live)
|
|
2001
2096
|
if (this.#cells[i]?.kind === "tool")
|
|
@@ -2042,31 +2137,142 @@ export class Body {
|
|
|
2042
2137
|
out.push(moreRunningRow(hidden, W));
|
|
2043
2138
|
return out;
|
|
2044
2139
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2140
|
+
// R7a — ONE PATH, whether or not anything is in flight.
|
|
2141
|
+
//
|
|
2142
|
+
// There used to be two: the in-flight composition, and a
|
|
2143
|
+
// "last finished call plus its output" composition for the gap
|
|
2144
|
+
// between stretches. The moment the last call of a burst
|
|
2145
|
+
// returned, the block re-composed from four head rows to one
|
|
2146
|
+
// head and a tail — a row shorter, so on a full screen every
|
|
2147
|
+
// row above slid DOWN. The block is supposed to change its
|
|
2148
|
+
// CONTENTS, not its shape; the last call returning is not a
|
|
2149
|
+
// reason to redraw the stretch differently.
|
|
2150
|
+
if (tools.length > 0) {
|
|
2151
|
+
// the one-call special case is GONE: the path below draws a
|
|
2152
|
+
// lone running call by its own component (the W8 block
|
|
2153
|
+
// verbatim, which is what 0.17.0 drew) and a lone finished
|
|
2154
|
+
// one as its head plus its output. The special case only
|
|
2155
|
+
// differed once the call SETTLED, where it collapsed to the
|
|
2156
|
+
// bare head row — a three-row shrink the moment a single
|
|
2157
|
+
// call returned.
|
|
2158
|
+
// R7a — EVERY call of the stretch keeps its row, not just the
|
|
2159
|
+
// ones still in flight.
|
|
2160
|
+
//
|
|
2161
|
+
// R4 showed the in-flight calls only, so a finished one left
|
|
2162
|
+
// the block and its target went with it: a four-file burst
|
|
2163
|
+
// ended having shown four names and left none of them, while
|
|
2164
|
+
// the rows below shuffled up one at a time. Two complaints in
|
|
2165
|
+
// one — "I can't see what it read" and "the rows keep moving".
|
|
2166
|
+
//
|
|
2167
|
+
// A call now takes a row when it STARTS and changes in place
|
|
2168
|
+
// when it finishes: `● read x · 1s` becomes the settled head.
|
|
2169
|
+
// Nothing moves, every target stays, and exactly ONE row wears
|
|
2170
|
+
// the breathing mark — the running one — which is the mark's
|
|
2171
|
+
// whole job (§7.4: only the call still running carries one,
|
|
2172
|
+
// because only it is moving).
|
|
2173
|
+
//
|
|
2174
|
+
// The slot's fixed height is what pays for this: the rows are
|
|
2175
|
+
// already allocated, so the names fill blanks rather than
|
|
2176
|
+
// pushing anything.
|
|
2177
|
+
// TRUNCATION NEVER DROPS A CALL THAT IS STILL RUNNING.
|
|
2178
|
+
//
|
|
2179
|
+
// Taking the first N is wrong the moment a burst outlives the
|
|
2180
|
+
// slot: four reads that finished held every row while the
|
|
2181
|
+
// shell still running was cut, so the screen said "4 files"
|
|
2182
|
+
// and showed nothing of the work actually in flight. The
|
|
2183
|
+
// in-flight set is admitted first, then the most RECENT
|
|
2184
|
+
// finished calls fill what is left — newest first, because
|
|
2185
|
+
// the oldest is the one the eye has already read.
|
|
2186
|
+
const live = tools.filter((i) => !toolAt(i).done);
|
|
2187
|
+
const past = tools.filter((i) => toolAt(i).done);
|
|
2188
|
+
// WHAT IS HAPPENING NOW OUTRANKS WHAT HAPPENED. In order:
|
|
2189
|
+
// the in-flight rows, then that call's output when it is the
|
|
2190
|
+
// only one running, then the finished NAMES, newest first.
|
|
2191
|
+
//
|
|
2192
|
+
// This is the line between R3i P1 and the owner's R7a ruling,
|
|
2193
|
+
// which look contradictory and are not. The ruling is about a
|
|
2194
|
+
// parallel burst — four reads whose names vanished one at a
|
|
2195
|
+
// time, so the turn ended having shown four files and left
|
|
2196
|
+
// none of them. P1 is about a burst that is OVER and a new
|
|
2197
|
+
// call running: there the finished names have had their time
|
|
2198
|
+
// on screen and the work in flight has not. Recency decides
|
|
2199
|
+
// both, and neither gate has to give.
|
|
2200
|
+
// the lone in-flight call is drawn by its OWN component, head
|
|
2201
|
+
// and tail together — that is where the waiting row, VD-4's
|
|
2202
|
+
// never-blank-first-row rule and the shell's live window all
|
|
2203
|
+
// already live. Reaching past it to slotTail() lost every one
|
|
2204
|
+
// of them: a running shell with no output yet drew three
|
|
2205
|
+
// blank rows where `└ waiting for output` belongs.
|
|
2206
|
+
// `grouped` says "an activity line above wears the mark for
|
|
2207
|
+
// us". A stretch of ONE call draws no such line (R7a), so
|
|
2208
|
+
// there is nothing above to carry it and the head keeps its
|
|
2209
|
+
// own — otherwise a lone running call breathes nowhere.
|
|
2210
|
+
const grouped = { ...ctx, grouped: tools.length > 1 };
|
|
2211
|
+
const soloRows = live.length === 1 ? tint(live[0], cellComponent(this.#cells[live[0]]).render(W, grouped)) : [];
|
|
2212
|
+
const tailWant = Math.max(0, soloRows.length - 1);
|
|
2213
|
+
// the overflow row is itself a row: an in-flight set larger
|
|
2214
|
+
// than the slot gives one back so `+N more running` fits.
|
|
2215
|
+
const liveRows = live.slice(0, live.length > budget ? Math.max(1, budget - 1) : budget);
|
|
2216
|
+
const spare = Math.max(0, budget - liveRows.length - tailWant);
|
|
2217
|
+
const nameRoom = past.length > spare ? Math.max(0, spare - 1) : spare;
|
|
2218
|
+
const keep = new Set([...liveRows, ...past.slice(past.length - nameRoom)]);
|
|
2219
|
+
const shown = tools.filter((i) => keep.has(i));
|
|
2220
|
+
// `+N more running` COUNTS ONLY CALLS THAT ARE RUNNING.
|
|
2221
|
+
//
|
|
2222
|
+
// Counting every dropped call said "+1 more running" over a
|
|
2223
|
+
// read that had already returned — a false sentence of the
|
|
2224
|
+
// R3h class, and the stretch line above had ALREADY counted
|
|
2225
|
+
// that read ("read 1 file"), so the row was both wrong and
|
|
2226
|
+
// redundant. A finished name giving way to live work is the
|
|
2227
|
+
// recency rule doing its job, not an overflow.
|
|
2228
|
+
const hidden = live.length - shown.filter((i) => !toolAt(i).done).length;
|
|
2053
2229
|
const rows = [];
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2230
|
+
// the mark lives on the ACTIVITY line above, so the members
|
|
2231
|
+
// wear a plain gutter — see FrameCtx.grouped.
|
|
2232
|
+
for (const i of shown) {
|
|
2233
|
+
if (i === live[0] && live.length === 1)
|
|
2234
|
+
rows.push(...soloRows.slice(0, Math.max(1, budget - rows.length)));
|
|
2235
|
+
else
|
|
2236
|
+
rows.push(tint(i, cellComponent(this.#cells[i]).render(W, grouped))[0] ?? "");
|
|
2237
|
+
}
|
|
2059
2238
|
if (hidden > 0)
|
|
2060
2239
|
rows.push(moreRunningRow(hidden, W));
|
|
2240
|
+
// R3i P3 SURVIVES: the call in flight keeps its row AND its
|
|
2241
|
+
// output. R7a gave every call a row, which spent the budget
|
|
2242
|
+
// the tail used to hold — but a running shell with no output
|
|
2243
|
+
// on screen is the defect R3i named, and the owner's ruling
|
|
2244
|
+
// was about the finished calls' NAMES, not about this. The
|
|
2245
|
+
// tail takes whatever the head rows leave, so it is full
|
|
2246
|
+
// height for a lone call and gives way to the names first.
|
|
2247
|
+
// R4 B SURVIVES THE UNIFICATION: between two calls — nothing
|
|
2248
|
+
// in flight — the slot still shows the call that just
|
|
2249
|
+
// finished AND its output. It is appended UNDER the head
|
|
2250
|
+
// rows now instead of replacing them, so the block's shape
|
|
2251
|
+
// does not change when the last call of a burst returns.
|
|
2252
|
+
const rest = budget - rows.length;
|
|
2253
|
+
if (live.length === 0 && rest > 0 && tools.length > 0)
|
|
2254
|
+
rows.push(...slotTail(toolAt(tools[tools.length - 1]).resultText, W, rest));
|
|
2061
2255
|
return slotPad(rows, budget);
|
|
2062
2256
|
}
|
|
2063
|
-
const settled = tools.length > 0 ? tools[tools.length - 1] : null;
|
|
2064
|
-
if (settled !== null) {
|
|
2065
|
-
const head = tint(settled, cellComponent(this.#cells[settled]).render(W, ctx))[0] ?? "";
|
|
2066
|
-
return slotPad([head, ...slotTail(toolAt(settled).resultText, W, budget - 1)], budget);
|
|
2067
|
-
}
|
|
2068
2257
|
const think = [...live].reverse().find((i) => this.#cells[i]?.kind === "thinking");
|
|
2069
|
-
|
|
2258
|
+
// R7a — A SLOT WITH NOTHING TO SHOW TAKES NO ROWS.
|
|
2259
|
+
//
|
|
2260
|
+
// R7 moved thinking OUT of the slot (it is words, and words do
|
|
2261
|
+
// not fold), which left this branch — the pre-tool phase of a
|
|
2262
|
+
// stretch — with nothing to put in the rows it was still
|
|
2263
|
+
// reserving. It padded them anyway: six blank rows between the
|
|
2264
|
+
// thought and the composer, on 653 of a 733-frame dogfood
|
|
2265
|
+
// replay. Until today those rows were drawn as `│`, so the
|
|
2266
|
+
// blank-run guard never saw them and the owner saw a gutter
|
|
2267
|
+
// running down the screen marking nothing; blanking the gutter
|
|
2268
|
+
// (law 1.3) revealed the hole the gutter had been covering.
|
|
2269
|
+
//
|
|
2270
|
+
// Reserving height buys stability only where the content
|
|
2271
|
+
// CHANGES under it — a stretch whose calls come and go. Before
|
|
2272
|
+
// the first call there is nothing to stabilise, so the rows are
|
|
2273
|
+
// pure cost, and both complaints are the same complaint.
|
|
2274
|
+
const tail = think === undefined ? [] : slotTail(this.#cells[think].text, W, budget);
|
|
2275
|
+
return tail.length === 0 ? [] : slotPad(tail, budget);
|
|
2070
2276
|
}
|
|
2071
2277
|
/** R4 — the tool names with a call still IN FLIGHT in this segment.
|
|
2072
2278
|
* The stretch line's tense is per term, so a finished shell reads
|
|
@@ -2454,6 +2660,37 @@ export class Body {
|
|
|
2454
2660
|
* the only place that knows it. Every other pair is untouched,
|
|
2455
2661
|
* including the boundary INTO a markdown message (the blank under the
|
|
2456
2662
|
* user chip is still W11's). */
|
|
2663
|
+
/** R7a — the live block's spacing is the spacing its COMMITTED form
|
|
2664
|
+
* will get, never its own.
|
|
2665
|
+
*
|
|
2666
|
+
* W11 gives a blank when either side is multi-row. The block is
|
|
2667
|
+
* always multi-row and the fold it commits into is always ONE row,
|
|
2668
|
+
* so the two sides disagreed by construction and a blank appeared
|
|
2669
|
+
* or vanished at every settle, shoving the whole transcript by a
|
|
2670
|
+
* row. Both directions occur: after a two-row thought the settle
|
|
2671
|
+
* ADDED one (my R6/D1 note saw only this case and removed the
|
|
2672
|
+
* block's blank, which fixed that direction and broke the other);
|
|
2673
|
+
* after a one-row thought it REMOVED one.
|
|
2674
|
+
*
|
|
2675
|
+
* Deciding on a one-row stand-in makes the block spaced exactly as
|
|
2676
|
+
* its fold will be, so the settle changes the row's CONTENT and
|
|
2677
|
+
* never its position — which is the whole claim of the standing
|
|
2678
|
+
* block. */
|
|
2679
|
+
/** R7a — is any call of this stretch actually RUNNING?
|
|
2680
|
+
*
|
|
2681
|
+
* The phase is not the same question. A stretch stays "acting" from
|
|
2682
|
+
* its first tool to its close, so between two bursts — every call
|
|
2683
|
+
* returned, the model is composing the next one — the phase still
|
|
2684
|
+
* said acting and the activity line went on breathing over four
|
|
2685
|
+
* finished reads. A mark that is lit when nothing moves is the
|
|
2686
|
+
* spinner-implies-progress error §5.3 forbids, one scale up. */
|
|
2687
|
+
#inFlight(seg) {
|
|
2688
|
+
return seg.cells.some((i) => { const c = this.#cells[i]; return c?.kind === "tool" && !c.done; });
|
|
2689
|
+
}
|
|
2690
|
+
#blockSpace(i, prev, rows) {
|
|
2691
|
+
const lead = bodySpacing(this.#lastDrawn(i, prev), ["x"]).length > 1 ? [""] : [];
|
|
2692
|
+
return [...lead, ...rows];
|
|
2693
|
+
}
|
|
2457
2694
|
#space(i, prev, rows) {
|
|
2458
2695
|
if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
|
|
2459
2696
|
return rows;
|
|
@@ -2687,6 +2924,12 @@ export class Body {
|
|
|
2687
2924
|
// is not going to speak for it.
|
|
2688
2925
|
if (cell.kind === "tool" && cell.name === "ask_user")
|
|
2689
2926
|
return false;
|
|
2927
|
+
// R7: and neither is THINKING. It is words now (law 1.7 — work
|
|
2928
|
+
// folds, words do not), so it commits when it is done, like prose,
|
|
2929
|
+
// and is never held for a fold that no longer speaks for it. The
|
|
2930
|
+
// ask_user exemption one line up is the precedent this follows.
|
|
2931
|
+
if (cell.kind === "thinking")
|
|
2932
|
+
return false;
|
|
2690
2933
|
const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
2691
2934
|
if (turn === undefined || turn !== this.#turns[this.#turns.length - 1])
|
|
2692
2935
|
return false;
|
|
@@ -3225,6 +3468,21 @@ export class Body {
|
|
|
3225
3468
|
// march below is clamped to the window instead, which makes the
|
|
3226
3469
|
// sheet displace content ON SCREEN. Closing takes the full-redraw
|
|
3227
3470
|
// path with the same #lastSkip and every displaced row comes back.
|
|
3471
|
+
// R7a — a monotone skip was TRIED HERE AND REJECTED, measured.
|
|
3472
|
+
//
|
|
3473
|
+
// The seam it aimed at is real: the turn boundary releases the
|
|
3474
|
+
// block into a one-row fold, so a full screen's computed skip
|
|
3475
|
+
// drops and every row above slides down one. Holding skip at its
|
|
3476
|
+
// high-water mark removes that motion exactly.
|
|
3477
|
+
//
|
|
3478
|
+
// It also holds the window BELOW the content, and the a7 dogfood
|
|
3479
|
+
// replay prices that at 40x24: the frame from which the screen
|
|
3480
|
+
// durably fills (no blank run over 2) goes 65 -> 692 of 733 —
|
|
3481
|
+
// a three-row hole above the composer through most of a real
|
|
3482
|
+
// session. One row of motion once per turn, at the moment the
|
|
3483
|
+
// answer lands and the eye is on it, is the cheaper of the two.
|
|
3484
|
+
// The A8b guard is written against a real session; this is what
|
|
3485
|
+
// it exists to catch.
|
|
3228
3486
|
const skip = overlay
|
|
3229
3487
|
? this.#lastSkip
|
|
3230
3488
|
: Math.max(0, all.length + CHROME_ROWS + inputExtra + queueRows.length + menuRows.length - H);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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.20.0"
|
|
39
39
|
}
|
|
40
40
|
}
|