@vincemakes/kiso-tui 0.9.0 → 0.10.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 +8 -7
- package/dist/ask-panel.js +22 -11
- package/dist/compositor.d.ts +8 -9
- package/dist/compositor.js +185 -28
- package/dist/editor.d.ts +8 -1
- package/dist/editor.js +266 -17
- package/dist/index.d.ts +4 -3
- package/dist/index.js +9 -3
- package/dist/session-picker.d.ts +113 -0
- package/dist/session-picker.js +249 -0
- 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 PanelSel, type PanelState, type PanelView } from "./approval-panel.js";
|
|
30
|
+
import { type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, type PanelPhase, type PanelSel, 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,13 +77,14 @@ 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, sel: PanelSel, W: number, maxRows: number, ask?: AskRuntime): string[];
|
|
81
|
-
export declare function panelLead(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime): string;
|
|
82
|
-
export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime): string;
|
|
83
|
-
export declare function panelStatus(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime): string;
|
|
84
|
-
export declare function panelAffordance(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime): string;
|
|
80
|
+
export declare function panelBlockRows(view: PanelView, phase: PanelPhase, sel: PanelSel, W: number, maxRows: number, ask?: AskRuntime, pick?: PickRuntime): string[];
|
|
81
|
+
export declare function panelLead(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime, pick?: PickRuntime): string;
|
|
82
|
+
export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime, pick?: PickRuntime): string;
|
|
83
|
+
export declare function panelStatus(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime, pick?: PickRuntime): string;
|
|
84
|
+
export declare function panelAffordance(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime, pick?: PickRuntime): string;
|
|
85
85
|
/** The whole panel state in one call — the compositor's four reads share
|
|
86
|
-
* one source, so an ask can never render half as an approval
|
|
86
|
+
* one source, so an ask can never render half as an approval (TUI2-R2
|
|
87
|
+
* ④: nor a pick as either). */
|
|
87
88
|
export declare const panelRowsOf: (s: PanelState, W: number, maxRows: number) => string[];
|
|
88
89
|
export declare const panelLeadOf: (s: PanelState) => string;
|
|
89
90
|
export declare const panelStatusOf: (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, } from "./approval-panel.js";
|
|
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";
|
|
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,38 +225,49 @@ 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, sel, W, maxRows, ask) {
|
|
228
|
+
export function panelBlockRows(view, phase, sel, W, maxRows, ask, pick) {
|
|
229
|
+
if (view.pick !== undefined && pick !== undefined)
|
|
230
|
+
return pickBlockRows(view, pick, W, maxRows);
|
|
229
231
|
if (view.ask !== undefined && ask !== undefined)
|
|
230
232
|
return askBlockRows(view, ask, W, maxRows);
|
|
231
233
|
return basePanelBlockRows(view, phase, sel, W, maxRows);
|
|
232
234
|
}
|
|
233
|
-
export function panelLead(view, phase, sel, ask) {
|
|
235
|
+
export function panelLead(view, phase, sel, ask, pick) {
|
|
234
236
|
const p = palette();
|
|
237
|
+
if (view.pick !== undefined && pick !== undefined)
|
|
238
|
+
return pickLead(view, pick);
|
|
235
239
|
if (view.ask !== undefined && ask !== undefined)
|
|
236
240
|
return `${p.bold}${askLeadPlain(ask)}${p.reset}`;
|
|
237
241
|
return basePanelLead(view, phase, sel);
|
|
238
242
|
}
|
|
239
|
-
export function panelLeadPlain(view, phase, sel, ask) {
|
|
243
|
+
export function panelLeadPlain(view, phase, sel, ask, pick) {
|
|
244
|
+
if (view.pick !== undefined && pick !== undefined)
|
|
245
|
+
return pickLeadPlain(view, pick);
|
|
240
246
|
if (view.ask !== undefined && ask !== undefined)
|
|
241
247
|
return askLeadPlain(ask);
|
|
242
248
|
return basePanelLeadPlain(view, phase, sel);
|
|
243
249
|
}
|
|
244
|
-
export function panelStatus(view, phase, sel, ask) {
|
|
250
|
+
export function panelStatus(view, phase, sel, ask, pick) {
|
|
251
|
+
if (view.pick !== undefined && pick !== undefined)
|
|
252
|
+
return pickStatus(view);
|
|
245
253
|
if (view.ask !== undefined && ask !== undefined)
|
|
246
254
|
return askStatus(view, ask);
|
|
247
255
|
return basePanelStatus(view, phase, sel);
|
|
248
256
|
}
|
|
249
|
-
export function panelAffordance(view, phase, sel, ask) {
|
|
257
|
+
export function panelAffordance(view, phase, sel, ask, pick) {
|
|
258
|
+
if (view.pick !== undefined && pick !== undefined)
|
|
259
|
+
return pickAffordance(pick);
|
|
250
260
|
if (view.ask !== undefined && ask !== undefined)
|
|
251
261
|
return askAffordance(ask);
|
|
252
262
|
return basePanelAffordance(view, phase, sel);
|
|
253
263
|
}
|
|
254
264
|
/** The whole panel state in one call — the compositor's four reads share
|
|
255
|
-
* one source, so an ask can never render half as an approval
|
|
256
|
-
|
|
257
|
-
export const
|
|
258
|
-
export const
|
|
259
|
-
export const
|
|
265
|
+
* one source, so an ask can never render half as an approval (TUI2-R2
|
|
266
|
+
* ④: nor a pick as either). */
|
|
267
|
+
export const panelRowsOf = (s, W, maxRows) => panelBlockRows(s.view, s.phase, s.sel, W, maxRows, s.ask, s.pick);
|
|
268
|
+
export const panelLeadOf = (s) => panelLead(s.view, s.phase, s.sel, s.ask, s.pick);
|
|
269
|
+
export const panelStatusOf = (s) => panelStatus(s.view, s.phase, s.sel, s.ask, s.pick);
|
|
270
|
+
export const panelAffordanceOf = (s) => panelAffordance(s.view, s.phase, s.sel, s.ask, s.pick);
|
|
260
271
|
// ── the view: what the human reads when the model asks ────────────────
|
|
261
272
|
/** The ask's PanelView. The dock-less fallback question is HONEST: a
|
|
262
273
|
* terminal without a panel cannot walk options, so it says the ask is
|
package/dist/compositor.d.ts
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
import { type MenuItem } from "./editor.js";
|
|
46
46
|
import type { PanelState } from "./approval-panel.js";
|
|
47
47
|
import { type AtMatch } from "./at-picker.js";
|
|
48
|
+
import { type SessionPickState } from "./session-picker.js";
|
|
48
49
|
/** KC3 §4 — the @ picker's bound state (the editor's atState()). */
|
|
49
50
|
export interface AtPanelState {
|
|
50
51
|
readonly matches: readonly AtMatch[];
|
|
@@ -178,15 +179,6 @@ export declare class Body {
|
|
|
178
179
|
isError: boolean;
|
|
179
180
|
};
|
|
180
181
|
} | null;
|
|
181
|
-
/** W15 — the expand key's target (ctrl+r). A cell still in the LIVE
|
|
182
|
-
* region (the newest live tool) TOGGLES in place — the compositor
|
|
183
|
-
* owns those rows and redraws them (the body flips to the full
|
|
184
|
-
* form, no cap). A committed cell can never toggle — history is
|
|
185
|
-
* never rewritten (ADR-0046) — so the key APPENDS a fresh expanded
|
|
186
|
-
* block at the bottom instead, the /last idiom aimed at a chosen
|
|
187
|
-
* cell: the pointer cycles the collapsed history, newest first, and
|
|
188
|
-
* the header names the target ("N turns back" — the user cells
|
|
189
|
-
* after it), so every press tells the user what they got. */
|
|
190
182
|
expandNext(): {
|
|
191
183
|
kind: "toggled";
|
|
192
184
|
} | {
|
|
@@ -232,6 +224,9 @@ export declare class Body {
|
|
|
232
224
|
* band — see #menuRows for why that is a decision and not a
|
|
233
225
|
* shortcut. */
|
|
234
226
|
bindAt(state: () => AtPanelState | null): void;
|
|
227
|
+
/** TUI2-R2 ②: bind the editor's session picker — the band's third
|
|
228
|
+
* occupant (see #menuRows for why they share one). */
|
|
229
|
+
bindPick(state: () => SessionPickState | null): void;
|
|
235
230
|
/** Bind the pending-turn queue — the CLI's live slots (chat.ts):
|
|
236
231
|
* the chips render in the menu-rows family, the live caps shrink
|
|
237
232
|
* by their rows, and the +N queued hint rides the status row. */
|
|
@@ -280,6 +275,10 @@ export declare class Dock {
|
|
|
280
275
|
* menu (see Body#menuRows). Unbound, the picker cannot render, and
|
|
281
276
|
* every frame is byte-identical to before the round. */
|
|
282
277
|
bindAt(state: () => AtPanelState | null): void;
|
|
278
|
+
/** TUI2-R2 ②: bind the editor's session picker — the band's third
|
|
279
|
+
* occupant. Unbound (every path but bare `kiso resume`), the picker
|
|
280
|
+
* cannot render and every frame is byte-identical to before. */
|
|
281
|
+
bindPick(state: () => SessionPickState | null): void;
|
|
283
282
|
/** W22: bind the pending-turn queue — the chips + the +N queued
|
|
284
283
|
* hint (the CLI binds it from chat(); the editor's pop keys ride
|
|
285
284
|
* the LineInput's own bindQueue). */
|
package/dist/compositor.js
CHANGED
|
@@ -49,9 +49,11 @@ import { leadWidth } from "./width.js"; // W23: the ONE width authority (the edi
|
|
|
49
49
|
// for four reads, so an ask can never render half as an approval.
|
|
50
50
|
import { panelAffordanceOf, panelLeadOf, panelRowsOf, panelStatusOf } from "./ask-panel.js";
|
|
51
51
|
import { atPanelRows, bandHeader } from "./at-picker.js";
|
|
52
|
-
|
|
52
|
+
// TUI2-R2 ②: the session picker's rows — the band's third occupant.
|
|
53
|
+
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";
|
|
53
55
|
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
|
|
54
|
-
import { keysSheetRows } from "./strings.js";
|
|
56
|
+
import { displayVerb, keysSheetRows } from "./strings.js";
|
|
55
57
|
/** The cursor marker — an APC private sequence the focus component
|
|
56
58
|
* embeds at the edit position; the compositor strips it and moves
|
|
57
59
|
* relatively (it never reaches the terminal). */
|
|
@@ -134,6 +136,9 @@ export class Body {
|
|
|
134
136
|
// KC3 §4: the @ picker's bound state — the SAME band as the menu
|
|
135
137
|
// (see #menuRows: the two are mutually exclusive by construction).
|
|
136
138
|
#atState = null;
|
|
139
|
+
// TUI2-R2 ②: the session picker's bound state — the same band again
|
|
140
|
+
// (see #menuRows), and modal, so it takes the band first.
|
|
141
|
+
#pickState = null;
|
|
137
142
|
// W22: the pending-turn queue's bound state — the CLI's live slots
|
|
138
143
|
// (chat.ts); the chips render in the menu-rows family (above the
|
|
139
144
|
// box top), the live caps shrink by their rows, and the status
|
|
@@ -146,6 +151,16 @@ export class Body {
|
|
|
146
151
|
// v6: the single writer — the compositor IS the dock; the CLI's
|
|
147
152
|
// onDock callback (which used to re-pin the dock after a scroll)
|
|
148
153
|
// is retired with the split.
|
|
154
|
+
// TUI2-R2pre ③: taking the ref SUPERSEDES whatever held it, so the
|
|
155
|
+
// outgoing compositor's resize listener comes off here. It is not
|
|
156
|
+
// only listener hygiene: every Dock call now reaches THIS instance,
|
|
157
|
+
// so a resize heard by the old one would have a compositor that owns
|
|
158
|
+
// no part of the screen paint a full redraw over it.
|
|
159
|
+
// (an explicit null check, not `?.#` — TS18030: an optional chain
|
|
160
|
+
// cannot contain a private identifier, and vitest transpiles without
|
|
161
|
+
// type-checking, so only `npm run typecheck` sees the difference)
|
|
162
|
+
if (compositorRef !== null)
|
|
163
|
+
compositorRef.#detachResize();
|
|
149
164
|
compositorRef = this;
|
|
150
165
|
// the Dock façade's bindings may arrive BEFORE this construction
|
|
151
166
|
// (the CLI binds the editor state in makeLineInput, then constructs
|
|
@@ -162,6 +177,8 @@ export class Body {
|
|
|
162
177
|
this.#menuState = dockBindings.menu;
|
|
163
178
|
if (dockBindings.at !== null)
|
|
164
179
|
this.#atState = dockBindings.at;
|
|
180
|
+
if (dockBindings.pick !== null)
|
|
181
|
+
this.#pickState = dockBindings.pick;
|
|
165
182
|
this.#panelState = dockBindings.panel;
|
|
166
183
|
this.#sheetState = dockBindings.sheet;
|
|
167
184
|
if (dockBindings.queue !== null)
|
|
@@ -570,6 +587,30 @@ export class Body {
|
|
|
570
587
|
* cell: the pointer cycles the collapsed history, newest first, and
|
|
571
588
|
* the header names the target ("N turns back" — the user cells
|
|
572
589
|
* after it), so every press tells the user what they got. */
|
|
590
|
+
/**
|
|
591
|
+
* TUI2-R2 ⑤ — the cell the next ctrl+r will act on, or -1.
|
|
592
|
+
*
|
|
593
|
+
* The rule is expandNext's own first loop, extracted verbatim: the
|
|
594
|
+
* LAST live cell that can toggle. It is a separate method rather than
|
|
595
|
+
* a shared constant because the marker and the key must not merely
|
|
596
|
+
* agree today — the marker is a PROMISE about what the key will do,
|
|
597
|
+
* and the only way to keep it is to derive it from the same scan.
|
|
598
|
+
*
|
|
599
|
+
* The committed fallback (the #collapsed ring) is deliberately NOT
|
|
600
|
+
* marked: those rows are frozen history, never re-emitted, so a tint
|
|
601
|
+
* on them could not be moved when the pointer advances. A live target
|
|
602
|
+
* is the one the marker can tell the truth about.
|
|
603
|
+
*/
|
|
604
|
+
#focusIndex() {
|
|
605
|
+
for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
|
|
606
|
+
const cell = this.#cells[i];
|
|
607
|
+
if (cell.kind === "tool" && cell.state !== "pending")
|
|
608
|
+
return i;
|
|
609
|
+
if (cell.kind === "checklist" && !cell.done)
|
|
610
|
+
return i;
|
|
611
|
+
}
|
|
612
|
+
return -1;
|
|
613
|
+
}
|
|
573
614
|
expandNext() {
|
|
574
615
|
for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
|
|
575
616
|
const cell = this.#cells[i];
|
|
@@ -612,7 +653,7 @@ export class Body {
|
|
|
612
653
|
return { kind: "appended", lines: [header, ...exploreRows(cell.rolled.parts, this.#opts.width())] };
|
|
613
654
|
}
|
|
614
655
|
const noun = ROLLUP_NOUN[cell.name] ?? "calls";
|
|
615
|
-
const header = `${p.bold}▞${p.reset} expanded · ${escapeTerminal(`${cell.name
|
|
656
|
+
const header = `${p.bold}▞${p.reset} expanded · ${escapeTerminal(`${displayVerb(cell.name)} ${cell.rolled.count} ${noun}`)} · ${back}`;
|
|
616
657
|
return {
|
|
617
658
|
kind: "appended",
|
|
618
659
|
lines: [header, ...cell.rolled.targets.map((t) => ` ${p.dim}└ ${escapeTerminal(t)}${p.reset}`)],
|
|
@@ -628,14 +669,16 @@ export class Body {
|
|
|
628
669
|
}
|
|
629
670
|
const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
|
|
630
671
|
const p = palette();
|
|
631
|
-
const header = `${p.bold}▞${p.reset} expanded · ${escapeTerminal(`${cell.name
|
|
672
|
+
const header = `${p.bold}▞${p.reset} expanded · ${escapeTerminal(`${displayVerb(cell.name)} ${toolTarget(cell.name, input)}`)} · ${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`;
|
|
632
673
|
return {
|
|
633
674
|
kind: "appended",
|
|
634
675
|
lines: [
|
|
635
676
|
header,
|
|
636
|
-
|
|
677
|
+
// TUI2-R2pre ④: the SECTION HEADERS say the act; the payloads
|
|
678
|
+
// below them (inputFull, resultText) are RAW and byte-identical.
|
|
679
|
+
`--- ${displayVerb(cell.name)} input ---`,
|
|
637
680
|
cell.inputFull,
|
|
638
|
-
`--- ${cell.name} output${cell.isError ? " (error)" : ""} ---`,
|
|
681
|
+
`--- ${displayVerb(cell.name)} output${cell.isError ? " (error)" : ""} ---`,
|
|
639
682
|
cell.resultText,
|
|
640
683
|
],
|
|
641
684
|
};
|
|
@@ -645,13 +688,29 @@ export class Body {
|
|
|
645
688
|
get active() {
|
|
646
689
|
return this.#docked && this.#isActive();
|
|
647
690
|
}
|
|
691
|
+
/** TUI2-R2pre ③ — the ONE place a resize listener is installed, and it
|
|
692
|
+
* removes the previous one first. `process.stdout` is process-wide and
|
|
693
|
+
* its listeners outlive the object that added them, so "add" without
|
|
694
|
+
* "remove first" is a leak by construction: the old closure is
|
|
695
|
+
* unreachable the moment #resizeHandler is overwritten, and not even
|
|
696
|
+
* exit() can take it off. */
|
|
697
|
+
#attachResize() {
|
|
698
|
+
this.#detachResize();
|
|
699
|
+
this.#resizeHandler = () => this.onResize();
|
|
700
|
+
process.stdout.on("resize", this.#resizeHandler);
|
|
701
|
+
}
|
|
702
|
+
#detachResize() {
|
|
703
|
+
if (this.#resizeHandler === null)
|
|
704
|
+
return;
|
|
705
|
+
process.stdout.off("resize", this.#resizeHandler);
|
|
706
|
+
this.#resizeHandler = null;
|
|
707
|
+
}
|
|
648
708
|
enter() {
|
|
649
709
|
const rows = process.stdout.rows ?? 0;
|
|
650
710
|
if (process.stdout.isTTY !== true || palette().bold === "" || rows < 4)
|
|
651
711
|
return;
|
|
652
712
|
this.#docked = true;
|
|
653
|
-
this.#
|
|
654
|
-
process.stdout.on("resize", this.#resizeHandler);
|
|
713
|
+
this.#attachResize();
|
|
655
714
|
this.#fullRedraw = true;
|
|
656
715
|
this.#dirty = true;
|
|
657
716
|
this.render(); // the FIRST frame — the full-redraw path, no pre-clear
|
|
@@ -659,13 +718,16 @@ export class Body {
|
|
|
659
718
|
/** Teardown — CSI r (the "no broken terminal" contract byte), the
|
|
660
719
|
* chrome rows cleared, the cursor home at the input line. */
|
|
661
720
|
exit() {
|
|
662
|
-
if (!this.#docked)
|
|
721
|
+
if (!this.#docked) {
|
|
722
|
+
// TUI2-R2pre ③: an un-docked compositor can still hold a listener
|
|
723
|
+
// (it was superseded, or enter() ran and the dock was torn down by
|
|
724
|
+
// another path) — the teardown is unconditional, the CHROME clear
|
|
725
|
+
// below is not.
|
|
726
|
+
this.#detachResize();
|
|
663
727
|
return;
|
|
664
|
-
this.#docked = false;
|
|
665
|
-
if (this.#resizeHandler !== null) {
|
|
666
|
-
process.stdout.off("resize", this.#resizeHandler);
|
|
667
|
-
this.#resizeHandler = null;
|
|
668
728
|
}
|
|
729
|
+
this.#docked = false;
|
|
730
|
+
this.#detachResize();
|
|
669
731
|
const H = this.#lastH > 0 ? this.#lastH : process.stdout.rows ?? 24;
|
|
670
732
|
const out = [];
|
|
671
733
|
out.push("\x1b[r");
|
|
@@ -750,6 +812,11 @@ export class Body {
|
|
|
750
812
|
bindAt(state) {
|
|
751
813
|
this.#atState = state;
|
|
752
814
|
}
|
|
815
|
+
/** TUI2-R2 ②: bind the editor's session picker — the band's third
|
|
816
|
+
* occupant (see #menuRows for why they share one). */
|
|
817
|
+
bindPick(state) {
|
|
818
|
+
this.#pickState = state;
|
|
819
|
+
}
|
|
753
820
|
/** Bind the pending-turn queue — the CLI's live slots (chat.ts):
|
|
754
821
|
* the chips render in the menu-rows family, the live caps shrink
|
|
755
822
|
* by their rows, and the +N queued hint rides the status row. */
|
|
@@ -960,9 +1027,21 @@ export class Body {
|
|
|
960
1027
|
liveLines = panelRowsOf(panel, W, Math.max(1, H - 4 - inputExtra - queueRows.length));
|
|
961
1028
|
}
|
|
962
1029
|
else {
|
|
1030
|
+
// TUI2-R2 ⑤ (D, candidate 1): the FOCUS — the cell the next ctrl+r
|
|
1031
|
+
// will act on brightens its own token. The index is derived from
|
|
1032
|
+
// the SAME scan expandNext performs (#focusIndex shares its rule
|
|
1033
|
+
// by construction), so the marker can never point at a cell the
|
|
1034
|
+
// key would not take — which is the only way a focus marker is
|
|
1035
|
+
// worth having.
|
|
1036
|
+
const focus = this.#focusIndex();
|
|
963
1037
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
964
|
-
for (
|
|
1038
|
+
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1039
|
+
const cell = this.#cells[i];
|
|
965
1040
|
const rows = cellComponent(cell).render(W, ctx);
|
|
1041
|
+
// the head row carries the affordance; the tint lands on it and
|
|
1042
|
+
// nowhere else, which is what makes "exactly one" structural
|
|
1043
|
+
if (i === focus && rows.length > 0)
|
|
1044
|
+
rows[0] = focusToken(rows[0], W);
|
|
966
1045
|
liveLines.push(...bodySpacing(prev, rows));
|
|
967
1046
|
prev = rows;
|
|
968
1047
|
}
|
|
@@ -975,9 +1054,15 @@ export class Body {
|
|
|
975
1054
|
this.#commitCell(this.#committed, W, ctx);
|
|
976
1055
|
liveLines = [];
|
|
977
1056
|
{
|
|
1057
|
+
// TUI2-R2 ⑤: the focus re-derives after a commit — the cell it
|
|
1058
|
+
// pointed at may have just left the live region
|
|
1059
|
+
const focus = this.#focusIndex();
|
|
978
1060
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
979
|
-
for (
|
|
1061
|
+
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1062
|
+
const cell = this.#cells[i];
|
|
980
1063
|
const rows = cellComponent(cell).render(W, ctx);
|
|
1064
|
+
if (i === focus && rows.length > 0)
|
|
1065
|
+
rows[0] = focusToken(rows[0], W);
|
|
981
1066
|
liveLines.push(...bodySpacing(prev, rows));
|
|
982
1067
|
prev = rows;
|
|
983
1068
|
}
|
|
@@ -1289,6 +1374,15 @@ export class Body {
|
|
|
1289
1374
|
* occupant knowing the other exists.
|
|
1290
1375
|
*/
|
|
1291
1376
|
#menuRows(W) {
|
|
1377
|
+
// TUI2-R2 ②: the session picker is the band's THIRD occupant and
|
|
1378
|
+
// takes it first. It is modal — it opens before a session exists,
|
|
1379
|
+
// so neither the menu nor the @ picker can be up beside it — and
|
|
1380
|
+
// riding this channel buys it the same geometry every other band
|
|
1381
|
+
// occupant already has: counted in chromeRows, clamped with the
|
|
1382
|
+
// composer, redrawn with the frame.
|
|
1383
|
+
const pick = this.#pickState?.() ?? null;
|
|
1384
|
+
if (pick !== null)
|
|
1385
|
+
return sessionPickerRows(pick, W, Date.now());
|
|
1292
1386
|
const at = this.#atState?.() ?? null;
|
|
1293
1387
|
if (at !== null)
|
|
1294
1388
|
return atPanelRows(at, W);
|
|
@@ -1503,7 +1597,20 @@ export class Body {
|
|
|
1503
1597
|
if (leaving < skip)
|
|
1504
1598
|
out.push(`\x1b[${leaving + 1};1H\x1b[0J`);
|
|
1505
1599
|
out.push(`\x1b[${H};1H`);
|
|
1506
|
-
|
|
1600
|
+
// TUI2-R2pre ② — scroll the rows that LEFT THE WINDOW SINCE THE
|
|
1601
|
+
// LAST FRAME (`leaving`), never `skip`, which is the window's
|
|
1602
|
+
// ABSOLUTE top. Scrolling the absolute top re-pushed the whole
|
|
1603
|
+
// history's worth of rows on EVERY full redraw — and a live-region
|
|
1604
|
+
// shrink takes this path, so that was most frames of a real
|
|
1605
|
+
// session. The ED above had just blanked everything below row
|
|
1606
|
+
// `leaving`, so what those surplus LFs carried into the terminal's
|
|
1607
|
+
// scrollback was blank rows: the large blank bands mid-history of
|
|
1608
|
+
// the owner's field report. The SCREEN never showed it because the
|
|
1609
|
+
// repaint below covers every row 1..H (the V6-1 rule), and the
|
|
1610
|
+
// house emulator drops scrolled rows on the floor — so no gate
|
|
1611
|
+
// could see it either. Measured on the 5-turn 80x24 repro: the
|
|
1612
|
+
// scrollback went from 162 blank rows of 168 to 14 of 52.
|
|
1613
|
+
for (let i = 0; i < leaving; i += 1)
|
|
1507
1614
|
out.push("\n");
|
|
1508
1615
|
}
|
|
1509
1616
|
if (!overlay)
|
|
@@ -1549,8 +1656,7 @@ export class Body {
|
|
|
1549
1656
|
// retired (the CHA is absolute — the base is irrelevant; the
|
|
1550
1657
|
// CUB's base was the LAST write's end column, which the steady
|
|
1551
1658
|
// frame's ELs leave at col 1 — the A3 finding)
|
|
1552
|
-
out
|
|
1553
|
-
out.push(`\x1b[${editor.markerCol}G`);
|
|
1659
|
+
this.#parkCursor(out, H, H - 2 - inputExtra + editor.markerRow, editor.markerCol);
|
|
1554
1660
|
}
|
|
1555
1661
|
/** The steady-state frame — RELATIVE moves only (invariant ②); the
|
|
1556
1662
|
* commits scroll via the CUP-free real LF at the last row, and the
|
|
@@ -1626,6 +1732,16 @@ export class Body {
|
|
|
1626
1732
|
if (H > anchorRow)
|
|
1627
1733
|
out.push(`\x1b[${H - anchorRow}B`);
|
|
1628
1734
|
}
|
|
1735
|
+
// TUI2-R2pre ②: this count is the COMMIT count on purpose, and it
|
|
1736
|
+
// stays. It reads like the same mistake the full path made, but the
|
|
1737
|
+
// two paths are doing different jobs: the full path REPAINTS every
|
|
1738
|
+
// row, so anything it scrolls is a duplicate of what it is about to
|
|
1739
|
+
// draw; the steady path does not repaint the frozen band, and the
|
|
1740
|
+
// rows it scrolls carry the PRE-FRAME live copies of the cells that
|
|
1741
|
+
// just committed — the A7 single-copy discipline (the old live band
|
|
1742
|
+
// is EL'd first, so the repaint below is the only copy left). Making
|
|
1743
|
+
// this `leaving` was measured: the A7 gate fails at 40x24 frame 106
|
|
1744
|
+
// with the greeting duplicated in the terminal.
|
|
1629
1745
|
for (let i = 0; i < committed.length; i += 1)
|
|
1630
1746
|
out.push("\n");
|
|
1631
1747
|
// the bottom-up repaint, from the last row up — V6-3 + W6 + KC1:
|
|
@@ -1672,6 +1788,11 @@ export class Body {
|
|
|
1672
1788
|
}
|
|
1673
1789
|
// 2. the STALE rows above the committed section — the scrolled old
|
|
1674
1790
|
// live copies (a live-drawn cell's pre-commit position): EL.
|
|
1791
|
+
// TUI2-R2pre ②: the old band's POST-SCROLL origin shifted up by
|
|
1792
|
+
// the rows that actually left — `leaving`. It read the commit
|
|
1793
|
+
// count only because the scroll above used to BE the commit
|
|
1794
|
+
// count; with the two decoupled, the old expression erases rows
|
|
1795
|
+
// of frozen content that never moved.
|
|
1675
1796
|
const staleFrom = Math.max(1, this.#lastLiveTop - committed.length);
|
|
1676
1797
|
for (let r = staleFrom; r < liveTop - committed.length; r += 1) {
|
|
1677
1798
|
out.push(`\x1b[${r};1H\x1b[0K`);
|
|
@@ -1708,21 +1829,47 @@ export class Body {
|
|
|
1708
1829
|
: H - 3 - inputExtra;
|
|
1709
1830
|
// the anchor: the MARKER'S row inside the composer (N = 1,
|
|
1710
1831
|
// markerRow 0 ⇒ the retired H−2)
|
|
1711
|
-
|
|
1712
|
-
if (down > 0)
|
|
1713
|
-
out.push(`\x1b[${down}B`);
|
|
1714
|
-
// W23: the CHA to the frame-derived column — the cursor rests AT
|
|
1715
|
-
// the marker from ANY base (the retired afterW CUB clamped at col
|
|
1716
|
-
// 1 — the steady frame's LAST write is the gap/stale EL: the A3
|
|
1717
|
-
// finding; the A5/A8 live lines end mid-row, the ELs at col 1 —
|
|
1718
|
-
// the CHA ignores the base by construction)
|
|
1719
|
-
out.push(`\x1b[${editor.markerCol}G`);
|
|
1832
|
+
this.#parkCursor(out, lastRow, H - 2 - inputExtra + editor.markerRow, editor.markerCol);
|
|
1720
1833
|
// A8b: the steady path moves the window too (the scroll + the
|
|
1721
1834
|
// repaint) — record its top so the next full-redraw's leaving count
|
|
1722
1835
|
// is the rows the window dropped since the last frame, whatever the
|
|
1723
1836
|
// path of the frames between (same formula as `skip` above).
|
|
1724
1837
|
this.#lastSkip = skip;
|
|
1725
1838
|
}
|
|
1839
|
+
/**
|
|
1840
|
+
* TUI2-R2 ⑤ (the R1.5 parked ⑩) — CURSOR AUTHORITY: the ONE frame-tail
|
|
1841
|
+
* positioning sequence, and the compositor's alone.
|
|
1842
|
+
*
|
|
1843
|
+
* Both draw paths ended with their own hand-rolled park — the full
|
|
1844
|
+
* path counting rows up from the status line, the steady path counting
|
|
1845
|
+
* down from a six-branch re-derivation of which write happened to be
|
|
1846
|
+
* last. Two implementations of one contract, each re-deriving byte
|
|
1847
|
+
* order the drawing code already knew, and the walkthrough found the
|
|
1848
|
+
* consequence three times over (the cursor resting in the status
|
|
1849
|
+
* line's "de▮ault", at the end of streamed text, inside an approval
|
|
1850
|
+
* panel's rule row). A terminal cursor is the product's claim about
|
|
1851
|
+
* where the next keystroke lands; a claim made in two places is a
|
|
1852
|
+
* claim that will eventually disagree with itself.
|
|
1853
|
+
*
|
|
1854
|
+
* One owner, one sequence: a single vertical move to the marker's row
|
|
1855
|
+
* — in EITHER direction, which the steady path could not do (its move
|
|
1856
|
+
* was `if (down > 0)`, so a cursor left BELOW the composer simply
|
|
1857
|
+
* stayed there) — then the CHA to the frame-derived column.
|
|
1858
|
+
*
|
|
1859
|
+
* Relative, not a CUP, and deliberately: invariant ② reserves absolute
|
|
1860
|
+
* addressing for the content area, and the composer is chrome. The
|
|
1861
|
+
* CHA is absolute in the COLUMN only, which is what makes the park
|
|
1862
|
+
* independent of wherever the last write ended (the A3 finding: the
|
|
1863
|
+
* retired CUB's base was the gap EL's column 1, left of the lead).
|
|
1864
|
+
*/
|
|
1865
|
+
#parkCursor(out, fromRow, toRow, col) {
|
|
1866
|
+
const delta = toRow - fromRow;
|
|
1867
|
+
if (delta > 0)
|
|
1868
|
+
out.push(`\x1b[${delta}B`);
|
|
1869
|
+
else if (delta < 0)
|
|
1870
|
+
out.push(`\x1b[${-delta}A`);
|
|
1871
|
+
out.push(`\x1b[${col}G`);
|
|
1872
|
+
}
|
|
1726
1873
|
/** Invariant ①: every emitted line fits the width — a violation is a
|
|
1727
1874
|
* CRASH with the diagnostic, never a silent truncate. */
|
|
1728
1875
|
#checked(line, W) {
|
|
@@ -1825,6 +1972,16 @@ export class Dock {
|
|
|
1825
1972
|
}
|
|
1826
1973
|
compositorRef.bindAt(state);
|
|
1827
1974
|
}
|
|
1975
|
+
/** TUI2-R2 ②: bind the editor's session picker — the band's third
|
|
1976
|
+
* occupant. Unbound (every path but bare `kiso resume`), the picker
|
|
1977
|
+
* cannot render and every frame is byte-identical to before. */
|
|
1978
|
+
bindPick(state) {
|
|
1979
|
+
if (compositorRef === null) {
|
|
1980
|
+
dockBindings.pick = state;
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
compositorRef.bindPick(state);
|
|
1984
|
+
}
|
|
1828
1985
|
/** W22: bind the pending-turn queue — the chips + the +N queued
|
|
1829
1986
|
* hint (the CLI binds it from chat(); the editor's pop keys ride
|
|
1830
1987
|
* the LineInput's own bindQueue). */
|
|
@@ -1850,4 +2007,4 @@ let compositorRef = null;
|
|
|
1850
2007
|
* — the old snapshot froze `menu` at bindInput time and the slash-
|
|
1851
2008
|
* command menu silently never bound in the real CLI (the e2e gates
|
|
1852
2009
|
* bind the Body directly and could not see it). */
|
|
1853
|
-
const dockBindings = { state: null, prompt: "", menu: null, at: null, panel: null, sheet: null, queue: null };
|
|
2010
|
+
const dockBindings = { state: null, prompt: "", menu: null, at: null, pick: null, panel: null, sheet: null, queue: null };
|
package/dist/editor.d.ts
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
25
25
|
export { charWidth, displayWidth, widthOf };
|
|
26
|
-
import type
|
|
26
|
+
import { type PanelState, type PanelVerdict, type PanelView } from "./approval-panel.js";
|
|
27
27
|
import { type AtItem, type AtMatch } from "./at-picker.js";
|
|
28
|
+
import { type SessionCardView, type SessionPickState } from "./session-picker.js";
|
|
28
29
|
export declare const PROMPT = "\u258C ";
|
|
29
30
|
export declare const PROMPT_WIDTH: number;
|
|
30
31
|
/** v3 §04 — the slash-command menu's command table (English one-liners). */
|
|
@@ -101,6 +102,12 @@ export declare class Editor {
|
|
|
101
102
|
selected: number;
|
|
102
103
|
capped: boolean;
|
|
103
104
|
} | null;
|
|
105
|
+
/** Open the picker on a bound card source. The composer is cleared
|
|
106
|
+
* (the buffer becomes the filter query) and `onPick` receives the
|
|
107
|
+
* chosen id — or null when the human leaves without picking, which
|
|
108
|
+
* is a first-class outcome and not an error. */
|
|
109
|
+
beginPick(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void): void;
|
|
110
|
+
pickState(): SessionPickState | null;
|
|
104
111
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
105
112
|
question(_query: string, cb: (answer: string) => void): void;
|
|
106
113
|
/** Cancel a pending question — the buffer stays (its text becomes the
|