omk-tui 1.3.2 → 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.
Files changed (80) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/components/box.d.ts +5 -0
  3. package/dist/components/box.d.ts.map +1 -1
  4. package/dist/components/box.js +50 -15
  5. package/dist/components/box.js.map +1 -1
  6. package/dist/components/editor.d.ts +3 -1
  7. package/dist/components/editor.d.ts.map +1 -1
  8. package/dist/components/editor.js +4 -7
  9. package/dist/components/editor.js.map +1 -1
  10. package/dist/components/image.d.ts +2 -0
  11. package/dist/components/image.d.ts.map +1 -1
  12. package/dist/components/image.js +4 -0
  13. package/dist/components/image.js.map +1 -1
  14. package/dist/components/markdown.d.ts +4 -0
  15. package/dist/components/markdown.d.ts.map +1 -1
  16. package/dist/components/markdown.js +14 -0
  17. package/dist/components/markdown.js.map +1 -1
  18. package/dist/components/spacer.d.ts +2 -0
  19. package/dist/components/spacer.d.ts.map +1 -1
  20. package/dist/components/spacer.js +8 -0
  21. package/dist/components/spacer.js.map +1 -1
  22. package/dist/components/text.d.ts +4 -0
  23. package/dist/components/text.d.ts.map +1 -1
  24. package/dist/components/text.js +14 -0
  25. package/dist/components/text.js.map +1 -1
  26. package/dist/editor-component.d.ts +2 -0
  27. package/dist/editor-component.d.ts.map +1 -1
  28. package/dist/editor-component.js.map +1 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/line-reset-memo.d.ts +27 -0
  34. package/dist/line-reset-memo.d.ts.map +1 -0
  35. package/dist/line-reset-memo.js +101 -0
  36. package/dist/line-reset-memo.js.map +1 -0
  37. package/dist/render-generation.d.ts +24 -0
  38. package/dist/render-generation.d.ts.map +1 -0
  39. package/dist/render-generation.js +48 -0
  40. package/dist/render-generation.js.map +1 -0
  41. package/dist/terminal-output.d.ts +20 -1
  42. package/dist/terminal-output.d.ts.map +1 -1
  43. package/dist/terminal-output.js +62 -1
  44. package/dist/terminal-output.js.map +1 -1
  45. package/dist/terminal-resize-log.d.ts +39 -0
  46. package/dist/terminal-resize-log.d.ts.map +1 -0
  47. package/dist/terminal-resize-log.js +57 -0
  48. package/dist/terminal-resize-log.js.map +1 -0
  49. package/dist/terminal-resync.d.ts +74 -0
  50. package/dist/terminal-resync.d.ts.map +1 -0
  51. package/dist/terminal-resync.js +112 -0
  52. package/dist/terminal-resync.js.map +1 -0
  53. package/dist/terminal.d.ts.map +1 -1
  54. package/dist/terminal.js +1 -1
  55. package/dist/terminal.js.map +1 -1
  56. package/dist/tui.d.ts +29 -9
  57. package/dist/tui.d.ts.map +1 -1
  58. package/dist/tui.js +65 -86
  59. package/dist/tui.js.map +1 -1
  60. package/dist/undo-stack.d.ts +8 -2
  61. package/dist/undo-stack.d.ts.map +1 -1
  62. package/dist/undo-stack.js +12 -2
  63. package/dist/undo-stack.js.map +1 -1
  64. package/dist/utils.d.ts +2 -4
  65. package/dist/utils.d.ts.map +1 -1
  66. package/dist/utils.js +2 -128
  67. package/dist/utils.js.map +1 -1
  68. package/dist/visible-width.d.ts +14 -0
  69. package/dist/visible-width.d.ts.map +1 -0
  70. package/dist/visible-width.js +163 -0
  71. package/dist/visible-width.js.map +1 -0
  72. package/dist/windowed-container.d.ts +120 -0
  73. package/dist/windowed-container.d.ts.map +1 -0
  74. package/dist/windowed-container.js +258 -0
  75. package/dist/windowed-container.js.map +1 -0
  76. package/dist/windowed-segments.d.ts +37 -0
  77. package/dist/windowed-segments.d.ts.map +1 -0
  78. package/dist/windowed-segments.js +133 -0
  79. package/dist/windowed-segments.js.map +1 -0
  80. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ /** Shared grapheme segmenter; `getGraphemeSegmenter()` in utils.ts returns this instance. */
