@vincemakes/kiso-tui 0.10.0 → 0.12.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 +30 -6
- package/dist/ask-panel.js +40 -15
- package/dist/compositor.d.ts +15 -0
- package/dist/compositor.js +135 -19
- package/dist/editor.d.ts +25 -2
- package/dist/editor.js +442 -91
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/status.d.ts +3 -1
- package/dist/status.js +3 -1
- package/package.json +2 -2
package/dist/ask-panel.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* durability would need a new durable mechanism), no "chat about this"
|
|
28
28
|
* hand-off, no timeout and no countdown.
|
|
29
29
|
*/
|
|
30
|
-
import { type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, type PanelPhase, type
|
|
30
|
+
import { type SaferRuntime, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, type PanelPhase, type PanelState, type PanelView, type PickRuntime } from "./approval-panel.js";
|
|
31
31
|
/** The schema's own bounds — the registry refuses anything outside them
|
|
32
32
|
* (extensions/ask validates; these are the numbers it validates to). */
|
|
33
33
|
export declare const ASK_MAX_QUESTIONS = 4;
|
|
@@ -77,15 +77,39 @@ export declare function askStatus(view: PanelView, state: AskRuntime): string;
|
|
|
77
77
|
/** The input row's lead: the digit lead while picking, the typing lead
|
|
78
78
|
* in the custom phase (the rule-input phase's shape, reused). */
|
|
79
79
|
export declare function askLeadPlain(state: AskRuntime): string;
|
|
80
|
-
export declare function panelBlockRows(view: PanelView, phase: PanelPhase,
|
|
81
|
-
export declare function panelLead(view: PanelView, phase: PanelPhase,
|
|
82
|
-
export declare function panelLeadPlain(view: PanelView, phase: PanelPhase,
|
|
83
|
-
export declare function panelStatus(view: PanelView, phase: PanelPhase,
|
|
84
|
-
export declare function panelAffordance(view: PanelView, phase: PanelPhase,
|
|
80
|
+
export declare function panelBlockRows(view: PanelView, phase: PanelPhase, cursor: number, W: number, maxRows: number, ask?: AskRuntime, pick?: PickRuntime, note?: string, safer?: SaferRuntime): string[];
|
|
81
|
+
export declare function panelLead(view: PanelView, phase: PanelPhase, cursor: number, ask?: AskRuntime, pick?: PickRuntime): string;
|
|
82
|
+
export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, cursor: number, ask?: AskRuntime, pick?: PickRuntime): string;
|
|
83
|
+
export declare function panelStatus(view: PanelView, phase: PanelPhase, cursor: number, ask?: AskRuntime, pick?: PickRuntime): string;
|
|
84
|
+
export declare function panelAffordance(view: PanelView, phase: PanelPhase, cursor: number, ask?: AskRuntime, pick?: PickRuntime, safer?: SaferRuntime): string;
|
|
85
85
|
/** The whole panel state in one call — the compositor's four reads share
|
|
86
86
|
* one source, so an ask can never render half as an approval (TUI2-R2
|
|
87
87
|
* ④: nor a pick as either). */
|
|
88
88
|
export declare const panelRowsOf: (s: PanelState, W: number, maxRows: number) => string[];
|
|
89
|
+
/**
|
|
90
|
+
* TUI2-R3v2 ② — the rows AND where the clickable ones are, from ONE
|
|
91
|
+
* call.
|
|
92
|
+
*
|
|
93
|
+
* The compositor needs both and must not compute the second from the
|
|
94
|
+
* first: the args cap, the note row and the option window all move the
|
|
95
|
+
* list, so a hit-test that re-derived the offset would drift from the
|
|
96
|
+
* picture exactly when the block is under pressure — which is when a
|
|
97
|
+
* misrouted click is most expensive.
|
|
98
|
+
*
|
|
99
|
+
* `options` is null for the ask and pick flavors: their rows are placed
|
|
100
|
+
* by their own renderers, and this round does not claim a click on them
|
|
101
|
+
* (the enable/disable invariant covers those surfaces; the gesture does
|
|
102
|
+
* not). A null here means every click is inert, which is the safe way
|
|
103
|
+
* to not-implement something.
|
|
104
|
+
*/
|
|
105
|
+
export declare const panelFrameOf: (s: PanelState, W: number, maxRows: number) => {
|
|
106
|
+
rows: string[];
|
|
107
|
+
options: {
|
|
108
|
+
offset: number;
|
|
109
|
+
count: number;
|
|
110
|
+
first: number;
|
|
111
|
+
} | null;
|
|
112
|
+
};
|
|
89
113
|
export declare const panelLeadOf: (s: PanelState) => string;
|
|
90
114
|
export declare const panelStatusOf: (s: PanelState) => string;
|
|
91
115
|
export declare const panelAffordanceOf: (s: PanelState) => string;
|
package/dist/ask-panel.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* durability would need a new durable mechanism), no "chat about this"
|
|
28
28
|
* hand-off, no timeout and no countdown.
|
|
29
29
|
*/
|
|
30
|
-
import { panelAffordance as basePanelAffordance, panelBlockRows as basePanelBlockRows, panelLead as basePanelLead, panelLeadPlain as basePanelLeadPlain, panelStatus as basePanelStatus, pickAffordance, pickBlockRows, pickLead, pickLeadPlain, pickStatus, } from "./approval-panel.js";
|
|
30
|
+
import { panelBlockLayout, panelAffordance as basePanelAffordance, panelBlockRows as basePanelBlockRows, panelLead as basePanelLead, panelLeadPlain as basePanelLeadPlain, panelStatus as basePanelStatus, pickAffordance, pickBlockRows, pickLead, pickLeadPlain, pickStatus, } from "./approval-panel.js";
|
|
31
31
|
import { cutLine } from "@vincemakes/kiso-tui-cells/components";
|
|
32
32
|
import { escapeTerminal, palette } from "./render.js";
|
|
33
33
|
/** The schema's own bounds — the registry refuses anything outside them
|
|
@@ -225,49 +225,74 @@ export function askLeadPlain(state) {
|
|
|
225
225
|
return state.phase === "custom" ? "your answer: " : "1-4> ";
|
|
226
226
|
}
|
|
227
227
|
// ── the dispatchers: the panel slot, with the ask branch folded in ────
|
|
228
|
-
export function panelBlockRows(view, phase,
|
|
228
|
+
export function panelBlockRows(view, phase, cursor, W, maxRows, ask, pick, note, safer) {
|
|
229
229
|
if (view.pick !== undefined && pick !== undefined)
|
|
230
230
|
return pickBlockRows(view, pick, W, maxRows);
|
|
231
231
|
if (view.ask !== undefined && ask !== undefined)
|
|
232
232
|
return askBlockRows(view, ask, W, maxRows);
|
|
233
|
-
return basePanelBlockRows(view, phase,
|
|
233
|
+
return basePanelBlockRows(view, phase, cursor, W, maxRows, note, safer);
|
|
234
234
|
}
|
|
235
|
-
export function panelLead(view, phase,
|
|
235
|
+
export function panelLead(view, phase, cursor, ask, pick) {
|
|
236
236
|
const p = palette();
|
|
237
237
|
if (view.pick !== undefined && pick !== undefined)
|
|
238
238
|
return pickLead(view, pick);
|
|
239
239
|
if (view.ask !== undefined && ask !== undefined)
|
|
240
240
|
return `${p.bold}${askLeadPlain(ask)}${p.reset}`;
|
|
241
|
-
return basePanelLead(view, phase,
|
|
241
|
+
return basePanelLead(view, phase, cursor);
|
|
242
242
|
}
|
|
243
|
-
export function panelLeadPlain(view, phase,
|
|
243
|
+
export function panelLeadPlain(view, phase, cursor, ask, pick) {
|
|
244
244
|
if (view.pick !== undefined && pick !== undefined)
|
|
245
245
|
return pickLeadPlain(view, pick);
|
|
246
246
|
if (view.ask !== undefined && ask !== undefined)
|
|
247
247
|
return askLeadPlain(ask);
|
|
248
|
-
return basePanelLeadPlain(view, phase,
|
|
248
|
+
return basePanelLeadPlain(view, phase, cursor);
|
|
249
249
|
}
|
|
250
|
-
export function panelStatus(view, phase,
|
|
250
|
+
export function panelStatus(view, phase, cursor, ask, pick) {
|
|
251
251
|
if (view.pick !== undefined && pick !== undefined)
|
|
252
252
|
return pickStatus(view);
|
|
253
253
|
if (view.ask !== undefined && ask !== undefined)
|
|
254
254
|
return askStatus(view, ask);
|
|
255
|
-
return basePanelStatus(view, phase,
|
|
255
|
+
return basePanelStatus(view, phase, cursor);
|
|
256
256
|
}
|
|
257
|
-
export function panelAffordance(view, phase,
|
|
257
|
+
export function panelAffordance(view, phase, cursor, ask, pick, safer) {
|
|
258
258
|
if (view.pick !== undefined && pick !== undefined)
|
|
259
259
|
return pickAffordance(pick);
|
|
260
260
|
if (view.ask !== undefined && ask !== undefined)
|
|
261
261
|
return askAffordance(ask);
|
|
262
|
-
return basePanelAffordance(view, phase,
|
|
262
|
+
return basePanelAffordance(view, phase, cursor, safer);
|
|
263
263
|
}
|
|
264
264
|
/** The whole panel state in one call — the compositor's four reads share
|
|
265
265
|
* one source, so an ask can never render half as an approval (TUI2-R2
|
|
266
266
|
* ④: nor a pick as either). */
|
|
267
|
-
export const panelRowsOf = (s, W, maxRows) => panelBlockRows(s.view, s.phase, s.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
export const panelRowsOf = (s, W, maxRows) => panelBlockRows(s.view, s.phase, s.cursor, W, maxRows, s.ask, s.pick, s.note, s.safer);
|
|
268
|
+
/**
|
|
269
|
+
* TUI2-R3v2 ② — the rows AND where the clickable ones are, from ONE
|
|
270
|
+
* call.
|
|
271
|
+
*
|
|
272
|
+
* The compositor needs both and must not compute the second from the
|
|
273
|
+
* first: the args cap, the note row and the option window all move the
|
|
274
|
+
* list, so a hit-test that re-derived the offset would drift from the
|
|
275
|
+
* picture exactly when the block is under pressure — which is when a
|
|
276
|
+
* misrouted click is most expensive.
|
|
277
|
+
*
|
|
278
|
+
* `options` is null for the ask and pick flavors: their rows are placed
|
|
279
|
+
* by their own renderers, and this round does not claim a click on them
|
|
280
|
+
* (the enable/disable invariant covers those surfaces; the gesture does
|
|
281
|
+
* not). A null here means every click is inert, which is the safe way
|
|
282
|
+
* to not-implement something.
|
|
283
|
+
*/
|
|
284
|
+
export const panelFrameOf = (s, W, maxRows) => {
|
|
285
|
+
if (s.view.pick !== undefined || s.view.ask !== undefined)
|
|
286
|
+
return { rows: panelRowsOf(s, W, maxRows), options: null };
|
|
287
|
+
const layout = panelBlockLayout(s.view, s.phase, s.cursor, W, maxRows, s.note, s.safer);
|
|
288
|
+
return {
|
|
289
|
+
rows: layout.rows,
|
|
290
|
+
options: layout.count === 0 ? null : { offset: layout.offset, count: layout.count, first: layout.first },
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
export const panelLeadOf = (s) => panelLead(s.view, s.phase, s.cursor, s.ask, s.pick);
|
|
294
|
+
export const panelStatusOf = (s) => panelStatus(s.view, s.phase, s.cursor, s.ask, s.pick);
|
|
295
|
+
export const panelAffordanceOf = (s) => panelAffordance(s.view, s.phase, s.cursor, s.ask, s.pick, s.safer);
|
|
271
296
|
// ── the view: what the human reads when the model asks ────────────────
|
|
272
297
|
/** The ask's PanelView. The dock-less fallback question is HONEST: a
|
|
273
298
|
* terminal without a panel cannot walk options, so it says the ask is
|
package/dist/compositor.d.ts
CHANGED
|
@@ -193,6 +193,13 @@ export declare class Body {
|
|
|
193
193
|
/** Teardown — CSI r (the "no broken terminal" contract byte), the
|
|
194
194
|
* chrome rows cleared, the cursor home at the input line. */
|
|
195
195
|
exit(): void;
|
|
196
|
+
/** TUI2-R3v2 ②: the panel's option rows as this frame placed them —
|
|
197
|
+
* absolute, 1-based. The click hit-test's single source. */
|
|
198
|
+
panelOptionRows(): {
|
|
199
|
+
top: number;
|
|
200
|
+
count: number;
|
|
201
|
+
first: number;
|
|
202
|
+
} | null;
|
|
196
203
|
/** SIGWINCH: clear the OLD live area (recorded geometry, ED only —
|
|
197
204
|
* zero LF, zero \x1b[3J — the shell history untouched), then the
|
|
198
205
|
* full-redraw path at the NEW geometry (O(height), zero replay).
|
|
@@ -257,6 +264,14 @@ export declare class Dock {
|
|
|
257
264
|
enter(): void;
|
|
258
265
|
exit(): void;
|
|
259
266
|
onResize(): void;
|
|
267
|
+
/** TUI2-R3v2 ②: where the last frame put the panel's clickable option
|
|
268
|
+
* rows (absolute screen rows). The editor binds this and does no row
|
|
269
|
+
* arithmetic of its own. */
|
|
270
|
+
panelOptionRows(): {
|
|
271
|
+
top: number;
|
|
272
|
+
count: number;
|
|
273
|
+
first: number;
|
|
274
|
+
} | null;
|
|
260
275
|
setStatus(text: string, hint?: string | null): void;
|
|
261
276
|
setTail(tail: string): void;
|
|
262
277
|
/** W21: bind the editor's panel state — the PanelSelect slot
|
package/dist/compositor.js
CHANGED
|
@@ -47,11 +47,12 @@ 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
48
|
// KC3.5: the panel-slot reads come from the DISPATCHERS — one source
|
|
49
49
|
// for four reads, so an ask can never render half as an approval.
|
|
50
|
-
import { panelAffordanceOf, panelLeadOf, panelRowsOf, panelStatusOf } from "./ask-panel.js";
|
|
50
|
+
import { panelAffordanceOf, panelFrameOf, panelLeadOf, panelRowsOf, panelStatusOf } from "./ask-panel.js";
|
|
51
|
+
import { MOUSE_OFF } from "./editor.js";
|
|
51
52
|
import { atPanelRows, bandHeader } from "./at-picker.js";
|
|
52
53
|
// TUI2-R2 ②: the session picker's rows — the band's third occupant.
|
|
53
54
|
import { sessionPickerRows } from "./session-picker.js";
|
|
54
|
-
import { Container, ROLLUP_NOUN, SPINNER, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, focusToken, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
|
|
55
|
+
import { Container, ROLLUP_NOUN, SPINNER, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, focusToken, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
|
|
55
56
|
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
|
|
56
57
|
import { displayVerb, keysSheetRows } from "./strings.js";
|
|
57
58
|
/** The cursor marker — an APC private sequence the focus component
|
|
@@ -96,6 +97,11 @@ export class Body {
|
|
|
96
97
|
#lastTool = null;
|
|
97
98
|
#pendingCalls = new Map();
|
|
98
99
|
#pipeBuf = ""; // the passthrough's thinking buffer
|
|
100
|
+
/** TUI2-MD ⑤ — the markdown scanner of the message currently
|
|
101
|
+
* streaming, and the cell index its first block landed at. Null
|
|
102
|
+
* between messages: the scanner's life is one assistant message. */
|
|
103
|
+
#md = null;
|
|
104
|
+
#mdBase = 0;
|
|
99
105
|
#toolCells = new Map(); // callId → cell index (parallel tools)
|
|
100
106
|
// W15: the collapsed (cut) tool cells — committed cells whose last
|
|
101
107
|
// rendered row carried the "ctrl+r" affordance; the expand key's
|
|
@@ -112,6 +118,9 @@ export class Body {
|
|
|
112
118
|
#rolledHeads = new Set();
|
|
113
119
|
#write;
|
|
114
120
|
#resizeHandler = null;
|
|
121
|
+
/** TUI2-R3v2 ②: the panel option rows' absolute screen span, as of the
|
|
122
|
+
* last frame. Null whenever no clickable list is on screen. */
|
|
123
|
+
#panelRowSpan = null;
|
|
115
124
|
// the chrome state (the Dock façade)
|
|
116
125
|
#status = "";
|
|
117
126
|
#statusHint = null;
|
|
@@ -398,22 +407,71 @@ export class Body {
|
|
|
398
407
|
const turn = this.#turns[this.#turns.length - 1];
|
|
399
408
|
if (turn !== undefined)
|
|
400
409
|
turn.hasText = true;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
410
|
+
// TUI2-MD ⑤: assistant body text is MARKDOWN, scanned as it
|
|
411
|
+
// streams. The scanner yields CLOSED blocks (final source, final
|
|
412
|
+
// render) and one OPEN tail block; each becomes a cell, and the
|
|
413
|
+
// cell is the commit unit the compositor already had — so
|
|
414
|
+
// block-freeze needs no new commit machinery at all. A closed
|
|
415
|
+
// block is a DONE cell the natural loop freezes; the tail is the
|
|
416
|
+
// one cell left live, repainting in place.
|
|
417
|
+
if (this.#md === null) {
|
|
406
418
|
this.#closeOpenThinking();
|
|
407
419
|
this.#closeOpenText();
|
|
408
|
-
this.#
|
|
420
|
+
this.#md = new MdStream();
|
|
421
|
+
this.#mdBase = this.#cells.length;
|
|
409
422
|
}
|
|
423
|
+
this.#md.push(text);
|
|
424
|
+
this.#syncMd();
|
|
410
425
|
this.#mark();
|
|
411
426
|
}
|
|
427
|
+
/** TUI2-MD ⑤ — mirror the scanner's blocks onto cells. Append-only by
|
|
428
|
+
* construction: a block that has closed never changes, so a cell that
|
|
429
|
+
* is done is never touched again (and a committed one could not be).
|
|
430
|
+
* Only the trailing tail cell is rewritten per delta. */
|
|
431
|
+
#syncMd() {
|
|
432
|
+
if (this.#md === null)
|
|
433
|
+
return;
|
|
434
|
+
const blocks = this.#md.blocks();
|
|
435
|
+
const closed = this.#md.closed();
|
|
436
|
+
for (let i = 0; i < blocks.length; i += 1) {
|
|
437
|
+
const at = this.#mdBase + i;
|
|
438
|
+
const cell = this.#cells[at];
|
|
439
|
+
if (cell === undefined) {
|
|
440
|
+
this.#cells.push({ kind: "md", block: blocks[i], done: i < closed });
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (cell.kind !== "md" || cell.done)
|
|
444
|
+
continue;
|
|
445
|
+
cell.block = blocks[i];
|
|
446
|
+
cell.done = i < closed;
|
|
447
|
+
}
|
|
448
|
+
// the tail can vanish (a lone whitespace delta that turns out to be
|
|
449
|
+
// a blank line). Drop it only where dropping is safe: never below
|
|
450
|
+
// the commit frontier, where the bytes are already the terminal's.
|
|
451
|
+
const want = this.#mdBase + blocks.length;
|
|
452
|
+
while (this.#cells.length > Math.max(want, this.#committed) && this.#cells[this.#cells.length - 1].kind === "md")
|
|
453
|
+
this.#cells.pop();
|
|
454
|
+
}
|
|
455
|
+
/** TUI2-MD ⑤ — the message ends: the tail block closes and every md
|
|
456
|
+
* cell of this message is final. */
|
|
457
|
+
#endMd() {
|
|
458
|
+
if (this.#md === null)
|
|
459
|
+
return;
|
|
460
|
+
this.#md.end();
|
|
461
|
+
this.#syncMd();
|
|
462
|
+
for (let i = this.#mdBase; i < this.#cells.length; i += 1) {
|
|
463
|
+
const cell = this.#cells[i];
|
|
464
|
+
if (cell.kind === "md")
|
|
465
|
+
cell.done = true;
|
|
466
|
+
}
|
|
467
|
+
this.#md = null;
|
|
468
|
+
}
|
|
412
469
|
textEnd() {
|
|
413
470
|
if (!this.#isActive()) {
|
|
414
471
|
this.#write("\n");
|
|
415
472
|
return;
|
|
416
473
|
}
|
|
474
|
+
this.#endMd();
|
|
417
475
|
const last = this.#cells[this.#cells.length - 1];
|
|
418
476
|
if (last !== undefined && last.kind === "text" && !last.done)
|
|
419
477
|
last.done = true;
|
|
@@ -718,6 +776,20 @@ export class Body {
|
|
|
718
776
|
/** Teardown — CSI r (the "no broken terminal" contract byte), the
|
|
719
777
|
* chrome rows cleared, the cursor home at the input line. */
|
|
720
778
|
exit() {
|
|
779
|
+
// TUI2-R3v2 ②: the mouse disable rides the SAME teardown as CSI r,
|
|
780
|
+
// and rides it BEFORE the docked guard — an un-docked compositor is
|
|
781
|
+
// exactly the state a superseded or half-torn-down one is in, and
|
|
782
|
+
// that is when a leak survives. The editor disables it too; this is
|
|
783
|
+
// the second belt on the one contract the round calls blocker-class.
|
|
784
|
+
//
|
|
785
|
+
// TTY-GATED, and the gate is not a nicety. A pipe has no mouse mode
|
|
786
|
+
// to reset, so the bytes would be pure noise there — and the pipe
|
|
787
|
+
// path is byte-identical by ruling. The unguarded version put
|
|
788
|
+
// "[?1000l[?1006l" into piped stdout and four gates caught it
|
|
789
|
+
// (compact-cli, tui-modes, tui-v7-planmode, tui2-r2-resume-picker):
|
|
790
|
+
// the invariant is about terminals, and a pipe is not one.
|
|
791
|
+
if (process.stdout.isTTY === true)
|
|
792
|
+
this.#write(MOUSE_OFF);
|
|
721
793
|
if (!this.#docked) {
|
|
722
794
|
// TUI2-R2pre ③: an un-docked compositor can still hold a listener
|
|
723
795
|
// (it was superseded, or enter() ran and the dock was torn down by
|
|
@@ -739,6 +811,11 @@ export class Body {
|
|
|
739
811
|
out.push(`\x1b[${Math.max(1, H - 1)};1H`);
|
|
740
812
|
this.#write(out.join(""));
|
|
741
813
|
}
|
|
814
|
+
/** TUI2-R3v2 ②: the panel's option rows as this frame placed them —
|
|
815
|
+
* absolute, 1-based. The click hit-test's single source. */
|
|
816
|
+
panelOptionRows() {
|
|
817
|
+
return this.#panelRowSpan;
|
|
818
|
+
}
|
|
742
819
|
/** SIGWINCH: clear the OLD live area (recorded geometry, ED only —
|
|
743
820
|
* zero LF, zero \x1b[3J — the shell history untouched), then the
|
|
744
821
|
* full-redraw path at the NEW geometry (O(height), zero replay).
|
|
@@ -923,14 +1000,13 @@ export class Body {
|
|
|
923
1000
|
inputExtra +
|
|
924
1001
|
queueRows.length);
|
|
925
1002
|
}
|
|
926
|
-
const live = this.#cells.slice(this.#committed);
|
|
927
1003
|
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
|
|
928
1004
|
const W = this.#opts.width();
|
|
929
1005
|
let lines = 0;
|
|
930
1006
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
931
|
-
for (
|
|
932
|
-
const rows = cellComponent(
|
|
933
|
-
lines +=
|
|
1007
|
+
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1008
|
+
const rows = cellComponent(this.#cells[i]).render(W, ctx);
|
|
1009
|
+
lines += this.#space(i, prev, rows).length;
|
|
934
1010
|
prev = rows;
|
|
935
1011
|
}
|
|
936
1012
|
return lines + CHROME_ROWS + inputExtra + this.#menuRows(W).length + queueRows.length;
|
|
@@ -963,7 +1039,7 @@ export class Body {
|
|
|
963
1039
|
const cell = this.#cells[i];
|
|
964
1040
|
const lines = cellComponent(cell).render(W, ctx);
|
|
965
1041
|
this.#lineCache[i] = lines; // the cell's OWN rows — the cache stays raw
|
|
966
|
-
this.#committedLines +=
|
|
1042
|
+
this.#committedLines += this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines).length;
|
|
967
1043
|
}
|
|
968
1044
|
}
|
|
969
1045
|
// 1. the natural commits — the leading DONE cells freeze: their
|
|
@@ -998,6 +1074,10 @@ export class Body {
|
|
|
998
1074
|
const inputExtra = editor.rows.length - 1;
|
|
999
1075
|
const chromeRows = CHROME_ROWS + inputExtra + menuRows.length + queueRows.length;
|
|
1000
1076
|
let liveLines = [];
|
|
1077
|
+
// TUI2-R3v2 ②: where this frame put the panel's option rows, relative
|
|
1078
|
+
// to the live region's top. Resolved to ABSOLUTE screen rows once
|
|
1079
|
+
// liveTop is known, below.
|
|
1080
|
+
let panelSpan = null;
|
|
1001
1081
|
const panel = this.#panelState?.() ?? null;
|
|
1002
1082
|
// TUI2-R1.5 ⑦(a) (VD-8): the sheet is an OVERLAY, and the frame it
|
|
1003
1083
|
// opens on — and the one it closes on — take the full-redraw path.
|
|
@@ -1024,7 +1104,12 @@ export class Body {
|
|
|
1024
1104
|
// the W11 blank would separate it from the frozen content).
|
|
1025
1105
|
// The cap is exact, so the force-commit loop never fires. W22:
|
|
1026
1106
|
// the queue band sits below the panel — the cap shrinks by it.
|
|
1027
|
-
|
|
1107
|
+
// TUI2-R3v2 ②: the rows and the CLICKABLE span come from one
|
|
1108
|
+
// call, so the hit-test reads the arithmetic that placed the
|
|
1109
|
+
// rows rather than a second copy of it.
|
|
1110
|
+
const frame = panelFrameOf(panel, W, Math.max(1, H - 4 - inputExtra - queueRows.length));
|
|
1111
|
+
liveLines = frame.rows;
|
|
1112
|
+
panelSpan = frame.options;
|
|
1028
1113
|
}
|
|
1029
1114
|
else {
|
|
1030
1115
|
// TUI2-R2 ⑤ (D, candidate 1): the FOCUS — the cell the next ctrl+r
|
|
@@ -1042,7 +1127,7 @@ export class Body {
|
|
|
1042
1127
|
// nowhere else, which is what makes "exactly one" structural
|
|
1043
1128
|
if (i === focus && rows.length > 0)
|
|
1044
1129
|
rows[0] = focusToken(rows[0], W);
|
|
1045
|
-
liveLines.push(...
|
|
1130
|
+
liveLines.push(...this.#space(i, prev, rows));
|
|
1046
1131
|
prev = rows;
|
|
1047
1132
|
}
|
|
1048
1133
|
}
|
|
@@ -1063,7 +1148,7 @@ export class Body {
|
|
|
1063
1148
|
const rows = cellComponent(cell).render(W, ctx);
|
|
1064
1149
|
if (i === focus && rows.length > 0)
|
|
1065
1150
|
rows[0] = focusToken(rows[0], W);
|
|
1066
|
-
liveLines.push(...
|
|
1151
|
+
liveLines.push(...this.#space(i, prev, rows));
|
|
1067
1152
|
prev = rows;
|
|
1068
1153
|
}
|
|
1069
1154
|
}
|
|
@@ -1073,6 +1158,11 @@ export class Body {
|
|
|
1073
1158
|
// shows the bottom H rows; the live region anchors to the bottom.
|
|
1074
1159
|
const liveRowsTotal = liveLines.length + chromeRows;
|
|
1075
1160
|
const liveTop = Math.min(this.#committedLines, H - liveRowsTotal) + 1;
|
|
1161
|
+
// TUI2-R3v2 ②: the option rows' ABSOLUTE screen rows, recorded per
|
|
1162
|
+
// frame. A click is answered against the frame the human was looking
|
|
1163
|
+
// at when they clicked, which is this one.
|
|
1164
|
+
this.#panelRowSpan =
|
|
1165
|
+
panelSpan === null ? null : { top: liveTop + panelSpan.offset, count: panelSpan.count, first: panelSpan.first };
|
|
1076
1166
|
// 5. the frame bytes.
|
|
1077
1167
|
const out = [];
|
|
1078
1168
|
out.push("\x1b[?2026h"); // synchronized output ON (DEC 2026)
|
|
@@ -1106,6 +1196,23 @@ export class Body {
|
|
|
1106
1196
|
// composer (N = 1 ⇒ H−2, the retired hard-coded anchor).
|
|
1107
1197
|
this.#lastAnchorRow = H - 1 - editor.rows.length + editor.markerRow;
|
|
1108
1198
|
}
|
|
1199
|
+
/** TUI2-MD ⑤ — the join blank between cell i−1 and cell i.
|
|
1200
|
+
*
|
|
1201
|
+
* W11's formula ("a blank above a row that is itself a block, or
|
|
1202
|
+
* whose previous sibling was taller than one row") reads ROW COUNTS,
|
|
1203
|
+
* and markdown's rhythm is not a row count: a heading wants a blank
|
|
1204
|
+
* above and below it even between two one-row paragraphs, and two
|
|
1205
|
+
* rows of one fence want none even when a long code line folds to
|
|
1206
|
+
* two. So between two MARKDOWN cells the formula steps aside and the
|
|
1207
|
+
* block's own `gap` decides — the renderer owns the rhythm, which is
|
|
1208
|
+
* the only place that knows it. Every other pair is untouched,
|
|
1209
|
+
* including the boundary INTO a markdown message (the blank under the
|
|
1210
|
+
* user chip is still W11's). */
|
|
1211
|
+
#space(i, prev, rows) {
|
|
1212
|
+
if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
|
|
1213
|
+
return rows;
|
|
1214
|
+
return bodySpacing(prev, rows);
|
|
1215
|
+
}
|
|
1109
1216
|
/** Commit the cell at index i: render + cache its lines (immutable —
|
|
1110
1217
|
* the force-committed form freezes at the current render), advance
|
|
1111
1218
|
* the bookkeeping — and collect the lines for this frame's writes.
|
|
@@ -1128,7 +1235,7 @@ export class Body {
|
|
|
1128
1235
|
if (cell.kind === "tool" && lines.some((l) => l.includes("ctrl+r")))
|
|
1129
1236
|
this.#collapsed.unshift(i);
|
|
1130
1237
|
this.#lineCache[i] = lines;
|
|
1131
|
-
const placed =
|
|
1238
|
+
const placed = this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines);
|
|
1132
1239
|
this.#committed += 1;
|
|
1133
1240
|
this.#committedLines += placed.length;
|
|
1134
1241
|
this.#committedLinesThisFrame.push(...placed);
|
|
@@ -1544,7 +1651,7 @@ export class Body {
|
|
|
1544
1651
|
// cell (the V6-1 frozen-loop finding — the banner vanished).
|
|
1545
1652
|
const frozen = [];
|
|
1546
1653
|
for (let i = 0; i < this.#committedAtFrameStart; i += 1) {
|
|
1547
|
-
frozen.push(...
|
|
1654
|
+
frozen.push(...this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, this.#lineCache[i]));
|
|
1548
1655
|
}
|
|
1549
1656
|
// A8: the march is the WINDOW — the model's last H rows. When the
|
|
1550
1657
|
// model total (committed + live + chrome) exceeds H, the window's
|
|
@@ -1884,8 +1991,11 @@ export class Body {
|
|
|
1884
1991
|
return i === undefined ? null : (this.#cells[i] ?? null);
|
|
1885
1992
|
}
|
|
1886
1993
|
/** Close an open TEXT cell when a new cell starts (see v2d — the
|
|
1887
|
-
* runtime emits no text_end; the next cell is the close signal).
|
|
1994
|
+
* runtime emits no text_end; the next cell is the close signal).
|
|
1995
|
+
* TUI2-MD ⑤: that same signal ends the markdown message — the open
|
|
1996
|
+
* tail block closes and its cell becomes commit-eligible. */
|
|
1888
1997
|
#closeOpenText() {
|
|
1998
|
+
this.#endMd();
|
|
1889
1999
|
const last = this.#cells[this.#cells.length - 1];
|
|
1890
2000
|
if (last !== undefined && last.kind === "text" && !last.done)
|
|
1891
2001
|
last.done = true;
|
|
@@ -1922,6 +2032,12 @@ export class Dock {
|
|
|
1922
2032
|
onResize() {
|
|
1923
2033
|
compositorRef?.onResize();
|
|
1924
2034
|
}
|
|
2035
|
+
/** TUI2-R3v2 ②: where the last frame put the panel's clickable option
|
|
2036
|
+
* rows (absolute screen rows). The editor binds this and does no row
|
|
2037
|
+
* arithmetic of its own. */
|
|
2038
|
+
panelOptionRows() {
|
|
2039
|
+
return compositorRef?.panelOptionRows() ?? null;
|
|
2040
|
+
}
|
|
1925
2041
|
setStatus(text, hint) {
|
|
1926
2042
|
compositorRef?.setStatus(text, hint ?? null);
|
|
1927
2043
|
}
|
package/dist/editor.d.ts
CHANGED
|
@@ -23,9 +23,20 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
25
25
|
export { charWidth, displayWidth, widthOf };
|
|
26
|
-
import { type PanelState, type PanelVerdict, type PanelView } from "./approval-panel.js";
|
|
26
|
+
import { type PanelState, type PanelVerdict, type PanelView, type SaferOption } from "./approval-panel.js";
|
|
27
27
|
import { type AtItem, type AtMatch } from "./at-picker.js";
|
|
28
28
|
import { type SessionCardView, type SessionPickState } from "./session-picker.js";
|
|
29
|
+
/**
|
|
30
|
+
* TUI2-R3v2 ② — the mouse-mode bytes, stated once.
|
|
31
|
+
*
|
|
32
|
+
* ?1000 is the button-event report and ?1006 is the SGR encoding that
|
|
33
|
+
* makes it parseable past column 95 (the legacy X10 encoding packs the
|
|
34
|
+
* coordinate into one byte and simply breaks on a wide terminal). Both
|
|
35
|
+
* go on together and come off together; a terminal left with either one
|
|
36
|
+
* set is a terminal that prints escape bytes at the shell prompt.
|
|
37
|
+
*/
|
|
38
|
+
export declare const MOUSE_ON = "\u001B[?1000h\u001B[?1006h";
|
|
39
|
+
export declare const MOUSE_OFF = "\u001B[?1000l\u001B[?1006l";
|
|
29
40
|
export declare const PROMPT = "\u258C ";
|
|
30
41
|
export declare const PROMPT_WIDTH: number;
|
|
31
42
|
/** v3 §04 — the slash-command menu's command table (English one-liners). */
|
|
@@ -116,7 +127,9 @@ export declare class Editor {
|
|
|
116
127
|
/** W21: open the approval panel. The current buffer is stashed
|
|
117
128
|
* (restored at close — commit AND cancel), the panel takes the
|
|
118
129
|
* keys and the input row's lead, the menu closes. */
|
|
119
|
-
beginPanel(view: PanelView, onCommit: (v: PanelVerdict) => void
|
|
130
|
+
beginPanel(view: PanelView, onCommit: (v: PanelVerdict) => void, opts?: {
|
|
131
|
+
safer?: () => Promise<readonly SaferOption[] | null>;
|
|
132
|
+
}): void;
|
|
120
133
|
/** W21: cancel the panel — the SIGINT path's pair to beginPanel. */
|
|
121
134
|
cancelPanel(): void;
|
|
122
135
|
/** W21: the compositor's bound view — the phase/selection while the
|
|
@@ -130,4 +143,14 @@ export declare class Editor {
|
|
|
130
143
|
selfRender(): void;
|
|
131
144
|
/** Feed raw stdin bytes — the parser. Public for unit tests. */
|
|
132
145
|
feed(raw: Uint8Array): void;
|
|
146
|
+
/** TUI2-R3v2 ②: the compositor reports where it PUT the option rows.
|
|
147
|
+
* The editor does no row arithmetic of its own — the surface that
|
|
148
|
+
* placed them is the only thing that can say where they are, and a
|
|
149
|
+
* second copy of that sum is how a hit-test comes to disagree with
|
|
150
|
+
* the picture. */
|
|
151
|
+
bindPanelRows(fn: (() => {
|
|
152
|
+
top: number;
|
|
153
|
+
count: number;
|
|
154
|
+
first?: number;
|
|
155
|
+
} | null) | null): void;
|
|
133
156
|
}
|