omk-tui 1.4.0 → 1.4.1
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/CHANGELOG.md +11 -0
- package/dist/components/editor.d.ts +3 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +4 -7
- package/dist/components/editor.js.map +1 -1
- package/dist/editor-component.d.ts +2 -0
- package/dist/editor-component.d.ts.map +1 -1
- package/dist/editor-component.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +2 -1
- package/dist/tui.js.map +1 -1
- package/dist/windowed-container.d.ts +61 -4
- package/dist/windowed-container.d.ts.map +1 -1
- package/dist/windowed-container.js +126 -127
- package/dist/windowed-container.js.map +1 -1
- package/dist/windowed-segments.d.ts +37 -0
- package/dist/windowed-segments.d.ts.map +1 -0
- package/dist/windowed-segments.js +133 -0
- package/dist/windowed-segments.js.map +1 -0
- package/package.json +1 -1
|
@@ -6,6 +6,8 @@ export interface WindowedRenderStats {
|
|
|
6
6
|
refreshedChildren: number;
|
|
7
7
|
/** Frozen segments whose line buffer was rebuilt this frame. */
|
|
8
8
|
refreshedSegments: number;
|
|
9
|
+
/** Settled children skipped entirely this frame (virtual prefix). */
|
|
10
|
+
virtualChildren: number;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Container that freezes settled children above a live-line budget into
|
|
@@ -19,6 +21,15 @@ export interface WindowedRenderStats {
|
|
|
19
21
|
* `invalidate()`, `clear()` and foreign edits of `children` thaw everything.
|
|
20
22
|
* - Freezing releases the child's render caches (frozen lines are the only
|
|
21
23
|
* retained copy); a later change re-renders that one child from source.
|
|
24
|
+
*
|
|
25
|
+
* Virtual prefix (opt-in via `setPrefixSeed`/`setPrefixSeedViewport`):
|
|
26
|
+
* when a prefix seed is configured, children above the live budget PLUS the
|
|
27
|
+
* seed budget never render at all — `render()` returns zero lines for them.
|
|
28
|
+
* Rendered line work is bounded by live + seed; prefix identity/settledness
|
|
29
|
+
* checks still visit skipped children. The
|
|
30
|
+
* prefix is a contiguous run anchored at index 0; children above the seed
|
|
31
|
+
* budget never enter the output, so callers that need the whole transcript
|
|
32
|
+
* (`buildFinalSessionLog`, exports) must use `renderAll(width)`.
|
|
22
33
|
*/
|
|
23
34
|
export declare class WindowedContainer extends Container {
|
|
24
35
|
private segments;
|
|
@@ -27,6 +38,18 @@ export declare class WindowedContainer extends Container {
|
|
|
27
38
|
private viewportRows?;
|
|
28
39
|
private lastLiveLineBudget;
|
|
29
40
|
private stats;
|
|
41
|
+
/**
|
|
42
|
+
* Children above live+seed that never render (opt-in). Contiguous from
|
|
43
|
+
* index 0; emits zero lines every frame once established.
|
|
44
|
+
*/
|
|
45
|
+
private virtualChildren;
|
|
46
|
+
/** Unset preserves full-history rendering; zero seeds only the live tail. */
|
|
47
|
+
private seedLines?;
|
|
48
|
+
private seedViewportRows?;
|
|
49
|
+
private seedScreens?;
|
|
50
|
+
private lastPrefixSeedLines?;
|
|
51
|
+
/** Bound `isChildSettled` for the extracted segment helpers. */
|
|
52
|
+
private readonly settled;
|
|
30
53
|
/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */
|
|
31
54
|
setLiveLineBudget(lines: number): void;
|
|
32
55
|
/**
|
|
@@ -35,6 +58,19 @@ export declare class WindowedContainer extends Container {
|
|
|
35
58
|
*/
|
|
36
59
|
setViewportRows(rows: () => number): void;
|
|
37
60
|
getLiveLineBudget(): number;
|
|
61
|
+
/**
|
|
62
|
+
* Opt in to the virtual prefix: settled children above
|
|
63
|
+
* `live budget + seedLines` never render — the first frame of a long
|
|
64
|
+
* transcript renders only live + seed lines. Zero seeds only the live
|
|
65
|
+
* tail; unset or non-finite input restores full-history rendering.
|
|
66
|
+
*/
|
|
67
|
+
setPrefixSeed(lines?: number): void;
|
|
68
|
+
/** Seed budget follows the terminal height: `screens × rows`, re-read on every render. */
|
|
69
|
+
setPrefixSeedViewport(screens: number, rows: () => number): void;
|
|
70
|
+
/** Current seed budget in lines; undefined means virtualization is off. */
|
|
71
|
+
getPrefixSeedLines(): number | undefined;
|
|
72
|
+
/** Children skipped entirely by the last render (virtual prefix). */
|
|
73
|
+
getVirtualChildCount(): number;
|
|
38
74
|
getFrozenChildCount(): number;
|
|
39
75
|
getFrozenLineCount(): number;
|
|
40
76
|
/** Diagnostics: number of frozen segments. */
|
|
@@ -51,13 +87,34 @@ export declare class WindowedContainer extends Container {
|
|
|
51
87
|
removeChild(component: Component): void;
|
|
52
88
|
clear(): void;
|
|
53
89
|
invalidate(): void;
|
|
54
|
-
/** Drop every frozen segment; the next render re-
|
|
90
|
+
/** Drop every frozen segment and the virtual prefix; the next render re-lays out all children. */
|
|
55
91
|
thaw(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Render every child without any windowing or virtualization. For callers
|
|
94
|
+
* that need the complete transcript (final session log, exports) — the
|
|
95
|
+
* windowed state is untouched and the next `render()` resumes windowing.
|
|
96
|
+
*/
|
|
97
|
+
renderAll(width: number): string[];
|
|
56
98
|
render(width: number): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Establish the virtual prefix in a single bottom-up pass: render children
|
|
101
|
+
* from the tail until `live + seed` lines are covered and mark everything
|
|
102
|
+
* above as virtual. Returns the rendered lines keyed by child index for
|
|
103
|
+
* the emit pass to reuse (children render at most once per frame).
|
|
104
|
+
*
|
|
105
|
+
* An unsettled child may end up inside the virtual prefix (the scan does
|
|
106
|
+
* not check settledness): `reconcileVirtual` then truncates the prefix
|
|
107
|
+
* there and the emit pass renders it live as usual, so no output is lost.
|
|
108
|
+
*/
|
|
109
|
+
private scanVirtualBoundary;
|
|
110
|
+
/**
|
|
111
|
+
* Verify the virtual prefix is still a valid prefix of `children` and still
|
|
112
|
+
* settled. On the first mismatch (identity, reorder, un-settled) the prefix
|
|
113
|
+
* is truncated there: children past the cut render live this frame and
|
|
114
|
+
* their lines materialize as new output (a bounded repaint repair).
|
|
115
|
+
*/
|
|
116
|
+
private reconcileVirtual;
|
|
57
117
|
/** Drop unsettled segments and refresh children whose generation moved. */
|
|
58
118
|
private reconcile;
|
|
59
|
-
private refreshSegment;
|
|
60
|
-
/** Freeze settled live children that sit above the live-line budget. */
|
|
61
|
-
private freezeAboveBudget;
|
|
62
119
|
}
|
|
63
120
|
//# sourceMappingURL=windowed-container.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"windowed-container.d.ts","sourceRoot":"","sources":["../src/windowed-container.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAgCrD,MAAM,WAAW,mBAAmB;IACnC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,iBAAkB,SAAQ,SAAS;IAC/C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAAwB;IAClD,OAAO,CAAC,KAAK,CAAwF;IAErG,uFAAuF;IACvF,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAGrC;IAED;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAExC;IAED,iBAAiB,IAAI,MAAM,CAI1B;IAED,mBAAmB,IAAI,MAAM,CAI5B;IAED,kBAAkB,IAAI,MAAM,CAI3B;IAED,8CAA8C;IAC9C,qBAAqB,IAAI,MAAM,CAE9B;IAED,yDAAyD;IACzD,eAAe,IAAI,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAE7D;IAED,8DAA8D;IAC9D,kBAAkB,IAAI,QAAQ,CAAC,mBAAmB,CAAC,CAElD;IAED,qFAAqF;IACrF,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAElD;IAEQ,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAkB/C;IAEQ,KAAK,IAAI,IAAI,CAGrB;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,0EAA0E;IAC1E,IAAI,IAAI,IAAI,CAGX;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAsCvC;IAED,2EAA2E;IAC3E,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,cAAc;IAwBtB,wEAAwE;IACxE,OAAO,CAAC,iBAAiB;CA8CzB","sourcesContent":["import { isRenderSettled, releaseRenderCache, renderGeneration } from \"./render-generation.ts\";\nimport { type Component, Container } from \"./tui.ts\";\n\n/**\n * A frozen segment never grows past this many lines (a single larger child\n * gets a segment of its own). Bounds the work of refreshing one changed child:\n * only its segment's line buffer is rebuilt, never the whole frozen history.\n */\nconst SEGMENT_MAX_LINES = 512;\n\n/**\n * Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always\n * render live. Two viewports keep components that change without announcing\n * it (animations, stateful extension renderers) live wherever the user can\n * see them, also after scrolling back one screen; the 120 floor is the budget\n * when no viewport is attached and on small terminals.\n */\nconst MIN_LIVE_LINE_BUDGET = 120;\nconst LIVE_VIEWPORTS = 2;\n\n/** Contiguous settled children whose rendered lines are reused verbatim. */\ntype FrozenSegment = {\n\t/** Index in `children` of the first frozen child. */\n\tfrom: number;\n\t/** The frozen children themselves (identity-checked every frame). */\n\tchildren: Component[];\n\tlineCounts: number[];\n\t/** `getRenderGeneration()` of each child when its lines were taken. */\n\tgenerations: number[];\n\t/** Concatenation of the children's lines. */\n\tlines: string[];\n};\n\nexport interface WindowedRenderStats {\n\t/** Children rendered live this frame (live tail plus unsettled gaps). */\n\tliveChildren: number;\n\t/** Frozen children re-rendered because their generation moved. */\n\trefreshedChildren: number;\n\t/** Frozen segments whose line buffer was rebuilt this frame. */\n\trefreshedSegments: number;\n}\n\n/**\n * Container that freezes settled children above a live-line budget into\n * bounded line segments and stops re-rendering them.\n *\n * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled\n * children always render live; settled runs on both sides of them freeze.\n * - Each frozen child remembers its `getRenderGeneration()`. When it moves,\n * only that child is re-rendered and only its segment is rebuilt.\n * - An unsettled frozen child drops just its segment. Width changes,\n * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.\n * - Freezing releases the child's render caches (frozen lines are the only\n * retained copy); a later change re-renders that one child from source.\n */\nexport class WindowedContainer extends Container {\n\tprivate segments: FrozenSegment[] = [];\n\tprivate frozenWidth = -1;\n\tprivate liveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate viewportRows?: () => number;\n\tprivate lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate stats: WindowedRenderStats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };\n\n\t/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */\n\tsetLiveLineBudget(lines: number): void {\n\t\tthis.viewportRows = undefined;\n\t\tthis.liveLineBudget = Math.max(1, Math.floor(lines));\n\t}\n\n\t/**\n\t * Follow the terminal height: budget = max(120, 2 × rows), re-read on every\n\t * render so a resize applies on the next frame.\n\t */\n\tsetViewportRows(rows: () => number): void {\n\t\tthis.viewportRows = rows;\n\t}\n\n\tgetLiveLineBudget(): number {\n\t\tif (!this.viewportRows) return this.liveLineBudget;\n\t\tconst rows = Math.floor(this.viewportRows());\n\t\treturn Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);\n\t}\n\n\tgetFrozenChildCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.children.length;\n\t\treturn count;\n\t}\n\n\tgetFrozenLineCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.lines.length;\n\t\treturn count;\n\t}\n\n\t/** Diagnostics: number of frozen segments. */\n\tgetFrozenSegmentCount(): number {\n\t\treturn this.segments.length;\n\t}\n\n\t/** Diagnostics: frozen child index ranges [from, to). */\n\tgetFrozenRanges(): ReadonlyArray<{ from: number; to: number }> {\n\t\treturn this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));\n\t}\n\n\t/** Diagnostics: what the most recent `render` had to redo. */\n\tgetLastRenderStats(): Readonly<WindowedRenderStats> {\n\t\treturn this.stats;\n\t}\n\n\t/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */\n\tprotected isChildSettled(child: Component): boolean {\n\t\treturn isRenderSettled(child);\n\t}\n\n\toverride removeChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index === -1) return;\n\t\tsuper.removeChild(component);\n\t\tfor (const segment of this.segments) {\n\t\t\tif (index < segment.from) {\n\t\t\t\tsegment.from -= 1;\n\t\t\t} else if (index < segment.from + segment.children.length) {\n\t\t\t\tconst j = index - segment.from;\n\t\t\t\tlet offset = 0;\n\t\t\t\tfor (let k = 0; k < j; k++) offset += segment.lineCounts[k];\n\t\t\t\tsegment.lines.splice(offset, segment.lineCounts[j]);\n\t\t\t\tsegment.children.splice(j, 1);\n\t\t\t\tsegment.lineCounts.splice(j, 1);\n\t\t\t\tsegment.generations.splice(j, 1);\n\t\t\t}\n\t\t}\n\t\tthis.segments = this.segments.filter((segment) => segment.children.length > 0);\n\t}\n\n\toverride clear(): void {\n\t\tthis.thaw();\n\t\tsuper.clear();\n\t}\n\n\toverride invalidate(): void {\n\t\tthis.thaw();\n\t\tsuper.invalidate();\n\t}\n\n\t/** Drop every frozen segment; the next render re-renders all children. */\n\tthaw(): void {\n\t\tthis.segments = [];\n\t\tthis.frozenWidth = -1;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst budget = this.getLiveLineBudget();\n\t\t// A taller terminal widens the live window: thaw so rows now inside it render live.\n\t\tif (width !== this.frozenWidth || budget > this.lastLiveLineBudget) this.thaw();\n\t\tthis.lastLiveLineBudget = budget;\n\t\tthis.frozenWidth = width;\n\t\tthis.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };\n\t\tthis.reconcile(width);\n\n\t\tconst parts: string[][] = [];\n\t\tconst partStarts: number[] = [];\n\t\tconst live: number[] = [];\n\t\tconst liveLines: string[][] = [];\n\t\tlet segmentIndex = 0;\n\t\tfor (let i = 0; i < this.children.length; ) {\n\t\t\tconst segment = this.segments[segmentIndex];\n\t\t\tlet lines: string[];\n\t\t\tpartStarts.push(i);\n\t\t\tif (segment && segment.from === i) {\n\t\t\t\tlines = segment.lines;\n\t\t\t\ti += segment.children.length;\n\t\t\t\tsegmentIndex += 1;\n\t\t\t} else {\n\t\t\t\tlines = this.children[i].render(width);\n\t\t\t\tlive.push(i);\n\t\t\t\tliveLines.push(lines);\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tparts.push(lines);\n\t\t}\n\t\tthis.stats.liveChildren = live.length;\n\n\t\t// Copy before freezing: freezing appends to segment line buffers. Native\n\t\t// concat is a bulk copy; spread arguments stay far below engine limits\n\t\t// because frozen rows arrive as a few hundred segment arrays.\n\t\tconst out = parts.length < 8192 ? ([] as string[]).concat(...parts) : parts.flat();\n\t\tthis.freezeAboveBudget(parts, partStarts, live, liveLines, budget);\n\t\treturn out;\n\t}\n\n\t/** Drop unsettled segments and refresh children whose generation moved. */\n\tprivate reconcile(width: number): void {\n\t\tconst kept: FrozenSegment[] = [];\n\t\tfor (const segment of this.segments) {\n\t\t\tlet dirty: number[] | undefined;\n\t\t\tlet settled = true;\n\t\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\t\tconst child = segment.children[j];\n\t\t\t\tif (this.children[segment.from + j] !== child) {\n\t\t\t\t\t// `children` was edited behind our back: nothing frozen can be trusted.\n\t\t\t\t\tthis.segments = [];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.isChildSettled(child)) {\n\t\t\t\t\tsettled = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (renderGeneration(child) !== segment.generations[j]) {\n\t\t\t\t\tdirty ??= [];\n\t\t\t\t\tdirty.push(j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!settled) continue;\n\t\t\tif (dirty) this.refreshSegment(segment, dirty, width);\n\t\t\tkept.push(segment);\n\t\t}\n\t\tthis.segments = kept;\n\t}\n\n\tprivate refreshSegment(segment: FrozenSegment, dirty: number[], width: number): void {\n\t\tconst lines: string[] = [];\n\t\tlet offset = 0;\n\t\tlet next = 0;\n\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\tconst count = segment.lineCounts[j];\n\t\t\tif (dirty[next] === j) {\n\t\t\t\tnext += 1;\n\t\t\t\tconst child = segment.children[j];\n\t\t\t\tconst fresh = child.render(width);\n\t\t\t\tfor (const line of fresh) lines.push(line);\n\t\t\t\tsegment.lineCounts[j] = fresh.length;\n\t\t\t\tsegment.generations[j] = renderGeneration(child);\n\t\t\t\treleaseRenderCache(child);\n\t\t\t} else {\n\t\t\t\tfor (let l = offset; l < offset + count; l++) lines.push(segment.lines[l]);\n\t\t\t}\n\t\t\toffset += count;\n\t\t}\n\t\tsegment.lines = lines;\n\t\tthis.stats.refreshedChildren += dirty.length;\n\t\tthis.stats.refreshedSegments += 1;\n\t}\n\n\t/** Freeze settled live children that sit above the live-line budget. */\n\tprivate freezeAboveBudget(\n\t\tparts: string[][],\n\t\tpartStarts: number[],\n\t\tlive: number[],\n\t\tliveLines: string[][],\n\t\tbudget: number,\n\t): void {\n\t\tlet acc = 0;\n\t\tlet liveStart = this.children.length;\n\t\tfor (let p = parts.length - 1; p >= 0; p--) {\n\t\t\tacc += parts[p].length;\n\t\t\tliveStart = partStarts[p];\n\t\t\tif (acc >= budget) break;\n\t\t}\n\t\tlet segmentIndex = 0;\n\t\tfor (let n = 0; n < live.length && live[n] < liveStart; n++) {\n\t\t\tconst index = live[n];\n\t\t\tconst child = this.children[index];\n\t\t\tif (!this.isChildSettled(child)) continue;\n\t\t\tconst lines = liveLines[n];\n\t\t\twhile (segmentIndex < this.segments.length && this.segments[segmentIndex].from < index) segmentIndex += 1;\n\t\t\tconst previous = this.segments[segmentIndex - 1];\n\t\t\tconst generation = renderGeneration(child);\n\t\t\tif (\n\t\t\t\tprevious &&\n\t\t\t\tprevious.from + previous.children.length === index &&\n\t\t\t\tprevious.lines.length + lines.length <= SEGMENT_MAX_LINES\n\t\t\t) {\n\t\t\t\tprevious.children.push(child);\n\t\t\t\tprevious.lineCounts.push(lines.length);\n\t\t\t\tprevious.generations.push(generation);\n\t\t\t\tfor (const line of lines) previous.lines.push(line);\n\t\t\t} else {\n\t\t\t\tconst segment: FrozenSegment = {\n\t\t\t\t\tfrom: index,\n\t\t\t\t\tchildren: [child],\n\t\t\t\t\tlineCounts: [lines.length],\n\t\t\t\t\tgenerations: [generation],\n\t\t\t\t\tlines: lines.slice(),\n\t\t\t\t};\n\t\t\t\tthis.segments.splice(segmentIndex, 0, segment);\n\t\t\t\tsegmentIndex += 1;\n\t\t\t}\n\t\t\treleaseRenderCache(child);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"windowed-container.d.ts","sourceRoot":"","sources":["../src/windowed-container.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAarD,MAAM,WAAW,mBAAmB;IACnC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,iBAAkB,SAAQ,SAAS;IAC/C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAAwB;IAClD,OAAO,CAAC,KAAK,CAKX;IACF;;;OAGG;IACH,OAAO,CAAC,eAAe,CAAmB;IAC1C,6EAA6E;IAC7E,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAC,CAAe;IACxC,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,mBAAmB,CAAC,CAAS;IACrC,gEAAgE;IAChE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6D;IAErF,uFAAuF;IACvF,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAGrC;IAED;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAExC;IAED,iBAAiB,IAAI,MAAM,CAI1B;IAED;;;;;OAKG;IACH,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlC;IAED,2FAA0F;IAC1F,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAI/D;IAED,2EAA2E;IAC3E,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAKvC;IAED,qEAAqE;IACrE,oBAAoB,IAAI,MAAM,CAE7B;IAED,mBAAmB,IAAI,MAAM,CAI5B;IAED,kBAAkB,IAAI,MAAM,CAI3B;IAED,8CAA8C;IAC9C,qBAAqB,IAAI,MAAM,CAE9B;IAED,yDAAyD;IACzD,eAAe,IAAI,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAE7D;IAED,8DAA8D;IAC9D,kBAAkB,IAAI,QAAQ,CAAC,mBAAmB,CAAC,CAElD;IAED,qFAAqF;IACrF,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAElD;IAEQ,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAM/C;IAEQ,KAAK,IAAI,IAAI,CAGrB;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,kGAAkG;IAClG,IAAI,IAAI,IAAI,CAIX;IAED;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjC;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiDvC;IAED;;;;;;;;;OASG;IACH,OAAO,CAAC,mBAAmB;IAe3B;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAOxB,2EAA2E;IAC3E,OAAO,CAAC,SAAS;CAIjB","sourcesContent":["import { isRenderSettled } from \"./render-generation.ts\";\nimport { type Component, Container } from \"./tui.ts\";\nimport { dropFrozenChild, type FrozenSegment, freezeAboveBudget, reconcileSegments } from \"./windowed-segments.ts\";\n\n/**\n * Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always\n * render live. Two viewports keep components that change without announcing\n * it (animations, stateful extension renderers) live wherever the user can\n * see them, also after scrolling back one screen; the 120 floor is the budget\n * when no viewport is attached and on small terminals.\n */\nconst MIN_LIVE_LINE_BUDGET = 120;\nconst LIVE_VIEWPORTS = 2;\n\nexport interface WindowedRenderStats {\n\t/** Children rendered live this frame (live tail plus unsettled gaps). */\n\tliveChildren: number;\n\t/** Frozen children re-rendered because their generation moved. */\n\trefreshedChildren: number;\n\t/** Frozen segments whose line buffer was rebuilt this frame. */\n\trefreshedSegments: number;\n\t/** Settled children skipped entirely this frame (virtual prefix). */\n\tvirtualChildren: number;\n}\n\n/**\n * Container that freezes settled children above a live-line budget into\n * bounded line segments and stops re-rendering them.\n *\n * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled\n * children always render live; settled runs on both sides of them freeze.\n * - Each frozen child remembers its `getRenderGeneration()`. When it moves,\n * only that child is re-rendered and only its segment is rebuilt.\n * - An unsettled frozen child drops just its segment. Width changes,\n * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.\n * - Freezing releases the child's render caches (frozen lines are the only\n * retained copy); a later change re-renders that one child from source.\n *\n * Virtual prefix (opt-in via `setPrefixSeed`/`setPrefixSeedViewport`):\n * when a prefix seed is configured, children above the live budget PLUS the\n * seed budget never render at all — `render()` returns zero lines for them.\n * Rendered line work is bounded by live + seed; prefix identity/settledness\n * checks still visit skipped children. The\n * prefix is a contiguous run anchored at index 0; children above the seed\n * budget never enter the output, so callers that need the whole transcript\n * (`buildFinalSessionLog`, exports) must use `renderAll(width)`.\n */\nexport class WindowedContainer extends Container {\n\tprivate segments: FrozenSegment[] = [];\n\tprivate frozenWidth = -1;\n\tprivate liveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate viewportRows?: () => number;\n\tprivate lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate stats: WindowedRenderStats = {\n\t\tliveChildren: 0,\n\t\trefreshedChildren: 0,\n\t\trefreshedSegments: 0,\n\t\tvirtualChildren: 0,\n\t};\n\t/**\n\t * Children above live+seed that never render (opt-in). Contiguous from\n\t * index 0; emits zero lines every frame once established.\n\t */\n\tprivate virtualChildren: Component[] = [];\n\t/** Unset preserves full-history rendering; zero seeds only the live tail. */\n\tprivate seedLines?: number;\n\tprivate seedViewportRows?: () => number;\n\tprivate seedScreens?: number;\n\tprivate lastPrefixSeedLines?: number;\n\t/** Bound `isChildSettled` for the extracted segment helpers. */\n\tprivate readonly settled = (child: Component): boolean => this.isChildSettled(child);\n\n\t/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */\n\tsetLiveLineBudget(lines: number): void {\n\t\tthis.viewportRows = undefined;\n\t\tthis.liveLineBudget = Math.max(1, Math.floor(lines));\n\t}\n\n\t/**\n\t * Follow the terminal height: budget = max(120, 2 × rows), re-read on every\n\t * render so a resize applies on the next frame.\n\t */\n\tsetViewportRows(rows: () => number): void {\n\t\tthis.viewportRows = rows;\n\t}\n\n\tgetLiveLineBudget(): number {\n\t\tif (!this.viewportRows) return this.liveLineBudget;\n\t\tconst rows = Math.floor(this.viewportRows());\n\t\treturn Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);\n\t}\n\n\t/**\n\t * Opt in to the virtual prefix: settled children above\n\t * `live budget + seedLines` never render — the first frame of a long\n\t * transcript renders only live + seed lines. Zero seeds only the live\n\t * tail; unset or non-finite input restores full-history rendering.\n\t */\n\tsetPrefixSeed(lines?: number): void {\n\t\tthis.seedViewportRows = undefined;\n\t\tthis.seedScreens = undefined;\n\t\tthis.seedLines = lines !== undefined && Number.isFinite(lines) ? Math.max(0, Math.floor(lines)) : undefined;\n\t}\n\n\t/** Seed budget follows the terminal height: `screens × rows`, re-read on every render. */\n\tsetPrefixSeedViewport(screens: number, rows: () => number): void {\n\t\tthis.seedLines = undefined;\n\t\tthis.seedScreens = Number.isFinite(screens) ? Math.max(0, Math.floor(screens)) : undefined;\n\t\tthis.seedViewportRows = rows;\n\t}\n\n\t/** Current seed budget in lines; undefined means virtualization is off. */\n\tgetPrefixSeedLines(): number | undefined {\n\t\tif (!this.seedViewportRows) return this.seedLines;\n\t\tif (this.seedScreens === undefined) return undefined;\n\t\tconst rows = Math.floor(this.seedViewportRows());\n\t\treturn Math.max(0, Number.isFinite(rows) ? rows * this.seedScreens : 0);\n\t}\n\n\t/** Children skipped entirely by the last render (virtual prefix). */\n\tgetVirtualChildCount(): number {\n\t\treturn this.virtualChildren.length;\n\t}\n\n\tgetFrozenChildCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.children.length;\n\t\treturn count;\n\t}\n\n\tgetFrozenLineCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.lines.length;\n\t\treturn count;\n\t}\n\n\t/** Diagnostics: number of frozen segments. */\n\tgetFrozenSegmentCount(): number {\n\t\treturn this.segments.length;\n\t}\n\n\t/** Diagnostics: frozen child index ranges [from, to). */\n\tgetFrozenRanges(): ReadonlyArray<{ from: number; to: number }> {\n\t\treturn this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));\n\t}\n\n\t/** Diagnostics: what the most recent `render` had to redo. */\n\tgetLastRenderStats(): Readonly<WindowedRenderStats> {\n\t\treturn this.stats;\n\t}\n\n\t/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */\n\tprotected isChildSettled(child: Component): boolean {\n\t\treturn isRenderSettled(child);\n\t}\n\n\toverride removeChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index === -1) return;\n\t\tsuper.removeChild(component);\n\t\tif (index < this.virtualChildren.length) this.virtualChildren.splice(index, 1);\n\t\tdropFrozenChild(this.segments, index);\n\t}\n\n\toverride clear(): void {\n\t\tthis.thaw();\n\t\tsuper.clear();\n\t}\n\n\toverride invalidate(): void {\n\t\tthis.thaw();\n\t\tsuper.invalidate();\n\t}\n\n\t/** Drop every frozen segment and the virtual prefix; the next render re-lays out all children. */\n\tthaw(): void {\n\t\tthis.segments = [];\n\t\tthis.virtualChildren = [];\n\t\tthis.frozenWidth = -1;\n\t}\n\n\t/**\n\t * Render every child without any windowing or virtualization. For callers\n\t * that need the complete transcript (final session log, exports) — the\n\t * windowed state is untouched and the next `render()` resumes windowing.\n\t */\n\trenderAll(width: number): string[] {\n\t\treturn super.render(width);\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst budget = this.getLiveLineBudget();\n\t\tconst seed = this.getPrefixSeedLines();\n\t\t// Rebuild on seed changes too: a taller terminal can grow it without growing the live budget.\n\t\tif (width !== this.frozenWidth || budget > this.lastLiveLineBudget || seed !== this.lastPrefixSeedLines)\n\t\t\tthis.thaw();\n\t\tthis.lastLiveLineBudget = budget;\n\t\tthis.lastPrefixSeedLines = seed;\n\t\tthis.frozenWidth = width;\n\t\tthis.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0, virtualChildren: 0 };\n\t\t// Build the virtual prefix once, while no frozen segments exist. Lines\n\t\t// rendered during the boundary scan are memoized into the emit pass.\n\t\tconst memo =\n\t\t\tseed !== undefined && this.virtualChildren.length === 0 && this.segments.length === 0\n\t\t\t\t? this.scanVirtualBoundary(width, budget, seed)\n\t\t\t\t: undefined;\n\t\tthis.reconcileVirtual();\n\t\tthis.reconcile(width);\n\t\tthis.stats.virtualChildren = this.virtualChildren.length;\n\n\t\tconst parts: string[][] = [];\n\t\tconst partStarts: number[] = [];\n\t\tconst live: number[] = [];\n\t\tconst liveLines: string[][] = [];\n\t\tlet segmentIndex = 0;\n\t\tfor (let i = this.virtualChildren.length; i < this.children.length; ) {\n\t\t\tconst segment = this.segments[segmentIndex];\n\t\t\tlet lines: string[];\n\t\t\tpartStarts.push(i);\n\t\t\tif (segment && segment.from === i) {\n\t\t\t\tlines = segment.lines;\n\t\t\t\ti += segment.children.length;\n\t\t\t\tsegmentIndex += 1;\n\t\t\t} else {\n\t\t\t\tlines = memo?.get(i) ?? this.children[i].render(width);\n\t\t\t\tlive.push(i);\n\t\t\t\tliveLines.push(lines);\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tparts.push(lines);\n\t\t}\n\t\tthis.stats.liveChildren = live.length;\n\n\t\t// Copy before freezing: freezing appends to segment line buffers. Native\n\t\t// concat is a bulk copy; spread arguments stay far below engine limits\n\t\t// because frozen rows arrive as a few hundred segment arrays.\n\t\tconst out = parts.length < 8192 ? ([] as string[]).concat(...parts) : parts.flat();\n\t\tfreezeAboveBudget(this.segments, this.children, this.settled, parts, partStarts, live, liveLines, budget);\n\t\treturn out;\n\t}\n\n\t/**\n\t * Establish the virtual prefix in a single bottom-up pass: render children\n\t * from the tail until `live + seed` lines are covered and mark everything\n\t * above as virtual. Returns the rendered lines keyed by child index for\n\t * the emit pass to reuse (children render at most once per frame).\n\t *\n\t * An unsettled child may end up inside the virtual prefix (the scan does\n\t * not check settledness): `reconcileVirtual` then truncates the prefix\n\t * there and the emit pass renders it live as usual, so no output is lost.\n\t */\n\tprivate scanVirtualBoundary(width: number, budget: number, seed: number): Map<number, string[]> {\n\t\tconst memo = new Map<number, string[]>();\n\t\tconst target = budget + seed;\n\t\tlet acc = 0;\n\t\tlet i = this.children.length;\n\t\twhile (i > 0 && acc < target) {\n\t\t\ti -= 1;\n\t\t\tconst lines = this.children[i].render(width);\n\t\t\tmemo.set(i, lines);\n\t\t\tacc += lines.length;\n\t\t}\n\t\tthis.virtualChildren = this.children.slice(0, i);\n\t\treturn memo;\n\t}\n\n\t/**\n\t * Verify the virtual prefix is still a valid prefix of `children` and still\n\t * settled. On the first mismatch (identity, reorder, un-settled) the prefix\n\t * is truncated there: children past the cut render live this frame and\n\t * their lines materialize as new output (a bounded repaint repair).\n\t */\n\tprivate reconcileVirtual(): void {\n\t\tconst count = this.virtualChildren.length;\n\t\tlet v = 0;\n\t\twhile (v < count && this.children[v] === this.virtualChildren[v] && this.isChildSettled(this.children[v])) v += 1;\n\t\tif (v < count) this.virtualChildren.length = v;\n\t}\n\n\t/** Drop unsettled segments and refresh children whose generation moved. */\n\tprivate reconcile(width: number): void {\n\t\tconst kept = reconcileSegments(this.segments, this.children, this.settled, width, this.stats);\n\t\tthis.segments = kept ?? [];\n\t}\n}\n"]}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import { isRenderSettled
|
|
1
|
+
import { isRenderSettled } from "./render-generation.js";
|
|
2
2
|
import { Container } from "./tui.js";
|
|
3
|
-
|
|
4
|
-
* A frozen segment never grows past this many lines (a single larger child
|
|
5
|
-
* gets a segment of its own). Bounds the work of refreshing one changed child:
|
|
6
|
-
* only its segment's line buffer is rebuilt, never the whole frozen history.
|
|
7
|
-
*/
|
|
8
|
-
const SEGMENT_MAX_LINES = 512;
|
|
3
|
+
import { dropFrozenChild, freezeAboveBudget, reconcileSegments } from "./windowed-segments.js";
|
|
9
4
|
/**
|
|
10
5
|
* Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always
|
|
11
6
|
* render live. Two viewports keep components that change without announcing
|
|
@@ -27,6 +22,15 @@ const LIVE_VIEWPORTS = 2;
|
|
|
27
22
|
* `invalidate()`, `clear()` and foreign edits of `children` thaw everything.
|
|
28
23
|
* - Freezing releases the child's render caches (frozen lines are the only
|
|
29
24
|
* retained copy); a later change re-renders that one child from source.
|
|
25
|
+
*
|
|
26
|
+
* Virtual prefix (opt-in via `setPrefixSeed`/`setPrefixSeedViewport`):
|
|
27
|
+
* when a prefix seed is configured, children above the live budget PLUS the
|
|
28
|
+
* seed budget never render at all — `render()` returns zero lines for them.
|
|
29
|
+
* Rendered line work is bounded by live + seed; prefix identity/settledness
|
|
30
|
+
* checks still visit skipped children. The
|
|
31
|
+
* prefix is a contiguous run anchored at index 0; children above the seed
|
|
32
|
+
* budget never enter the output, so callers that need the whole transcript
|
|
33
|
+
* (`buildFinalSessionLog`, exports) must use `renderAll(width)`.
|
|
30
34
|
*/
|
|
31
35
|
export class WindowedContainer extends Container {
|
|
32
36
|
segments = [];
|
|
@@ -34,7 +38,24 @@ export class WindowedContainer extends Container {
|
|
|
34
38
|
liveLineBudget = MIN_LIVE_LINE_BUDGET;
|
|
35
39
|
viewportRows;
|
|
36
40
|
lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;
|
|
37
|
-
stats = {
|
|
41
|
+
stats = {
|
|
42
|
+
liveChildren: 0,
|
|
43
|
+
refreshedChildren: 0,
|
|
44
|
+
refreshedSegments: 0,
|
|
45
|
+
virtualChildren: 0,
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Children above live+seed that never render (opt-in). Contiguous from
|
|
49
|
+
* index 0; emits zero lines every frame once established.
|
|
50
|
+
*/
|
|
51
|
+
virtualChildren = [];
|
|
52
|
+
/** Unset preserves full-history rendering; zero seeds only the live tail. */
|
|
53
|
+
seedLines;
|
|
54
|
+
seedViewportRows;
|
|
55
|
+
seedScreens;
|
|
56
|
+
lastPrefixSeedLines;
|
|
57
|
+
/** Bound `isChildSettled` for the extracted segment helpers. */
|
|
58
|
+
settled = (child) => this.isChildSettled(child);
|
|
38
59
|
/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */
|
|
39
60
|
setLiveLineBudget(lines) {
|
|
40
61
|
this.viewportRows = undefined;
|
|
@@ -53,6 +74,36 @@ export class WindowedContainer extends Container {
|
|
|
53
74
|
const rows = Math.floor(this.viewportRows());
|
|
54
75
|
return Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);
|
|
55
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Opt in to the virtual prefix: settled children above
|
|
79
|
+
* `live budget + seedLines` never render — the first frame of a long
|
|
80
|
+
* transcript renders only live + seed lines. Zero seeds only the live
|
|
81
|
+
* tail; unset or non-finite input restores full-history rendering.
|
|
82
|
+
*/
|
|
83
|
+
setPrefixSeed(lines) {
|
|
84
|
+
this.seedViewportRows = undefined;
|
|
85
|
+
this.seedScreens = undefined;
|
|
86
|
+
this.seedLines = lines !== undefined && Number.isFinite(lines) ? Math.max(0, Math.floor(lines)) : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Seed budget follows the terminal height: `screens × rows`, re-read on every render. */
|
|
89
|
+
setPrefixSeedViewport(screens, rows) {
|
|
90
|
+
this.seedLines = undefined;
|
|
91
|
+
this.seedScreens = Number.isFinite(screens) ? Math.max(0, Math.floor(screens)) : undefined;
|
|
92
|
+
this.seedViewportRows = rows;
|
|
93
|
+
}
|
|
94
|
+
/** Current seed budget in lines; undefined means virtualization is off. */
|
|
95
|
+
getPrefixSeedLines() {
|
|
96
|
+
if (!this.seedViewportRows)
|
|
97
|
+
return this.seedLines;
|
|
98
|
+
if (this.seedScreens === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
const rows = Math.floor(this.seedViewportRows());
|
|
101
|
+
return Math.max(0, Number.isFinite(rows) ? rows * this.seedScreens : 0);
|
|
102
|
+
}
|
|
103
|
+
/** Children skipped entirely by the last render (virtual prefix). */
|
|
104
|
+
getVirtualChildCount() {
|
|
105
|
+
return this.virtualChildren.length;
|
|
106
|
+
}
|
|
56
107
|
getFrozenChildCount() {
|
|
57
108
|
let count = 0;
|
|
58
109
|
for (const segment of this.segments)
|
|
@@ -86,22 +137,9 @@ export class WindowedContainer extends Container {
|
|
|
86
137
|
if (index === -1)
|
|
87
138
|
return;
|
|
88
139
|
super.removeChild(component);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
else if (index < segment.from + segment.children.length) {
|
|
94
|
-
const j = index - segment.from;
|
|
95
|
-
let offset = 0;
|
|
96
|
-
for (let k = 0; k < j; k++)
|
|
97
|
-
offset += segment.lineCounts[k];
|
|
98
|
-
segment.lines.splice(offset, segment.lineCounts[j]);
|
|
99
|
-
segment.children.splice(j, 1);
|
|
100
|
-
segment.lineCounts.splice(j, 1);
|
|
101
|
-
segment.generations.splice(j, 1);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
this.segments = this.segments.filter((segment) => segment.children.length > 0);
|
|
140
|
+
if (index < this.virtualChildren.length)
|
|
141
|
+
this.virtualChildren.splice(index, 1);
|
|
142
|
+
dropFrozenChild(this.segments, index);
|
|
105
143
|
}
|
|
106
144
|
clear() {
|
|
107
145
|
this.thaw();
|
|
@@ -111,26 +149,44 @@ export class WindowedContainer extends Container {
|
|
|
111
149
|
this.thaw();
|
|
112
150
|
super.invalidate();
|
|
113
151
|
}
|
|
114
|
-
/** Drop every frozen segment; the next render re-
|
|
152
|
+
/** Drop every frozen segment and the virtual prefix; the next render re-lays out all children. */
|
|
115
153
|
thaw() {
|
|
116
154
|
this.segments = [];
|
|
155
|
+
this.virtualChildren = [];
|
|
117
156
|
this.frozenWidth = -1;
|
|
118
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Render every child without any windowing or virtualization. For callers
|
|
160
|
+
* that need the complete transcript (final session log, exports) — the
|
|
161
|
+
* windowed state is untouched and the next `render()` resumes windowing.
|
|
162
|
+
*/
|
|
163
|
+
renderAll(width) {
|
|
164
|
+
return super.render(width);
|
|
165
|
+
}
|
|
119
166
|
render(width) {
|
|
120
167
|
const budget = this.getLiveLineBudget();
|
|
121
|
-
|
|
122
|
-
|
|
168
|
+
const seed = this.getPrefixSeedLines();
|
|
169
|
+
// Rebuild on seed changes too: a taller terminal can grow it without growing the live budget.
|
|
170
|
+
if (width !== this.frozenWidth || budget > this.lastLiveLineBudget || seed !== this.lastPrefixSeedLines)
|
|
123
171
|
this.thaw();
|
|
124
172
|
this.lastLiveLineBudget = budget;
|
|
173
|
+
this.lastPrefixSeedLines = seed;
|
|
125
174
|
this.frozenWidth = width;
|
|
126
|
-
this.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };
|
|
175
|
+
this.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0, virtualChildren: 0 };
|
|
176
|
+
// Build the virtual prefix once, while no frozen segments exist. Lines
|
|
177
|
+
// rendered during the boundary scan are memoized into the emit pass.
|
|
178
|
+
const memo = seed !== undefined && this.virtualChildren.length === 0 && this.segments.length === 0
|
|
179
|
+
? this.scanVirtualBoundary(width, budget, seed)
|
|
180
|
+
: undefined;
|
|
181
|
+
this.reconcileVirtual();
|
|
127
182
|
this.reconcile(width);
|
|
183
|
+
this.stats.virtualChildren = this.virtualChildren.length;
|
|
128
184
|
const parts = [];
|
|
129
185
|
const partStarts = [];
|
|
130
186
|
const live = [];
|
|
131
187
|
const liveLines = [];
|
|
132
188
|
let segmentIndex = 0;
|
|
133
|
-
for (let i =
|
|
189
|
+
for (let i = this.virtualChildren.length; i < this.children.length;) {
|
|
134
190
|
const segment = this.segments[segmentIndex];
|
|
135
191
|
let lines;
|
|
136
192
|
partStarts.push(i);
|
|
@@ -140,7 +196,7 @@ export class WindowedContainer extends Container {
|
|
|
140
196
|
segmentIndex += 1;
|
|
141
197
|
}
|
|
142
198
|
else {
|
|
143
|
-
lines = this.children[i].render(width);
|
|
199
|
+
lines = memo?.get(i) ?? this.children[i].render(width);
|
|
144
200
|
live.push(i);
|
|
145
201
|
liveLines.push(lines);
|
|
146
202
|
i += 1;
|
|
@@ -152,108 +208,51 @@ export class WindowedContainer extends Container {
|
|
|
152
208
|
// concat is a bulk copy; spread arguments stay far below engine limits
|
|
153
209
|
// because frozen rows arrive as a few hundred segment arrays.
|
|
154
210
|
const out = parts.length < 8192 ? [].concat(...parts) : parts.flat();
|
|
155
|
-
this.
|
|
211
|
+
freezeAboveBudget(this.segments, this.children, this.settled, parts, partStarts, live, liveLines, budget);
|
|
156
212
|
return out;
|
|
157
213
|
}
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (!settled)
|
|
181
|
-
continue;
|
|
182
|
-
if (dirty)
|
|
183
|
-
this.refreshSegment(segment, dirty, width);
|
|
184
|
-
kept.push(segment);
|
|
214
|
+
/**
|
|
215
|
+
* Establish the virtual prefix in a single bottom-up pass: render children
|
|
216
|
+
* from the tail until `live + seed` lines are covered and mark everything
|
|
217
|
+
* above as virtual. Returns the rendered lines keyed by child index for
|
|
218
|
+
* the emit pass to reuse (children render at most once per frame).
|
|
219
|
+
*
|
|
220
|
+
* An unsettled child may end up inside the virtual prefix (the scan does
|
|
221
|
+
* not check settledness): `reconcileVirtual` then truncates the prefix
|
|
222
|
+
* there and the emit pass renders it live as usual, so no output is lost.
|
|
223
|
+
*/
|
|
224
|
+
scanVirtualBoundary(width, budget, seed) {
|
|
225
|
+
const memo = new Map();
|
|
226
|
+
const target = budget + seed;
|
|
227
|
+
let acc = 0;
|
|
228
|
+
let i = this.children.length;
|
|
229
|
+
while (i > 0 && acc < target) {
|
|
230
|
+
i -= 1;
|
|
231
|
+
const lines = this.children[i].render(width);
|
|
232
|
+
memo.set(i, lines);
|
|
233
|
+
acc += lines.length;
|
|
185
234
|
}
|
|
186
|
-
this.
|
|
235
|
+
this.virtualChildren = this.children.slice(0, i);
|
|
236
|
+
return memo;
|
|
187
237
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
segment.generations[j] = renderGeneration(child);
|
|
202
|
-
releaseRenderCache(child);
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
for (let l = offset; l < offset + count; l++)
|
|
206
|
-
lines.push(segment.lines[l]);
|
|
207
|
-
}
|
|
208
|
-
offset += count;
|
|
209
|
-
}
|
|
210
|
-
segment.lines = lines;
|
|
211
|
-
this.stats.refreshedChildren += dirty.length;
|
|
212
|
-
this.stats.refreshedSegments += 1;
|
|
238
|
+
/**
|
|
239
|
+
* Verify the virtual prefix is still a valid prefix of `children` and still
|
|
240
|
+
* settled. On the first mismatch (identity, reorder, un-settled) the prefix
|
|
241
|
+
* is truncated there: children past the cut render live this frame and
|
|
242
|
+
* their lines materialize as new output (a bounded repaint repair).
|
|
243
|
+
*/
|
|
244
|
+
reconcileVirtual() {
|
|
245
|
+
const count = this.virtualChildren.length;
|
|
246
|
+
let v = 0;
|
|
247
|
+
while (v < count && this.children[v] === this.virtualChildren[v] && this.isChildSettled(this.children[v]))
|
|
248
|
+
v += 1;
|
|
249
|
+
if (v < count)
|
|
250
|
+
this.virtualChildren.length = v;
|
|
213
251
|
}
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (let p = parts.length - 1; p >= 0; p--) {
|
|
219
|
-
acc += parts[p].length;
|
|
220
|
-
liveStart = partStarts[p];
|
|
221
|
-
if (acc >= budget)
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
let segmentIndex = 0;
|
|
225
|
-
for (let n = 0; n < live.length && live[n] < liveStart; n++) {
|
|
226
|
-
const index = live[n];
|
|
227
|
-
const child = this.children[index];
|
|
228
|
-
if (!this.isChildSettled(child))
|
|
229
|
-
continue;
|
|
230
|
-
const lines = liveLines[n];
|
|
231
|
-
while (segmentIndex < this.segments.length && this.segments[segmentIndex].from < index)
|
|
232
|
-
segmentIndex += 1;
|
|
233
|
-
const previous = this.segments[segmentIndex - 1];
|
|
234
|
-
const generation = renderGeneration(child);
|
|
235
|
-
if (previous &&
|
|
236
|
-
previous.from + previous.children.length === index &&
|
|
237
|
-
previous.lines.length + lines.length <= SEGMENT_MAX_LINES) {
|
|
238
|
-
previous.children.push(child);
|
|
239
|
-
previous.lineCounts.push(lines.length);
|
|
240
|
-
previous.generations.push(generation);
|
|
241
|
-
for (const line of lines)
|
|
242
|
-
previous.lines.push(line);
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
const segment = {
|
|
246
|
-
from: index,
|
|
247
|
-
children: [child],
|
|
248
|
-
lineCounts: [lines.length],
|
|
249
|
-
generations: [generation],
|
|
250
|
-
lines: lines.slice(),
|
|
251
|
-
};
|
|
252
|
-
this.segments.splice(segmentIndex, 0, segment);
|
|
253
|
-
segmentIndex += 1;
|
|
254
|
-
}
|
|
255
|
-
releaseRenderCache(child);
|
|
256
|
-
}
|
|
252
|
+
/** Drop unsettled segments and refresh children whose generation moved. */
|
|
253
|
+
reconcile(width) {
|
|
254
|
+
const kept = reconcileSegments(this.segments, this.children, this.settled, width, this.stats);
|
|
255
|
+
this.segments = kept ?? [];
|
|
257
256
|
}
|
|
258
257
|
}
|
|
259
258
|
//# sourceMappingURL=windowed-container.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"windowed-container.js","sourceRoot":"","sources":["../src/windowed-container.ts"],"names":[],"mappings":"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","sourcesContent":["import { isRenderSettled, releaseRenderCache, renderGeneration } from \"./render-generation.ts\";\nimport { type Component, Container } from \"./tui.ts\";\n\n/**\n * A frozen segment never grows past this many lines (a single larger child\n * gets a segment of its own). Bounds the work of refreshing one changed child:\n * only its segment's line buffer is rebuilt, never the whole frozen history.\n */\nconst SEGMENT_MAX_LINES = 512;\n\n/**\n * Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always\n * render live. Two viewports keep components that change without announcing\n * it (animations, stateful extension renderers) live wherever the user can\n * see them, also after scrolling back one screen; the 120 floor is the budget\n * when no viewport is attached and on small terminals.\n */\nconst MIN_LIVE_LINE_BUDGET = 120;\nconst LIVE_VIEWPORTS = 2;\n\n/** Contiguous settled children whose rendered lines are reused verbatim. */\ntype FrozenSegment = {\n\t/** Index in `children` of the first frozen child. */\n\tfrom: number;\n\t/** The frozen children themselves (identity-checked every frame). */\n\tchildren: Component[];\n\tlineCounts: number[];\n\t/** `getRenderGeneration()` of each child when its lines were taken. */\n\tgenerations: number[];\n\t/** Concatenation of the children's lines. */\n\tlines: string[];\n};\n\nexport interface WindowedRenderStats {\n\t/** Children rendered live this frame (live tail plus unsettled gaps). */\n\tliveChildren: number;\n\t/** Frozen children re-rendered because their generation moved. */\n\trefreshedChildren: number;\n\t/** Frozen segments whose line buffer was rebuilt this frame. */\n\trefreshedSegments: number;\n}\n\n/**\n * Container that freezes settled children above a live-line budget into\n * bounded line segments and stops re-rendering them.\n *\n * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled\n * children always render live; settled runs on both sides of them freeze.\n * - Each frozen child remembers its `getRenderGeneration()`. When it moves,\n * only that child is re-rendered and only its segment is rebuilt.\n * - An unsettled frozen child drops just its segment. Width changes,\n * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.\n * - Freezing releases the child's render caches (frozen lines are the only\n * retained copy); a later change re-renders that one child from source.\n */\nexport class WindowedContainer extends Container {\n\tprivate segments: FrozenSegment[] = [];\n\tprivate frozenWidth = -1;\n\tprivate liveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate viewportRows?: () => number;\n\tprivate lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate stats: WindowedRenderStats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };\n\n\t/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */\n\tsetLiveLineBudget(lines: number): void {\n\t\tthis.viewportRows = undefined;\n\t\tthis.liveLineBudget = Math.max(1, Math.floor(lines));\n\t}\n\n\t/**\n\t * Follow the terminal height: budget = max(120, 2 × rows), re-read on every\n\t * render so a resize applies on the next frame.\n\t */\n\tsetViewportRows(rows: () => number): void {\n\t\tthis.viewportRows = rows;\n\t}\n\n\tgetLiveLineBudget(): number {\n\t\tif (!this.viewportRows) return this.liveLineBudget;\n\t\tconst rows = Math.floor(this.viewportRows());\n\t\treturn Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);\n\t}\n\n\tgetFrozenChildCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.children.length;\n\t\treturn count;\n\t}\n\n\tgetFrozenLineCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.lines.length;\n\t\treturn count;\n\t}\n\n\t/** Diagnostics: number of frozen segments. */\n\tgetFrozenSegmentCount(): number {\n\t\treturn this.segments.length;\n\t}\n\n\t/** Diagnostics: frozen child index ranges [from, to). */\n\tgetFrozenRanges(): ReadonlyArray<{ from: number; to: number }> {\n\t\treturn this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));\n\t}\n\n\t/** Diagnostics: what the most recent `render` had to redo. */\n\tgetLastRenderStats(): Readonly<WindowedRenderStats> {\n\t\treturn this.stats;\n\t}\n\n\t/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */\n\tprotected isChildSettled(child: Component): boolean {\n\t\treturn isRenderSettled(child);\n\t}\n\n\toverride removeChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index === -1) return;\n\t\tsuper.removeChild(component);\n\t\tfor (const segment of this.segments) {\n\t\t\tif (index < segment.from) {\n\t\t\t\tsegment.from -= 1;\n\t\t\t} else if (index < segment.from + segment.children.length) {\n\t\t\t\tconst j = index - segment.from;\n\t\t\t\tlet offset = 0;\n\t\t\t\tfor (let k = 0; k < j; k++) offset += segment.lineCounts[k];\n\t\t\t\tsegment.lines.splice(offset, segment.lineCounts[j]);\n\t\t\t\tsegment.children.splice(j, 1);\n\t\t\t\tsegment.lineCounts.splice(j, 1);\n\t\t\t\tsegment.generations.splice(j, 1);\n\t\t\t}\n\t\t}\n\t\tthis.segments = this.segments.filter((segment) => segment.children.length > 0);\n\t}\n\n\toverride clear(): void {\n\t\tthis.thaw();\n\t\tsuper.clear();\n\t}\n\n\toverride invalidate(): void {\n\t\tthis.thaw();\n\t\tsuper.invalidate();\n\t}\n\n\t/** Drop every frozen segment; the next render re-renders all children. */\n\tthaw(): void {\n\t\tthis.segments = [];\n\t\tthis.frozenWidth = -1;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst budget = this.getLiveLineBudget();\n\t\t// A taller terminal widens the live window: thaw so rows now inside it render live.\n\t\tif (width !== this.frozenWidth || budget > this.lastLiveLineBudget) this.thaw();\n\t\tthis.lastLiveLineBudget = budget;\n\t\tthis.frozenWidth = width;\n\t\tthis.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };\n\t\tthis.reconcile(width);\n\n\t\tconst parts: string[][] = [];\n\t\tconst partStarts: number[] = [];\n\t\tconst live: number[] = [];\n\t\tconst liveLines: string[][] = [];\n\t\tlet segmentIndex = 0;\n\t\tfor (let i = 0; i < this.children.length; ) {\n\t\t\tconst segment = this.segments[segmentIndex];\n\t\t\tlet lines: string[];\n\t\t\tpartStarts.push(i);\n\t\t\tif (segment && segment.from === i) {\n\t\t\t\tlines = segment.lines;\n\t\t\t\ti += segment.children.length;\n\t\t\t\tsegmentIndex += 1;\n\t\t\t} else {\n\t\t\t\tlines = this.children[i].render(width);\n\t\t\t\tlive.push(i);\n\t\t\t\tliveLines.push(lines);\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tparts.push(lines);\n\t\t}\n\t\tthis.stats.liveChildren = live.length;\n\n\t\t// Copy before freezing: freezing appends to segment line buffers. Native\n\t\t// concat is a bulk copy; spread arguments stay far below engine limits\n\t\t// because frozen rows arrive as a few hundred segment arrays.\n\t\tconst out = parts.length < 8192 ? ([] as string[]).concat(...parts) : parts.flat();\n\t\tthis.freezeAboveBudget(parts, partStarts, live, liveLines, budget);\n\t\treturn out;\n\t}\n\n\t/** Drop unsettled segments and refresh children whose generation moved. */\n\tprivate reconcile(width: number): void {\n\t\tconst kept: FrozenSegment[] = [];\n\t\tfor (const segment of this.segments) {\n\t\t\tlet dirty: number[] | undefined;\n\t\t\tlet settled = true;\n\t\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\t\tconst child = segment.children[j];\n\t\t\t\tif (this.children[segment.from + j] !== child) {\n\t\t\t\t\t// `children` was edited behind our back: nothing frozen can be trusted.\n\t\t\t\t\tthis.segments = [];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.isChildSettled(child)) {\n\t\t\t\t\tsettled = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (renderGeneration(child) !== segment.generations[j]) {\n\t\t\t\t\tdirty ??= [];\n\t\t\t\t\tdirty.push(j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!settled) continue;\n\t\t\tif (dirty) this.refreshSegment(segment, dirty, width);\n\t\t\tkept.push(segment);\n\t\t}\n\t\tthis.segments = kept;\n\t}\n\n\tprivate refreshSegment(segment: FrozenSegment, dirty: number[], width: number): void {\n\t\tconst lines: string[] = [];\n\t\tlet offset = 0;\n\t\tlet next = 0;\n\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\tconst count = segment.lineCounts[j];\n\t\t\tif (dirty[next] === j) {\n\t\t\t\tnext += 1;\n\t\t\t\tconst child = segment.children[j];\n\t\t\t\tconst fresh = child.render(width);\n\t\t\t\tfor (const line of fresh) lines.push(line);\n\t\t\t\tsegment.lineCounts[j] = fresh.length;\n\t\t\t\tsegment.generations[j] = renderGeneration(child);\n\t\t\t\treleaseRenderCache(child);\n\t\t\t} else {\n\t\t\t\tfor (let l = offset; l < offset + count; l++) lines.push(segment.lines[l]);\n\t\t\t}\n\t\t\toffset += count;\n\t\t}\n\t\tsegment.lines = lines;\n\t\tthis.stats.refreshedChildren += dirty.length;\n\t\tthis.stats.refreshedSegments += 1;\n\t}\n\n\t/** Freeze settled live children that sit above the live-line budget. */\n\tprivate freezeAboveBudget(\n\t\tparts: string[][],\n\t\tpartStarts: number[],\n\t\tlive: number[],\n\t\tliveLines: string[][],\n\t\tbudget: number,\n\t): void {\n\t\tlet acc = 0;\n\t\tlet liveStart = this.children.length;\n\t\tfor (let p = parts.length - 1; p >= 0; p--) {\n\t\t\tacc += parts[p].length;\n\t\t\tliveStart = partStarts[p];\n\t\t\tif (acc >= budget) break;\n\t\t}\n\t\tlet segmentIndex = 0;\n\t\tfor (let n = 0; n < live.length && live[n] < liveStart; n++) {\n\t\t\tconst index = live[n];\n\t\t\tconst child = this.children[index];\n\t\t\tif (!this.isChildSettled(child)) continue;\n\t\t\tconst lines = liveLines[n];\n\t\t\twhile (segmentIndex < this.segments.length && this.segments[segmentIndex].from < index) segmentIndex += 1;\n\t\t\tconst previous = this.segments[segmentIndex - 1];\n\t\t\tconst generation = renderGeneration(child);\n\t\t\tif (\n\t\t\t\tprevious &&\n\t\t\t\tprevious.from + previous.children.length === index &&\n\t\t\t\tprevious.lines.length + lines.length <= SEGMENT_MAX_LINES\n\t\t\t) {\n\t\t\t\tprevious.children.push(child);\n\t\t\t\tprevious.lineCounts.push(lines.length);\n\t\t\t\tprevious.generations.push(generation);\n\t\t\t\tfor (const line of lines) previous.lines.push(line);\n\t\t\t} else {\n\t\t\t\tconst segment: FrozenSegment = {\n\t\t\t\t\tfrom: index,\n\t\t\t\t\tchildren: [child],\n\t\t\t\t\tlineCounts: [lines.length],\n\t\t\t\t\tgenerations: [generation],\n\t\t\t\t\tlines: lines.slice(),\n\t\t\t\t};\n\t\t\t\tthis.segments.splice(segmentIndex, 0, segment);\n\t\t\t\tsegmentIndex += 1;\n\t\t\t}\n\t\t\treleaseRenderCache(child);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"windowed-container.js","sourceRoot":"","sources":["../src/windowed-container.ts"],"names":[],"mappings":"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","sourcesContent":["import { isRenderSettled } from \"./render-generation.ts\";\nimport { type Component, Container } from \"./tui.ts\";\nimport { dropFrozenChild, type FrozenSegment, freezeAboveBudget, reconcileSegments } from \"./windowed-segments.ts\";\n\n/**\n * Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always\n * render live. Two viewports keep components that change without announcing\n * it (animations, stateful extension renderers) live wherever the user can\n * see them, also after scrolling back one screen; the 120 floor is the budget\n * when no viewport is attached and on small terminals.\n */\nconst MIN_LIVE_LINE_BUDGET = 120;\nconst LIVE_VIEWPORTS = 2;\n\nexport interface WindowedRenderStats {\n\t/** Children rendered live this frame (live tail plus unsettled gaps). */\n\tliveChildren: number;\n\t/** Frozen children re-rendered because their generation moved. */\n\trefreshedChildren: number;\n\t/** Frozen segments whose line buffer was rebuilt this frame. */\n\trefreshedSegments: number;\n\t/** Settled children skipped entirely this frame (virtual prefix). */\n\tvirtualChildren: number;\n}\n\n/**\n * Container that freezes settled children above a live-line budget into\n * bounded line segments and stops re-rendering them.\n *\n * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled\n * children always render live; settled runs on both sides of them freeze.\n * - Each frozen child remembers its `getRenderGeneration()`. When it moves,\n * only that child is re-rendered and only its segment is rebuilt.\n * - An unsettled frozen child drops just its segment. Width changes,\n * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.\n * - Freezing releases the child's render caches (frozen lines are the only\n * retained copy); a later change re-renders that one child from source.\n *\n * Virtual prefix (opt-in via `setPrefixSeed`/`setPrefixSeedViewport`):\n * when a prefix seed is configured, children above the live budget PLUS the\n * seed budget never render at all — `render()` returns zero lines for them.\n * Rendered line work is bounded by live + seed; prefix identity/settledness\n * checks still visit skipped children. The\n * prefix is a contiguous run anchored at index 0; children above the seed\n * budget never enter the output, so callers that need the whole transcript\n * (`buildFinalSessionLog`, exports) must use `renderAll(width)`.\n */\nexport class WindowedContainer extends Container {\n\tprivate segments: FrozenSegment[] = [];\n\tprivate frozenWidth = -1;\n\tprivate liveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate viewportRows?: () => number;\n\tprivate lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate stats: WindowedRenderStats = {\n\t\tliveChildren: 0,\n\t\trefreshedChildren: 0,\n\t\trefreshedSegments: 0,\n\t\tvirtualChildren: 0,\n\t};\n\t/**\n\t * Children above live+seed that never render (opt-in). Contiguous from\n\t * index 0; emits zero lines every frame once established.\n\t */\n\tprivate virtualChildren: Component[] = [];\n\t/** Unset preserves full-history rendering; zero seeds only the live tail. */\n\tprivate seedLines?: number;\n\tprivate seedViewportRows?: () => number;\n\tprivate seedScreens?: number;\n\tprivate lastPrefixSeedLines?: number;\n\t/** Bound `isChildSettled` for the extracted segment helpers. */\n\tprivate readonly settled = (child: Component): boolean => this.isChildSettled(child);\n\n\t/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */\n\tsetLiveLineBudget(lines: number): void {\n\t\tthis.viewportRows = undefined;\n\t\tthis.liveLineBudget = Math.max(1, Math.floor(lines));\n\t}\n\n\t/**\n\t * Follow the terminal height: budget = max(120, 2 × rows), re-read on every\n\t * render so a resize applies on the next frame.\n\t */\n\tsetViewportRows(rows: () => number): void {\n\t\tthis.viewportRows = rows;\n\t}\n\n\tgetLiveLineBudget(): number {\n\t\tif (!this.viewportRows) return this.liveLineBudget;\n\t\tconst rows = Math.floor(this.viewportRows());\n\t\treturn Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);\n\t}\n\n\t/**\n\t * Opt in to the virtual prefix: settled children above\n\t * `live budget + seedLines` never render — the first frame of a long\n\t * transcript renders only live + seed lines. Zero seeds only the live\n\t * tail; unset or non-finite input restores full-history rendering.\n\t */\n\tsetPrefixSeed(lines?: number): void {\n\t\tthis.seedViewportRows = undefined;\n\t\tthis.seedScreens = undefined;\n\t\tthis.seedLines = lines !== undefined && Number.isFinite(lines) ? Math.max(0, Math.floor(lines)) : undefined;\n\t}\n\n\t/** Seed budget follows the terminal height: `screens × rows`, re-read on every render. */\n\tsetPrefixSeedViewport(screens: number, rows: () => number): void {\n\t\tthis.seedLines = undefined;\n\t\tthis.seedScreens = Number.isFinite(screens) ? Math.max(0, Math.floor(screens)) : undefined;\n\t\tthis.seedViewportRows = rows;\n\t}\n\n\t/** Current seed budget in lines; undefined means virtualization is off. */\n\tgetPrefixSeedLines(): number | undefined {\n\t\tif (!this.seedViewportRows) return this.seedLines;\n\t\tif (this.seedScreens === undefined) return undefined;\n\t\tconst rows = Math.floor(this.seedViewportRows());\n\t\treturn Math.max(0, Number.isFinite(rows) ? rows * this.seedScreens : 0);\n\t}\n\n\t/** Children skipped entirely by the last render (virtual prefix). */\n\tgetVirtualChildCount(): number {\n\t\treturn this.virtualChildren.length;\n\t}\n\n\tgetFrozenChildCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.children.length;\n\t\treturn count;\n\t}\n\n\tgetFrozenLineCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.lines.length;\n\t\treturn count;\n\t}\n\n\t/** Diagnostics: number of frozen segments. */\n\tgetFrozenSegmentCount(): number {\n\t\treturn this.segments.length;\n\t}\n\n\t/** Diagnostics: frozen child index ranges [from, to). */\n\tgetFrozenRanges(): ReadonlyArray<{ from: number; to: number }> {\n\t\treturn this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));\n\t}\n\n\t/** Diagnostics: what the most recent `render` had to redo. */\n\tgetLastRenderStats(): Readonly<WindowedRenderStats> {\n\t\treturn this.stats;\n\t}\n\n\t/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */\n\tprotected isChildSettled(child: Component): boolean {\n\t\treturn isRenderSettled(child);\n\t}\n\n\toverride removeChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index === -1) return;\n\t\tsuper.removeChild(component);\n\t\tif (index < this.virtualChildren.length) this.virtualChildren.splice(index, 1);\n\t\tdropFrozenChild(this.segments, index);\n\t}\n\n\toverride clear(): void {\n\t\tthis.thaw();\n\t\tsuper.clear();\n\t}\n\n\toverride invalidate(): void {\n\t\tthis.thaw();\n\t\tsuper.invalidate();\n\t}\n\n\t/** Drop every frozen segment and the virtual prefix; the next render re-lays out all children. */\n\tthaw(): void {\n\t\tthis.segments = [];\n\t\tthis.virtualChildren = [];\n\t\tthis.frozenWidth = -1;\n\t}\n\n\t/**\n\t * Render every child without any windowing or virtualization. For callers\n\t * that need the complete transcript (final session log, exports) — the\n\t * windowed state is untouched and the next `render()` resumes windowing.\n\t */\n\trenderAll(width: number): string[] {\n\t\treturn super.render(width);\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst budget = this.getLiveLineBudget();\n\t\tconst seed = this.getPrefixSeedLines();\n\t\t// Rebuild on seed changes too: a taller terminal can grow it without growing the live budget.\n\t\tif (width !== this.frozenWidth || budget > this.lastLiveLineBudget || seed !== this.lastPrefixSeedLines)\n\t\t\tthis.thaw();\n\t\tthis.lastLiveLineBudget = budget;\n\t\tthis.lastPrefixSeedLines = seed;\n\t\tthis.frozenWidth = width;\n\t\tthis.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0, virtualChildren: 0 };\n\t\t// Build the virtual prefix once, while no frozen segments exist. Lines\n\t\t// rendered during the boundary scan are memoized into the emit pass.\n\t\tconst memo =\n\t\t\tseed !== undefined && this.virtualChildren.length === 0 && this.segments.length === 0\n\t\t\t\t? this.scanVirtualBoundary(width, budget, seed)\n\t\t\t\t: undefined;\n\t\tthis.reconcileVirtual();\n\t\tthis.reconcile(width);\n\t\tthis.stats.virtualChildren = this.virtualChildren.length;\n\n\t\tconst parts: string[][] = [];\n\t\tconst partStarts: number[] = [];\n\t\tconst live: number[] = [];\n\t\tconst liveLines: string[][] = [];\n\t\tlet segmentIndex = 0;\n\t\tfor (let i = this.virtualChildren.length; i < this.children.length; ) {\n\t\t\tconst segment = this.segments[segmentIndex];\n\t\t\tlet lines: string[];\n\t\t\tpartStarts.push(i);\n\t\t\tif (segment && segment.from === i) {\n\t\t\t\tlines = segment.lines;\n\t\t\t\ti += segment.children.length;\n\t\t\t\tsegmentIndex += 1;\n\t\t\t} else {\n\t\t\t\tlines = memo?.get(i) ?? this.children[i].render(width);\n\t\t\t\tlive.push(i);\n\t\t\t\tliveLines.push(lines);\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tparts.push(lines);\n\t\t}\n\t\tthis.stats.liveChildren = live.length;\n\n\t\t// Copy before freezing: freezing appends to segment line buffers. Native\n\t\t// concat is a bulk copy; spread arguments stay far below engine limits\n\t\t// because frozen rows arrive as a few hundred segment arrays.\n\t\tconst out = parts.length < 8192 ? ([] as string[]).concat(...parts) : parts.flat();\n\t\tfreezeAboveBudget(this.segments, this.children, this.settled, parts, partStarts, live, liveLines, budget);\n\t\treturn out;\n\t}\n\n\t/**\n\t * Establish the virtual prefix in a single bottom-up pass: render children\n\t * from the tail until `live + seed` lines are covered and mark everything\n\t * above as virtual. Returns the rendered lines keyed by child index for\n\t * the emit pass to reuse (children render at most once per frame).\n\t *\n\t * An unsettled child may end up inside the virtual prefix (the scan does\n\t * not check settledness): `reconcileVirtual` then truncates the prefix\n\t * there and the emit pass renders it live as usual, so no output is lost.\n\t */\n\tprivate scanVirtualBoundary(width: number, budget: number, seed: number): Map<number, string[]> {\n\t\tconst memo = new Map<number, string[]>();\n\t\tconst target = budget + seed;\n\t\tlet acc = 0;\n\t\tlet i = this.children.length;\n\t\twhile (i > 0 && acc < target) {\n\t\t\ti -= 1;\n\t\t\tconst lines = this.children[i].render(width);\n\t\t\tmemo.set(i, lines);\n\t\t\tacc += lines.length;\n\t\t}\n\t\tthis.virtualChildren = this.children.slice(0, i);\n\t\treturn memo;\n\t}\n\n\t/**\n\t * Verify the virtual prefix is still a valid prefix of `children` and still\n\t * settled. On the first mismatch (identity, reorder, un-settled) the prefix\n\t * is truncated there: children past the cut render live this frame and\n\t * their lines materialize as new output (a bounded repaint repair).\n\t */\n\tprivate reconcileVirtual(): void {\n\t\tconst count = this.virtualChildren.length;\n\t\tlet v = 0;\n\t\twhile (v < count && this.children[v] === this.virtualChildren[v] && this.isChildSettled(this.children[v])) v += 1;\n\t\tif (v < count) this.virtualChildren.length = v;\n\t}\n\n\t/** Drop unsettled segments and refresh children whose generation moved. */\n\tprivate reconcile(width: number): void {\n\t\tconst kept = reconcileSegments(this.segments, this.children, this.settled, width, this.stats);\n\t\tthis.segments = kept ?? [];\n\t}\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Component } from "./tui.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A frozen segment never grows past this many lines (a single larger child
|
|
4
|
+
* gets a segment of its own). Bounds the work of refreshing one changed child:
|
|
5
|
+
* only its segment's line buffer is rebuilt, never the whole frozen history.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SEGMENT_MAX_LINES = 512;
|
|
8
|
+
/** Contiguous settled children whose rendered lines are reused verbatim. */
|
|
9
|
+
export type FrozenSegment = {
|
|
10
|
+
/** Index in `children` of the first frozen child. */
|
|
11
|
+
from: number;
|
|
12
|
+
/** The frozen children themselves (identity-checked every frame). */
|
|
13
|
+
children: Component[];
|
|
14
|
+
lineCounts: number[];
|
|
15
|
+
/** `getRenderGeneration()` of each child when its lines were taken. */
|
|
16
|
+
generations: number[];
|
|
17
|
+
/** Concatenation of the children's lines. */
|
|
18
|
+
lines: string[];
|
|
19
|
+
};
|
|
20
|
+
/** Per-frame counters a refresh accumulates into. */
|
|
21
|
+
export type SegmentRefreshStats = {
|
|
22
|
+
refreshedChildren: number;
|
|
23
|
+
refreshedSegments: number;
|
|
24
|
+
};
|
|
25
|
+
type SettledCheck = (child: Component) => boolean;
|
|
26
|
+
/** Splice one child out of the segment table after `children[index]` was removed. */
|
|
27
|
+
export declare function dropFrozenChild(segments: FrozenSegment[], index: number): void;
|
|
28
|
+
/**
|
|
29
|
+
* Drop unsettled segments and refresh children whose generation moved.
|
|
30
|
+
* Returns the kept segments, or null when a foreign edit of `children`
|
|
31
|
+
* invalidated every segment (the caller drops all state).
|
|
32
|
+
*/
|
|
33
|
+
export declare function reconcileSegments(segments: FrozenSegment[], children: readonly Component[], isSettled: SettledCheck, width: number, stats: SegmentRefreshStats): FrozenSegment[] | null;
|
|
34
|
+
/** Freeze settled live children that sit above the live-line budget. */
|
|
35
|
+
export declare function freezeAboveBudget(segments: FrozenSegment[], children: readonly Component[], isSettled: SettledCheck, parts: string[][], partStarts: number[], live: number[], liveLines: string[][], budget: number): void;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=windowed-segments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windowed-segments.d.ts","sourceRoot":"","sources":["../src/windowed-segments.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,4EAA4E;AAC5E,MAAM,MAAM,aAAa,GAAG;IAC3B,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uEAAuE;IACvE,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,mBAAmB,GAAG;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F,KAAK,YAAY,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC;AAElD,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAiB9E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAChC,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,EAAE,SAAS,SAAS,EAAE,EAC9B,SAAS,EAAE,YAAY,EACvB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,mBAAmB,GACxB,aAAa,EAAE,GAAG,IAAI,CAyBxB;AA0BD,wEAAwE;AACxE,wBAAgB,iBAAiB,CAChC,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,EAAE,SAAS,SAAS,EAAE,EAC9B,SAAS,EAAE,YAAY,EACvB,KAAK,EAAE,MAAM,EAAE,EAAE,EACjB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,EAAE,EAAE,EACrB,MAAM,EAAE,MAAM,GACZ,IAAI,CAuCN","sourcesContent":["import { releaseRenderCache, renderGeneration } from \"./render-generation.ts\";\nimport type { Component } from \"./tui.ts\";\n\n/**\n * A frozen segment never grows past this many lines (a single larger child\n * gets a segment of its own). Bounds the work of refreshing one changed child:\n * only its segment's line buffer is rebuilt, never the whole frozen history.\n */\nexport const SEGMENT_MAX_LINES = 512;\n\n/** Contiguous settled children whose rendered lines are reused verbatim. */\nexport type FrozenSegment = {\n\t/** Index in `children` of the first frozen child. */\n\tfrom: number;\n\t/** The frozen children themselves (identity-checked every frame). */\n\tchildren: Component[];\n\tlineCounts: number[];\n\t/** `getRenderGeneration()` of each child when its lines were taken. */\n\tgenerations: number[];\n\t/** Concatenation of the children's lines. */\n\tlines: string[];\n};\n\n/** Per-frame counters a refresh accumulates into. */\nexport type SegmentRefreshStats = { refreshedChildren: number; refreshedSegments: number };\n\ntype SettledCheck = (child: Component) => boolean;\n\n/** Splice one child out of the segment table after `children[index]` was removed. */\nexport function dropFrozenChild(segments: FrozenSegment[], index: number): void {\n\tfor (const segment of segments) {\n\t\tif (index < segment.from) {\n\t\t\tsegment.from -= 1;\n\t\t} else if (index < segment.from + segment.children.length) {\n\t\t\tconst j = index - segment.from;\n\t\t\tlet offset = 0;\n\t\t\tfor (let k = 0; k < j; k++) offset += segment.lineCounts[k];\n\t\t\tsegment.lines.splice(offset, segment.lineCounts[j]);\n\t\t\tsegment.children.splice(j, 1);\n\t\t\tsegment.lineCounts.splice(j, 1);\n\t\t\tsegment.generations.splice(j, 1);\n\t\t}\n\t}\n\tfor (let i = segments.length - 1; i >= 0; i--) {\n\t\tif (segments[i].children.length === 0) segments.splice(i, 1);\n\t}\n}\n\n/**\n * Drop unsettled segments and refresh children whose generation moved.\n * Returns the kept segments, or null when a foreign edit of `children`\n * invalidated every segment (the caller drops all state).\n */\nexport function reconcileSegments(\n\tsegments: FrozenSegment[],\n\tchildren: readonly Component[],\n\tisSettled: SettledCheck,\n\twidth: number,\n\tstats: SegmentRefreshStats,\n): FrozenSegment[] | null {\n\tconst kept: FrozenSegment[] = [];\n\tfor (const segment of segments) {\n\t\tlet dirty: number[] | undefined;\n\t\tlet settled = true;\n\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\tconst child = segment.children[j];\n\t\t\tif (children[segment.from + j] !== child) {\n\t\t\t\t// `children` was edited behind our back: nothing frozen can be trusted.\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (!isSettled(child)) {\n\t\t\t\tsettled = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (renderGeneration(child) !== segment.generations[j]) {\n\t\t\t\tdirty ??= [];\n\t\t\t\tdirty.push(j);\n\t\t\t}\n\t\t}\n\t\tif (!settled) continue;\n\t\tif (dirty) refreshSegment(segment, dirty, width, stats);\n\t\tkept.push(segment);\n\t}\n\treturn kept;\n}\n\nfunction refreshSegment(segment: FrozenSegment, dirty: number[], width: number, stats: SegmentRefreshStats): void {\n\tconst lines: string[] = [];\n\tlet offset = 0;\n\tlet next = 0;\n\tfor (let j = 0; j < segment.children.length; j++) {\n\t\tconst count = segment.lineCounts[j];\n\t\tif (dirty[next] === j) {\n\t\t\tnext += 1;\n\t\t\tconst child = segment.children[j];\n\t\t\tconst fresh = child.render(width);\n\t\t\tfor (const line of fresh) lines.push(line);\n\t\t\tsegment.lineCounts[j] = fresh.length;\n\t\t\tsegment.generations[j] = renderGeneration(child);\n\t\t\treleaseRenderCache(child);\n\t\t} else {\n\t\t\tfor (let l = offset; l < offset + count; l++) lines.push(segment.lines[l]);\n\t\t}\n\t\toffset += count;\n\t}\n\tsegment.lines = lines;\n\tstats.refreshedChildren += dirty.length;\n\tstats.refreshedSegments += 1;\n}\n\n/** Freeze settled live children that sit above the live-line budget. */\nexport function freezeAboveBudget(\n\tsegments: FrozenSegment[],\n\tchildren: readonly Component[],\n\tisSettled: SettledCheck,\n\tparts: string[][],\n\tpartStarts: number[],\n\tlive: number[],\n\tliveLines: string[][],\n\tbudget: number,\n): void {\n\tlet acc = 0;\n\tlet liveStart = children.length;\n\tfor (let p = parts.length - 1; p >= 0; p--) {\n\t\tacc += parts[p].length;\n\t\tliveStart = partStarts[p];\n\t\tif (acc >= budget) break;\n\t}\n\tlet segmentIndex = 0;\n\tfor (let n = 0; n < live.length && live[n] < liveStart; n++) {\n\t\tconst index = live[n];\n\t\tconst child = children[index];\n\t\tif (!isSettled(child)) continue;\n\t\tconst lines = liveLines[n];\n\t\twhile (segmentIndex < segments.length && segments[segmentIndex].from < index) segmentIndex += 1;\n\t\tconst previous = segments[segmentIndex - 1];\n\t\tconst generation = renderGeneration(child);\n\t\tif (\n\t\t\tprevious &&\n\t\t\tprevious.from + previous.children.length === index &&\n\t\t\tprevious.lines.length + lines.length <= SEGMENT_MAX_LINES\n\t\t) {\n\t\t\tprevious.children.push(child);\n\t\t\tprevious.lineCounts.push(lines.length);\n\t\t\tprevious.generations.push(generation);\n\t\t\tfor (const line of lines) previous.lines.push(line);\n\t\t} else {\n\t\t\tconst segment: FrozenSegment = {\n\t\t\t\tfrom: index,\n\t\t\t\tchildren: [child],\n\t\t\t\tlineCounts: [lines.length],\n\t\t\t\tgenerations: [generation],\n\t\t\t\tlines: lines.slice(),\n\t\t\t};\n\t\t\tsegments.splice(segmentIndex, 0, segment);\n\t\t\tsegmentIndex += 1;\n\t\t}\n\t\treleaseRenderCache(child);\n\t}\n}\n"]}
|