2
+ export declare const graphemeSegmenter: Intl.Segmenter;
3
+ export declare function isPrintableAscii(str: string): boolean;
4
+ /**
5
+ * Calculate the terminal width of a single grapheme cluster.
6
+ * Based on code from the string-width library, but includes a possible-emoji
7
+ * check to avoid running the RGI_Emoji regex unnecessarily.
8
+ */
9
+ export declare function graphemeWidth(segment: string): number;
10
+ /**
11
+ * Calculate the visible width of a string in terminal columns.
12
+ */
13
+ export declare function visibleWidth(str: string): number;
14
+ //# sourceMappingURL=visible-width.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visible-width.d.ts","sourceRoot":"","sources":["../src/visible-width.ts"],"names":[],"mappings":"AAIA,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB,gBAA6D,CAAC;AAa5F,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQrD;AAoBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA4CrD;AAyCD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAuChD","sourcesContent":["import { eastAsianWidth } from \"get-east-asian-width\";\nimport { extractAnsiCode } from \"./ansi-codes.ts\";\nimport { leadingNonPrintingRegex, rgiEmojiRegex, zeroWidthRegex } from \"./unicode-regex.ts\";\n\n/** Shared grapheme segmenter; `getGraphemeSegmenter()` in utils.ts returns this instance. */\nexport const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n\n/**\n * Width cache for strings that need grapheme segmentation. Styled ASCII, Hangul and CJK\n * ideographs never reach it (see `simpleWidth`), so the entries go to emoji, combining\n * marks and other scripts. A render pass over a long transcript touches thousands of distinct lines;\n * 512 entries made such passes miss on every line (FIFO eviction under a cyclic scan).\n */\nconst WIDTH_CACHE_SIZE = 8192;\n/** Longer strings are measured without caching so the cache cannot pin large text. */\nconst WIDTH_CACHE_MAX_KEY_LENGTH = 4096;\nconst widthCache = new Map<string, number>();\n\nexport function isPrintableAscii(str: string): boolean {\n\tfor (let i = 0; i < str.length; i++) {\n\t\tconst code = str.charCodeAt(i);\n\t\tif (code < 0x20 || code > 0x7e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n/**\n * Check if a grapheme cluster (after segmentation) could possibly be an RGI emoji.\n * This is a fast heuristic to avoid the expensive rgiEmojiRegex test.\n * The tested Unicode blocks are deliberately broad to account for future\n * Unicode additions.\n */\nfunction couldBeEmoji(segment: string): boolean {\n\tconst cp = segment.codePointAt(0)!;\n\treturn (\n\t\t(cp >= 0x1f000 && cp <= 0x1fbff) || // Emoji and Pictograph\n\t\t(cp >= 0x2300 && cp <= 0x23ff) || // Misc technical\n\t\t(cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols, dingbats\n\t\t(cp >= 0x2b50 && cp <= 0x2b55) || // Specific stars/circles\n\t\tsegment.includes(\"\\uFE0F\") || // Contains VS16 (emoji presentation selector)\n\t\tsegment.length > 2 // Multi-codepoint sequences (ZWJ, skin tones, etc.)\n\t);\n}\n\n/**\n * Calculate the terminal width of a single grapheme cluster.\n * Based on code from the string-width library, but includes a possible-emoji\n * check to avoid running the RGI_Emoji regex unnecessarily.\n */\nexport function graphemeWidth(segment: string): number {\n\tif (segment === \"\\t\") {\n\t\treturn 3;\n\t}\n\n\t// Zero-width clusters\n\tif (zeroWidthRegex.test(segment)) {\n\t\treturn 0;\n\t}\n\n\t// Emoji check with pre-filter\n\tif (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) {\n\t\treturn 2;\n\t}\n\n\t// Get base visible codepoint\n\tconst base = segment.replace(leadingNonPrintingRegex, \"\");\n\tconst cp = base.codePointAt(0);\n\tif (cp === undefined) {\n\t\treturn 0;\n\t}\n\n\t// Regional indicator symbols (U+1F1E6..U+1F1FF) are often rendered as\n\t// full-width emoji in terminals, even when isolated during streaming.\n\t// Keep width conservative (2) to avoid terminal auto-wrap drift artifacts.\n\tif (cp >= 0x1f1e6 && cp <= 0x1f1ff) {\n\t\treturn 2;\n\t}\n\n\tlet width = eastAsianWidth(cp);\n\n\t// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.\n\tif (segment.length > 1) {\n\t\tfor (const char of segment.slice(1)) {\n\t\t\tconst c = char.codePointAt(0)!;\n\t\t\tif (c >= 0xff00 && c <= 0xffef) {\n\t\t\t\twidth += eastAsianWidth(c);\n\t\t\t} else if (c === 0x0e33 || c === 0x0eb3) {\n\t\t\t\twidth += 1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn width;\n}\n\n/**\n * Normalize for measuring: tabs become 3 spaces and supported ANSI/OSC/APC escape\n * sequences (CSI styling/cursor codes, OSC hyperlinks and prompt markers, APC sequences\n * like CURSOR_MARKER) are removed. An ESC that starts no complete sequence is kept.\n */\nfunction stripForWidth(str: string): string {\n\tconst clean = str.includes(\"\\t\") ? str.replace(/\\t/g, \" \") : str;\n\tlet esc = clean.indexOf(\"\\x1b\");\n\tif (esc === -1) return clean;\n\tlet stripped = \"\";\n\tlet copied = 0;\n\twhile (esc !== -1) {\n\t\tconst ansi = extractAnsiCode(clean, esc);\n\t\tif (ansi) {\n\t\t\tstripped += clean.slice(copied, esc);\n\t\t\tcopied = esc + ansi.length;\n\t\t}\n\t\tesc = clean.indexOf(\"\\x1b\", ansi ? copied : esc + 1);\n\t}\n\treturn stripped + clean.slice(copied);\n}\n\n/**\n * Width of text made only of printable ASCII (1 column), Hangul syllables and CJK unified\n * ideographs (2 columns each, UAX #11 Wide), or -1 when anything else appears. Each of these\n * code points is a grapheme cluster on its own: a Hangul syllable joins only trailing jamo\n * (U+1160..U+11FF) and an ideograph only variation selectors, both of which fall back here.\n */\nfunction simpleWidth(clean: string): number {\n\tlet width = 0;\n\tfor (let i = 0; i < clean.length; i++) {\n\t\tconst code = clean.charCodeAt(i);\n\t\tif (code >= 0x20 && code <= 0x7e) width += 1;\n\t\telse if ((code >= 0xac00 && code <= 0xd7a3) || (code >= 0x4e00 && code <= 0x9fff)) width += 2;\n\t\telse return -1;\n\t}\n\treturn width;\n}\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tif (str.length === 0) {\n\t\treturn 0;\n\t}\n\n\t// Fast path: pure ASCII printable\n\tif (isPrintableAscii(str)) {\n\t\treturn str.length;\n\t}\n\n\t// Styled ASCII, Hangul and CJK (the bulk of rendered lines): once escapes are\n\t// stripped, every character is its own cluster, so segmentation is unnecessary.\n\tconst clean = stripForWidth(str);\n\tconst simple = simpleWidth(clean);\n\tif (simple >= 0) {\n\t\treturn simple;\n\t}\n\n\tconst cached = widthCache.get(str);\n\tif (cached !== undefined) {\n\t\treturn cached;\n\t}\n\n\tlet width = 0;\n\tfor (const { segment } of graphemeSegmenter.segment(clean)) {\n\t\twidth += graphemeWidth(segment);\n\t}\n\n\tif (str.length <= WIDTH_CACHE_MAX_KEY_LENGTH) {\n\t\tif (widthCache.size >= WIDTH_CACHE_SIZE) {\n\t\t\tconst firstKey = widthCache.keys().next().value;\n\t\t\tif (firstKey !== undefined) {\n\t\t\t\twidthCache.delete(firstKey);\n\t\t\t}\n\t\t}\n\t\twidthCache.set(str, width);\n\t}\n\n\treturn width;\n}\n"]}
@@ -0,0 +1,163 @@
1
+ import { eastAsianWidth } from "get-east-asian-width";
2
+ import { extractAnsiCode } from "./ansi-codes.js";
3
+ import { leadingNonPrintingRegex, rgiEmojiRegex, zeroWidthRegex } from "./unicode-regex.js";
4
+ /** Shared grapheme segmenter; `getGraphemeSegmenter()` in utils.ts returns this instance. */
5
+ export const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
6
+ /**
7
+ * Width cache for strings that need grapheme segmentation. Styled ASCII, Hangul and CJK
8
+ * ideographs never reach it (see `simpleWidth`), so the entries go to emoji, combining
9
+ * marks and other scripts. A render pass over a long transcript touches thousands of distinct lines;
10
+ * 512 entries made such passes miss on every line (FIFO eviction under a cyclic scan).
11
+ */
12
+ const WIDTH_CACHE_SIZE = 8192;
13
+ /** Longer strings are measured without caching so the cache cannot pin large text. */
14
+ const WIDTH_CACHE_MAX_KEY_LENGTH = 4096;
15
+ const widthCache = new Map();
16
+ export function isPrintableAscii(str) {
17
+ for (let i = 0; i < str.length; i++) {
18
+ const code = str.charCodeAt(i);
19
+ if (code < 0x20 || code > 0x7e) {
20
+ return false;
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+ /**
26
+ * Check if a grapheme cluster (after segmentation) could possibly be an RGI emoji.
27
+ * This is a fast heuristic to avoid the expensive rgiEmojiRegex test.
28
+ * The tested Unicode blocks are deliberately broad to account for future
29
+ * Unicode additions.
30
+ */
31
+ function couldBeEmoji(segment) {
32
+ const cp = segment.codePointAt(0);
33
+ return ((cp >= 0x1f000 && cp <= 0x1fbff) || // Emoji and Pictograph
34
+ (cp >= 0x2300 && cp <= 0x23ff) || // Misc technical
35
+ (cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols, dingbats
36
+ (cp >= 0x2b50 && cp <= 0x2b55) || // Specific stars/circles
37
+ segment.includes("\uFE0F") || // Contains VS16 (emoji presentation selector)
38
+ segment.length > 2 // Multi-codepoint sequences (ZWJ, skin tones, etc.)
39
+ );
40
+ }
41
+ /**
42
+ * Calculate the terminal width of a single grapheme cluster.
43
+ * Based on code from the string-width library, but includes a possible-emoji
44
+ * check to avoid running the RGI_Emoji regex unnecessarily.
45
+ */
46
+ export function graphemeWidth(segment) {
47
+ if (segment === "\t") {
48
+ return 3;
49
+ }
50
+ // Zero-width clusters
51
+ if (zeroWidthRegex.test(segment)) {
52
+ return 0;
53
+ }
54
+ // Emoji check with pre-filter
55
+ if (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) {
56
+ return 2;
57
+ }
58
+ // Get base visible codepoint
59
+ const base = segment.replace(leadingNonPrintingRegex, "");
60
+ const cp = base.codePointAt(0);
61
+ if (cp === undefined) {
62
+ return 0;
63
+ }
64
+ // Regional indicator symbols (U+1F1E6..U+1F1FF) are often rendered as
65
+ // full-width emoji in terminals, even when isolated during streaming.
66
+ // Keep width conservative (2) to avoid terminal auto-wrap drift artifacts.
67
+ if (cp >= 0x1f1e6 && cp <= 0x1f1ff) {
68
+ return 2;
69
+ }
70
+ let width = eastAsianWidth(cp);
71
+ // Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.
72
+ if (segment.length > 1) {
73
+ for (const char of segment.slice(1)) {
74
+ const c = char.codePointAt(0);
75
+ if (c >= 0xff00 && c <= 0xffef) {
76
+ width += eastAsianWidth(c);
77
+ }
78
+ else if (c === 0x0e33 || c === 0x0eb3) {
79
+ width += 1;
80
+ }
81
+ }
82
+ }
83
+ return width;
84
+ }
85
+ /**
86
+ * Normalize for measuring: tabs become 3 spaces and supported ANSI/OSC/APC escape
87
+ * sequences (CSI styling/cursor codes, OSC hyperlinks and prompt markers, APC sequences
88
+ * like CURSOR_MARKER) are removed. An ESC that starts no complete sequence is kept.
89
+ */
90
+ function stripForWidth(str) {
91
+ const clean = str.includes("\t") ? str.replace(/\t/g, " ") : str;
92
+ let esc = clean.indexOf("\x1b");
93
+ if (esc === -1)
94
+ return clean;
95
+ let stripped = "";
96
+ let copied = 0;
97
+ while (esc !== -1) {
98
+ const ansi = extractAnsiCode(clean, esc);
99
+ if (ansi) {
100
+ stripped += clean.slice(copied, esc);
101
+ copied = esc + ansi.length;
102
+ }
103
+ esc = clean.indexOf("\x1b", ansi ? copied : esc + 1);
104
+ }
105
+ return stripped + clean.slice(copied);
106
+ }
107
+ /**
108
+ * Width of text made only of printable ASCII (1 column), Hangul syllables and CJK unified
109
+ * ideographs (2 columns each, UAX #11 Wide), or -1 when anything else appears. Each of these
110
+ * code points is a grapheme cluster on its own: a Hangul syllable joins only trailing jamo
111
+ * (U+1160..U+11FF) and an ideograph only variation selectors, both of which fall back here.
112
+ */
113
+ function simpleWidth(clean) {
114
+ let width = 0;
115
+ for (let i = 0; i < clean.length; i++) {
116
+ const code = clean.charCodeAt(i);
117
+ if (code >= 0x20 && code <= 0x7e)
118
+ width += 1;
119
+ else if ((code >= 0xac00 && code <= 0xd7a3) || (code >= 0x4e00 && code <= 0x9fff))
120
+ width += 2;
121
+ else
122
+ return -1;
123
+ }
124
+ return width;
125
+ }
126
+ /**
127
+ * Calculate the visible width of a string in terminal columns.
128
+ */
129
+ export function visibleWidth(str) {
130
+ if (str.length === 0) {
131
+ return 0;
132
+ }
133
+ // Fast path: pure ASCII printable
134
+ if (isPrintableAscii(str)) {
135
+ return str.length;
136
+ }
137
+ // Styled ASCII, Hangul and CJK (the bulk of rendered lines): once escapes are
138
+ // stripped, every character is its own cluster, so segmentation is unnecessary.
139
+ const clean = stripForWidth(str);
140
+ const simple = simpleWidth(clean);
141
+ if (simple >= 0) {
142
+ return simple;
143
+ }
144
+ const cached = widthCache.get(str);
145
+ if (cached !== undefined) {
146
+ return cached;
147
+ }
148
+ let width = 0;
149
+ for (const { segment } of graphemeSegmenter.segment(clean)) {
150
+ width += graphemeWidth(segment);
151
+ }
152
+ if (str.length <= WIDTH_CACHE_MAX_KEY_LENGTH) {
153
+ if (widthCache.size >= WIDTH_CACHE_SIZE) {
154
+ const firstKey = widthCache.keys().next().value;
155
+ if (firstKey !== undefined) {
156
+ widthCache.delete(firstKey);
157
+ }
158
+ }
159
+ widthCache.set(str, width);
160
+ }
161
+ return width;
162
+ }
163
+ //# sourceMappingURL=visible-width.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visible-width.js","sourceRoot":"","sources":["../src/visible-width.ts"],"names":[],"mappings":"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","sourcesContent":["import { eastAsianWidth } from \"get-east-asian-width\";\nimport { extractAnsiCode } from \"./ansi-codes.ts\";\nimport { leadingNonPrintingRegex, rgiEmojiRegex, zeroWidthRegex } from \"./unicode-regex.ts\";\n\n/** Shared grapheme segmenter; `getGraphemeSegmenter()` in utils.ts returns this instance. */\nexport const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n\n/**\n * Width cache for strings that need grapheme segmentation. Styled ASCII, Hangul and CJK\n * ideographs never reach it (see `simpleWidth`), so the entries go to emoji, combining\n * marks and other scripts. A render pass over a long transcript touches thousands of distinct lines;\n * 512 entries made such passes miss on every line (FIFO eviction under a cyclic scan).\n */\nconst WIDTH_CACHE_SIZE = 8192;\n/** Longer strings are measured without caching so the cache cannot pin large text. */\nconst WIDTH_CACHE_MAX_KEY_LENGTH = 4096;\nconst widthCache = new Map<string, number>();\n\nexport function isPrintableAscii(str: string): boolean {\n\tfor (let i = 0; i < str.length; i++) {\n\t\tconst code = str.charCodeAt(i);\n\t\tif (code < 0x20 || code > 0x7e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n/**\n * Check if a grapheme cluster (after segmentation) could possibly be an RGI emoji.\n * This is a fast heuristic to avoid the expensive rgiEmojiRegex test.\n * The tested Unicode blocks are deliberately broad to account for future\n * Unicode additions.\n */\nfunction couldBeEmoji(segment: string): boolean {\n\tconst cp = segment.codePointAt(0)!;\n\treturn (\n\t\t(cp >= 0x1f000 && cp <= 0x1fbff) || // Emoji and Pictograph\n\t\t(cp >= 0x2300 && cp <= 0x23ff) || // Misc technical\n\t\t(cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols, dingbats\n\t\t(cp >= 0x2b50 && cp <= 0x2b55) || // Specific stars/circles\n\t\tsegment.includes(\"\\uFE0F\") || // Contains VS16 (emoji presentation selector)\n\t\tsegment.length > 2 // Multi-codepoint sequences (ZWJ, skin tones, etc.)\n\t);\n}\n\n/**\n * Calculate the terminal width of a single grapheme cluster.\n * Based on code from the string-width library, but includes a possible-emoji\n * check to avoid running the RGI_Emoji regex unnecessarily.\n */\nexport function graphemeWidth(segment: string): number {\n\tif (segment === \"\\t\") {\n\t\treturn 3;\n\t}\n\n\t// Zero-width clusters\n\tif (zeroWidthRegex.test(segment)) {\n\t\treturn 0;\n\t}\n\n\t// Emoji check with pre-filter\n\tif (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) {\n\t\treturn 2;\n\t}\n\n\t// Get base visible codepoint\n\tconst base = segment.replace(leadingNonPrintingRegex, \"\");\n\tconst cp = base.codePointAt(0);\n\tif (cp === undefined) {\n\t\treturn 0;\n\t}\n\n\t// Regional indicator symbols (U+1F1E6..U+1F1FF) are often rendered as\n\t// full-width emoji in terminals, even when isolated during streaming.\n\t// Keep width conservative (2) to avoid terminal auto-wrap drift artifacts.\n\tif (cp >= 0x1f1e6 && cp <= 0x1f1ff) {\n\t\treturn 2;\n\t}\n\n\tlet width = eastAsianWidth(cp);\n\n\t// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.\n\tif (segment.length > 1) {\n\t\tfor (const char of segment.slice(1)) {\n\t\t\tconst c = char.codePointAt(0)!;\n\t\t\tif (c >= 0xff00 && c <= 0xffef) {\n\t\t\t\twidth += eastAsianWidth(c);\n\t\t\t} else if (c === 0x0e33 || c === 0x0eb3) {\n\t\t\t\twidth += 1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn width;\n}\n\n/**\n * Normalize for measuring: tabs become 3 spaces and supported ANSI/OSC/APC escape\n * sequences (CSI styling/cursor codes, OSC hyperlinks and prompt markers, APC sequences\n * like CURSOR_MARKER) are removed. An ESC that starts no complete sequence is kept.\n */\nfunction stripForWidth(str: string): string {\n\tconst clean = str.includes(\"\\t\") ? str.replace(/\\t/g, \" \") : str;\n\tlet esc = clean.indexOf(\"\\x1b\");\n\tif (esc === -1) return clean;\n\tlet stripped = \"\";\n\tlet copied = 0;\n\twhile (esc !== -1) {\n\t\tconst ansi = extractAnsiCode(clean, esc);\n\t\tif (ansi) {\n\t\t\tstripped += clean.slice(copied, esc);\n\t\t\tcopied = esc + ansi.length;\n\t\t}\n\t\tesc = clean.indexOf(\"\\x1b\", ansi ? copied : esc + 1);\n\t}\n\treturn stripped + clean.slice(copied);\n}\n\n/**\n * Width of text made only of printable ASCII (1 column), Hangul syllables and CJK unified\n * ideographs (2 columns each, UAX #11 Wide), or -1 when anything else appears. Each of these\n * code points is a grapheme cluster on its own: a Hangul syllable joins only trailing jamo\n * (U+1160..U+11FF) and an ideograph only variation selectors, both of which fall back here.\n */\nfunction simpleWidth(clean: string): number {\n\tlet width = 0;\n\tfor (let i = 0; i < clean.length; i++) {\n\t\tconst code = clean.charCodeAt(i);\n\t\tif (code >= 0x20 && code <= 0x7e) width += 1;\n\t\telse if ((code >= 0xac00 && code <= 0xd7a3) || (code >= 0x4e00 && code <= 0x9fff)) width += 2;\n\t\telse return -1;\n\t}\n\treturn width;\n}\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tif (str.length === 0) {\n\t\treturn 0;\n\t}\n\n\t// Fast path: pure ASCII printable\n\tif (isPrintableAscii(str)) {\n\t\treturn str.length;\n\t}\n\n\t// Styled ASCII, Hangul and CJK (the bulk of rendered lines): once escapes are\n\t// stripped, every character is its own cluster, so segmentation is unnecessary.\n\tconst clean = stripForWidth(str);\n\tconst simple = simpleWidth(clean);\n\tif (simple >= 0) {\n\t\treturn simple;\n\t}\n\n\tconst cached = widthCache.get(str);\n\tif (cached !== undefined) {\n\t\treturn cached;\n\t}\n\n\tlet width = 0;\n\tfor (const { segment } of graphemeSegmenter.segment(clean)) {\n\t\twidth += graphemeWidth(segment);\n\t}\n\n\tif (str.length <= WIDTH_CACHE_MAX_KEY_LENGTH) {\n\t\tif (widthCache.size >= WIDTH_CACHE_SIZE) {\n\t\t\tconst firstKey = widthCache.keys().next().value;\n\t\t\tif (firstKey !== undefined) {\n\t\t\t\twidthCache.delete(firstKey);\n\t\t\t}\n\t\t}\n\t\twidthCache.set(str, width);\n\t}\n\n\treturn width;\n}\n"]}
@@ -0,0 +1,120 @@
1
+ import { type Component, Container } from "./tui.ts";
2
+ export interface WindowedRenderStats {
3
+ /** Children rendered live this frame (live tail plus unsettled gaps). */
4
+ liveChildren: number;
5
+ /** Frozen children re-rendered because their generation moved. */
6
+ refreshedChildren: number;
7
+ /** Frozen segments whose line buffer was rebuilt this frame. */
8
+ refreshedSegments: number;
9
+ /** Settled children skipped entirely this frame (virtual prefix). */
10
+ virtualChildren: number;
11
+ }
12
+ /**
13
+ * Container that freezes settled children above a live-line budget into
14
+ * bounded line segments and stops re-rendering them.
15
+ *
16
+ * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled
17
+ * children always render live; settled runs on both sides of them freeze.
18
+ * - Each frozen child remembers its `getRenderGeneration()`. When it moves,
19
+ * only that child is re-rendered and only its segment is rebuilt.
20
+ * - An unsettled frozen child drops just its segment. Width changes,
21
+ * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.
22
+ * - Freezing releases the child's render caches (frozen lines are the only
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)`.
33
+ */
34
+ export declare class WindowedContainer extends Container {
35
+ private segments;
36
+ private frozenWidth;
37
+ private liveLineBudget;
38
+ private viewportRows?;
39
+ private lastLiveLineBudget;
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;
53
+ /** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */
54
+ setLiveLineBudget(lines: number): void;
55
+ /**
56
+ * Follow the terminal height: budget = max(120, 2 × rows), re-read on every
57
+ * render so a resize applies on the next frame.
58
+ */
59
+ setViewportRows(rows: () => number): void;
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;
74
+ getFrozenChildCount(): number;
75
+ getFrozenLineCount(): number;
76
+ /** Diagnostics: number of frozen segments. */
77
+ getFrozenSegmentCount(): number;
78
+ /** Diagnostics: frozen child index ranges [from, to). */
79
+ getFrozenRanges(): ReadonlyArray<{
80
+ from: number;
81
+ to: number;
82
+ }>;
83
+ /** Diagnostics: what the most recent `render` had to redo. */
84
+ getLastRenderStats(): Readonly<WindowedRenderStats>;
85
+ /** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */
86
+ protected isChildSettled(child: Component): boolean;
87
+ removeChild(component: Component): void;
88
+ clear(): void;
89
+ invalidate(): void;
90
+ /** Drop every frozen segment and the virtual prefix; the next render re-lays out all children. */
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[];
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;
117
+ /** Drop unsettled segments and refresh children whose generation moved. */
118
+ private reconcile;
119
+ }
120
+ //# sourceMappingURL=windowed-container.d.ts.map
@@ -0,0 +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;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"]}
@@ -0,0 +1,258 @@
1
+ import { isRenderSettled } from "./render-generation.js";
2
+ import { Container } from "./tui.js";
3
+ import { dropFrozenChild, freezeAboveBudget, reconcileSegments } from "./windowed-segments.js";
4
+ /**
5
+ * Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always
6
+ * render live. Two viewports keep components that change without announcing
7
+ * it (animations, stateful extension renderers) live wherever the user can
8
+ * see them, also after scrolling back one screen; the 120 floor is the budget
9
+ * when no viewport is attached and on small terminals.
10
+ */
11
+ const MIN_LIVE_LINE_BUDGET = 120;
12
+ const LIVE_VIEWPORTS = 2;
13
+ /**
14
+ * Container that freezes settled children above a live-line budget into
15
+ * bounded line segments and stops re-rendering them.
16
+ *
17
+ * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled
18
+ * children always render live; settled runs on both sides of them freeze.
19
+ * - Each frozen child remembers its `getRenderGeneration()`. When it moves,
20
+ * only that child is re-rendered and only its segment is rebuilt.
21
+ * - An unsettled frozen child drops just its segment. Width changes,
22
+ * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.
23
+ * - Freezing releases the child's render caches (frozen lines are the only
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)`.
34
+ */
35
+ export class WindowedContainer extends Container {
36
+ segments = [];
37
+ frozenWidth = -1;
38
+ liveLineBudget = MIN_LIVE_LINE_BUDGET;
39
+ viewportRows;
40
+ lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;
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);
59
+ /** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */
60
+ setLiveLineBudget(lines) {
61
+ this.viewportRows = undefined;
62
+ this.liveLineBudget = Math.max(1, Math.floor(lines));
63
+ }
64
+ /**
65
+ * Follow the terminal height: budget = max(120, 2 × rows), re-read on every
66
+ * render so a resize applies on the next frame.
67
+ */
68
+ setViewportRows(rows) {
69
+ this.viewportRows = rows;
70
+ }
71
+ getLiveLineBudget() {
72
+ if (!this.viewportRows)
73
+ return this.liveLineBudget;
74
+ const rows = Math.floor(this.viewportRows());
75
+ return Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);
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
+ }
107
+ getFrozenChildCount() {
108
+ let count = 0;
109
+ for (const segment of this.segments)
110
+ count += segment.children.length;
111
+ return count;
112
+ }
113
+ getFrozenLineCount() {
114
+ let count = 0;
115
+ for (const segment of this.segments)
116
+ count += segment.lines.length;
117
+ return count;
118
+ }
119
+ /** Diagnostics: number of frozen segments. */
120
+ getFrozenSegmentCount() {
121
+ return this.segments.length;
122
+ }
123
+ /** Diagnostics: frozen child index ranges [from, to). */
124
+ getFrozenRanges() {
125
+ return this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));
126
+ }
127
+ /** Diagnostics: what the most recent `render` had to redo. */
128
+ getLastRenderStats() {
129
+ return this.stats;
130
+ }
131
+ /** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */
132
+ isChildSettled(child) {
133
+ return isRenderSettled(child);
134
+ }
135
+ removeChild(component) {
136
+ const index = this.children.indexOf(component);
137
+ if (index === -1)
138
+ return;
139
+ super.removeChild(component);
140
+ if (index < this.virtualChildren.length)
141
+ this.virtualChildren.splice(index, 1);
142
+ dropFrozenChild(this.segments, index);
143
+ }
144
+ clear() {
145
+ this.thaw();
146
+ super.clear();
147
+ }
148
+ invalidate() {
149
+ this.thaw();
150
+ super.invalidate();
151
+ }
152
+ /** Drop every frozen segment and the virtual prefix; the next render re-lays out all children. */
153
+ thaw() {
154
+ this.segments = [];
155
+ this.virtualChildren = [];
156
+ this.frozenWidth = -1;
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
+ }
166
+ render(width) {
167
+ const budget = this.getLiveLineBudget();
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)
171
+ this.thaw();
172
+ this.lastLiveLineBudget = budget;
173
+ this.lastPrefixSeedLines = seed;
174
+ this.frozenWidth = width;
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();
182
+ this.reconcile(width);
183
+ this.stats.virtualChildren = this.virtualChildren.length;
184
+ const parts = [];
185
+ const partStarts = [];
186
+ const live = [];
187
+ const liveLines = [];
188
+ let segmentIndex = 0;
189
+ for (let i = this.virtualChildren.length; i < this.children.length;) {
190
+ const segment = this.segments[segmentIndex];
191
+ let lines;
192
+ partStarts.push(i);
193
+ if (segment && segment.from === i) {
194
+ lines = segment.lines;
195
+ i += segment.children.length;
196
+ segmentIndex += 1;
197
+ }
198
+ else {
199
+ lines = memo?.get(i) ?? this.children[i].render(width);
200
+ live.push(i);
201
+ liveLines.push(lines);
202
+ i += 1;
203
+ }
204
+ parts.push(lines);
205
+ }
206
+ this.stats.liveChildren = live.length;
207
+ // Copy before freezing: freezing appends to segment line buffers. Native
208
+ // concat is a bulk copy; spread arguments stay far below engine limits
209
+ // because frozen rows arrive as a few hundred segment arrays.
210
+ const out = parts.length < 8192 ? [].concat(...parts) : parts.flat();
211
+ freezeAboveBudget(this.segments, this.children, this.settled, parts, partStarts, live, liveLines, budget);
212
+ return out;
213
+ }
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;
234
+ }
235
+ this.virtualChildren = this.children.slice(0, i);
236
+ return memo;
237
+ }
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;
251
+ }
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 ?? [];
256
+ }
257
+ }
258
+ //# sourceMappingURL=windowed-container.js.map