silvery 0.17.0 → 0.17.2

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 (171) hide show
  1. package/dist/UPNG-AVSMjiFE.mjs +5076 -0
  2. package/dist/UPNG-AVSMjiFE.mjs.map +1 -0
  3. package/dist/__vite-browser-external-2447137e-D3GdsvS_.mjs +6 -0
  4. package/dist/__vite-browser-external-2447137e-D3GdsvS_.mjs.map +1 -0
  5. package/dist/animation-C_PTO0uH.mjs +304 -0
  6. package/dist/animation-C_PTO0uH.mjs.map +1 -0
  7. package/dist/ansi-CXLE_pt1.mjs +71 -0
  8. package/dist/ansi-CXLE_pt1.mjs.map +1 -0
  9. package/dist/ansi-zmNzgkPB.d.mts +49 -0
  10. package/dist/ansi-zmNzgkPB.d.mts.map +1 -0
  11. package/dist/apng-DCWY913R.mjs +3 -0
  12. package/dist/apng-ENBAJk-H.mjs +70 -0
  13. package/dist/apng-ENBAJk-H.mjs.map +1 -0
  14. package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
  15. package/dist/backend-CkIkIHR-.mjs +13396 -0
  16. package/dist/backend-CkIkIHR-.mjs.map +1 -0
  17. package/dist/backends-CkvbG3js.mjs +1181 -0
  18. package/dist/backends-CkvbG3js.mjs.map +1 -0
  19. package/dist/backends-CyJqNLeK.mjs +3 -0
  20. package/dist/chunk-BSw8zbkd.mjs +37 -0
  21. package/dist/cli-B-k7Bm56.mjs +4 -0
  22. package/dist/context-QreF3UHr.mjs +64 -0
  23. package/dist/context-QreF3UHr.mjs.map +1 -0
  24. package/dist/derive-D7bFJdfU.d.mts +28 -0
  25. package/dist/derive-D7bFJdfU.d.mts.map +1 -0
  26. package/dist/devtools-CscuKaDK.mjs +89 -0
  27. package/dist/devtools-CscuKaDK.mjs.map +1 -0
  28. package/dist/devtools-D4oGc6LY.mjs +2 -0
  29. package/dist/eta-DLiVPaSD.mjs +110 -0
  30. package/dist/eta-DLiVPaSD.mjs.map +1 -0
  31. package/dist/flexily-zero-adapter-DmG4Ge8t.mjs +3376 -0
  32. package/dist/flexily-zero-adapter-DmG4Ge8t.mjs.map +1 -0
  33. package/dist/flexily-zero-adapter-GHwEW11s.mjs +2 -0
  34. package/dist/gif-BaJNREpP.mjs +3 -0
  35. package/dist/gif-Bp6fIyN3.mjs +73 -0
  36. package/dist/gif-Bp6fIyN3.mjs.map +1 -0
  37. package/dist/gifenc-GiVCZ9-3.mjs +730 -0
  38. package/dist/gifenc-GiVCZ9-3.mjs.map +1 -0
  39. package/dist/image-Dx7gYjkq.mjs +346 -0
  40. package/dist/image-Dx7gYjkq.mjs.map +1 -0
  41. package/dist/index-CBcSpGSM.d.mts +3416 -0
  42. package/dist/index-CBcSpGSM.d.mts.map +1 -0
  43. package/dist/index-DCVL3jHo.d.mts +634 -0
  44. package/dist/index-DCVL3jHo.d.mts.map +1 -0
  45. package/dist/index-p-wBs_wH.d.mts +175 -0
  46. package/dist/index-p-wBs_wH.d.mts.map +1 -0
  47. package/dist/index.d.mts +7296 -0
  48. package/dist/index.d.mts.map +1 -0
  49. package/dist/index.mjs +9399 -0
  50. package/dist/index.mjs.map +1 -0
  51. package/dist/key-mapping-BsUHe_nk.mjs +3 -0
  52. package/dist/key-mapping-DsyfLEdC.mjs +132 -0
  53. package/dist/key-mapping-DsyfLEdC.mjs.map +1 -0
  54. package/dist/layout-engine-B3dsnVLU.mjs +50 -0
  55. package/dist/layout-engine-B3dsnVLU.mjs.map +1 -0
  56. package/dist/layout-engine-D_lSR4i9.mjs +2 -0
  57. package/dist/multi-progress-C0-rkn86.d.mts +180 -0
  58. package/dist/multi-progress-C0-rkn86.d.mts.map +1 -0
  59. package/dist/multi-progress-CQVB9lES.mjs +219 -0
  60. package/dist/multi-progress-CQVB9lES.mjs.map +1 -0
  61. package/dist/node-Dedx-6xF.mjs +1085 -0
  62. package/dist/node-Dedx-6xF.mjs.map +1 -0
  63. package/dist/pipeline-DDOPrjuY.mjs +4387 -0
  64. package/dist/pipeline-DDOPrjuY.mjs.map +1 -0
  65. package/dist/progress-bar-COPSBlT9.mjs +155 -0
  66. package/dist/progress-bar-COPSBlT9.mjs.map +1 -0
  67. package/dist/reconciler-2lp5VXK7.mjs +16506 -0
  68. package/dist/reconciler-2lp5VXK7.mjs.map +1 -0
  69. package/dist/render-string-BXvxTg5P.mjs +201 -0
  70. package/dist/render-string-BXvxTg5P.mjs.map +1 -0
  71. package/dist/render-string-hvfpVtoP.mjs +2 -0
  72. package/dist/resvg-js-V6oMi8CY.mjs +203 -0
  73. package/dist/resvg-js-V6oMi8CY.mjs.map +1 -0
  74. package/dist/runtime-BjDHNTxJ.mjs +8723 -0
  75. package/dist/runtime-BjDHNTxJ.mjs.map +1 -0
  76. package/dist/runtime.d.mts +2 -0
  77. package/dist/runtime.mjs +3 -0
  78. package/dist/spinner-Cgej6Vnb.d.mts +127 -0
  79. package/dist/spinner-Cgej6Vnb.d.mts.map +1 -0
  80. package/dist/spinner-DSByknyx.mjs +298 -0
  81. package/dist/spinner-DSByknyx.mjs.map +1 -0
  82. package/dist/src-9B5k0JmY.mjs +1629 -0
  83. package/dist/src-9B5k0JmY.mjs.map +1 -0
  84. package/dist/src-C9f3hiVG.mjs +3620 -0
  85. package/dist/src-C9f3hiVG.mjs.map +1 -0
  86. package/dist/src-fJVbhdn-.mjs +816 -0
  87. package/dist/src-fJVbhdn-.mjs.map +1 -0
  88. package/dist/theme.d.mts +115 -0
  89. package/dist/theme.d.mts.map +1 -0
  90. package/dist/theme.mjs +8 -0
  91. package/dist/theme.mjs.map +1 -0
  92. package/dist/types-Bhj5QkIQ.mjs +13 -0
  93. package/dist/types-Bhj5QkIQ.mjs.map +1 -0
  94. package/dist/types-CDgkE-Rw.d.mts +241 -0
  95. package/dist/types-CDgkE-Rw.d.mts.map +1 -0
  96. package/dist/ui/animation.d.mts +2 -0
  97. package/dist/ui/animation.mjs +2 -0
  98. package/dist/ui/ansi.d.mts +2 -0
  99. package/dist/ui/ansi.mjs +2 -0
  100. package/dist/ui/cli.d.mts +5 -0
  101. package/dist/ui/cli.mjs +7 -0
  102. package/dist/ui/display.d.mts +35 -0
  103. package/dist/ui/display.d.mts.map +1 -0
  104. package/dist/ui/display.mjs +123 -0
  105. package/dist/ui/display.mjs.map +1 -0
  106. package/dist/ui/image.d.mts +2 -0
  107. package/dist/ui/image.mjs +2 -0
  108. package/dist/ui/input.d.mts +184 -0
  109. package/dist/ui/input.d.mts.map +1 -0
  110. package/dist/ui/input.mjs +285 -0
  111. package/dist/ui/input.mjs.map +1 -0
  112. package/dist/ui/progress.d.mts +249 -0
  113. package/dist/ui/progress.d.mts.map +1 -0
  114. package/dist/ui/progress.mjs +858 -0
  115. package/dist/ui/progress.mjs.map +1 -0
  116. package/dist/ui/react.d.mts +280 -0
  117. package/dist/ui/react.d.mts.map +1 -0
  118. package/dist/ui/react.mjs +413 -0
  119. package/dist/ui/react.mjs.map +1 -0
  120. package/dist/ui/utils.d.mts +86 -0
  121. package/dist/ui/utils.d.mts.map +1 -0
  122. package/dist/ui/utils.mjs +2 -0
  123. package/dist/ui/wrappers.d.mts +3 -0
  124. package/dist/ui/wrappers.mjs +2 -0
  125. package/dist/ui.d.mts +6 -0
  126. package/dist/ui.mjs +7 -0
  127. package/dist/useLatest-BMIYXd6e.d.mts +154 -0
  128. package/dist/useLatest-BMIYXd6e.d.mts.map +1 -0
  129. package/dist/useLayout-BG2cGl15.mjs +139 -0
  130. package/dist/useLayout-BG2cGl15.mjs.map +1 -0
  131. package/dist/with-text-input-CmHf_9d6.d.mts +284 -0
  132. package/dist/with-text-input-CmHf_9d6.d.mts.map +1 -0
  133. package/dist/wrapper-Dqh0zi2W.mjs +3527 -0
  134. package/dist/wrapper-Dqh0zi2W.mjs.map +1 -0
  135. package/dist/wrappers-hhL8EQ_n.mjs +810 -0
  136. package/dist/wrappers-hhL8EQ_n.mjs.map +1 -0
  137. package/dist/yoga-adapter-BJ9SOhTY.mjs +245 -0
  138. package/dist/yoga-adapter-BJ9SOhTY.mjs.map +1 -0
  139. package/dist/yoga-adapter-Daq6-dw1.mjs +2 -0
  140. package/package.json +48 -75
  141. package/CHANGELOG.md +0 -319
  142. package/dist/chalk.js +0 -4
  143. package/dist/index.js +0 -270
  144. package/dist/ink.js +0 -142
  145. package/dist/runtime.js +0 -135
  146. package/dist/theme.js +0 -7
  147. package/dist/ui/animation.js +0 -3
  148. package/dist/ui/ansi.js +0 -3
  149. package/dist/ui/cli.js +0 -9
  150. package/dist/ui/display.js +0 -4
  151. package/dist/ui/image.js +0 -4
  152. package/dist/ui/input.js +0 -3
  153. package/dist/ui/progress.js +0 -9
  154. package/dist/ui/react.js +0 -4
  155. package/dist/ui/utils.js +0 -3
  156. package/dist/ui/wrappers.js +0 -15
  157. package/dist/ui.js +0 -18
  158. package/src/index.ts +0 -73
  159. package/src/runtime.ts +0 -4
  160. package/src/theme.ts +0 -4
  161. package/src/ui/animation.ts +0 -2
  162. package/src/ui/ansi.ts +0 -2
  163. package/src/ui/cli.ts +0 -3
  164. package/src/ui/display.ts +0 -2
  165. package/src/ui/image.ts +0 -2
  166. package/src/ui/input.ts +0 -2
  167. package/src/ui/progress.ts +0 -2
  168. package/src/ui/react.ts +0 -2
  169. package/src/ui/utils.ts +0 -2
  170. package/src/ui/wrappers.ts +0 -2
  171. package/src/ui.ts +0 -4
@@ -0,0 +1,4387 @@
1
+ import { F as outputPhase, Ft as createMutableCell, G as getActiveLineHeight, It as createTextFrame, J as graphemeWidth, Lt as init_buffer, Ot as DEFAULT_BG, R as canBreakAnywhere, U as displayWidthAnsi, W as ensureEmojiPresentation, Y as hasAnsi, _ as isCurrentEpoch, c as clearDirtyTracking, ct as runWithMeasurer, d as hasScrollDirty, dt as sliceByWidth, f as measureStats, ft as sliceByWidthFromEnd, g as isAnyDirty, h as getRenderEpoch, ht as splitGraphemesAnsiAware, kt as TerminalBuffer, l as clearLayoutDirtyTracking, m as advanceRenderEpoch, mt as splitGraphemes, nt as isWordBoundary, p as collectPlainText, st as parseAnsiText, u as hasLayoutDirty, v as isDirty, yt as wrapText } from "./reconciler-2lp5VXK7.mjs";
2
+ import { C as resolveThemeColor } from "./src-9B5k0JmY.mjs";
3
+ import { D as init_state, E as getActiveTheme, O as popContextTheme, k as pushContextTheme, x as init_resolve } from "./src-C9f3hiVG.mjs";
4
+ import { r as getLayoutEngine } from "./layout-engine-B3dsnVLU.mjs";
5
+ import { t as rectEqual } from "./types-Bhj5QkIQ.mjs";
6
+ import { createLogger } from "loggily";
7
+ //#region packages/ag-term/src/pipeline/prepared-text.ts
8
+ const MAX_FORMAT_ENTRIES = 4;
9
+ /** Content-affecting flags that invalidate plain text. */
10
+ const PLAIN_TEXT_DIRTY = 9;
11
+ /**
12
+ * All flags that affect collected text (ANSI codes, bg segments, child spans).
13
+ * SUBTREE_BIT is included because collectTextWithBg recurses into virtual text
14
+ * children — a child's style change sets SUBTREE_BIT on the parent without
15
+ * setting STYLE_PROPS_BIT (the parent's own props didn't change).
16
+ */
17
+ const COLLECTED_TEXT_DIRTY = 31;
18
+ /** Set to true to disable all caching (for testing/debugging). */
19
+ let _cacheDisabled = !!process.env.SILVERY_NO_TEXT_CACHE;
20
+ const textCaches = /* @__PURE__ */ new WeakMap();
21
+ function getOrCreate(node) {
22
+ let entry = textCaches.get(node);
23
+ if (!entry) {
24
+ entry = {
25
+ plainText: null,
26
+ plainTextLineCount: 0,
27
+ collected: null,
28
+ collectedMaxDisplayWidth: void 0,
29
+ formats: [],
30
+ analysis: null
31
+ };
32
+ textCaches.set(node, entry);
33
+ }
34
+ return entry;
35
+ }
36
+ /**
37
+ * Get cached plain text and line count.
38
+ * Returns null on cache miss (content/children changed or first access).
39
+ */
40
+ function getCachedPlainText(node) {
41
+ if (_cacheDisabled) return null;
42
+ const entry = textCaches.get(node);
43
+ if (entry?.plainText == null) return null;
44
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, PLAIN_TEXT_DIRTY)) {
45
+ entry.plainText = null;
46
+ return null;
47
+ }
48
+ return {
49
+ text: entry.plainText,
50
+ lineCount: entry.plainTextLineCount
51
+ };
52
+ }
53
+ /** Store plain text in cache. */
54
+ function setCachedPlainText(node, text, lineCount) {
55
+ const entry = getOrCreate(node);
56
+ entry.plainText = text;
57
+ entry.plainTextLineCount = lineCount;
58
+ }
59
+ /**
60
+ * Get cached collected text (from collectTextWithBg).
61
+ * Invalidated by content, children, style, or bg changes, or maxDisplayWidth mismatch.
62
+ */
63
+ function getCachedCollectedText(node, maxDisplayWidth) {
64
+ if (_cacheDisabled) return null;
65
+ const entry = textCaches.get(node);
66
+ if (!entry?.collected) return null;
67
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, COLLECTED_TEXT_DIRTY)) {
68
+ entry.collected = null;
69
+ entry.formats = [];
70
+ entry.analysis = null;
71
+ return null;
72
+ }
73
+ if (entry.collectedMaxDisplayWidth !== maxDisplayWidth) {
74
+ entry.collected = null;
75
+ entry.formats = [];
76
+ entry.analysis = null;
77
+ return null;
78
+ }
79
+ return entry.collected;
80
+ }
81
+ /** Store collected text in cache. */
82
+ function setCachedCollectedText(node, result, maxDisplayWidth) {
83
+ const entry = getOrCreate(node);
84
+ entry.collected = result;
85
+ entry.collectedMaxDisplayWidth = maxDisplayWidth;
86
+ }
87
+ /**
88
+ * Get cached formatted lines for the given width/wrap/trim.
89
+ * Returns null on cache miss.
90
+ */
91
+ function getCachedFormat(node, width, wrap, trim) {
92
+ if (_cacheDisabled) return null;
93
+ const entry = textCaches.get(node);
94
+ if (!entry || entry.formats.length === 0) return null;
95
+ for (let i = 0; i < entry.formats.length; i++) {
96
+ const f = entry.formats[i];
97
+ if (f.width === width && f.wrap === wrap && f.trim === trim) {
98
+ if (i < entry.formats.length - 1) {
99
+ entry.formats.splice(i, 1);
100
+ entry.formats.push(f);
101
+ }
102
+ return f;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+ /** Store formatted lines in cache (LRU, evicts oldest when full). */
108
+ function setCachedFormat(node, width, wrap, trim, lines, lineOffsets, hasLineOffsets) {
109
+ const entry = getOrCreate(node);
110
+ for (let i = 0; i < entry.formats.length; i++) {
111
+ const f = entry.formats[i];
112
+ if (f.width === width && f.wrap === wrap && f.trim === trim) {
113
+ entry.formats[i] = {
114
+ width,
115
+ wrap,
116
+ trim,
117
+ lines,
118
+ lineOffsets,
119
+ hasLineOffsets
120
+ };
121
+ return;
122
+ }
123
+ }
124
+ if (entry.formats.length >= MAX_FORMAT_ENTRIES) entry.formats.shift();
125
+ entry.formats.push({
126
+ width,
127
+ wrap,
128
+ trim,
129
+ lines,
130
+ lineOffsets,
131
+ hasLineOffsets
132
+ });
133
+ }
134
+ /**
135
+ * Get cached text analysis. Invalidated when content changes.
136
+ * Uses PLAIN_TEXT_DIRTY (not COLLECTED_TEXT_DIRTY) because analysis
137
+ * is built from plain text in measure phase, not styled text.
138
+ */
139
+ function getCachedAnalysis(node) {
140
+ if (_cacheDisabled) return null;
141
+ const entry = textCaches.get(node);
142
+ if (!entry?.analysis) return null;
143
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, PLAIN_TEXT_DIRTY)) {
144
+ entry.analysis = null;
145
+ return null;
146
+ }
147
+ return entry.analysis;
148
+ }
149
+ /** Store text analysis in cache. */
150
+ function setCachedAnalysis(node, analysis) {
151
+ const entry = getOrCreate(node);
152
+ entry.analysis = analysis;
153
+ }
154
+ //#endregion
155
+ //#region packages/ag-term/src/pipeline/pretext.ts
156
+ /**
157
+ * Pretext: Grapheme-indexed text analysis for layout queries.
158
+ *
159
+ * Inspired by https://chenglou.me/pretext/ — prepare text once, measure at
160
+ * any width cheaply. Enables layout algorithms CSS can't express:
161
+ *
162
+ * - **Shrinkwrap**: find the narrowest width that keeps the same line count
163
+ * - **Balanced**: equalize line widths (reduce raggedness)
164
+ * - **Knuth-Plass**: optimal paragraph breaking (minimize total raggedness)
165
+ * - **Height prediction**: exact line count at any width without full wrapping
166
+ */
167
+ /**
168
+ * Build text analysis from an ANSI-embedded text string.
169
+ * O(N) where N is grapheme count. Call once per text change (cached by PreparedText).
170
+ */
171
+ function buildTextAnalysis(text, gWidthFn = graphemeWidth) {
172
+ const graphemes = splitGraphemesAnsiAware(text);
173
+ const len = graphemes.length;
174
+ const widths = new Array(len);
175
+ const cumWidths = new Array(len + 1);
176
+ const newlineIndices = [];
177
+ const breakIndices = [];
178
+ cumWidths[0] = 0;
179
+ let maxWordWidth = 0;
180
+ let maxGraphemeWidth = 0;
181
+ let currentWordWidth = 0;
182
+ for (let i = 0; i < len; i++) {
183
+ const g = graphemes[i];
184
+ const w = gWidthFn(g);
185
+ widths[i] = w;
186
+ cumWidths[i + 1] = cumWidths[i] + w;
187
+ if (w > maxGraphemeWidth) maxGraphemeWidth = w;
188
+ if (g === "\n") {
189
+ newlineIndices.push(i);
190
+ maxWordWidth = Math.max(maxWordWidth, currentWordWidth);
191
+ currentWordWidth = 0;
192
+ } else if (isWordBoundary(g)) {
193
+ breakIndices.push(i + 1);
194
+ maxWordWidth = Math.max(maxWordWidth, currentWordWidth);
195
+ currentWordWidth = 0;
196
+ } else if (canBreakAnywhere(g)) {
197
+ breakIndices.push(i);
198
+ maxWordWidth = Math.max(maxWordWidth, currentWordWidth);
199
+ currentWordWidth = w;
200
+ } else if (w > 0) currentWordWidth += w;
201
+ }
202
+ maxWordWidth = Math.max(maxWordWidth, currentWordWidth);
203
+ return {
204
+ graphemes,
205
+ widths,
206
+ cumWidths,
207
+ totalWidth: cumWidths[len],
208
+ maxWordWidth,
209
+ maxGraphemeWidth,
210
+ newlineIndices,
211
+ breakIndices,
212
+ text
213
+ };
214
+ }
215
+ /**
216
+ * Count how many lines text would occupy at a given width.
217
+ *
218
+ * Delegates to wrapText for correctness — the greedy wrapping algorithm has
219
+ * subtle boundary-char handling (spaces consumed on overflow, leading space
220
+ * trimming on continuation lines) that's error-prone to reimplement.
221
+ *
222
+ * For terminal text (20-200 chars), wrapText is ~5-12µs per call.
223
+ * Shrinkwrap does ~7-9 calls (log2(width)), so total is ~50-100µs.
224
+ */
225
+ function countLinesAtWidth(analysis, width) {
226
+ if (width <= 0) return Infinity;
227
+ if (analysis.totalWidth <= width && analysis.newlineIndices.length === 0) return 1;
228
+ return wrapText(analysis.text, width, true, true).length;
229
+ }
230
+ /**
231
+ * Find the narrowest integer width that produces the same line count as maxWidth.
232
+ *
233
+ * CSS fit-content uses the widest wrapped line — leaving dead space when the
234
+ * last line is short. Shrinkwrap binary-searches for the tightest width that
235
+ * keeps the same number of lines, eliminating wasted area in bubbles/cards.
236
+ *
237
+ * O(log(maxWidth) × wrapText) — ~7-9 iterations for terminal widths.
238
+ */
239
+ function shrinkwrapWidth(analysis, maxWidth) {
240
+ if (maxWidth <= 0) return 0;
241
+ const targetLineCount = countLinesAtWidth(analysis, maxWidth);
242
+ if (targetLineCount <= 1) return Math.min(Math.ceil(analysis.totalWidth), maxWidth);
243
+ let lo = Math.max(1, analysis.maxGraphemeWidth);
244
+ let hi = maxWidth;
245
+ if (lo >= hi) return Math.min(hi, maxWidth);
246
+ while (lo < hi) {
247
+ const mid = lo + hi >> 1;
248
+ if (countLinesAtWidth(analysis, mid) <= targetLineCount) hi = mid;
249
+ else lo = mid + 1;
250
+ }
251
+ return Math.min(lo, maxWidth);
252
+ }
253
+ /**
254
+ * Find optimal line breaks that minimize total raggedness.
255
+ *
256
+ * Runs per-paragraph (split by newlines) to avoid penalty interactions
257
+ * around forced breaks. Falls back to greedy wrapping for paragraphs
258
+ * where the DP finds no feasible solution (overlong words).
259
+ *
260
+ * O(breakpoints²) per paragraph, typically much less with pruning.
261
+ */
262
+ function knuthPlassBreaks(analysis, width) {
263
+ if (width <= 0) return [];
264
+ if (analysis.totalWidth <= width && analysis.newlineIndices.length === 0) return [];
265
+ const { newlineIndices, graphemes } = analysis;
266
+ const allBreaks = [];
267
+ const paragraphStarts = [0];
268
+ for (const nl of newlineIndices) paragraphStarts.push(nl + 1);
269
+ for (let p = 0; p < paragraphStarts.length; p++) {
270
+ const pStart = paragraphStarts[p];
271
+ const pEnd = p + 1 < paragraphStarts.length ? paragraphStarts[p + 1] - 1 : graphemes.length;
272
+ if (pStart >= pEnd) continue;
273
+ const breaks = knuthPlassForParagraph(analysis, pStart, pEnd, width);
274
+ allBreaks.push(...breaks);
275
+ if (p < paragraphStarts.length - 1 && pEnd < graphemes.length) allBreaks.push(pEnd + 1);
276
+ }
277
+ return allBreaks;
278
+ }
279
+ /** DP for a single paragraph (no newlines). */
280
+ function knuthPlassForParagraph(analysis, pStart, pEnd, width) {
281
+ const { cumWidths, breakIndices, widths, graphemes } = analysis;
282
+ const candidates = [pStart];
283
+ for (const bp of breakIndices) if (bp > pStart && bp <= pEnd) candidates.push(bp);
284
+ candidates.push(pEnd);
285
+ const n = candidates.length;
286
+ if (n <= 2) return [];
287
+ const cost = new Array(n).fill(Infinity);
288
+ const next = new Array(n).fill(-1);
289
+ cost[n - 1] = 0;
290
+ for (let i = n - 2; i >= 0; i--) {
291
+ const lineStart = candidates[i];
292
+ const lineStartCum = cumWidths[lineStart];
293
+ for (let j = i + 1; j < n; j++) {
294
+ let trimEnd = candidates[j];
295
+ while (trimEnd > lineStart) {
296
+ const prevG = graphemes[trimEnd - 1];
297
+ if (widths[trimEnd - 1] === 0) {
298
+ trimEnd--;
299
+ continue;
300
+ }
301
+ if (prevG === " " || prevG === " ") {
302
+ trimEnd--;
303
+ continue;
304
+ }
305
+ break;
306
+ }
307
+ const lineWidth = cumWidths[trimEnd] - lineStartCum;
308
+ if (lineWidth > width) break;
309
+ const leftover = width - lineWidth;
310
+ const totalCost = (j === n - 1 ? 0 : leftover * leftover) + cost[j];
311
+ if (totalCost < cost[i]) {
312
+ cost[i] = totalCost;
313
+ next[i] = j;
314
+ }
315
+ }
316
+ }
317
+ if (cost[0] === Infinity) return [];
318
+ const breaks = [];
319
+ let idx = 0;
320
+ while (idx < n - 1 && next[idx] >= 0) {
321
+ idx = next[idx];
322
+ if (idx < n - 1) breaks.push(candidates[idx]);
323
+ }
324
+ return breaks;
325
+ }
326
+ /**
327
+ * Wrap text using Knuth-Plass optimal breaks.
328
+ * Returns line strings — drop-in replacement for greedy wrap.
329
+ * Falls back to greedy wrapText when DP finds no feasible solution.
330
+ */
331
+ function optimalWrap(text, analysis, width) {
332
+ const breaks = knuthPlassBreaks(analysis, width);
333
+ if (breaks.length === 0) {
334
+ if (analysis.totalWidth <= width && analysis.newlineIndices.length === 0) return [text];
335
+ return wrapText(text, width, true, true);
336
+ }
337
+ const { graphemes, widths } = analysis;
338
+ const lines = [];
339
+ let lineStart = 0;
340
+ for (const bp of breaks) {
341
+ let lineEnd = bp;
342
+ while (lineEnd > lineStart) {
343
+ if (widths[lineEnd - 1] === 0) {
344
+ lineEnd--;
345
+ continue;
346
+ }
347
+ const g = graphemes[lineEnd - 1];
348
+ if (g === " " || g === " " || g === "\n") {
349
+ lineEnd--;
350
+ continue;
351
+ }
352
+ break;
353
+ }
354
+ lines.push(graphemes.slice(lineStart, lineEnd).join(""));
355
+ lineStart = bp;
356
+ while (lineStart < graphemes.length) {
357
+ const g = graphemes[lineStart];
358
+ if (g === " " || g === " ") {
359
+ lineStart++;
360
+ continue;
361
+ }
362
+ break;
363
+ }
364
+ }
365
+ if (lineStart < graphemes.length) lines.push(graphemes.slice(lineStart).join(""));
366
+ return lines;
367
+ }
368
+ //#endregion
369
+ //#region packages/ag-term/src/pipeline/helpers.ts
370
+ /**
371
+ * Get padding values from props.
372
+ */
373
+ function getPadding(props) {
374
+ return {
375
+ top: props.paddingTop ?? props.paddingY ?? props.padding ?? 0,
376
+ bottom: props.paddingBottom ?? props.paddingY ?? props.padding ?? 0,
377
+ left: props.paddingLeft ?? props.paddingX ?? props.padding ?? 0,
378
+ right: props.paddingRight ?? props.paddingX ?? props.padding ?? 0
379
+ };
380
+ }
381
+ /**
382
+ * Get border size (1 or 0 for each side).
383
+ * In pixel/canvas mode (lineHeight > 1), borders are visual-only (fillRoundedRect)
384
+ * and don't affect content positioning — returns 0.
385
+ */
386
+ function getBorderSize(props) {
387
+ if (!props.borderStyle || getActiveLineHeight() > 1) return {
388
+ top: 0,
389
+ bottom: 0,
390
+ left: 0,
391
+ right: 0
392
+ };
393
+ return {
394
+ top: props.borderTop !== false ? 1 : 0,
395
+ bottom: props.borderBottom !== false ? 1 : 0,
396
+ left: props.borderLeft !== false ? 1 : 0,
397
+ right: props.borderRight !== false ? 1 : 0
398
+ };
399
+ }
400
+ //#endregion
401
+ //#region packages/ag-term/src/pipeline/measure-phase.ts
402
+ /**
403
+ * Handle fit-content nodes by measuring their intrinsic content size.
404
+ *
405
+ * Traverses the tree and for any node with width="fit-content" or
406
+ * height="fit-content", measures the content and sets the Yoga constraint.
407
+ */
408
+ function measurePhase(root, ctx) {
409
+ traverseTree$1(root, (node) => {
410
+ if (!node.layoutNode) return;
411
+ const props = node.props;
412
+ const isFitContent = props.width === "fit-content" || props.height === "fit-content";
413
+ const isSnugContent = props.width === "snug-content";
414
+ if (isFitContent || isSnugContent) {
415
+ let availableWidth;
416
+ const widthIsFixed = typeof props.width === "number";
417
+ if (props.height === "fit-content" && widthIsFixed) {
418
+ const padding = getPadding(props);
419
+ availableWidth = props.width - padding.left - padding.right;
420
+ if (props.borderStyle) {
421
+ const border = getBorderSize(props);
422
+ availableWidth -= border.left + border.right;
423
+ }
424
+ if (availableWidth < 1) availableWidth = 1;
425
+ }
426
+ const intrinsicSize = measureIntrinsicSize(node, ctx, availableWidth);
427
+ if (isSnugContent) {
428
+ const shrunkWidth = computeSnugContentWidth(node, intrinsicSize.width, ctx);
429
+ node.layoutNode.setWidth(shrunkWidth);
430
+ } else if (props.width === "fit-content") node.layoutNode.setWidth(intrinsicSize.width);
431
+ if (props.height === "fit-content") node.layoutNode.setHeight(intrinsicSize.height);
432
+ }
433
+ });
434
+ }
435
+ /**
436
+ * Measure the intrinsic size of a node's content.
437
+ *
438
+ * For text nodes: measures the text width and line count.
439
+ * For box nodes: recursively measures children based on flex direction.
440
+ *
441
+ * @param availableWidth - When set, text nodes wrap at this width for height calculation.
442
+ * Used when a container has fixed width + fit-content height.
443
+ */
444
+ function measureIntrinsicSize(node, ctx, availableWidth) {
445
+ const props = node.props;
446
+ if (props.display === "none") return {
447
+ width: 0,
448
+ height: 0
449
+ };
450
+ if (node.type === "silvery-text") {
451
+ const textProps = props;
452
+ const cached = getCachedPlainText(node);
453
+ let text;
454
+ if (cached) text = cached.text;
455
+ else {
456
+ text = collectPlainText(node);
457
+ const lineCount = (text.match(/\n/g)?.length ?? 0) + 1;
458
+ setCachedPlainText(node, text, lineCount);
459
+ }
460
+ const transform = textProps.internal_transform;
461
+ let lines;
462
+ if (availableWidth !== void 0 && availableWidth > 0 && isWrapEnabled(textProps.wrap)) lines = ctx ? ctx.measurer.wrapText(text, availableWidth, true, true) : wrapText(text, availableWidth, true, true);
463
+ else lines = text.split("\n");
464
+ if (transform) lines = lines.map((line, index) => transform(line, index));
465
+ return {
466
+ width: Math.max(...lines.map((line) => getTextWidth$1(line, ctx))),
467
+ height: lines.length * getActiveLineHeight()
468
+ };
469
+ }
470
+ const isRow = props.flexDirection === "row" || props.flexDirection === "row-reverse";
471
+ let width = 0;
472
+ let height = 0;
473
+ let childCount = 0;
474
+ for (const child of node.children) {
475
+ const childSize = measureIntrinsicSize(child, ctx, availableWidth);
476
+ childCount++;
477
+ if (isRow) {
478
+ width += childSize.width;
479
+ height = Math.max(height, childSize.height);
480
+ } else {
481
+ width = Math.max(width, childSize.width);
482
+ height += childSize.height;
483
+ }
484
+ }
485
+ const gap = props.gap ?? 0;
486
+ if (gap > 0 && childCount > 1) {
487
+ const totalGap = gap * (childCount - 1);
488
+ if (isRow) width += totalGap;
489
+ else height += totalGap;
490
+ }
491
+ const padding = getPadding(props);
492
+ width += padding.left + padding.right;
493
+ height += padding.top + padding.bottom;
494
+ if (props.borderStyle) {
495
+ const border = getBorderSize(props);
496
+ width += border.left + border.right;
497
+ height += border.top + border.bottom;
498
+ }
499
+ return {
500
+ width,
501
+ height
502
+ };
503
+ }
504
+ /**
505
+ * Check if text wrapping is enabled for a text node.
506
+ */
507
+ function isWrapEnabled(wrap) {
508
+ return wrap === "wrap" || wrap === "hard" || wrap === "even" || wrap === true || wrap === void 0;
509
+ }
510
+ /**
511
+ * Compute snug-content width for a node.
512
+ * Uses Pretext analysis to binary-search for the tightest width
513
+ * that keeps the same line count as the fit-content width.
514
+ */
515
+ function computeSnugContentWidth(node, fitContentWidth, ctx) {
516
+ const props = node.props;
517
+ let overhead = 0;
518
+ const padding = getPadding(props);
519
+ overhead += padding.left + padding.right;
520
+ if (props.borderStyle) {
521
+ const border = getBorderSize(props);
522
+ overhead += border.left + border.right;
523
+ }
524
+ const contentWidth = fitContentWidth - overhead;
525
+ let analysis = getCachedAnalysis(node);
526
+ if (!analysis) {
527
+ const cached = getCachedPlainText(node);
528
+ const text = cached ? cached.text : collectPlainText(node);
529
+ analysis = buildTextAnalysis(text, ctx?.measurer?.graphemeWidth?.bind(ctx.measurer) ?? graphemeWidth);
530
+ setCachedAnalysis(node, analysis);
531
+ if (!cached) setCachedPlainText(node, text, (text.match(/\n/g)?.length ?? 0) + 1);
532
+ }
533
+ return shrinkwrapWidth(analysis, contentWidth) + overhead;
534
+ }
535
+ /**
536
+ * Traverse tree in depth-first order.
537
+ */
538
+ function traverseTree$1(node, callback) {
539
+ callback(node);
540
+ for (const child of node.children) traverseTree$1(child, callback);
541
+ }
542
+ /**
543
+ * Get text display width (accounting for wide characters and ANSI codes).
544
+ * Uses ANSI-aware width calculation to handle styled text.
545
+ */
546
+ function getTextWidth$1(text, ctx) {
547
+ if (ctx) return ctx.measurer.displayWidthAnsi(text);
548
+ return displayWidthAnsi(text);
549
+ }
550
+ //#endregion
551
+ //#region packages/ag-term/src/pipeline/layout-phase.ts
552
+ /**
553
+ * Phase 2: Layout Phase
554
+ *
555
+ * Run Yoga layout calculation and propagate dimensions to all nodes.
556
+ */
557
+ const log$3 = createLogger("silvery:layout");
558
+ /**
559
+ * Run Yoga layout calculation and propagate dimensions to all nodes.
560
+ *
561
+ * @param root The root SilveryNode
562
+ * @param width Terminal width in columns
563
+ * @param height Terminal height in rows
564
+ */
565
+ function layoutPhase(root, width, height) {
566
+ const prevLayout = root.boxRect;
567
+ const dimensionsChanged = prevLayout && (prevLayout.width !== width || prevLayout.height !== height);
568
+ if (!dimensionsChanged && !hasLayoutDirty()) return;
569
+ clearLayoutDirtyTracking();
570
+ if (root.layoutNode) {
571
+ const nodeCount = countNodes(root);
572
+ measureStats.reset();
573
+ const t0 = Date.now();
574
+ root.layoutNode.calculateLayout(width, height);
575
+ const elapsed = Date.now() - t0;
576
+ log$3.debug?.(`calculateLayout: ${elapsed}ms (${nodeCount} nodes) measure: calls=${measureStats.calls} hits=${measureStats.cacheHits} collects=${measureStats.textCollects} displayWidth=${measureStats.displayWidthCalls}`);
577
+ }
578
+ propagateLayout(root, 0, 0, !dimensionsChanged);
579
+ }
580
+ /**
581
+ * Count total nodes in tree.
582
+ */
583
+ function countNodes(node) {
584
+ let count = 1;
585
+ for (const child of node.children) count += countNodes(child);
586
+ return count;
587
+ }
588
+ /**
589
+ * Propagate computed layout from Yoga nodes to SilveryNodes.
590
+ * Sets boxRect (content-relative position) on each node.
591
+ *
592
+ * When `incrementalSkip` is true, nodes whose Flexily-computed rect matches
593
+ * their existing boxRect can skip the entire subtree — their layout is
594
+ * unchanged. This converts the O(N) tree walk into O(dirty) for frames
595
+ * where only a few nodes changed layout.
596
+ *
597
+ * The skip is safe because:
598
+ * - Flexily's internal fingerprint caching guarantees identical output for
599
+ * subtrees whose inputs didn't change
600
+ * - If the parent's rect matches, all descendants' rects also match
601
+ * (Flexily computes absolute positions from parent dimensions)
602
+ * - prevLayout, layoutDirty (already false), and layoutChangedThisFrame
603
+ * (stale epoch, won't match current) all retain correct values
604
+ *
605
+ * @param node The node to process
606
+ * @param parentX Absolute X position of parent
607
+ * @param parentY Absolute Y position of parent
608
+ * @param incrementalSkip When true, skip subtrees where Flexily results match existing boxRect
609
+ */
610
+ function propagateLayout(node, parentX, parentY, incrementalSkip) {
611
+ if (!node.layoutNode) {
612
+ node.prevLayout = node.boxRect;
613
+ node.boxRect = {
614
+ x: parentX,
615
+ y: parentY,
616
+ width: 0,
617
+ height: 0
618
+ };
619
+ node.layoutDirty = false;
620
+ for (const child of node.children) propagateLayout(child, parentX, parentY, incrementalSkip);
621
+ return;
622
+ }
623
+ const rect = {
624
+ x: parentX + node.layoutNode.getComputedLeft(),
625
+ y: parentY + node.layoutNode.getComputedTop(),
626
+ width: node.layoutNode.getComputedWidth(),
627
+ height: node.layoutNode.getComputedHeight()
628
+ };
629
+ if (incrementalSkip && node.boxRect && !node.layoutDirty && !isDirty(node.dirtyBits, node.dirtyEpoch, 16) && !isDirty(node.dirtyBits, node.dirtyEpoch, 8)) {
630
+ if (rect.x === node.boxRect.x && rect.y === node.boxRect.y && rect.width === node.boxRect.width && rect.height === node.boxRect.height) return;
631
+ }
632
+ node.prevLayout = node.boxRect;
633
+ node.boxRect = rect;
634
+ node.layoutDirty = false;
635
+ node.layoutChangedThisFrame = !!(node.prevLayout && !rectEqual(node.prevLayout, node.boxRect)) ? getRenderEpoch() : -1;
636
+ if (process?.env?.SILVERY_STRICT && isCurrentEpoch(node.layoutChangedThisFrame)) {
637
+ if (rectEqual(node.prevLayout, node.boxRect)) {
638
+ const props = node.props;
639
+ throw new Error(`[SILVERY_STRICT] layoutChangedThisFrame=true but prevLayout equals boxRect (node: ${props.id ?? node.type}, rect: ${JSON.stringify(node.boxRect)})`);
640
+ }
641
+ }
642
+ if (isCurrentEpoch(node.layoutChangedThisFrame)) {
643
+ const epoch = getRenderEpoch();
644
+ let ancestor = node.parent;
645
+ while (ancestor && !isDirty(ancestor.dirtyBits, ancestor.dirtyEpoch, 16)) {
646
+ if (ancestor.dirtyEpoch !== epoch) {
647
+ ancestor.dirtyBits = 16;
648
+ ancestor.dirtyEpoch = epoch;
649
+ } else ancestor.dirtyBits |= 16;
650
+ ancestor = ancestor.parent;
651
+ }
652
+ }
653
+ for (const child of node.children) propagateLayout(child, rect.x, rect.y, incrementalSkip);
654
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 16) && node.children.length > 0) {
655
+ const epoch = getRenderEpoch();
656
+ const absChild = _hasAbsoluteChildMutated(node.children);
657
+ const descOverflow = _hasDescendantOverflowChanged(node, rect);
658
+ let bits = node.dirtyBits;
659
+ if (absChild) bits |= 32;
660
+ else bits &= -33;
661
+ if (descOverflow) bits |= 64;
662
+ else bits &= -65;
663
+ node.dirtyBits = bits;
664
+ node.dirtyEpoch = epoch;
665
+ } else if (node.dirtyEpoch === getRenderEpoch()) node.dirtyBits &= -97;
666
+ }
667
+ /**
668
+ * Check if any direct child is position="absolute" and had structural changes.
669
+ */
670
+ function _hasAbsoluteChildMutated(children) {
671
+ for (const child of children) if (child.props.position === "absolute" && (isDirty(child.dirtyBits, child.dirtyEpoch, 8) || isCurrentEpoch(child.layoutChangedThisFrame) || _hasChildPositionChanged(child))) return true;
672
+ return false;
673
+ }
674
+ /**
675
+ * Check if any child's position changed (boxRect vs prevLayout).
676
+ */
677
+ function _hasChildPositionChanged(node) {
678
+ for (const child of node.children) if (child.boxRect && child.prevLayout) {
679
+ if (child.boxRect.x !== child.prevLayout.x || child.boxRect.y !== child.prevLayout.y) return true;
680
+ }
681
+ return false;
682
+ }
683
+ /**
684
+ * Check if any descendant was overflowing THIS node's rect and had its layout change.
685
+ * Recursive: follows subtreeDirty paths for efficiency.
686
+ */
687
+ function _hasDescendantOverflowChanged(node, rect) {
688
+ return _checkDescendantOverflow(node.children, rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
689
+ }
690
+ function _checkDescendantOverflow(children, nodeLeft, nodeTop, nodeRight, nodeBottom) {
691
+ for (const child of children) {
692
+ if (child.prevLayout && isCurrentEpoch(child.layoutChangedThisFrame)) {
693
+ const prev = child.prevLayout;
694
+ if (prev.x + prev.width > nodeRight || prev.y + prev.height > nodeBottom || prev.x < nodeLeft || prev.y < nodeTop) return true;
695
+ }
696
+ if (isDirty(child.dirtyBits, child.dirtyEpoch, 16) && child.children !== void 0) {
697
+ if (_checkDescendantOverflow(child.children, nodeLeft, nodeTop, nodeRight, nodeBottom)) return true;
698
+ }
699
+ }
700
+ return false;
701
+ }
702
+ /**
703
+ * Notify all layout subscribers of dimension changes.
704
+ *
705
+ * Called from executeRender AFTER scrollrectPhase completes,
706
+ * so useScrollRect can read correct screen positions.
707
+ *
708
+ * Notifies when EITHER boxRect, scrollRect, or screenRect changed.
709
+ * scrollRect can change from scroll offset changes even when
710
+ * boxRect stays the same — subscribers (like useScrollRect)
711
+ * need notification in both cases. screenRect can change from sticky
712
+ * offset changes even when scrollRect stays the same.
713
+ */
714
+ function notifyLayoutSubscribers(node) {
715
+ const contentChanged = !rectEqual(node.prevLayout, node.boxRect);
716
+ const screenChanged = !rectEqual(node.prevScrollRect, node.scrollRect);
717
+ const renderChanged = !rectEqual(node.prevScreenRect, node.screenRect);
718
+ if (contentChanged || screenChanged || renderChanged) for (const subscriber of node.layoutSubscribers) subscriber();
719
+ for (const child of node.children) notifyLayoutSubscribers(child);
720
+ }
721
+ /**
722
+ * Calculate scroll state for all overflow='scroll' containers.
723
+ *
724
+ * This phase runs after layout to determine which children are visible
725
+ * within each scrollable container.
726
+ */
727
+ function scrollPhase(root, options = {}) {
728
+ const { skipStateUpdates = false } = options;
729
+ traverseTree(root, (node) => {
730
+ const props = node.props;
731
+ if (props.overflow !== "scroll") return;
732
+ calculateScrollState(node, props, skipStateUpdates);
733
+ });
734
+ }
735
+ /**
736
+ * Calculate scroll state for a single scrollable container.
737
+ */
738
+ function calculateScrollState(node, props, skipStateUpdates) {
739
+ const layout = node.boxRect;
740
+ if (!layout || !node.layoutNode) return;
741
+ const border = props.borderStyle ? getBorderSize(props) : {
742
+ top: 0,
743
+ bottom: 0,
744
+ left: 0,
745
+ right: 0
746
+ };
747
+ const padding = getPadding(props);
748
+ const rawViewportHeight = layout.height - border.top - border.bottom - padding.top - padding.bottom;
749
+ let contentHeight = 0;
750
+ const childPositions = [];
751
+ for (let i = 0; i < node.children.length; i++) {
752
+ const child = node.children[i];
753
+ if (!child.layoutNode || !child.boxRect) continue;
754
+ const childTop = child.boxRect.y - layout.y - border.top - padding.top;
755
+ const childBottom = childTop + child.boxRect.height;
756
+ const childProps = child.props;
757
+ childPositions.push({
758
+ child,
759
+ top: childTop,
760
+ bottom: childBottom,
761
+ index: i,
762
+ isSticky: childProps.position === "sticky",
763
+ stickyTop: childProps.stickyTop,
764
+ stickyBottom: childProps.stickyBottom
765
+ });
766
+ contentHeight = Math.max(contentHeight, childBottom);
767
+ }
768
+ const viewportHeight = rawViewportHeight;
769
+ const indicatorReserve = props.overflowIndicator === true && !props.borderStyle && contentHeight > rawViewportHeight ? 1 : 0;
770
+ const prevOffset = node.scrollState?.offset;
771
+ let scrollOffset = props.scrollOffset ?? prevOffset ?? 0;
772
+ const scrollTo = props.scrollTo;
773
+ if (scrollTo !== void 0 && scrollTo >= 0 && scrollTo < childPositions.length) {
774
+ const target = childPositions.find((c) => c.index === scrollTo);
775
+ if (target) {
776
+ const effectiveHeight = viewportHeight - indicatorReserve;
777
+ const visibleTop = scrollOffset;
778
+ const visibleBottom = scrollOffset + effectiveHeight;
779
+ if (target.top < visibleTop) scrollOffset = target.top;
780
+ else if (target.bottom > visibleBottom) scrollOffset = target.bottom - effectiveHeight;
781
+ }
782
+ }
783
+ scrollOffset = Math.max(0, scrollOffset);
784
+ scrollOffset = Math.min(scrollOffset, Math.max(0, contentHeight - viewportHeight));
785
+ const visibleTop = scrollOffset;
786
+ const visibleBottom = scrollOffset + viewportHeight - indicatorReserve;
787
+ let firstVisible = -1;
788
+ let lastVisible = -1;
789
+ let hiddenAbove = 0;
790
+ let hiddenBelow = 0;
791
+ for (const cp of childPositions) {
792
+ if (cp.isSticky) {
793
+ if (firstVisible === -1) firstVisible = cp.index;
794
+ lastVisible = Math.max(lastVisible, cp.index);
795
+ continue;
796
+ }
797
+ if (cp.top === cp.bottom) continue;
798
+ if (cp.bottom <= visibleTop) hiddenAbove++;
799
+ else if (cp.top >= visibleBottom) hiddenBelow++;
800
+ else if (cp.top < visibleTop) {
801
+ if (firstVisible === -1) firstVisible = cp.index;
802
+ lastVisible = Math.max(lastVisible, cp.index);
803
+ } else if (cp.bottom > visibleBottom) {
804
+ if (firstVisible === -1) firstVisible = cp.index;
805
+ lastVisible = cp.index;
806
+ if (indicatorReserve > 0) hiddenBelow++;
807
+ } else {
808
+ if (firstVisible === -1) firstVisible = cp.index;
809
+ lastVisible = cp.index;
810
+ }
811
+ }
812
+ const stickyChildren = [];
813
+ for (const cp of childPositions) {
814
+ if (!cp.isSticky) continue;
815
+ const childHeight = cp.bottom - cp.top;
816
+ const stickyTop = cp.stickyTop ?? 0;
817
+ const stickyBottom = cp.stickyBottom;
818
+ const naturalRenderY = cp.top - scrollOffset;
819
+ let renderOffset;
820
+ if (stickyBottom !== void 0) {
821
+ const bottomPinPosition = viewportHeight - stickyBottom - childHeight;
822
+ renderOffset = Math.min(naturalRenderY, bottomPinPosition);
823
+ } else if (naturalRenderY >= stickyTop) renderOffset = naturalRenderY;
824
+ else if (childHeight > viewportHeight) renderOffset = Math.max(viewportHeight - childHeight, naturalRenderY);
825
+ else renderOffset = stickyTop;
826
+ if (renderOffset !== naturalRenderY) if (childHeight > viewportHeight) renderOffset = Math.max(viewportHeight - childHeight, renderOffset);
827
+ else renderOffset = Math.max(0, Math.min(renderOffset, viewportHeight - childHeight));
828
+ if (renderOffset + childHeight <= 0 || renderOffset >= viewportHeight) continue;
829
+ stickyChildren.push({
830
+ index: cp.index,
831
+ renderOffset,
832
+ naturalTop: cp.top,
833
+ height: childHeight
834
+ });
835
+ }
836
+ if (skipStateUpdates) return;
837
+ const prevFirstVisible = node.scrollState?.firstVisibleChild ?? firstVisible;
838
+ const prevLastVisible = node.scrollState?.lastVisibleChild ?? lastVisible;
839
+ if (scrollOffset !== prevOffset || firstVisible !== prevFirstVisible || lastVisible !== prevLastVisible) {
840
+ const epoch = getRenderEpoch();
841
+ if (node.dirtyEpoch !== epoch) {
842
+ node.dirtyBits = 16;
843
+ node.dirtyEpoch = epoch;
844
+ } else node.dirtyBits |= 16;
845
+ }
846
+ node.scrollState = {
847
+ offset: scrollOffset,
848
+ prevOffset: prevOffset ?? scrollOffset,
849
+ contentHeight,
850
+ viewportHeight,
851
+ firstVisibleChild: firstVisible,
852
+ lastVisibleChild: lastVisible,
853
+ prevFirstVisibleChild: prevFirstVisible,
854
+ prevLastVisibleChild: prevLastVisible,
855
+ hiddenAbove,
856
+ hiddenBelow,
857
+ stickyChildren: stickyChildren.length > 0 ? stickyChildren : void 0
858
+ };
859
+ }
860
+ /**
861
+ * Compute sticky offsets for non-scroll containers that have sticky children.
862
+ *
863
+ * Scroll containers handle their own sticky logic in calculateScrollState().
864
+ * This phase handles the remaining case: parents that are NOT overflow="scroll"
865
+ * but still contain position="sticky" children with stickyBottom.
866
+ *
867
+ * For non-scroll containers, sticky means: pin the child to the parent's bottom
868
+ * edge when content is shorter than the parent. When content fills the parent,
869
+ * the child stays at its natural position.
870
+ */
871
+ function stickyPhase(root) {
872
+ traverseTree(root, (node) => {
873
+ const props = node.props;
874
+ if (props.overflow === "scroll") return;
875
+ let hasStickyChildren = false;
876
+ for (const child of node.children) {
877
+ const childProps = child.props;
878
+ if (childProps.position === "sticky" && childProps.stickyBottom !== void 0) {
879
+ hasStickyChildren = true;
880
+ break;
881
+ }
882
+ }
883
+ if (!hasStickyChildren) {
884
+ if (node.stickyChildren !== void 0) {
885
+ node.stickyChildren = void 0;
886
+ const epoch = getRenderEpoch();
887
+ if (node.dirtyEpoch !== epoch) {
888
+ node.dirtyBits = 16;
889
+ node.dirtyEpoch = epoch;
890
+ } else node.dirtyBits |= 16;
891
+ }
892
+ return;
893
+ }
894
+ const layout = node.boxRect;
895
+ if (!layout || !node.layoutNode) return;
896
+ const border = props.borderStyle ? getBorderSize(props) : {
897
+ top: 0,
898
+ bottom: 0,
899
+ left: 0,
900
+ right: 0
901
+ };
902
+ const padding = getPadding(props);
903
+ const parentContentHeight = layout.height - border.top - border.bottom - padding.top - padding.bottom;
904
+ const newStickyChildren = [];
905
+ for (let i = 0; i < node.children.length; i++) {
906
+ const child = node.children[i];
907
+ const childProps = child.props;
908
+ if (childProps.position !== "sticky") continue;
909
+ if (childProps.stickyBottom === void 0) continue;
910
+ if (!child.boxRect) continue;
911
+ const naturalY = child.boxRect.y - layout.y - border.top - padding.top;
912
+ const childHeight = child.boxRect.height;
913
+ const bottomPin = parentContentHeight - childProps.stickyBottom - childHeight;
914
+ const renderOffset = Math.max(naturalY, bottomPin);
915
+ newStickyChildren.push({
916
+ index: i,
917
+ renderOffset,
918
+ naturalTop: naturalY,
919
+ height: childHeight
920
+ });
921
+ }
922
+ const prev = node.stickyChildren;
923
+ const next = newStickyChildren.length > 0 ? newStickyChildren : void 0;
924
+ const changed = !stickyChildrenEqual(prev, next);
925
+ node.stickyChildren = next;
926
+ if (changed) {
927
+ const epoch = getRenderEpoch();
928
+ if (node.dirtyEpoch !== epoch) {
929
+ node.dirtyBits = 16;
930
+ node.dirtyEpoch = epoch;
931
+ } else node.dirtyBits |= 16;
932
+ }
933
+ });
934
+ }
935
+ /**
936
+ * Compare two stickyChildren arrays for equality.
937
+ */
938
+ function stickyChildrenEqual(a, b) {
939
+ if (a === b) return true;
940
+ if (!a || !b) return false;
941
+ if (a.length !== b.length) return false;
942
+ for (let i = 0; i < a.length; i++) {
943
+ const ai = a[i];
944
+ const bi = b[i];
945
+ if (ai.index !== bi.index || ai.renderOffset !== bi.renderOffset || ai.naturalTop !== bi.naturalTop || ai.height !== bi.height) return false;
946
+ }
947
+ return true;
948
+ }
949
+ /**
950
+ * Traverse tree in depth-first order.
951
+ */
952
+ function traverseTree(node, callback) {
953
+ callback(node);
954
+ for (const child of node.children) traverseTree(child, callback);
955
+ }
956
+ /**
957
+ * Calculate screen-relative positions for all nodes.
958
+ *
959
+ * This phase runs after scroll phase to compute where each node actually
960
+ * appears on the terminal screen, accounting for all ancestor scroll offsets.
961
+ *
962
+ * Also computes `screenRect` which accounts for sticky render offsets.
963
+ * For non-sticky nodes, screenRect === scrollRect. For sticky nodes,
964
+ * screenRect reflects the actual pixel position where the node is painted.
965
+ *
966
+ * Screen position = content position - sum of ancestor scroll offsets
967
+ */
968
+ function scrollrectPhase(root) {
969
+ propagateScrollRect(root, 0);
970
+ }
971
+ /**
972
+ * Fast path for scrollrectPhase when no scroll containers or sticky nodes exist.
973
+ *
974
+ * When there are no scroll containers and no sticky nodes, ancestorScrollOffset
975
+ * is always 0, so scrollRect === boxRect and screenRect === scrollRect. This
976
+ * avoids the overhead of accumulating scroll offsets through the tree.
977
+ */
978
+ function scrollrectPhaseSimple(root) {
979
+ propagateScrollRectSimple(root);
980
+ }
981
+ /**
982
+ * Propagate screen-relative positions through the tree.
983
+ *
984
+ * @param node The node to process
985
+ * @param ancestorScrollOffset Sum of all ancestor scroll offsets
986
+ */
987
+ function propagateScrollRect(node, ancestorScrollOffset) {
988
+ node.prevScrollRect = node.scrollRect;
989
+ node.prevScreenRect = node.screenRect;
990
+ const content = node.boxRect;
991
+ if (!content) {
992
+ node.scrollRect = null;
993
+ node.screenRect = null;
994
+ for (const child of node.children) propagateScrollRect(child, ancestorScrollOffset);
995
+ return;
996
+ }
997
+ node.scrollRect = {
998
+ x: content.x,
999
+ y: content.y - ancestorScrollOffset,
1000
+ width: content.width,
1001
+ height: content.height
1002
+ };
1003
+ node.screenRect = node.scrollRect;
1004
+ const childScrollOffset = ancestorScrollOffset + (node.scrollState?.offset ?? 0);
1005
+ computeStickyScreenRects(node);
1006
+ for (const child of node.children) propagateScrollRect(child, childScrollOffset);
1007
+ }
1008
+ /**
1009
+ * Compute screenRect for sticky children of a node.
1010
+ *
1011
+ * For sticky children, the actual render position differs from the layout
1012
+ * position (scrollRect). The renderOffset from the scroll/sticky phase
1013
+ * determines where pixels are actually painted. This function sets
1014
+ * screenRect on those children to reflect the true screen position.
1015
+ *
1016
+ * @param parent The parent node whose sticky children need screenRect computation
1017
+ */
1018
+ function computeStickyScreenRects(parent) {
1019
+ const stickyList = parent.scrollState?.stickyChildren ?? parent.stickyChildren;
1020
+ if (!stickyList || stickyList.length === 0) return;
1021
+ const parentScrollRect = parent.scrollRect;
1022
+ if (!parentScrollRect) return;
1023
+ const props = parent.props;
1024
+ const border = props.borderStyle ? getBorderSize(props) : {
1025
+ top: 0,
1026
+ bottom: 0,
1027
+ left: 0,
1028
+ right: 0
1029
+ };
1030
+ const padding = getPadding(props);
1031
+ const contentOriginY = parentScrollRect.y + border.top + padding.top;
1032
+ for (const sticky of stickyList) {
1033
+ const child = parent.children[sticky.index];
1034
+ if (!child?.scrollRect) continue;
1035
+ child.screenRect = {
1036
+ x: child.scrollRect.x,
1037
+ y: contentOriginY + sticky.renderOffset,
1038
+ width: child.scrollRect.width,
1039
+ height: child.scrollRect.height
1040
+ };
1041
+ }
1042
+ }
1043
+ /**
1044
+ * Simple scrollRect propagation for trees without scroll containers or sticky nodes.
1045
+ * When ancestorScrollOffset is always 0, scrollRect === boxRect and screenRect === scrollRect.
1046
+ * Saves the overhead of accumulating scroll offsets and computing sticky screen rects.
1047
+ */
1048
+ function propagateScrollRectSimple(node) {
1049
+ node.prevScrollRect = node.scrollRect;
1050
+ node.prevScreenRect = node.screenRect;
1051
+ const content = node.boxRect;
1052
+ if (!content) {
1053
+ node.scrollRect = null;
1054
+ node.screenRect = null;
1055
+ for (const child of node.children) propagateScrollRectSimple(child);
1056
+ return;
1057
+ }
1058
+ node.scrollRect = {
1059
+ x: content.x,
1060
+ y: content.y,
1061
+ width: content.width,
1062
+ height: content.height
1063
+ };
1064
+ node.screenRect = node.scrollRect;
1065
+ for (const child of node.children) propagateScrollRectSimple(child);
1066
+ }
1067
+ /**
1068
+ * Scan the tree for features that require optional pipeline phases.
1069
+ *
1070
+ * Returns feature flags. This is called on every layout pass so newly
1071
+ * mounted components are detected. The caller should merge flags with
1072
+ * one-way semantics (false → true, never true → false).
1073
+ */
1074
+ function detectPipelineFeatures(root) {
1075
+ let hasScroll = false;
1076
+ let hasSticky = false;
1077
+ function scan(node) {
1078
+ const props = node.props;
1079
+ if (props.overflow === "scroll") hasScroll = true;
1080
+ if (props.position === "sticky") hasSticky = true;
1081
+ if (hasScroll && hasSticky) return;
1082
+ for (const child of node.children) {
1083
+ scan(child);
1084
+ if (hasScroll && hasSticky) return;
1085
+ }
1086
+ }
1087
+ scan(root);
1088
+ return {
1089
+ hasScroll,
1090
+ hasSticky
1091
+ };
1092
+ }
1093
+ //#endregion
1094
+ //#region packages/ag-term/src/pipeline/render-helpers.ts
1095
+ init_buffer();
1096
+ init_state();
1097
+ init_resolve();
1098
+ const namedColors = {
1099
+ black: 0,
1100
+ red: 1,
1101
+ green: 2,
1102
+ yellow: 3,
1103
+ blue: 4,
1104
+ magenta: 5,
1105
+ cyan: 6,
1106
+ white: 7,
1107
+ gray: 8,
1108
+ grey: 8,
1109
+ blackBright: 8,
1110
+ redBright: 9,
1111
+ greenBright: 10,
1112
+ yellowBright: 11,
1113
+ blueBright: 12,
1114
+ magentaBright: 13,
1115
+ cyanBright: 14,
1116
+ whiteBright: 15
1117
+ };
1118
+ /**
1119
+ * Blend two RGB colors in sRGB space.
1120
+ * Formula: result = c1 * (1 - t) + c2 * t, where t is 0..1.
1121
+ * Returns an RGB object with each channel clamped to 0-255.
1122
+ */
1123
+ function blendColors(c1, c2, t) {
1124
+ return {
1125
+ r: Math.round(c1.r * (1 - t) + c2.r * t),
1126
+ g: Math.round(c1.g * (1 - t) + c2.g * t),
1127
+ b: Math.round(c1.b * (1 - t) + c2.b * t)
1128
+ };
1129
+ }
1130
+ /**
1131
+ * Parse color string to Color type.
1132
+ * Supports: mix(c1,c2,amount), $token (theme), named colors, hex (#rgb, #rrggbb), rgb(r,g,b)
1133
+ */
1134
+ function parseColor(color) {
1135
+ if (color === "inherit") return null;
1136
+ if (color === "$default") return DEFAULT_BG;
1137
+ if (color.startsWith("mix(") && color.endsWith(")")) {
1138
+ const inner = color.slice(4, -1);
1139
+ const args = [];
1140
+ let depth = 0;
1141
+ let start = 0;
1142
+ for (let i = 0; i < inner.length; i++) if (inner[i] === "(") depth++;
1143
+ else if (inner[i] === ")") depth--;
1144
+ else if (inner[i] === "," && depth === 0) {
1145
+ args.push(inner.slice(start, i).trim());
1146
+ start = i + 1;
1147
+ }
1148
+ args.push(inner.slice(start).trim());
1149
+ if (args.length === 3) {
1150
+ const c1 = parseColor(args[0]);
1151
+ const c2 = parseColor(args[1]);
1152
+ const amountStr = args[2];
1153
+ let t;
1154
+ if (amountStr.endsWith("%")) t = Number.parseFloat(amountStr.slice(0, -1)) / 100;
1155
+ else t = Number.parseFloat(amountStr);
1156
+ if (c1 !== null && c2 !== null && typeof c1 === "object" && typeof c2 === "object" && !Number.isNaN(t)) return blendColors(c1, c2, Math.max(0, Math.min(1, t)));
1157
+ return null;
1158
+ }
1159
+ }
1160
+ if (color.startsWith("$")) {
1161
+ const resolved = resolveThemeColor(color, getActiveTheme());
1162
+ if (resolved && resolved !== color) return parseColor(resolved);
1163
+ return null;
1164
+ }
1165
+ if (color in namedColors) return namedColors[color];
1166
+ if (color.startsWith("#")) {
1167
+ const hex = color.slice(1);
1168
+ if (hex.length === 3) return {
1169
+ r: Number.parseInt(hex[0] + hex[0], 16),
1170
+ g: Number.parseInt(hex[1] + hex[1], 16),
1171
+ b: Number.parseInt(hex[2] + hex[2], 16)
1172
+ };
1173
+ if (hex.length === 6) return {
1174
+ r: Number.parseInt(hex.slice(0, 2), 16),
1175
+ g: Number.parseInt(hex.slice(2, 4), 16),
1176
+ b: Number.parseInt(hex.slice(4, 6), 16)
1177
+ };
1178
+ }
1179
+ const rgbMatch = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
1180
+ if (rgbMatch) return {
1181
+ r: Number.parseInt(rgbMatch[1], 10),
1182
+ g: Number.parseInt(rgbMatch[2], 10),
1183
+ b: Number.parseInt(rgbMatch[3], 10)
1184
+ };
1185
+ const ansi256Match = color.match(/^ansi256\s*\(\s*(\d+)\s*\)$/i);
1186
+ if (ansi256Match) return Number.parseInt(ansi256Match[1], 10);
1187
+ return null;
1188
+ }
1189
+ /**
1190
+ * Border character sets by style. Hoisted to module scope to avoid
1191
+ * re-allocating 7 objects on every call.
1192
+ */
1193
+ const borders = {
1194
+ single: {
1195
+ topLeft: "┌",
1196
+ topRight: "┐",
1197
+ bottomLeft: "└",
1198
+ bottomRight: "┘",
1199
+ horizontal: "─",
1200
+ vertical: "│"
1201
+ },
1202
+ double: {
1203
+ topLeft: "╔",
1204
+ topRight: "╗",
1205
+ bottomLeft: "╚",
1206
+ bottomRight: "╝",
1207
+ horizontal: "═",
1208
+ vertical: "║"
1209
+ },
1210
+ round: {
1211
+ topLeft: "╭",
1212
+ topRight: "╮",
1213
+ bottomLeft: "╰",
1214
+ bottomRight: "╯",
1215
+ horizontal: "─",
1216
+ vertical: "│"
1217
+ },
1218
+ bold: {
1219
+ topLeft: "┏",
1220
+ topRight: "┓",
1221
+ bottomLeft: "┗",
1222
+ bottomRight: "┛",
1223
+ horizontal: "━",
1224
+ vertical: "┃"
1225
+ },
1226
+ singleDouble: {
1227
+ topLeft: "╓",
1228
+ topRight: "╖",
1229
+ bottomLeft: "╙",
1230
+ bottomRight: "╜",
1231
+ horizontal: "─",
1232
+ vertical: "║"
1233
+ },
1234
+ doubleSingle: {
1235
+ topLeft: "╒",
1236
+ topRight: "╕",
1237
+ bottomLeft: "╘",
1238
+ bottomRight: "╛",
1239
+ horizontal: "═",
1240
+ vertical: "│"
1241
+ },
1242
+ classic: {
1243
+ topLeft: "+",
1244
+ topRight: "+",
1245
+ bottomLeft: "+",
1246
+ bottomRight: "+",
1247
+ horizontal: "-",
1248
+ vertical: "|"
1249
+ }
1250
+ };
1251
+ /**
1252
+ * Get border characters for a style.
1253
+ */
1254
+ function getBorderChars(style) {
1255
+ if (style && typeof style === "object") {
1256
+ const obj = style;
1257
+ const topHorizontal = obj.top ?? obj.horizontal ?? "-";
1258
+ const leftVertical = obj.left ?? obj.vertical ?? "|";
1259
+ return {
1260
+ topLeft: obj.topLeft ?? "+",
1261
+ topRight: obj.topRight ?? "+",
1262
+ bottomLeft: obj.bottomLeft ?? "+",
1263
+ bottomRight: obj.bottomRight ?? "+",
1264
+ horizontal: topHorizontal,
1265
+ vertical: leftVertical,
1266
+ bottomHorizontal: obj.bottom && obj.bottom !== topHorizontal ? obj.bottom : void 0,
1267
+ rightVertical: obj.right && obj.right !== leftVertical ? obj.right : void 0
1268
+ };
1269
+ }
1270
+ return borders[style ?? "single"];
1271
+ }
1272
+ /**
1273
+ * Get text style from props.
1274
+ */
1275
+ function getTextStyle(props) {
1276
+ let underlineStyle;
1277
+ if (props.underlineStyle !== void 0) underlineStyle = props.underlineStyle;
1278
+ else if (props.underline) underlineStyle = "single";
1279
+ return {
1280
+ fg: props.color ? parseColor(props.color) : null,
1281
+ bg: props.backgroundColor ? parseColor(props.backgroundColor) : null,
1282
+ underlineColor: props.underlineColor ? parseColor(props.underlineColor) : null,
1283
+ attrs: {
1284
+ bold: props.bold,
1285
+ dim: props.dim || props.dimColor,
1286
+ italic: props.italic,
1287
+ underline: props.underline || !!underlineStyle,
1288
+ underlineStyle,
1289
+ strikethrough: props.strikethrough,
1290
+ inverse: props.inverse
1291
+ }
1292
+ };
1293
+ }
1294
+ /**
1295
+ * Get text display width (accounting for wide characters and ANSI codes).
1296
+ * Uses ANSI-aware width calculation to handle styled text.
1297
+ *
1298
+ * When a PipelineContext is provided, uses the context's measurer for
1299
+ * terminal-capability-aware width calculation. Falls back to the module-level
1300
+ * displayWidthAnsi (which reads the scoped measurer or default).
1301
+ */
1302
+ function getTextWidth(text, ctx) {
1303
+ if (ctx) return ctx.measurer.displayWidthAnsi(text);
1304
+ return displayWidthAnsi(text);
1305
+ }
1306
+ //#endregion
1307
+ //#region packages/ag-term/src/pipeline/render-text.ts
1308
+ init_buffer();
1309
+ const log$2 = createLogger("silvery:content");
1310
+ /** Cached bg conflict mode. Read from env once at module load. */
1311
+ let bgConflictMode = (() => {
1312
+ const env = typeof process !== "undefined" ? process.env.SILVERY_BG_CONFLICT?.toLowerCase() : void 0;
1313
+ if (env === "ignore" || env === "warn" || env === "throw") return env;
1314
+ return "throw";
1315
+ })();
1316
+ /**
1317
+ * Get the current background conflict detection mode.
1318
+ */
1319
+ function getBgConflictMode() {
1320
+ return bgConflictMode;
1321
+ }
1322
+ const warnedBgConflicts = /* @__PURE__ */ new Set();
1323
+ /** Format a Color value for bg conflict diagnostics */
1324
+ function formatBgConflictColor(c) {
1325
+ if (c === null || c === void 0) return "none";
1326
+ if (typeof c === "number") {
1327
+ if (c & 16777216) {
1328
+ const r = c >> 16 & 255;
1329
+ const g = c >> 8 & 255;
1330
+ const b = c & 255;
1331
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
1332
+ }
1333
+ return {
1334
+ 40: "black",
1335
+ 41: "red",
1336
+ 42: "green",
1337
+ 43: "yellow",
1338
+ 44: "blue",
1339
+ 45: "magenta",
1340
+ 46: "cyan",
1341
+ 47: "white",
1342
+ 100: "brightBlack",
1343
+ 101: "brightRed",
1344
+ 102: "brightGreen",
1345
+ 103: "brightYellow",
1346
+ 104: "brightBlue",
1347
+ 105: "brightMagenta",
1348
+ 106: "brightCyan",
1349
+ 107: "brightWhite"
1350
+ }[c] ?? `palette(${c})`;
1351
+ }
1352
+ return `rgb(${c.r},${c.g},${c.b})`;
1353
+ }
1354
+ /**
1355
+ * Clear the background conflict warning cache.
1356
+ * Call this at the start of each render cycle to:
1357
+ * - Prevent memory leaks in long-running apps
1358
+ * - Allow warnings to repeat after user fixes issues
1359
+ */
1360
+ function clearBgConflictWarnings() {
1361
+ warnedBgConflicts.clear();
1362
+ }
1363
+ /**
1364
+ * Build ANSI escape sequence for a style context.
1365
+ *
1366
+ * Note: backgroundColor is intentionally NOT embedded as ANSI codes.
1367
+ * Background color is handled at the buffer level (via BgSegment tracking)
1368
+ * to prevent bg bleed across wrapped text lines. See km-silvery.bg-bleed.
1369
+ */
1370
+ function styleToAnsi(style) {
1371
+ const parts = [];
1372
+ if (style.color) {
1373
+ const color = parseColor(style.color);
1374
+ if (color !== null) if (typeof color === "number") parts.push(`38;5;${color}`);
1375
+ else parts.push(`38;2;${color.r};${color.g};${color.b}`);
1376
+ }
1377
+ if (style.bold) parts.push("1");
1378
+ if (style.dim) parts.push("2");
1379
+ if (style.italic) parts.push("3");
1380
+ if (style.underlineStyle) parts.push({
1381
+ single: "4:1",
1382
+ double: "4:2",
1383
+ curly: "4:3",
1384
+ dotted: "4:4",
1385
+ dashed: "4:5"
1386
+ }[style.underlineStyle] ?? "4");
1387
+ else if (style.underline) parts.push("4");
1388
+ if (style.underlineColor) {
1389
+ const ulColor = parseColor(style.underlineColor);
1390
+ if (ulColor !== null) if (typeof ulColor === "number") parts.push(`58;5;${ulColor}`);
1391
+ else parts.push(`58;2;${ulColor.r};${ulColor.g};${ulColor.b}`);
1392
+ }
1393
+ if (style.inverse) parts.push("7");
1394
+ if (style.strikethrough) parts.push("9");
1395
+ if (parts.length === 0) return "";
1396
+ return `\x1b[${parts.join(";")}m`;
1397
+ }
1398
+ /**
1399
+ * Merge child props into parent context.
1400
+ * Child values override parent values when specified.
1401
+ */
1402
+ function mergeStyleContext(parent, childProps) {
1403
+ return {
1404
+ color: childProps.color ?? parent.color,
1405
+ backgroundColor: childProps.backgroundColor ?? parent.backgroundColor,
1406
+ bold: childProps.bold ?? parent.bold,
1407
+ dim: childProps.dim ?? childProps.dimColor ?? parent.dim,
1408
+ italic: childProps.italic ?? parent.italic,
1409
+ underline: childProps.underline ?? childProps.underlineStyle ? true : parent.underline,
1410
+ underlineStyle: childProps.underlineStyle ?? parent.underlineStyle,
1411
+ underlineColor: childProps.underlineColor ?? parent.underlineColor,
1412
+ inverse: childProps.inverse ?? parent.inverse,
1413
+ strikethrough: childProps.strikethrough ?? parent.strikethrough
1414
+ };
1415
+ }
1416
+ /**
1417
+ * Apply text styles as ANSI escape codes with proper push/pop behavior.
1418
+ * After the child text, restores the parent context's styles.
1419
+ *
1420
+ * @param text - The text content to wrap
1421
+ * @param childStyle - The merged style for this child (child overrides parent)
1422
+ * @param parentStyle - The parent's style context to restore after
1423
+ */
1424
+ function applyTextStyleAnsi(text, childStyle, parentStyle) {
1425
+ if (!text) return text;
1426
+ const childAnsi = styleToAnsi(childStyle);
1427
+ const parentAnsi = styleToAnsi(parentStyle);
1428
+ if (!childAnsi) return text;
1429
+ return `${childAnsi}${text}\x1b[0m${parentAnsi}`;
1430
+ }
1431
+ /**
1432
+ * Collect text content and background color segments from a node tree.
1433
+ *
1434
+ * Like collectTextContent, but also tracks backgroundColor from nested Text
1435
+ * elements as separate BgSegment entries. Background is NOT embedded as ANSI
1436
+ * codes, preventing bg bleed when text wraps across lines.
1437
+ *
1438
+ * @param node - The node to collect text from
1439
+ * @param parentContext - The inherited style context from parent
1440
+ * @param offset - Current character offset in the collected text (for bg tracking)
1441
+ * @param maxDisplayWidth - Maximum display width (columns) to collect. When set,
1442
+ * stops collecting once this many display columns of content have been gathered.
1443
+ * This truncates at the DOM level BEFORE ANSI serialization, so escape sequences
1444
+ * (OSC 8, etc.) are never generated for content that won't be displayed.
1445
+ * Uses getTextWidth (ANSI-aware) so pre-styled leaf text is handled correctly.
1446
+ */
1447
+ function collectTextWithBg(node, parentContext = {}, offset = 0, maxDisplayWidth, ctx) {
1448
+ if (node.textContent !== void 0) {
1449
+ let text = node.textContent;
1450
+ if (maxDisplayWidth !== void 0) {
1451
+ if (getTextWidth(text, ctx) > maxDisplayWidth) text = (ctx ? ctx.measurer.sliceByWidth : sliceByWidth)(text, maxDisplayWidth);
1452
+ }
1453
+ const plainLen = getTextWidth(text, ctx);
1454
+ return {
1455
+ text,
1456
+ bgSegments: [],
1457
+ childSpans: [],
1458
+ plainLen
1459
+ };
1460
+ }
1461
+ let result = "";
1462
+ const bgSegments = [];
1463
+ const childSpans = [];
1464
+ let currentOffset = offset;
1465
+ let displayWidthCollected = 0;
1466
+ for (let i = 0; i < node.children.length; i++) {
1467
+ const child = node.children[i];
1468
+ if (maxDisplayWidth !== void 0 && displayWidthCollected >= maxDisplayWidth) break;
1469
+ const childBudget = maxDisplayWidth !== void 0 ? maxDisplayWidth - displayWidthCollected : void 0;
1470
+ if (child.type === "silvery-text" && child.props && !child.layoutNode) {
1471
+ const childProps = child.props;
1472
+ const childContext = mergeStyleContext(parentContext, childProps);
1473
+ const childResult = collectTextWithBg(child, childContext, currentOffset, childBudget, ctx);
1474
+ const childTransform = childProps.internal_transform;
1475
+ if (childTransform && childResult.text.length > 0) childResult.text = childTransform(childResult.text, i);
1476
+ const styledText = applyTextStyleAnsi(childResult.text, childContext, parentContext);
1477
+ result += styledText;
1478
+ if (childContext.backgroundColor) {
1479
+ const bg = parseColor(childContext.backgroundColor);
1480
+ if (bg !== null) {
1481
+ if (childResult.plainLen > 0) bgSegments.push({
1482
+ start: currentOffset,
1483
+ end: currentOffset + childResult.plainLen,
1484
+ bg
1485
+ });
1486
+ }
1487
+ } else if (childProps.backgroundColor === "" && childResult.plainLen > 0) bgSegments.push({
1488
+ start: currentOffset,
1489
+ end: currentOffset + childResult.plainLen,
1490
+ bg: null
1491
+ });
1492
+ if (childResult.plainLen > 0) childSpans.push({
1493
+ node: child,
1494
+ start: currentOffset,
1495
+ end: currentOffset + childResult.plainLen
1496
+ });
1497
+ bgSegments.push(...childResult.bgSegments);
1498
+ childSpans.push(...childResult.childSpans);
1499
+ currentOffset += childResult.plainLen;
1500
+ displayWidthCollected += childResult.plainLen;
1501
+ } else {
1502
+ const childResult = collectTextWithBg(child, parentContext, currentOffset, childBudget, ctx);
1503
+ result += childResult.text;
1504
+ bgSegments.push(...childResult.bgSegments);
1505
+ childSpans.push(...childResult.childSpans);
1506
+ currentOffset += childResult.plainLen;
1507
+ displayWidthCollected += childResult.plainLen;
1508
+ }
1509
+ }
1510
+ return {
1511
+ text: result,
1512
+ bgSegments,
1513
+ childSpans,
1514
+ plainLen: displayWidthCollected
1515
+ };
1516
+ }
1517
+ /**
1518
+ * Apply background segments to buffer cells for a single rendered line.
1519
+ *
1520
+ * Maps character offsets from the original collected text to screen positions,
1521
+ * accounting for text wrapping. Each bg segment fills only the cells that
1522
+ * correspond to actual text characters, not trailing whitespace.
1523
+ *
1524
+ * @param buffer - The terminal buffer to write to
1525
+ * @param x - Screen x position of the line start
1526
+ * @param y - Screen y position of the line
1527
+ * @param lineText - The rendered line text (may contain ANSI codes)
1528
+ * @param lineCharStart - Character offset in original text where this line starts
1529
+ * @param lineCharEnd - Character offset in original text where this line ends
1530
+ * @param bgSegments - Background color segments to apply
1531
+ */
1532
+ function applyBgSegmentsToLine(buffer, x, y, lineText, lineCharStart, lineCharEnd, bgSegments, ctx) {
1533
+ if (bgSegments.length === 0) return;
1534
+ if (y < 0 || y >= buffer.height) return;
1535
+ const bgCell = createMutableCell();
1536
+ const gWidthFn = ctx ? ctx.measurer.graphemeWidth : graphemeWidth;
1537
+ for (const seg of bgSegments) {
1538
+ const overlapStart = Math.max(seg.start, lineCharStart);
1539
+ const overlapEnd = Math.min(seg.end, lineCharEnd);
1540
+ if (overlapStart >= overlapEnd) continue;
1541
+ const relStart = overlapStart - lineCharStart;
1542
+ const relEnd = overlapEnd - lineCharStart;
1543
+ let col = x;
1544
+ const graphemes = splitGraphemes(hasAnsi(lineText) ? stripAnsiForBg(lineText) : lineText);
1545
+ for (const grapheme of graphemes) {
1546
+ const gWidth = gWidthFn(grapheme);
1547
+ if (gWidth === 0) continue;
1548
+ const displayOffset = col - x;
1549
+ if (displayOffset >= relStart && displayOffset < relEnd) {
1550
+ buffer.readCellInto(col, y, bgCell);
1551
+ bgCell.bg = seg.bg;
1552
+ buffer.setCell(col, y, bgCell);
1553
+ if (gWidth === 2 && col + 1 < buffer.width) {
1554
+ buffer.readCellInto(col + 1, y, bgCell);
1555
+ bgCell.bg = seg.bg;
1556
+ buffer.setCell(col + 1, y, bgCell);
1557
+ }
1558
+ }
1559
+ col += gWidth;
1560
+ if (col - x >= relEnd) break;
1561
+ }
1562
+ }
1563
+ }
1564
+ /**
1565
+ * Strip ANSI escape codes from text for character counting.
1566
+ */
1567
+ function stripAnsiForBg(text) {
1568
+ return text.replace(/\x1b\[[0-9;:?]*[A-Za-z]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b[DME78]/g, "").replace(/\x1b\(B/g, "");
1569
+ }
1570
+ /**
1571
+ * Map formatted lines back to character offsets in the original text.
1572
+ *
1573
+ * After wrapping/truncation, each output line corresponds to a range
1574
+ * of characters in the original text. This function computes those ranges
1575
+ * by searching for each line's content in the normalized text.
1576
+ *
1577
+ * Handles characters consumed by word wrapping (spaces at break points,
1578
+ * newlines) and characters added by truncation (ellipsis).
1579
+ *
1580
+ * Returns display-width offsets (not UTF-16 code units) to match BgSegment
1581
+ * coordinate system. BgSegments use display-width via getTextWidth/plainLen.
1582
+ *
1583
+ * @param originalText - The original collected text (with ANSI, before wrapping)
1584
+ * @param formattedLines - The wrapped/truncated output lines
1585
+ * @param ctx - Pipeline context for width measurement
1586
+ * @returns Array of { start, end } display-width offsets for each formatted line
1587
+ */
1588
+ function mapLinesToCharOffsets(originalText, formattedLines, ctx) {
1589
+ const normalized = (hasAnsi(originalText) ? stripAnsiForBg(originalText) : originalText).replace(/\t/g, " ");
1590
+ const result = [];
1591
+ let charOffset = 0;
1592
+ let displayOffset = 0;
1593
+ for (const line of formattedLines) {
1594
+ const plainLine = hasAnsi(line) ? stripAnsiForBg(line) : line;
1595
+ const lineStart = findLineStart(normalized, plainLine, charOffset);
1596
+ if (lineStart > charOffset) {
1597
+ const skipped = normalized.slice(charOffset, lineStart);
1598
+ displayOffset += getTextWidth(skipped, ctx);
1599
+ }
1600
+ const lineDisplayWidth = getTextWidth(plainLine, ctx);
1601
+ result.push({
1602
+ start: displayOffset,
1603
+ end: displayOffset + lineDisplayWidth
1604
+ });
1605
+ charOffset = lineStart + Math.min(plainLine.length, normalized.length - lineStart);
1606
+ displayOffset += lineDisplayWidth;
1607
+ }
1608
+ return result;
1609
+ }
1610
+ /**
1611
+ * Find where a formatted line starts in the normalized original text.
1612
+ *
1613
+ * Scans forward from the given offset, matching the line content
1614
+ * character by character. Skips newlines and whitespace that were
1615
+ * consumed by wrapping between lines.
1616
+ */
1617
+ function findLineStart(normalized, plainLine, fromOffset) {
1618
+ if (plainLine.length === 0) {
1619
+ let pos = fromOffset;
1620
+ while (pos < normalized.length && normalized[pos] === "\n") pos++;
1621
+ return pos;
1622
+ }
1623
+ if (normalized.startsWith(plainLine, fromOffset)) return fromOffset;
1624
+ const ellipsisIdx = plainLine.indexOf("…");
1625
+ const truncatedPrefix = ellipsisIdx > 0 ? plainLine.slice(0, ellipsisIdx) : null;
1626
+ if (truncatedPrefix && normalized.startsWith(truncatedPrefix, fromOffset)) return fromOffset;
1627
+ let pos = fromOffset;
1628
+ while (pos < normalized.length) {
1629
+ const ch = normalized[pos];
1630
+ if (ch === "\n" || ch === " ") {
1631
+ pos++;
1632
+ continue;
1633
+ }
1634
+ if (normalized.startsWith(plainLine, pos)) return pos;
1635
+ if (truncatedPrefix && normalized.startsWith(truncatedPrefix, pos)) return pos;
1636
+ pos++;
1637
+ }
1638
+ return fromOffset;
1639
+ }
1640
+ /**
1641
+ * Format text into lines based on wrap mode.
1642
+ *
1643
+ * @param trim - When true, trims trailing spaces on broken lines and skips leading
1644
+ * spaces on continuation lines. When false (e.g., text has backgroundColor),
1645
+ * preserves trailing spaces so background color covers them. Defaults to true.
1646
+ */
1647
+ function formatTextLines(text, width, wrap, ctx, trim = true) {
1648
+ if (width <= 0) return [];
1649
+ const normalizedText = text.replace(/\t/g, " ");
1650
+ const lines = normalizedText.split("\n");
1651
+ if (wrap === "clip") {
1652
+ const sliceFn = ctx ? ctx.measurer.sliceByWidth : sliceByWidth;
1653
+ return lines.map((line) => {
1654
+ if (getTextWidth(line, ctx) <= width) return line;
1655
+ return sliceFn(line, width);
1656
+ });
1657
+ }
1658
+ if (wrap === "hard") {
1659
+ const sliceFn = ctx ? ctx.measurer.sliceByWidth : sliceByWidth;
1660
+ const out = [];
1661
+ for (const line of lines) {
1662
+ if (line === "") {
1663
+ out.push("");
1664
+ continue;
1665
+ }
1666
+ let remaining = line;
1667
+ while (getTextWidth(remaining, ctx) > width) {
1668
+ const head = sliceFn(remaining, width);
1669
+ if (head.length === 0) break;
1670
+ out.push(head);
1671
+ remaining = remaining.slice(head.length);
1672
+ }
1673
+ out.push(remaining);
1674
+ }
1675
+ return out;
1676
+ }
1677
+ if (wrap === false || wrap === "truncate-end" || wrap === "truncate") return lines.map((line) => truncateText(line, width, "end", ctx));
1678
+ if (wrap === "truncate-start") return lines.map((line) => truncateText(line, width, "start", ctx));
1679
+ if (wrap === "truncate-middle") return lines.map((line) => truncateText(line, width, "middle", ctx));
1680
+ if (wrap === "even") return optimalWrap(normalizedText, buildTextAnalysis(normalizedText, ctx?.measurer?.graphemeWidth?.bind(ctx.measurer) ?? graphemeWidth), width);
1681
+ if (ctx) return ctx.measurer.wrapText(normalizedText, width, true, trim);
1682
+ return wrapText(normalizedText, width, true, trim);
1683
+ }
1684
+ /**
1685
+ * Truncate text to fit within width.
1686
+ */
1687
+ function truncateText(text, width, mode, ctx) {
1688
+ if (getTextWidth(text, ctx) <= width) return text;
1689
+ const ellipsis = "…";
1690
+ const availableWidth = width - 1;
1691
+ if (availableWidth <= 0) return width > 0 ? ellipsis : "";
1692
+ const sliceFn = ctx ? ctx.measurer.sliceByWidth : sliceByWidth;
1693
+ const sliceEndFn = ctx ? ctx.measurer.sliceByWidthFromEnd : sliceByWidthFromEnd;
1694
+ if (mode === "end") return sliceFn(text, availableWidth) + ellipsis;
1695
+ if (mode === "start") return ellipsis + sliceEndFn(text, availableWidth);
1696
+ const halfWidth = Math.floor(availableWidth / 2);
1697
+ const startPart = sliceFn(text, halfWidth);
1698
+ const endPart = sliceEndFn(text, availableWidth - halfWidth);
1699
+ return startPart + ellipsis + endPart;
1700
+ }
1701
+ /**
1702
+ * Render a single line of text to the buffer.
1703
+ *
1704
+ * @param maxCol - Right edge of the text node's layout area. Wide characters
1705
+ * whose continuation cell would exceed this boundary are replaced with a
1706
+ * space, matching terminal behavior for wide chars at the screen edge.
1707
+ * Without this, continuation cells overflow into adjacent containers and
1708
+ * become stale during incremental rendering (the owning container's dirty
1709
+ * tracking doesn't cover cells outside its layout bounds).
1710
+ */
1711
+ function renderTextLine(buffer, x, y, text, baseStyle, maxCol, inheritedBg, ctx) {
1712
+ if (hasAnsi(text)) {
1713
+ renderAnsiTextLine(buffer, x, y, text, baseStyle, maxCol, inheritedBg, ctx);
1714
+ return;
1715
+ }
1716
+ renderGraphemes(buffer, splitGraphemes(text), x, y, baseStyle, maxCol, inheritedBg, ctx);
1717
+ }
1718
+ /**
1719
+ * Like renderTextLine but returns the column position after the last rendered character.
1720
+ * Used by renderText to know where to clear remaining cells.
1721
+ */
1722
+ function renderTextLineReturn(buffer, x, y, text, baseStyle, maxCol, inheritedBg, ctx, minCol) {
1723
+ if (hasAnsi(text)) return renderAnsiTextLineReturn(buffer, x, y, text, baseStyle, maxCol, inheritedBg, ctx, minCol);
1724
+ return renderGraphemes(buffer, splitGraphemes(text), x, y, baseStyle, maxCol, inheritedBg, ctx, minCol);
1725
+ }
1726
+ /**
1727
+ * Render graphemes to buffer cells with proper Unicode handling.
1728
+ * Shared by renderTextLine (plain text) and renderAnsiTextLine (per-segment).
1729
+ *
1730
+ * @param maxCol - Right edge of the text node's layout area (exclusive).
1731
+ * Wide characters whose continuation cell would reach or exceed this
1732
+ * boundary are replaced with a space character. This matches terminal
1733
+ * behavior for wide chars at the right edge of a container and prevents
1734
+ * continuation cells from overflowing into adjacent containers, where
1735
+ * they become stale during incremental rendering.
1736
+ * @param minCol - Left edge of the visible region (inclusive). Graphemes
1737
+ * whose end position is at or before minCol are skipped (col still advances).
1738
+ * Used to clip text that overflows the LEFT edge of an overflow:hidden
1739
+ * container with a border (so the border isn't overwritten).
1740
+ *
1741
+ * Returns the column position after the last rendered grapheme.
1742
+ */
1743
+ function renderGraphemes(buffer, graphemes, startCol, y, style, maxCol, inheritedBg, ctx, minCol) {
1744
+ let col = startCol;
1745
+ const rightEdge = maxCol !== void 0 ? Math.min(maxCol, buffer.width) : buffer.width;
1746
+ const leftEdge = minCol !== void 0 ? Math.max(minCol, 0) : 0;
1747
+ const gWidthFn = ctx ? ctx.measurer.graphemeWidth : graphemeWidth;
1748
+ for (const grapheme of graphemes) {
1749
+ if (col >= rightEdge) break;
1750
+ const width = gWidthFn(grapheme);
1751
+ if (width === 0) continue;
1752
+ if (col + width <= leftEdge) {
1753
+ col += width;
1754
+ continue;
1755
+ }
1756
+ if (col < leftEdge) {
1757
+ col = leftEdge;
1758
+ continue;
1759
+ }
1760
+ const existingBg = style.bg !== null ? style.bg : inheritedBg !== void 0 ? inheritedBg : buffer.getCellBg(col, y);
1761
+ if (width === 2 && col + 1 >= rightEdge) {
1762
+ buffer.setCell(col, y, {
1763
+ char: " ",
1764
+ fg: style.fg,
1765
+ bg: existingBg,
1766
+ underlineColor: style.underlineColor ?? null,
1767
+ attrs: style.attrs,
1768
+ wide: false,
1769
+ continuation: false,
1770
+ hyperlink: style.hyperlink
1771
+ });
1772
+ col += 1;
1773
+ continue;
1774
+ }
1775
+ const outputChar = width === 2 ? ensureEmojiPresentation(grapheme) : grapheme;
1776
+ buffer.setCell(col, y, {
1777
+ char: outputChar,
1778
+ fg: style.fg,
1779
+ bg: existingBg,
1780
+ underlineColor: style.underlineColor ?? null,
1781
+ attrs: style.attrs,
1782
+ wide: width === 2,
1783
+ continuation: false,
1784
+ hyperlink: style.hyperlink
1785
+ });
1786
+ if (width === 2 && col + 1 < buffer.width) {
1787
+ const existingBg2 = style.bg !== null ? style.bg : inheritedBg !== void 0 ? inheritedBg : buffer.getCellBg(col + 1, y);
1788
+ buffer.setCell(col + 1, y, {
1789
+ char: "",
1790
+ fg: style.fg,
1791
+ bg: existingBg2,
1792
+ underlineColor: style.underlineColor ?? null,
1793
+ attrs: style.attrs,
1794
+ wide: false,
1795
+ continuation: true,
1796
+ hyperlink: style.hyperlink
1797
+ });
1798
+ col += 2;
1799
+ } else col += width;
1800
+ }
1801
+ return col;
1802
+ }
1803
+ /**
1804
+ * Render text line with ANSI escape sequences.
1805
+ * Parses ANSI codes and applies styles to individual segments.
1806
+ */
1807
+ function renderAnsiTextLine(buffer, x, y, text, baseStyle, maxCol, inheritedBg, ctx) {
1808
+ renderAnsiTextLineReturn(buffer, x, y, text, baseStyle, maxCol, inheritedBg, ctx);
1809
+ }
1810
+ /**
1811
+ * Like renderAnsiTextLine but returns the column position after the last rendered character.
1812
+ */
1813
+ function renderAnsiTextLineReturn(buffer, x, y, text, baseStyle, maxCol, inheritedBg, ctx, minCol) {
1814
+ const segments = parseAnsiText(text);
1815
+ let col = x;
1816
+ for (const segment of segments) {
1817
+ const style = mergeAnsiStyle(baseStyle, segment);
1818
+ const effectiveBgConflictMode = ctx?.bgConflictMode ?? getBgConflictMode();
1819
+ if (effectiveBgConflictMode !== "ignore" && !segment.bgOverride && segment.bg !== void 0 && segment.bg !== null) {
1820
+ const existingBufBg = col < buffer.width ? buffer.getCellBg(col, y) : null;
1821
+ if (baseStyle.bg !== null || existingBufBg !== null) {
1822
+ const preview = segment.text.slice(0, 30);
1823
+ const chalkBg = formatBgConflictColor(segment.bg);
1824
+ const silveryBg = baseStyle.bg !== null ? `Text.bg=${formatBgConflictColor(baseStyle.bg)}` : `bufferBg=${formatBgConflictColor(existingBufBg)}`;
1825
+ const textPreview = text.length > 80 ? text.slice(0, 80) + "…" : text;
1826
+ const msg = `[silvery] Background conflict at (${col},${y}): chalk bg=${chalkBg} on silvery ${silveryBg}. Text: "${preview}${segment.text.length > 30 ? "…" : ""}". Raw ANSI (first 80): ${JSON.stringify(textPreview)}. Chalk bg will override only text characters, causing visual gaps in padding. Use ansi.bgOverride() to suppress if intentional.`;
1827
+ if (effectiveBgConflictMode === "throw") throw new Error(msg);
1828
+ const effectiveWarnedBgConflicts = ctx?.warnedBgConflicts ?? warnedBgConflicts;
1829
+ const key = `${JSON.stringify(existingBufBg)}-${segment.bg}-${preview}`;
1830
+ if (!effectiveWarnedBgConflicts.has(key)) {
1831
+ effectiveWarnedBgConflicts.add(key);
1832
+ log$2.warn?.(msg);
1833
+ }
1834
+ }
1835
+ }
1836
+ col = renderGraphemes(buffer, splitGraphemes(segment.text), col, y, style, maxCol, inheritedBg, ctx, minCol);
1837
+ }
1838
+ return col;
1839
+ }
1840
+ /**
1841
+ * Merge two styles using category-based semantics.
1842
+ *
1843
+ * Categories and their merge behavior:
1844
+ * - Container (bg): overlay replaces base
1845
+ * - Text (fg): overlay replaces base
1846
+ * - Decorations (underline*, strikethrough): OR merge if preserveDecorations=true
1847
+ * - Emphasis (bold, dim, italic): OR merge if preserveEmphasis=true
1848
+ * - Transform (inverse, hidden, blink): overlay only, not inherited
1849
+ *
1850
+ * @param base - The base style (from parent/container)
1851
+ * @param overlay - The overlay style (from child/content)
1852
+ * @param options - Merge behavior options
1853
+ */
1854
+ function mergeStyles(base, overlay, options = {}) {
1855
+ const { preserveDecorations = true, preserveEmphasis = true } = options;
1856
+ const baseAttrs = base.attrs ?? {};
1857
+ const overlayAttrs = overlay.attrs ?? {};
1858
+ const attrs = {};
1859
+ if (preserveDecorations) {
1860
+ const hasBaseUnderline = baseAttrs.underline || baseAttrs.underlineStyle;
1861
+ const hasOverlayUnderline = overlayAttrs.underline || overlayAttrs.underlineStyle;
1862
+ if (hasBaseUnderline || hasOverlayUnderline) {
1863
+ attrs.underline = true;
1864
+ attrs.underlineStyle = overlayAttrs.underlineStyle ?? baseAttrs.underlineStyle ?? "single";
1865
+ }
1866
+ attrs.strikethrough = overlayAttrs.strikethrough || baseAttrs.strikethrough;
1867
+ } else {
1868
+ attrs.underline = overlayAttrs.underline ?? baseAttrs.underline;
1869
+ attrs.underlineStyle = overlayAttrs.underlineStyle ?? baseAttrs.underlineStyle;
1870
+ attrs.strikethrough = overlayAttrs.strikethrough ?? baseAttrs.strikethrough;
1871
+ }
1872
+ if (preserveEmphasis) {
1873
+ attrs.bold = overlayAttrs.bold || baseAttrs.bold;
1874
+ attrs.dim = overlayAttrs.dim || baseAttrs.dim;
1875
+ attrs.italic = overlayAttrs.italic || baseAttrs.italic;
1876
+ } else {
1877
+ attrs.bold = overlayAttrs.bold ?? baseAttrs.bold;
1878
+ attrs.dim = overlayAttrs.dim ?? baseAttrs.dim;
1879
+ attrs.italic = overlayAttrs.italic ?? baseAttrs.italic;
1880
+ }
1881
+ attrs.inverse = overlayAttrs.inverse;
1882
+ attrs.hidden = overlayAttrs.hidden;
1883
+ attrs.blink = overlayAttrs.blink;
1884
+ return {
1885
+ fg: overlay.fg ?? base.fg,
1886
+ bg: overlay.bg ?? base.bg,
1887
+ underlineColor: overlay.underlineColor ?? base.underlineColor,
1888
+ attrs
1889
+ };
1890
+ }
1891
+ /**
1892
+ * Merge ANSI segment style with base style.
1893
+ * Uses category-based merging to preserve decorations and emphasis.
1894
+ */
1895
+ function mergeAnsiStyle(base, segment, options = {}) {
1896
+ const { preserveDecorations = true, preserveEmphasis = true } = options;
1897
+ let fg = base.fg;
1898
+ let bg = base.bg;
1899
+ let underlineColor = base.underlineColor ?? null;
1900
+ if (segment.fg !== void 0 && segment.fg !== null) fg = ansiColorToColor(segment.fg);
1901
+ if (segment.bg !== void 0 && segment.bg !== null) bg = ansiColorToColor(segment.bg);
1902
+ if (segment.underlineColor !== void 0 && segment.underlineColor !== null) underlineColor = ansiColorToColor(segment.underlineColor);
1903
+ const overlayAttrs = {};
1904
+ if (segment.bold !== void 0) overlayAttrs.bold = segment.bold;
1905
+ if (segment.dim !== void 0) overlayAttrs.dim = segment.dim;
1906
+ if (segment.italic !== void 0) overlayAttrs.italic = segment.italic;
1907
+ if (segment.underline !== void 0) overlayAttrs.underline = segment.underline;
1908
+ if (segment.underlineStyle !== void 0) overlayAttrs.underlineStyle = segment.underlineStyle;
1909
+ if (segment.inverse !== void 0) overlayAttrs.inverse = segment.inverse;
1910
+ const merged = mergeStyles(base, {
1911
+ fg,
1912
+ bg,
1913
+ underlineColor,
1914
+ attrs: overlayAttrs
1915
+ }, {
1916
+ preserveDecorations,
1917
+ preserveEmphasis
1918
+ });
1919
+ if (segment.hyperlink) merged.hyperlink = segment.hyperlink;
1920
+ return merged;
1921
+ }
1922
+ /**
1923
+ * Convert ANSI SGR color code to our Color type.
1924
+ * Color is: number (256-color index) | { r, g, b } (true color) | null
1925
+ */
1926
+ function ansiColorToColor(code) {
1927
+ if (code >= 16777216) return {
1928
+ r: code >> 16 & 255,
1929
+ g: code >> 8 & 255,
1930
+ b: code & 255
1931
+ };
1932
+ if (code < 30 || code >= 38 && code < 40 || code >= 48 && code < 90) return code;
1933
+ if (code >= 30 && code <= 37) return code - 30;
1934
+ if (code >= 40 && code <= 47) return code - 40;
1935
+ if (code >= 90 && code <= 97) return code - 90 + 8;
1936
+ if (code >= 100 && code <= 107) return code - 100 + 8;
1937
+ return null;
1938
+ }
1939
+ /**
1940
+ * Render a Text node.
1941
+ *
1942
+ * Background colors from nested Text elements are handled at the buffer level
1943
+ * (not via ANSI codes) to prevent bg bleed across wrapped text lines.
1944
+ * See km-silvery.bg-bleed for details.
1945
+ */
1946
+ function renderText(node, buffer, layout, props, nodeState, inheritedBg, inheritedFg, ctx) {
1947
+ const { scrollOffset, clipBounds } = nodeState;
1948
+ const { x, width, height } = layout;
1949
+ let { y } = layout;
1950
+ y -= scrollOffset;
1951
+ if (props.backgroundColor === "") inheritedBg = null;
1952
+ if (clipBounds) {
1953
+ if (y + height <= clipBounds.top || y >= clipBounds.bottom) return;
1954
+ if (clipBounds.left !== void 0 && clipBounds.right !== void 0) {
1955
+ if (x + width <= clipBounds.left || x >= clipBounds.right) return;
1956
+ }
1957
+ }
1958
+ let maxDisplayWidth;
1959
+ if ((props.wrap === false || props.wrap === "truncate-end" || props.wrap === "truncate" || props.wrap === "clip") && width > 0) {
1960
+ const cachedPlain = getCachedPlainText(node);
1961
+ let lineCount;
1962
+ if (cachedPlain) lineCount = cachedPlain.lineCount;
1963
+ else {
1964
+ const plainText = collectPlainText(node);
1965
+ lineCount = (plainText.match(/\n/g)?.length ?? 0) + 1;
1966
+ setCachedPlainText(node, plainText, lineCount);
1967
+ }
1968
+ maxDisplayWidth = (width + 1) * lineCount;
1969
+ }
1970
+ let text;
1971
+ let bgSegments;
1972
+ let childSpans;
1973
+ const cachedCollected = getCachedCollectedText(node, maxDisplayWidth);
1974
+ if (cachedCollected) {
1975
+ text = cachedCollected.text;
1976
+ bgSegments = cachedCollected.bgSegments;
1977
+ childSpans = cachedCollected.childSpans;
1978
+ } else {
1979
+ const collected = collectTextWithBg(node, {}, 0, maxDisplayWidth, ctx);
1980
+ text = collected.text;
1981
+ bgSegments = collected.bgSegments;
1982
+ childSpans = collected.childSpans;
1983
+ setCachedCollectedText(node, collected, maxDisplayWidth);
1984
+ }
1985
+ const style = getTextStyle(props);
1986
+ if (style.fg === null && inheritedFg !== void 0) style.fg = inheritedFg;
1987
+ const trim = !(style.bg !== null || bgSegments.length > 0 || inheritedBg !== void 0 && inheritedBg !== null);
1988
+ const internalTransform = props.internal_transform;
1989
+ let lines;
1990
+ let lineOffsets;
1991
+ const cachedFmt = !internalTransform ? getCachedFormat(node, width, props.wrap, trim) : null;
1992
+ if (cachedFmt) {
1993
+ lines = cachedFmt.lines;
1994
+ lineOffsets = cachedFmt.hasLineOffsets ? cachedFmt.lineOffsets : [];
1995
+ } else {
1996
+ lines = formatTextLines(text, width, props.wrap, ctx, trim);
1997
+ if (internalTransform) lines = lines.map((line, index) => internalTransform(line, index));
1998
+ const needLineOffsets = bgSegments.length > 0 || childSpans.length > 0;
1999
+ lineOffsets = needLineOffsets ? mapLinesToCharOffsets(text, lines, ctx) : [];
2000
+ if (!internalTransform) setCachedFormat(node, width, props.wrap, trim, lines, lineOffsets, needLineOffsets);
2001
+ }
2002
+ for (let lineIdx = 0; lineIdx < lines.length && lineIdx < height; lineIdx++) {
2003
+ const lineY = y + lineIdx;
2004
+ if (clipBounds && (lineY < clipBounds.top || lineY >= clipBounds.bottom)) continue;
2005
+ const line = lines[lineIdx];
2006
+ const layoutRight = internalTransform ? buffer.width : x + width;
2007
+ const maxCol = clipBounds && "right" in clipBounds && clipBounds.right !== void 0 ? Math.min(layoutRight, clipBounds.right) : layoutRight;
2008
+ const minCol = clipBounds && "left" in clipBounds && clipBounds.left !== void 0 ? clipBounds.left : void 0;
2009
+ const endCol = renderTextLineReturn(buffer, x, lineY, line, style, maxCol, inheritedBg, ctx, minCol);
2010
+ const clearStart = minCol !== void 0 ? Math.max(endCol, minCol) : endCol;
2011
+ if (clearStart < maxCol) {
2012
+ const clearBg = inheritedBg ?? null;
2013
+ for (let cx = clearStart; cx < maxCol && cx < buffer.width; cx++) buffer.setCell(cx, lineY, {
2014
+ char: " ",
2015
+ fg: style.fg,
2016
+ bg: clearBg,
2017
+ underlineColor: null,
2018
+ attrs: {
2019
+ bold: false,
2020
+ dim: false,
2021
+ italic: false,
2022
+ underline: false,
2023
+ inverse: false,
2024
+ strikethrough: false,
2025
+ blink: false,
2026
+ hidden: false
2027
+ },
2028
+ wide: false,
2029
+ continuation: false
2030
+ });
2031
+ }
2032
+ if (bgSegments.length > 0 && lineIdx < lineOffsets.length) {
2033
+ const { start, end } = lineOffsets[lineIdx];
2034
+ applyBgSegmentsToLine(buffer, x, lineY, line, start, end, bgSegments, ctx);
2035
+ }
2036
+ }
2037
+ if (childSpans.length > 0 && lineOffsets.length > 0) computeInlineRects(childSpans, lineOffsets, x, y, lines.length, height);
2038
+ }
2039
+ /**
2040
+ * Compute inlineRects for virtual text children based on their display-width spans
2041
+ * and the formatted line offsets. For wrapped text, a child may span multiple lines,
2042
+ * producing one rect per line fragment.
2043
+ *
2044
+ * @param childSpans - Virtual text children with their display-width ranges
2045
+ * @param lineOffsets - Display-width offset ranges for each formatted line
2046
+ * @param parentX - Screen X of the parent Text node
2047
+ * @param parentY - Screen Y of the parent Text node (after scroll offset)
2048
+ * @param lineCount - Number of formatted lines
2049
+ * @param maxHeight - Maximum height (layout height) of the parent Text node
2050
+ */
2051
+ function computeInlineRects(childSpans, lineOffsets, parentX, parentY, lineCount, maxHeight) {
2052
+ for (const span of childSpans) {
2053
+ const rects = [];
2054
+ for (let lineIdx = 0; lineIdx < lineCount && lineIdx < maxHeight; lineIdx++) {
2055
+ const lineOffset = lineOffsets[lineIdx];
2056
+ if (!lineOffset) continue;
2057
+ const overlapStart = Math.max(span.start, lineOffset.start);
2058
+ const overlapEnd = Math.min(span.end, lineOffset.end);
2059
+ if (overlapStart >= overlapEnd) continue;
2060
+ const rectX = parentX + (overlapStart - lineOffset.start);
2061
+ const rectY = parentY + lineIdx;
2062
+ const rectWidth = overlapEnd - overlapStart;
2063
+ rects.push({
2064
+ x: rectX,
2065
+ y: rectY,
2066
+ width: rectWidth,
2067
+ height: 1
2068
+ });
2069
+ }
2070
+ span.node.inlineRects = rects.length > 0 ? rects : null;
2071
+ }
2072
+ }
2073
+ //#endregion
2074
+ //#region packages/ag-term/src/pipeline/render-box.ts
2075
+ /**
2076
+ * Get the effective background color string for a Box.
2077
+ * Returns explicit backgroundColor if set, otherwise theme.bg if theme is set.
2078
+ * Used by both renderBox (paint fill) and render-phase (cascade logic).
2079
+ */
2080
+ function getEffectiveBg(props) {
2081
+ if (props.backgroundColor) return props.backgroundColor;
2082
+ if (props.theme) return props.theme.bg;
2083
+ }
2084
+ /**
2085
+ * Render a Box node.
2086
+ */
2087
+ function renderBox(_node, buffer, layout, props, nodeState, skipBgFill = false, inheritedBg, bgOnlyChange = false) {
2088
+ const { scrollOffset, clipBounds } = nodeState;
2089
+ const { x, width, height } = layout;
2090
+ const y = layout.y - scrollOffset;
2091
+ if (clipBounds) {
2092
+ if (y + height <= clipBounds.top || y >= clipBounds.bottom) return;
2093
+ if (clipBounds.left !== void 0 && clipBounds.right !== void 0) {
2094
+ if (x + width <= clipBounds.left || x >= clipBounds.right) return;
2095
+ }
2096
+ }
2097
+ const effectiveBgStr = getEffectiveBg(props);
2098
+ if (effectiveBgStr && !skipBgFill) {
2099
+ const bg = parseColor(effectiveBgStr);
2100
+ if (clipBounds) {
2101
+ const clippedY = Math.max(y, clipBounds.top);
2102
+ const clippedHeight = Math.min(y + height, clipBounds.bottom) - clippedY;
2103
+ let clippedX = x;
2104
+ let clippedWidth = width;
2105
+ if (clipBounds.left !== void 0 && clipBounds.right !== void 0) {
2106
+ clippedX = Math.max(x, clipBounds.left);
2107
+ clippedWidth = Math.min(x + width, clipBounds.right) - clippedX;
2108
+ }
2109
+ if (clippedHeight > 0 && clippedWidth > 0) if (bgOnlyChange) buffer.fillBg(clippedX, clippedY, clippedWidth, clippedHeight, bg);
2110
+ else buffer.fill(clippedX, clippedY, clippedWidth, clippedHeight, { bg });
2111
+ } else if (bgOnlyChange) buffer.fillBg(x, y, width, height, bg);
2112
+ else buffer.fill(x, y, width, height, { bg });
2113
+ }
2114
+ if (props.borderStyle) renderBorder(buffer, x, y, width, height, props, clipBounds, inheritedBg);
2115
+ }
2116
+ /**
2117
+ * Render a border around a box.
2118
+ */
2119
+ function renderBorder(buffer, x, y, width, height, props, clipBounds, inheritedBg) {
2120
+ const chars = getBorderChars(props.borderStyle ?? "single");
2121
+ const color = props.borderColor ? parseColor(props.borderColor) : null;
2122
+ const baseBg = props.backgroundColor ? parseColor(props.backgroundColor) : inheritedBg ?? null;
2123
+ const borderBgStr = props.borderBackgroundColor;
2124
+ const borderBgBase = borderBgStr ? parseColor(borderBgStr) : baseBg;
2125
+ const topBorderBgStr = props.borderTopBackgroundColor;
2126
+ const bottomBorderBgStr = props.borderBottomBackgroundColor;
2127
+ const leftBorderBgStr = props.borderLeftBackgroundColor;
2128
+ const rightBorderBgStr = props.borderRightBackgroundColor;
2129
+ const topBg = topBorderBgStr ? parseColor(topBorderBgStr) : borderBgBase;
2130
+ const bottomBg = bottomBorderBgStr ? parseColor(bottomBorderBgStr) : borderBgBase;
2131
+ const leftBg = leftBorderBgStr ? parseColor(leftBorderBgStr) : borderBgBase;
2132
+ const rightBg = rightBorderBgStr ? parseColor(rightBorderBgStr) : borderBgBase;
2133
+ const showTop = props.borderTop !== false;
2134
+ const showBottom = props.borderBottom !== false;
2135
+ const showLeft = props.borderLeft !== false;
2136
+ const showRight = props.borderRight !== false;
2137
+ const isRowVisible = (row) => {
2138
+ if (!clipBounds) return row >= 0 && row < buffer.height;
2139
+ return row >= clipBounds.top && row < clipBounds.bottom && row < buffer.height;
2140
+ };
2141
+ const isColVisible = (col) => {
2142
+ if (clipBounds?.left === void 0 || clipBounds.right === void 0) return col >= 0 && col < buffer.width;
2143
+ return col >= clipBounds.left && col < clipBounds.right && col < buffer.width;
2144
+ };
2145
+ if (showTop && isRowVisible(y)) {
2146
+ if (showLeft && isColVisible(x)) buffer.setCell(x, y, {
2147
+ char: chars.topLeft,
2148
+ fg: color,
2149
+ bg: topBg
2150
+ });
2151
+ const hStart = showLeft ? x + 1 : x;
2152
+ const hEnd = showRight ? x + width - 1 : x + width;
2153
+ for (let col = hStart; col < hEnd && col < buffer.width; col++) if (isColVisible(col)) buffer.setCell(col, y, {
2154
+ char: chars.horizontal,
2155
+ fg: color,
2156
+ bg: topBg
2157
+ });
2158
+ if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, y, {
2159
+ char: chars.topRight,
2160
+ fg: color,
2161
+ bg: topBg
2162
+ });
2163
+ }
2164
+ const rightVertical = chars.rightVertical ?? chars.vertical;
2165
+ const sideStart = showTop ? y + 1 : y;
2166
+ const sideEnd = showBottom ? y + height - 1 : y + height;
2167
+ for (let row = sideStart; row < sideEnd; row++) {
2168
+ if (!isRowVisible(row)) continue;
2169
+ if (showLeft && isColVisible(x)) buffer.setCell(x, row, {
2170
+ char: chars.vertical,
2171
+ fg: color,
2172
+ bg: leftBg
2173
+ });
2174
+ if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, row, {
2175
+ char: rightVertical,
2176
+ fg: color,
2177
+ bg: rightBg
2178
+ });
2179
+ }
2180
+ const bottomHorizontal = chars.bottomHorizontal ?? chars.horizontal;
2181
+ const bottomY = y + height - 1;
2182
+ if (showBottom && isRowVisible(bottomY)) {
2183
+ if (showLeft && isColVisible(x)) buffer.setCell(x, bottomY, {
2184
+ char: chars.bottomLeft,
2185
+ fg: color,
2186
+ bg: bottomBg
2187
+ });
2188
+ const bStart = showLeft ? x + 1 : x;
2189
+ const bEnd = showRight ? x + width - 1 : x + width;
2190
+ for (let col = bStart; col < bEnd && col < buffer.width; col++) if (isColVisible(col)) buffer.setCell(col, bottomY, {
2191
+ char: bottomHorizontal,
2192
+ fg: color,
2193
+ bg: bottomBg
2194
+ });
2195
+ if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, bottomY, {
2196
+ char: chars.bottomRight,
2197
+ fg: color,
2198
+ bg: bottomBg
2199
+ });
2200
+ }
2201
+ }
2202
+ /**
2203
+ * Render an outline around a box.
2204
+ *
2205
+ * Unlike borders, outlines do NOT affect layout dimensions. They draw border
2206
+ * characters that OVERLAP the content area at the node's screen rect edges.
2207
+ * This is the CSS `outline` equivalent for terminal UI.
2208
+ */
2209
+ function renderOutline(buffer, x, y, width, height, props, clipBounds, inheritedBg) {
2210
+ const chars = getBorderChars(props.outlineStyle ?? "single");
2211
+ const color = props.outlineColor ? parseColor(props.outlineColor) : null;
2212
+ const bg = props.backgroundColor ? parseColor(props.backgroundColor) : inheritedBg ?? null;
2213
+ const attrs = props.outlineDimColor ? { dim: true } : {};
2214
+ const isRowVisible = (row) => {
2215
+ if (!clipBounds) return row >= 0 && row < buffer.height;
2216
+ return row >= clipBounds.top && row < clipBounds.bottom && row < buffer.height;
2217
+ };
2218
+ const isColVisible = (col) => {
2219
+ if (clipBounds?.left === void 0 || clipBounds.right === void 0) return col >= 0 && col < buffer.width;
2220
+ return col >= clipBounds.left && col < clipBounds.right && col < buffer.width;
2221
+ };
2222
+ const showTop = props.outlineTop !== false;
2223
+ const showBottom = props.outlineBottom !== false;
2224
+ const showLeft = props.outlineLeft !== false;
2225
+ const showRight = props.outlineRight !== false;
2226
+ if (showTop && isRowVisible(y)) {
2227
+ if (showLeft && isColVisible(x)) buffer.setCell(x, y, {
2228
+ char: chars.topLeft,
2229
+ fg: color,
2230
+ bg,
2231
+ attrs
2232
+ });
2233
+ for (let col = x + 1; col < x + width - 1 && col < buffer.width; col++) if (isColVisible(col)) buffer.setCell(col, y, {
2234
+ char: chars.horizontal,
2235
+ fg: color,
2236
+ bg,
2237
+ attrs
2238
+ });
2239
+ if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, y, {
2240
+ char: chars.topRight,
2241
+ fg: color,
2242
+ bg,
2243
+ attrs
2244
+ });
2245
+ }
2246
+ const outlineRightVertical = chars.rightVertical ?? chars.vertical;
2247
+ const sideStart = showTop ? y + 1 : y;
2248
+ const sideEnd = showBottom ? y + height - 1 : y + height;
2249
+ for (let row = sideStart; row < sideEnd; row++) {
2250
+ if (!isRowVisible(row)) continue;
2251
+ if (showLeft && isColVisible(x)) buffer.setCell(x, row, {
2252
+ char: chars.vertical,
2253
+ fg: color,
2254
+ bg,
2255
+ attrs
2256
+ });
2257
+ if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, row, {
2258
+ char: outlineRightVertical,
2259
+ fg: color,
2260
+ bg,
2261
+ attrs
2262
+ });
2263
+ }
2264
+ const outlineBottomHorizontal = chars.bottomHorizontal ?? chars.horizontal;
2265
+ const bottomY = y + height - 1;
2266
+ if (showBottom && isRowVisible(bottomY)) {
2267
+ if (showLeft && isColVisible(x)) buffer.setCell(x, bottomY, {
2268
+ char: chars.bottomLeft,
2269
+ fg: color,
2270
+ bg,
2271
+ attrs
2272
+ });
2273
+ for (let col = x + 1; col < x + width - 1 && col < buffer.width; col++) if (isColVisible(col)) buffer.setCell(col, bottomY, {
2274
+ char: outlineBottomHorizontal,
2275
+ fg: color,
2276
+ bg,
2277
+ attrs
2278
+ });
2279
+ if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, bottomY, {
2280
+ char: chars.bottomRight,
2281
+ fg: color,
2282
+ bg,
2283
+ attrs
2284
+ });
2285
+ }
2286
+ }
2287
+ /**
2288
+ * Render scroll indicators showing hidden items above/below viewport.
2289
+ *
2290
+ * Two rendering modes:
2291
+ * 1. Bordered containers: Indicators appear on the border (e.g., "───▲42───")
2292
+ * 2. Borderless containers with overflowIndicator: Indicators appear directly
2293
+ * after the last visible child (not at the viewport bottom)
2294
+ *
2295
+ * Uses ▲N for items hidden above, ▼N for items hidden below.
2296
+ */
2297
+ function renderScrollIndicators(_node, buffer, layout, props, ss, ctx) {
2298
+ const border = props.borderStyle ? getBorderSize(props) : {
2299
+ top: 0,
2300
+ bottom: 0,
2301
+ left: 0,
2302
+ right: 0
2303
+ };
2304
+ const indicatorStyle = {
2305
+ fg: 15,
2306
+ bg: 8,
2307
+ attrs: {}
2308
+ };
2309
+ const showBorderless = props.overflowIndicator === true;
2310
+ if (ss.hiddenAbove > 0) {
2311
+ const indicator = `\u25b2${ss.hiddenAbove}`;
2312
+ if (border.top > 0) {
2313
+ const contentWidth = layout.width - border.left - border.right;
2314
+ const bar = padCenter(indicator, contentWidth);
2315
+ const x = layout.x + border.left;
2316
+ const y = layout.y;
2317
+ renderTextLine(buffer, x, y, bar, indicatorStyle, x + contentWidth, void 0, ctx);
2318
+ } else if (showBorderless) {
2319
+ const padding = getPadding(props);
2320
+ const contentWidth = layout.width - padding.left - padding.right;
2321
+ const bar = padCenter(indicator, contentWidth);
2322
+ const x = layout.x + padding.left;
2323
+ renderTextLine(buffer, x, layout.y + padding.top, bar, indicatorStyle, x + contentWidth, void 0, ctx);
2324
+ }
2325
+ }
2326
+ if (ss.hiddenBelow > 0) {
2327
+ const indicator = `\u25bc${ss.hiddenBelow}`;
2328
+ if (border.bottom > 0) {
2329
+ const contentWidth = layout.width - border.left - border.right;
2330
+ const bar = padCenter(indicator, contentWidth);
2331
+ const x = layout.x + border.left;
2332
+ renderTextLine(buffer, x, layout.y + layout.height - 1, bar, indicatorStyle, x + contentWidth, void 0, ctx);
2333
+ } else if (showBorderless) {
2334
+ const padding = getPadding(props);
2335
+ const contentWidth = layout.width - padding.left - padding.right;
2336
+ const bar = padCenter(indicator, contentWidth);
2337
+ const x = layout.x + padding.left;
2338
+ renderTextLine(buffer, x, layout.y + layout.height - padding.bottom - 1, bar, indicatorStyle, x + contentWidth, void 0, ctx);
2339
+ }
2340
+ }
2341
+ }
2342
+ /** Center text within a fixed width, padding with spaces on both sides.
2343
+ * Truncates from the right if text exceeds available width. */
2344
+ function padCenter(text, width) {
2345
+ if (width <= 0) return "";
2346
+ if (text.length > width) return text.slice(0, width);
2347
+ if (text.length === width) return text;
2348
+ const leftPad = Math.floor((width - text.length) / 2);
2349
+ const rightPad = width - text.length - leftPad;
2350
+ return " ".repeat(leftPad) + text + " ".repeat(rightPad);
2351
+ }
2352
+ //#endregion
2353
+ //#region packages/ag-term/src/pipeline/cascade-predicates.ts
2354
+ /**
2355
+ * Compute all cascade predicate values from boolean inputs.
2356
+ *
2357
+ * This is a pure function — no side effects, no node dependencies.
2358
+ * The formulas exactly match those in render-phase.ts renderNodeToBuffer.
2359
+ */
2360
+ function computeCascade(inputs) {
2361
+ const { hasPrevBuffer, contentDirty, stylePropsDirty, layoutChanged, subtreeDirty, childrenDirty, childPositionChanged, ancestorLayoutChanged, ancestorCleared, bgDirty, isTextNode, hasBgColor, absoluteChildMutated, descendantOverflowChanged } = inputs;
2362
+ const canSkipEntireSubtree = hasPrevBuffer && !contentDirty && !stylePropsDirty && !layoutChanged && !subtreeDirty && !childrenDirty && !childPositionChanged && !ancestorLayoutChanged;
2363
+ const contentAreaAffected = contentDirty || layoutChanged || childPositionChanged || childrenDirty || bgDirty || isTextNode && stylePropsDirty || absoluteChildMutated || descendantOverflowChanged;
2364
+ const bgOnlyChange = false;
2365
+ const bgRefillNeeded = hasPrevBuffer && !contentAreaAffected && subtreeDirty && hasBgColor;
2366
+ return {
2367
+ canSkipEntireSubtree,
2368
+ contentAreaAffected,
2369
+ bgRefillNeeded,
2370
+ contentRegionCleared: (hasPrevBuffer || ancestorCleared) && contentAreaAffected && !hasBgColor,
2371
+ skipBgFill: hasPrevBuffer && !ancestorCleared && !contentAreaAffected && !bgRefillNeeded,
2372
+ childrenNeedFreshRender: (hasPrevBuffer || ancestorCleared) && (contentAreaAffected || bgRefillNeeded) && true,
2373
+ bgOnlyChange
2374
+ };
2375
+ }
2376
+ //#endregion
2377
+ //#region ../../node_modules/.bun/alien-signals@3.1.2/node_modules/alien-signals/esm/system.mjs
2378
+ function createReactiveSystem({ update, notify, unwatched }) {
2379
+ return {
2380
+ link,
2381
+ unlink,
2382
+ propagate,
2383
+ checkDirty,
2384
+ shallowPropagate
2385
+ };
2386
+ function link(dep, sub, version) {
2387
+ const prevDep = sub.depsTail;
2388
+ if (prevDep !== void 0 && prevDep.dep === dep) return;
2389
+ const nextDep = prevDep !== void 0 ? prevDep.nextDep : sub.deps;
2390
+ if (nextDep !== void 0 && nextDep.dep === dep) {
2391
+ nextDep.version = version;
2392
+ sub.depsTail = nextDep;
2393
+ return;
2394
+ }
2395
+ const prevSub = dep.subsTail;
2396
+ if (prevSub !== void 0 && prevSub.version === version && prevSub.sub === sub) return;
2397
+ const newLink = sub.depsTail = dep.subsTail = {
2398
+ version,
2399
+ dep,
2400
+ sub,
2401
+ prevDep,
2402
+ nextDep,
2403
+ prevSub,
2404
+ nextSub: void 0
2405
+ };
2406
+ if (nextDep !== void 0) nextDep.prevDep = newLink;
2407
+ if (prevDep !== void 0) prevDep.nextDep = newLink;
2408
+ else sub.deps = newLink;
2409
+ if (prevSub !== void 0) prevSub.nextSub = newLink;
2410
+ else dep.subs = newLink;
2411
+ }
2412
+ function unlink(link, sub = link.sub) {
2413
+ const dep = link.dep;
2414
+ const prevDep = link.prevDep;
2415
+ const nextDep = link.nextDep;
2416
+ const nextSub = link.nextSub;
2417
+ const prevSub = link.prevSub;
2418
+ if (nextDep !== void 0) nextDep.prevDep = prevDep;
2419
+ else sub.depsTail = prevDep;
2420
+ if (prevDep !== void 0) prevDep.nextDep = nextDep;
2421
+ else sub.deps = nextDep;
2422
+ if (nextSub !== void 0) nextSub.prevSub = prevSub;
2423
+ else dep.subsTail = prevSub;
2424
+ if (prevSub !== void 0) prevSub.nextSub = nextSub;
2425
+ else if ((dep.subs = nextSub) === void 0) unwatched(dep);
2426
+ return nextDep;
2427
+ }
2428
+ function propagate(link) {
2429
+ let next = link.nextSub;
2430
+ let stack;
2431
+ top: do {
2432
+ const sub = link.sub;
2433
+ let flags = sub.flags;
2434
+ if (!(flags & 60)) sub.flags = flags | 32;
2435
+ else if (!(flags & 12)) flags = 0;
2436
+ else if (!(flags & 4)) sub.flags = flags & -9 | 32;
2437
+ else if (!(flags & 48) && isValidLink(link, sub)) {
2438
+ sub.flags = flags | 40;
2439
+ flags &= 1;
2440
+ } else flags = 0;
2441
+ if (flags & 2) notify(sub);
2442
+ if (flags & 1) {
2443
+ const subSubs = sub.subs;
2444
+ if (subSubs !== void 0) {
2445
+ const nextSub = (link = subSubs).nextSub;
2446
+ if (nextSub !== void 0) {
2447
+ stack = {
2448
+ value: next,
2449
+ prev: stack
2450
+ };
2451
+ next = nextSub;
2452
+ }
2453
+ continue;
2454
+ }
2455
+ }
2456
+ if ((link = next) !== void 0) {
2457
+ next = link.nextSub;
2458
+ continue;
2459
+ }
2460
+ while (stack !== void 0) {
2461
+ link = stack.value;
2462
+ stack = stack.prev;
2463
+ if (link !== void 0) {
2464
+ next = link.nextSub;
2465
+ continue top;
2466
+ }
2467
+ }
2468
+ break;
2469
+ } while (true);
2470
+ }
2471
+ function checkDirty(link, sub) {
2472
+ let stack;
2473
+ let checkDepth = 0;
2474
+ let dirty = false;
2475
+ top: do {
2476
+ const dep = link.dep;
2477
+ const flags = dep.flags;
2478
+ if (sub.flags & 16) dirty = true;
2479
+ else if ((flags & 17) === 17) {
2480
+ if (update(dep)) {
2481
+ const subs = dep.subs;
2482
+ if (subs.nextSub !== void 0) shallowPropagate(subs);
2483
+ dirty = true;
2484
+ }
2485
+ } else if ((flags & 33) === 33) {
2486
+ if (link.nextSub !== void 0 || link.prevSub !== void 0) stack = {
2487
+ value: link,
2488
+ prev: stack
2489
+ };
2490
+ link = dep.deps;
2491
+ sub = dep;
2492
+ ++checkDepth;
2493
+ continue;
2494
+ }
2495
+ if (!dirty) {
2496
+ const nextDep = link.nextDep;
2497
+ if (nextDep !== void 0) {
2498
+ link = nextDep;
2499
+ continue;
2500
+ }
2501
+ }
2502
+ while (checkDepth--) {
2503
+ const firstSub = sub.subs;
2504
+ const hasMultipleSubs = firstSub.nextSub !== void 0;
2505
+ if (hasMultipleSubs) {
2506
+ link = stack.value;
2507
+ stack = stack.prev;
2508
+ } else link = firstSub;
2509
+ if (dirty) {
2510
+ if (update(sub)) {
2511
+ if (hasMultipleSubs) shallowPropagate(firstSub);
2512
+ sub = link.sub;
2513
+ continue;
2514
+ }
2515
+ dirty = false;
2516
+ } else sub.flags &= -33;
2517
+ sub = link.sub;
2518
+ const nextDep = link.nextDep;
2519
+ if (nextDep !== void 0) {
2520
+ link = nextDep;
2521
+ continue top;
2522
+ }
2523
+ }
2524
+ return dirty;
2525
+ } while (true);
2526
+ }
2527
+ function shallowPropagate(link) {
2528
+ do {
2529
+ const sub = link.sub;
2530
+ const flags = sub.flags;
2531
+ if ((flags & 48) === 32) {
2532
+ sub.flags = flags | 16;
2533
+ if ((flags & 6) === 2) notify(sub);
2534
+ }
2535
+ } while ((link = link.nextSub) !== void 0);
2536
+ }
2537
+ function isValidLink(checkLink, sub) {
2538
+ let link = sub.depsTail;
2539
+ while (link !== void 0) {
2540
+ if (link === checkLink) return true;
2541
+ link = link.prevDep;
2542
+ }
2543
+ return false;
2544
+ }
2545
+ }
2546
+ //#endregion
2547
+ //#region ../../node_modules/.bun/alien-signals@3.1.2/node_modules/alien-signals/esm/index.mjs
2548
+ let cycle = 0;
2549
+ let notifyIndex = 0;
2550
+ let queuedLength = 0;
2551
+ let activeSub;
2552
+ const queued = [];
2553
+ const { link, unlink, propagate, checkDirty, shallowPropagate } = createReactiveSystem({
2554
+ update(node) {
2555
+ if (node.depsTail !== void 0) return updateComputed(node);
2556
+ else return updateSignal(node);
2557
+ },
2558
+ notify(effect) {
2559
+ let insertIndex = queuedLength;
2560
+ let firstInsertedIndex = insertIndex;
2561
+ do {
2562
+ queued[insertIndex++] = effect;
2563
+ effect.flags &= -3;
2564
+ effect = effect.subs?.sub;
2565
+ if (effect === void 0 || !(effect.flags & 2)) break;
2566
+ } while (true);
2567
+ queuedLength = insertIndex;
2568
+ while (firstInsertedIndex < --insertIndex) {
2569
+ const left = queued[firstInsertedIndex];
2570
+ queued[firstInsertedIndex++] = queued[insertIndex];
2571
+ queued[insertIndex] = left;
2572
+ }
2573
+ },
2574
+ unwatched(node) {
2575
+ if (!(node.flags & 1)) effectScopeOper.call(node);
2576
+ else if (node.depsTail !== void 0) {
2577
+ node.depsTail = void 0;
2578
+ node.flags = 17;
2579
+ purgeDeps(node);
2580
+ }
2581
+ }
2582
+ });
2583
+ function setActiveSub(sub) {
2584
+ const prevSub = activeSub;
2585
+ activeSub = sub;
2586
+ return prevSub;
2587
+ }
2588
+ function signal(initialValue) {
2589
+ return signalOper.bind({
2590
+ currentValue: initialValue,
2591
+ pendingValue: initialValue,
2592
+ subs: void 0,
2593
+ subsTail: void 0,
2594
+ flags: 1
2595
+ });
2596
+ }
2597
+ function computed(getter) {
2598
+ return computedOper.bind({
2599
+ value: void 0,
2600
+ subs: void 0,
2601
+ subsTail: void 0,
2602
+ deps: void 0,
2603
+ depsTail: void 0,
2604
+ flags: 0,
2605
+ getter
2606
+ });
2607
+ }
2608
+ function updateComputed(c) {
2609
+ ++cycle;
2610
+ c.depsTail = void 0;
2611
+ c.flags = 5;
2612
+ const prevSub = setActiveSub(c);
2613
+ try {
2614
+ const oldValue = c.value;
2615
+ return oldValue !== (c.value = c.getter(oldValue));
2616
+ } finally {
2617
+ activeSub = prevSub;
2618
+ c.flags &= -5;
2619
+ purgeDeps(c);
2620
+ }
2621
+ }
2622
+ function updateSignal(s) {
2623
+ s.flags = 1;
2624
+ return s.currentValue !== (s.currentValue = s.pendingValue);
2625
+ }
2626
+ function run(e) {
2627
+ const flags = e.flags;
2628
+ if (flags & 16 || flags & 32 && checkDirty(e.deps, e)) {
2629
+ ++cycle;
2630
+ e.depsTail = void 0;
2631
+ e.flags = 6;
2632
+ const prevSub = setActiveSub(e);
2633
+ try {
2634
+ e.fn();
2635
+ } finally {
2636
+ activeSub = prevSub;
2637
+ e.flags &= -5;
2638
+ purgeDeps(e);
2639
+ }
2640
+ } else e.flags = 2;
2641
+ }
2642
+ function flush() {
2643
+ try {
2644
+ while (notifyIndex < queuedLength) {
2645
+ const effect = queued[notifyIndex];
2646
+ queued[notifyIndex++] = void 0;
2647
+ run(effect);
2648
+ }
2649
+ } finally {
2650
+ while (notifyIndex < queuedLength) {
2651
+ const effect = queued[notifyIndex];
2652
+ queued[notifyIndex++] = void 0;
2653
+ effect.flags |= 10;
2654
+ }
2655
+ notifyIndex = 0;
2656
+ queuedLength = 0;
2657
+ }
2658
+ }
2659
+ function computedOper() {
2660
+ const flags = this.flags;
2661
+ if (flags & 16 || flags & 32 && (checkDirty(this.deps, this) || (this.flags = flags & -33, false))) {
2662
+ if (updateComputed(this)) {
2663
+ const subs = this.subs;
2664
+ if (subs !== void 0) shallowPropagate(subs);
2665
+ }
2666
+ } else if (!flags) {
2667
+ this.flags = 5;
2668
+ const prevSub = setActiveSub(this);
2669
+ try {
2670
+ this.value = this.getter();
2671
+ } finally {
2672
+ activeSub = prevSub;
2673
+ this.flags &= -5;
2674
+ }
2675
+ }
2676
+ const sub = activeSub;
2677
+ if (sub !== void 0) link(this, sub, cycle);
2678
+ return this.value;
2679
+ }
2680
+ function signalOper(...value) {
2681
+ if (value.length) {
2682
+ if (this.pendingValue !== (this.pendingValue = value[0])) {
2683
+ this.flags = 17;
2684
+ const subs = this.subs;
2685
+ if (subs !== void 0) {
2686
+ propagate(subs);
2687
+ flush();
2688
+ }
2689
+ }
2690
+ } else {
2691
+ if (this.flags & 16) {
2692
+ if (updateSignal(this)) {
2693
+ const subs = this.subs;
2694
+ if (subs !== void 0) shallowPropagate(subs);
2695
+ }
2696
+ }
2697
+ let sub = activeSub;
2698
+ while (sub !== void 0) {
2699
+ if (sub.flags & 3) {
2700
+ link(this, sub, cycle);
2701
+ break;
2702
+ }
2703
+ sub = sub.subs?.sub;
2704
+ }
2705
+ return this.currentValue;
2706
+ }
2707
+ }
2708
+ function effectScopeOper() {
2709
+ this.depsTail = void 0;
2710
+ this.flags = 0;
2711
+ purgeDeps(this);
2712
+ const sub = this.subs;
2713
+ if (sub !== void 0) unlink(sub);
2714
+ }
2715
+ function purgeDeps(sub) {
2716
+ const depsTail = sub.depsTail;
2717
+ let dep = depsTail !== void 0 ? depsTail.nextDep : sub.deps;
2718
+ while (dep !== void 0) dep = unlink(dep, sub);
2719
+ }
2720
+ //#endregion
2721
+ //#region packages/ag-term/src/pipeline/reactive-node.ts
2722
+ /**
2723
+ * Reactive Node State — alien-signals wrappers for cascade derivations.
2724
+ *
2725
+ * E+ Phase 2: Replace manual cascade formula computation with reactive
2726
+ * computeds. The cascade-predicates.ts `computeCascade()` function is the
2727
+ * oracle — this module must produce identical outputs.
2728
+ *
2729
+ * Incremental approach:
2730
+ * 1. Writable signals mirror epoch-stamped dirty flags
2731
+ * 2. Computeds derive cascade outputs (isDirty, contentAreaAffected, etc.)
2732
+ * 3. `syncToSignals()` bridges epoch flags → signals at render-phase entry
2733
+ * 4. Dev-mode assertions verify reactive === oracle
2734
+ *
2735
+ * alien-signals computeds are lazy (pull-based): reading a computed re-evaluates
2736
+ * only if a dependency changed. No batching needed for correctness — the
2737
+ * reconciler's synchronous commit writes epoch stamps, and the render phase
2738
+ * reads computeds after all commits are done.
2739
+ */
2740
+ /**
2741
+ * Create a reactive state wrapper for a node.
2742
+ *
2743
+ * The computed formulas exactly replicate cascade-predicates.ts `computeCascade()`.
2744
+ * They are NOT a replacement — the oracle function remains authoritative.
2745
+ * In dev mode, assertions verify equivalence.
2746
+ */
2747
+ function createReactiveNodeState() {
2748
+ const contentDirty = signal(false);
2749
+ const stylePropsDirty = signal(false);
2750
+ const bgDirty = signal(false);
2751
+ const childrenDirty = signal(false);
2752
+ const subtreeDirty = signal(false);
2753
+ const layoutChanged = signal(false);
2754
+ const hasPrevBuffer = signal(false);
2755
+ const childPositionChanged = signal(false);
2756
+ const ancestorLayoutChanged = signal(false);
2757
+ const ancestorCleared = signal(false);
2758
+ const isTextNode = signal(false);
2759
+ const hasBgColor = signal(false);
2760
+ const absoluteChildMutated = signal(false);
2761
+ const descendantOverflowChanged = signal(false);
2762
+ const canSkipEntireSubtree = computed(() => hasPrevBuffer() && !contentDirty() && !stylePropsDirty() && !layoutChanged() && !subtreeDirty() && !childrenDirty() && !childPositionChanged() && !ancestorLayoutChanged());
2763
+ const textPaintDirty = computed(() => isTextNode() && stylePropsDirty());
2764
+ const contentAreaAffected = computed(() => contentDirty() || layoutChanged() || childPositionChanged() || childrenDirty() || bgDirty() || textPaintDirty() || absoluteChildMutated() || descendantOverflowChanged());
2765
+ const bgOnlyChange = computed(() => false);
2766
+ const bgRefillNeeded = computed(() => hasPrevBuffer() && !contentAreaAffected() && subtreeDirty() && hasBgColor());
2767
+ return {
2768
+ contentDirty,
2769
+ stylePropsDirty,
2770
+ bgDirty,
2771
+ childrenDirty,
2772
+ subtreeDirty,
2773
+ layoutChanged,
2774
+ hasPrevBuffer,
2775
+ childPositionChanged,
2776
+ ancestorLayoutChanged,
2777
+ ancestorCleared,
2778
+ isTextNode,
2779
+ hasBgColor,
2780
+ absoluteChildMutated,
2781
+ descendantOverflowChanged,
2782
+ canSkipEntireSubtree,
2783
+ textPaintDirty,
2784
+ contentAreaAffected,
2785
+ bgRefillNeeded,
2786
+ contentRegionCleared: computed(() => (hasPrevBuffer() || ancestorCleared()) && contentAreaAffected() && !hasBgColor()),
2787
+ skipBgFill: computed(() => hasPrevBuffer() && !ancestorCleared() && !contentAreaAffected() && !bgRefillNeeded()),
2788
+ childrenNeedFreshRender: computed(() => (hasPrevBuffer() || ancestorCleared()) && (contentAreaAffected() || bgRefillNeeded()) && !bgOnlyChange()),
2789
+ bgOnlyChange
2790
+ };
2791
+ }
2792
+ /**
2793
+ * Sync a node's epoch-stamped dirty flags into the reactive signals.
2794
+ *
2795
+ * Called once per node at the start of renderNodeToBuffer, BEFORE reading
2796
+ * any computed. Context-dependent inputs (hasPrevBuffer, ancestorCleared, etc.)
2797
+ * are also set here since they vary per tree-traversal position.
2798
+ */
2799
+ function syncToSignals(state, node, ctx) {
2800
+ state.contentDirty(isDirty(node.dirtyBits, node.dirtyEpoch, 1));
2801
+ state.stylePropsDirty(isDirty(node.dirtyBits, node.dirtyEpoch, 2));
2802
+ state.bgDirty(isDirty(node.dirtyBits, node.dirtyEpoch, 4));
2803
+ state.childrenDirty(isDirty(node.dirtyBits, node.dirtyEpoch, 8));
2804
+ state.subtreeDirty(isDirty(node.dirtyBits, node.dirtyEpoch, 16));
2805
+ state.layoutChanged(ctx.layoutChanged);
2806
+ state.hasPrevBuffer(ctx.hasPrevBuffer);
2807
+ state.childPositionChanged(ctx.childPositionChanged);
2808
+ state.ancestorLayoutChanged(ctx.ancestorLayoutChanged);
2809
+ state.ancestorCleared(ctx.ancestorCleared);
2810
+ state.isTextNode(node.type === "silvery-text");
2811
+ state.hasBgColor(ctx.hasBgColor);
2812
+ state.absoluteChildMutated(ctx.absoluteChildMutated);
2813
+ state.descendantOverflowChanged(ctx.descendantOverflowChanged);
2814
+ }
2815
+ /**
2816
+ * Read all cascade outputs from the reactive computeds.
2817
+ * Call AFTER `syncToSignals()`. Returns the same CascadeOutputs shape
2818
+ * as `computeCascade()` but derived from the signal graph.
2819
+ */
2820
+ function readReactiveCascade(state) {
2821
+ return {
2822
+ canSkipEntireSubtree: state.canSkipEntireSubtree(),
2823
+ contentAreaAffected: state.contentAreaAffected(),
2824
+ bgRefillNeeded: state.bgRefillNeeded(),
2825
+ contentRegionCleared: state.contentRegionCleared(),
2826
+ skipBgFill: state.skipBgFill(),
2827
+ childrenNeedFreshRender: state.childrenNeedFreshRender(),
2828
+ bgOnlyChange: state.bgOnlyChange()
2829
+ };
2830
+ }
2831
+ /**
2832
+ * Verify that reactive computeds match the cascade oracle.
2833
+ *
2834
+ * Call in dev mode (SILVERY_STRICT=1) after syncToSignals + computeCascade.
2835
+ * Throws on mismatch with a detailed diff.
2836
+ */
2837
+ function assertReactiveMatchesOracle(state, oracle, nodeId) {
2838
+ const fields = [
2839
+ "canSkipEntireSubtree",
2840
+ "contentAreaAffected",
2841
+ "bgRefillNeeded",
2842
+ "contentRegionCleared",
2843
+ "skipBgFill",
2844
+ "childrenNeedFreshRender",
2845
+ "bgOnlyChange"
2846
+ ];
2847
+ const mismatches = [];
2848
+ for (const field of fields) {
2849
+ const reactiveValue = state[field]();
2850
+ const oracleValue = oracle[field];
2851
+ if (reactiveValue !== oracleValue) mismatches.push(` ${field}: reactive=${reactiveValue}, oracle=${oracleValue}`);
2852
+ }
2853
+ if (mismatches.length > 0) throw new Error(`ReactiveNodeState mismatch for ${nodeId || "(unnamed)"}:\n${mismatches.join("\n")}`);
2854
+ }
2855
+ /**
2856
+ * WeakMap from AgNode to its reactive state.
2857
+ *
2858
+ * Lazily created on first access. Automatically garbage-collected when the
2859
+ * node is removed from the tree (WeakMap semantics).
2860
+ */
2861
+ const nodeStates = /* @__PURE__ */ new WeakMap();
2862
+ /**
2863
+ * Get or create the reactive state for a node.
2864
+ *
2865
+ * Uses a WeakMap so states are automatically cleaned up when nodes are GC'd.
2866
+ */
2867
+ function getReactiveState(node) {
2868
+ let state = nodeStates.get(node);
2869
+ if (!state) {
2870
+ state = createReactiveNodeState();
2871
+ nodeStates.set(node, state);
2872
+ }
2873
+ return state;
2874
+ }
2875
+ //#endregion
2876
+ //#region packages/ag-term/src/pipeline/render-phase.ts
2877
+ /**
2878
+ * Phase 3: Render Phase
2879
+ *
2880
+ * Render all nodes to a terminal buffer.
2881
+ *
2882
+ * This module orchestrates the rendering process by traversing the node tree
2883
+ * and delegating to specialized rendering functions for boxes and text.
2884
+ *
2885
+ * Layout (top-down):
2886
+ * renderPhase → renderNodeToBuffer → buildCascadeInputs + computeCascade (oracle)
2887
+ * → traceRenderDecision (diagnostics)
2888
+ * → executeRegionClearing
2889
+ * → renderOwnContent
2890
+ * → renderScrollContainerChildren / renderNormalChildren
2891
+ * Helpers: clearDirtyFlags, hasChildPositionChanged, computeChildClipBounds
2892
+ * Region clearing: clearNodeRegion, clearExcessArea, clippedFill
2893
+ */
2894
+ init_buffer();
2895
+ init_state();
2896
+ const contentLog = createLogger("silvery:content");
2897
+ const traceLog = createLogger("silvery:content:trace");
2898
+ const cellLog = createLogger("silvery:content:cell");
2899
+ /**
2900
+ * Render all nodes to a terminal buffer.
2901
+ *
2902
+ * @param root The root SilveryNode
2903
+ * @param prevBuffer Previous buffer for incremental rendering (optional)
2904
+ * @returns A TerminalBuffer with the rendered content
2905
+ */
2906
+ function renderPhase(root, prevBuffer, ctx) {
2907
+ const layout = root.boxRect;
2908
+ if (!layout) throw new Error("renderPhase called before layout phase");
2909
+ const instr = resolveInstrumentation(ctx);
2910
+ const hasPrevBuffer = prevBuffer && prevBuffer.width === layout.width && prevBuffer.height === layout.height;
2911
+ if (hasPrevBuffer && !isAnyDirty(root.dirtyBits, root.dirtyEpoch) && !isCurrentEpoch(root.layoutChangedThisFrame)) {
2912
+ if (instr.enabled) instr.stats._noopSkip = 1;
2913
+ advanceRenderEpoch();
2914
+ return prevBuffer;
2915
+ }
2916
+ if (instr.enabled) {
2917
+ instr.stats._prevBufferNull = prevBuffer == null ? 1 : 0;
2918
+ instr.stats._prevBufferDimMismatch = prevBuffer && !hasPrevBuffer ? 1 : 0;
2919
+ instr.stats._hasPrevBuffer = hasPrevBuffer ? 1 : 0;
2920
+ instr.stats._layoutW = layout.width;
2921
+ instr.stats._layoutH = layout.height;
2922
+ instr.stats._prevW = prevBuffer?.width ?? 0;
2923
+ instr.stats._prevH = prevBuffer?.height ?? 0;
2924
+ }
2925
+ const t0 = instr.enabled ? performance.now() : 0;
2926
+ const buffer = hasPrevBuffer ? prevBuffer.clone() : new TerminalBuffer(layout.width, layout.height);
2927
+ const tClone = instr.enabled ? performance.now() - t0 : 0;
2928
+ buffer.setSelectableMode(true);
2929
+ const t1 = instr.enabled ? performance.now() : 0;
2930
+ renderNodeToBuffer(root, buffer, {
2931
+ scrollOffset: 0,
2932
+ clipBounds: void 0,
2933
+ hasPrevBuffer: !!hasPrevBuffer,
2934
+ ancestorCleared: false,
2935
+ bufferIsCloned: !!hasPrevBuffer,
2936
+ ancestorLayoutChanged: false,
2937
+ inheritedBg: {
2938
+ color: null,
2939
+ ancestorRect: null
2940
+ },
2941
+ inheritedFg: null
2942
+ }, ctx);
2943
+ const tRender = instr.enabled ? performance.now() - t1 : 0;
2944
+ if (instr.enabled) emitRenderPhaseStats(instr.stats, instr.nodeTrace, instr.nodeTraceEnabled, tClone, tRender);
2945
+ syncPrevLayout(root, isCurrentEpoch(root.layoutChangedThisFrame) || isDirty(root.dirtyBits, root.dirtyEpoch, 16) || !hasPrevBuffer);
2946
+ advanceRenderEpoch();
2947
+ return buffer;
2948
+ }
2949
+ /**
2950
+ * Sync prevLayout to boxRect for all nodes in the tree.
2951
+ *
2952
+ * Called at the end of each renderPhase pass. This prevents:
2953
+ * 1. The O(N) staleness bug where prevLayout drifts from boxRect
2954
+ * causing !rectEqual to always be true on subsequent frames.
2955
+ * 2. Stale old-bounds references in clearExcessArea on doRender iteration 2+.
2956
+ * 3. Asymmetry between incremental and fresh renders — doFreshRender's layout
2957
+ * phase syncs prevLayout before content, so without this, the real render
2958
+ * has null/stale prevLayout while fresh has synced prevLayout, causing
2959
+ * different cascade behavior (layoutChanged true vs false).
2960
+ */
2961
+ /**
2962
+ * Sync prevLayout = boxRect for nodes whose layout changed.
2963
+ *
2964
+ * Previously walked ALL nodes O(N) every frame. Now only visits nodes
2965
+ * with layoutChangedThisFrame (set by propagateLayout in layout phase).
2966
+ * Falls back to full walk when layout phase ran (dimensions changed or
2967
+ * layoutDirty) since any node may have moved.
2968
+ *
2969
+ * For cursor move (no layout change): O(1) — no nodes to sync.
2970
+ * For resize: O(N) — all nodes may have moved (same as before).
2971
+ */
2972
+ function syncPrevLayout(root, layoutPhaseRan) {
2973
+ if (!layoutPhaseRan) return;
2974
+ const stack = [root];
2975
+ while (stack.length > 0) {
2976
+ const node = stack.pop();
2977
+ node.prevLayout = node.boxRect;
2978
+ const children = node.children;
2979
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
2980
+ }
2981
+ }
2982
+ /** Check if an env var is truthy (treats "0" and "false" as disabled). */
2983
+ function envTruthy(val) {
2984
+ return !!val && val !== "0" && val !== "false";
2985
+ }
2986
+ /** Instrumentation enabled when SILVERY_STRICT or SILVERY_INSTRUMENT is set */
2987
+ const _instrumentEnabled = typeof process !== "undefined" && (envTruthy(process.env?.SILVERY_STRICT) || envTruthy(process.env?.SILVERY_INSTRUMENT));
2988
+ /** Mutable stats counters — reset after each renderPhase call */
2989
+ const _renderPhaseStats = {
2990
+ nodesVisited: 0,
2991
+ nodesRendered: 0,
2992
+ nodesSkipped: 0,
2993
+ textNodes: 0,
2994
+ boxNodes: 0,
2995
+ clearOps: 0,
2996
+ noPrevBuffer: 0,
2997
+ flagContentDirty: 0,
2998
+ flagStylePropsDirty: 0,
2999
+ flagLayoutChanged: 0,
3000
+ flagSubtreeDirty: 0,
3001
+ flagChildrenDirty: 0,
3002
+ flagChildPositionChanged: 0,
3003
+ flagAncestorLayoutChanged: 0,
3004
+ scrollContainerCount: 0,
3005
+ scrollViewportCleared: 0,
3006
+ scrollClearReason: "",
3007
+ normalChildrenRepaint: 0,
3008
+ normalRepaintReason: "",
3009
+ cascadeMinDepth: 999,
3010
+ cascadeNodes: "",
3011
+ _noopSkip: 0,
3012
+ _prevBufferNull: 0,
3013
+ _prevBufferDimMismatch: 0,
3014
+ _hasPrevBuffer: 0,
3015
+ _layoutW: 0,
3016
+ _layoutH: 0,
3017
+ _prevW: 0,
3018
+ _prevH: 0,
3019
+ _callCount: 0
3020
+ };
3021
+ let _renderPhaseCallCount = 0;
3022
+ /** Module-level node trace (fallback when ctx.nodeTrace is not provided) */
3023
+ const _nodeTrace = [];
3024
+ const _nodeTraceEnabled = typeof process !== "undefined" && envTruthy(process.env?.SILVERY_STRICT);
3025
+ /**
3026
+ * Reactive cascade: alien-signals computeds drive rendering (production path).
3027
+ * SILVERY_REACTIVE=0: fall back to imperative computeCascade() (bench only).
3028
+ * SILVERY_STRICT=1: oracle verifies reactive output matches imperative.
3029
+ */
3030
+ let _reactiveEnabled = typeof process === "undefined" || process.env?.SILVERY_REACTIVE !== "0";
3031
+ let _reactiveVerifyEnabled = _reactiveEnabled && typeof process !== "undefined" && envTruthy(process.env?.SILVERY_STRICT);
3032
+ /** Resolve instrumentation from PipelineContext or module-level defaults. */
3033
+ function resolveInstrumentation(ctx) {
3034
+ return {
3035
+ enabled: ctx?.instrumentEnabled ?? _instrumentEnabled,
3036
+ stats: ctx?.stats ?? _renderPhaseStats,
3037
+ nodeTrace: ctx?.nodeTrace ?? _nodeTrace,
3038
+ nodeTraceEnabled: ctx?.nodeTraceEnabled ?? _nodeTraceEnabled
3039
+ };
3040
+ }
3041
+ /** Read the cell debug target (set by SILVERY_CELL_DEBUG env var). */
3042
+ function getCellDebug() {
3043
+ return globalThis.__silvery_cell_debug;
3044
+ }
3045
+ /** Check if a rect covers the cell debug target point. */
3046
+ function cellCoversPoint(cellDbg, x, y, width, height) {
3047
+ return x <= cellDbg.x && x + width > cellDbg.x && y <= cellDbg.y && y + height > cellDbg.y;
3048
+ }
3049
+ /** DIAG: compute node depth in tree */
3050
+ function _getNodeDepth(node) {
3051
+ let depth = 0;
3052
+ let n = node.parent;
3053
+ while (n) {
3054
+ depth++;
3055
+ n = n.parent;
3056
+ }
3057
+ return depth;
3058
+ }
3059
+ /**
3060
+ * Emit render-phase stats to globalThis + loggily, then reset counters.
3061
+ * Called at end of renderPhase when instrumentation is active.
3062
+ */
3063
+ function emitRenderPhaseStats(stats, nodeTrace, nodeTraceEnabled, tClone, tRender) {
3064
+ _renderPhaseCallCount++;
3065
+ stats._callCount = _renderPhaseCallCount;
3066
+ const snap = {
3067
+ clone: tClone,
3068
+ render: tRender,
3069
+ ...structuredClone(stats)
3070
+ };
3071
+ globalThis.__silvery_content_detail = snap;
3072
+ (globalThis.__silvery_content_all ??= []).push(snap);
3073
+ contentLog.debug?.(`frame ${snap._callCount}: ${snap.nodesRendered}/${snap.nodesVisited} rendered, ${snap.nodesSkipped} skipped (${tClone.toFixed(1)}ms clone, ${tRender.toFixed(1)}ms render)`);
3074
+ for (const key of Object.keys(stats)) stats[key] = 0;
3075
+ stats.cascadeMinDepth = 999;
3076
+ stats.cascadeNodes = "";
3077
+ stats.scrollClearReason = "";
3078
+ stats.normalRepaintReason = "";
3079
+ if (nodeTraceEnabled && nodeTrace.length > 0) {
3080
+ (globalThis.__silvery_node_trace ??= []).push([...nodeTrace]);
3081
+ traceLog.debug?.(`${nodeTrace.length} nodes traced`);
3082
+ nodeTrace.length = 0;
3083
+ }
3084
+ }
3085
+ /**
3086
+ * Render a single node to the buffer.
3087
+ */
3088
+ function renderNodeToBuffer(node, buffer, nodeState, ctx) {
3089
+ const { scrollOffset, clipBounds, hasPrevBuffer, ancestorCleared, bufferIsCloned, ancestorLayoutChanged = false } = nodeState;
3090
+ const instr = resolveInstrumentation(ctx);
3091
+ if (instr.enabled) instr.stats.nodesVisited++;
3092
+ const layout = node.boxRect;
3093
+ if (!layout) return;
3094
+ if (!node.layoutNode) {
3095
+ clearVirtualTextFlags(node);
3096
+ return;
3097
+ }
3098
+ if (node.hidden) {
3099
+ clearDirtyFlags(node);
3100
+ return;
3101
+ }
3102
+ const props = node.props;
3103
+ const prevSelectableMode = buffer.getSelectableMode();
3104
+ const userSelect = props.userSelect;
3105
+ if (userSelect === "none") buffer.setSelectableMode(false);
3106
+ else if (userSelect === "text" || userSelect === "contain") buffer.setSelectableMode(true);
3107
+ if (props.display === "none") {
3108
+ clearDirtyFlags(node);
3109
+ buffer.setSelectableMode(prevSelectableMode);
3110
+ return;
3111
+ }
3112
+ const screenY = layout.y - scrollOffset;
3113
+ if (screenY >= buffer.height || screenY + layout.height <= 0) {
3114
+ buffer.setSelectableMode(prevSelectableMode);
3115
+ return;
3116
+ }
3117
+ const layoutChanged = isCurrentEpoch(node.layoutChangedThisFrame);
3118
+ const childPositionChanged = !!(hasPrevBuffer && !layoutChanged && hasChildPositionChanged(node));
3119
+ const scrollOffsetChanged = !!(node.scrollState && node.scrollState.offset !== node.scrollState.prevOffset);
3120
+ const canSkipEntireSubtree = hasPrevBuffer && !isDirty(node.dirtyBits, node.dirtyEpoch, 1) && !isDirty(node.dirtyBits, node.dirtyEpoch, 2) && !layoutChanged && !isDirty(node.dirtyBits, node.dirtyEpoch, 16) && !isDirty(node.dirtyBits, node.dirtyEpoch, 8) && !childPositionChanged && !ancestorLayoutChanged && !scrollOffsetChanged;
3121
+ const _nodeId = instr.enabled ? props.id ?? "" : "";
3122
+ const _traceThis = instr.enabled && instr.nodeTraceEnabled && _nodeId;
3123
+ const _cellDbg = getCellDebug();
3124
+ const _coversCellNow = _cellDbg && cellCoversPoint(_cellDbg, layout.x, screenY, layout.width, layout.height);
3125
+ const _coversCellPrev = _cellDbg && node.prevLayout && cellCoversPoint(_cellDbg, node.prevLayout.x, node.prevLayout.y - scrollOffset, node.prevLayout.width, node.prevLayout.height);
3126
+ if (canSkipEntireSubtree) {
3127
+ if (_cellDbg && (_coversCellNow || _coversCellPrev)) {
3128
+ const id = props.id ?? node.type;
3129
+ const depth = _getNodeDepth(node);
3130
+ const prev = node.prevLayout;
3131
+ const msg = `SKIP ${id}@${depth} rect=${layout.x},${screenY} ${layout.width}x${layout.height} prev=${prev ? `${prev.x},${prev.y - scrollOffset} ${prev.width}x${prev.height}` : "null"} coversNow=${_coversCellNow} coversPrev=${_coversCellPrev}`;
3132
+ _cellDbg.log.push(msg);
3133
+ cellLog.debug?.(msg);
3134
+ }
3135
+ if (instr.enabled) {
3136
+ instr.stats.nodesSkipped++;
3137
+ if (_traceThis) instr.nodeTrace.push({
3138
+ id: _nodeId,
3139
+ type: node.type,
3140
+ depth: _getNodeDepth(node),
3141
+ rect: `${layout.x},${layout.y} ${layout.width}x${layout.height}`,
3142
+ prevLayout: node.prevLayout ? `${node.prevLayout.x},${node.prevLayout.y} ${node.prevLayout.width}x${node.prevLayout.height}` : "null",
3143
+ hasPrev: hasPrevBuffer,
3144
+ ancestorCleared,
3145
+ flags: "",
3146
+ decision: "SKIPPED",
3147
+ layoutChanged
3148
+ });
3149
+ }
3150
+ clearDirtyFlags(node);
3151
+ buffer.setSelectableMode(prevSelectableMode);
3152
+ return;
3153
+ }
3154
+ if (instr.enabled) {
3155
+ instr.stats.nodesRendered++;
3156
+ if (!hasPrevBuffer) instr.stats.noPrevBuffer++;
3157
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 1)) instr.stats.flagContentDirty++;
3158
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 2)) instr.stats.flagStylePropsDirty++;
3159
+ if (layoutChanged) instr.stats.flagLayoutChanged++;
3160
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 16)) instr.stats.flagSubtreeDirty++;
3161
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 8)) instr.stats.flagChildrenDirty++;
3162
+ if (childPositionChanged) instr.stats.flagChildPositionChanged++;
3163
+ if (ancestorLayoutChanged) instr.stats.flagAncestorLayoutChanged++;
3164
+ }
3165
+ const nodeTheme = props.theme;
3166
+ if (nodeTheme) pushContextTheme(nodeTheme);
3167
+ try {
3168
+ const isScrollContainer = props.overflow === "scroll" && node.scrollState;
3169
+ const { absoluteChildMutated, descendantOverflowChanged } = buildCascadeInputs(node, hasPrevBuffer);
3170
+ const cascadeInputs = {
3171
+ hasPrevBuffer,
3172
+ contentDirty: isDirty(node.dirtyBits, node.dirtyEpoch, 1),
3173
+ stylePropsDirty: isDirty(node.dirtyBits, node.dirtyEpoch, 2),
3174
+ layoutChanged,
3175
+ subtreeDirty: isDirty(node.dirtyBits, node.dirtyEpoch, 16),
3176
+ childrenDirty: isDirty(node.dirtyBits, node.dirtyEpoch, 8),
3177
+ childPositionChanged,
3178
+ ancestorLayoutChanged,
3179
+ ancestorCleared,
3180
+ bgDirty: isDirty(node.dirtyBits, node.dirtyEpoch, 4),
3181
+ isTextNode: node.type === "silvery-text",
3182
+ hasBgColor: !!getEffectiveBg(props),
3183
+ absoluteChildMutated,
3184
+ descendantOverflowChanged
3185
+ };
3186
+ let cascade;
3187
+ if (_reactiveEnabled) {
3188
+ const reactiveState = getReactiveState(node);
3189
+ syncToSignals(reactiveState, node, {
3190
+ hasPrevBuffer,
3191
+ layoutChanged,
3192
+ childPositionChanged,
3193
+ ancestorLayoutChanged,
3194
+ ancestorCleared,
3195
+ absoluteChildMutated,
3196
+ descendantOverflowChanged,
3197
+ hasBgColor: cascadeInputs.hasBgColor
3198
+ });
3199
+ cascade = readReactiveCascade(reactiveState);
3200
+ if (_reactiveVerifyEnabled) assertReactiveMatchesOracle(reactiveState, computeCascade(cascadeInputs), props.id ?? node.type);
3201
+ } else cascade = computeCascade(cascadeInputs);
3202
+ if (cascade.bgOnlyChange && hasDescendantWithBg(node)) {
3203
+ const childrenNeedFreshRender = (hasPrevBuffer || ancestorCleared) && (cascade.contentAreaAffected || cascade.bgRefillNeeded);
3204
+ cascade = {
3205
+ ...cascade,
3206
+ bgOnlyChange: false,
3207
+ childrenNeedFreshRender
3208
+ };
3209
+ }
3210
+ const { contentRegionCleared, skipBgFill, childrenNeedFreshRender } = cascade;
3211
+ if (instr.enabled || _cellDbg && (_coversCellNow || _coversCellPrev)) traceRenderDecision(node, props, layout, screenY, scrollOffset, hasPrevBuffer, ancestorCleared, layoutChanged, childPositionChanged, cascade, _nodeId, _traceThis, _cellDbg, _coversCellNow, _coversCellPrev, instr.enabled, instr.stats, instr.nodeTrace);
3212
+ const useTextStyleFastPath = false;
3213
+ executeRegionClearing(node, buffer, layout, scrollOffset, clipBounds, bufferIsCloned, layoutChanged, contentRegionCleared, descendantOverflowChanged, instr.enabled, instr.stats, nodeState.inheritedBg);
3214
+ const needsOwnRepaint = !hasPrevBuffer || ancestorCleared || ancestorLayoutChanged || cascade.contentAreaAffected || isDirty(node.dirtyBits, node.dirtyEpoch, 2) || cascade.bgRefillNeeded;
3215
+ const boxInheritedBg = node.type === "silvery-box" && !getEffectiveBg(props) ? nodeState.inheritedBg.color : void 0;
3216
+ if (needsOwnRepaint) renderOwnContent(node, buffer, layout, props, nodeState, skipBgFill, instr.enabled, instr.stats, ctx, cascade.bgOnlyChange, useTextStyleFastPath);
3217
+ const effectiveBg = getEffectiveBg(props);
3218
+ const childInheritedBg = effectiveBg ? {
3219
+ color: parseColor(effectiveBg),
3220
+ ancestorRect: node.boxRect
3221
+ } : nodeTheme ? {
3222
+ color: parseColor(nodeTheme.bg),
3223
+ ancestorRect: node.boxRect
3224
+ } : nodeState.inheritedBg;
3225
+ const childInheritedFg = props.color ? parseColor(props.color) : nodeTheme ? parseColor(nodeTheme.fg) : nodeState.inheritedFg;
3226
+ const childState = {
3227
+ ...nodeState,
3228
+ inheritedBg: childInheritedBg,
3229
+ inheritedFg: childInheritedFg
3230
+ };
3231
+ if (isScrollContainer) {
3232
+ renderScrollContainerChildren(node, buffer, props, childState, contentRegionCleared, childrenNeedFreshRender, ctx);
3233
+ renderScrollIndicators(node, buffer, layout, props, node.scrollState, ctx);
3234
+ } else renderNormalChildren(node, buffer, props, childState, childPositionChanged, contentRegionCleared, childrenNeedFreshRender, ctx);
3235
+ if (node.type === "silvery-box" && props.outlineStyle) {
3236
+ const { x, width, height } = layout;
3237
+ renderOutline(buffer, x, layout.y - scrollOffset, width, height, props, clipBounds, boxInheritedBg);
3238
+ }
3239
+ clearNodeDirtyFlags(node);
3240
+ } finally {
3241
+ if (nodeTheme) popContextTheme();
3242
+ buffer.setSelectableMode(prevSelectableMode);
3243
+ }
3244
+ }
3245
+ /**
3246
+ * Build tree-dependent cascade inputs that require child traversal.
3247
+ *
3248
+ * These feed into computeCascade() alongside the node's own dirty flags.
3249
+ * Separated from renderNodeToBuffer to keep the main function focused on
3250
+ * the rendering flow rather than child traversal details.
3251
+ */
3252
+ function buildCascadeInputs(node, hasPrevBuffer) {
3253
+ if (!hasPrevBuffer || !isDirty(node.dirtyBits, node.dirtyEpoch, 16) || node.children === void 0) return {
3254
+ absoluteChildMutated: false,
3255
+ descendantOverflowChanged: false
3256
+ };
3257
+ return {
3258
+ absoluteChildMutated: isDirty(node.dirtyBits, node.dirtyEpoch, 32),
3259
+ descendantOverflowChanged: isDirty(node.dirtyBits, node.dirtyEpoch, 64)
3260
+ };
3261
+ }
3262
+ /**
3263
+ * Log per-node trace, cascade tracking, and cell debug info.
3264
+ *
3265
+ * Gated on instrumentation or cell debug being active. Separated from
3266
+ * renderNodeToBuffer to keep the main function focused on rendering logic.
3267
+ */
3268
+ function traceRenderDecision(node, props, layout, screenY, scrollOffset, hasPrevBuffer, ancestorCleared, layoutChanged, childPositionChanged, cascade, _nodeId, _traceThis, _cellDbg, _coversCellNow, _coversCellPrev, instrumentEnabled, stats, nodeTrace) {
3269
+ const { contentAreaAffected, contentRegionCleared, skipBgFill, childrenNeedFreshRender } = cascade;
3270
+ if (instrumentEnabled) {
3271
+ if (_traceThis) {
3272
+ const flagStr = [
3273
+ isDirty(node.dirtyBits, node.dirtyEpoch, 1) && "C",
3274
+ isDirty(node.dirtyBits, node.dirtyEpoch, 2) && "P",
3275
+ isDirty(node.dirtyBits, node.dirtyEpoch, 4) && "B",
3276
+ isDirty(node.dirtyBits, node.dirtyEpoch, 16) && "S",
3277
+ isDirty(node.dirtyBits, node.dirtyEpoch, 8) && "Ch",
3278
+ childPositionChanged && "CP"
3279
+ ].filter(Boolean).join(",");
3280
+ const childHasPrev_ = isDirty(node.dirtyBits, node.dirtyEpoch, 8) || childPositionChanged || childrenNeedFreshRender ? false : hasPrevBuffer;
3281
+ const childAncestorCleared_ = contentRegionCleared || ancestorCleared && !getEffectiveBg(props);
3282
+ nodeTrace.push({
3283
+ id: _nodeId,
3284
+ type: node.type,
3285
+ depth: _getNodeDepth(node),
3286
+ rect: `${layout.x},${layout.y} ${layout.width}x${layout.height}`,
3287
+ prevLayout: node.prevLayout ? `${node.prevLayout.x},${node.prevLayout.y} ${node.prevLayout.width}x${node.prevLayout.height}` : "null",
3288
+ hasPrev: hasPrevBuffer,
3289
+ ancestorCleared,
3290
+ flags: flagStr,
3291
+ decision: "RENDER",
3292
+ layoutChanged,
3293
+ contentAreaAffected,
3294
+ contentRegionCleared,
3295
+ childrenNeedFreshRender,
3296
+ childHasPrev: childHasPrev_,
3297
+ childAncestorCleared: childAncestorCleared_,
3298
+ skipBgFill,
3299
+ bgColor: props.backgroundColor
3300
+ });
3301
+ }
3302
+ if (childrenNeedFreshRender && node.children.length > 0) {
3303
+ const depth = _getNodeDepth(node);
3304
+ if (depth < stats.cascadeMinDepth) stats.cascadeMinDepth = depth;
3305
+ const entry = `${node.props.id ?? node.type}@${depth}[${[
3306
+ isDirty(node.dirtyBits, node.dirtyEpoch, 1) && "C",
3307
+ isDirty(node.dirtyBits, node.dirtyEpoch, 2) && "P",
3308
+ isDirty(node.dirtyBits, node.dirtyEpoch, 8) && "Ch",
3309
+ layoutChanged && "L",
3310
+ childPositionChanged && "CP"
3311
+ ].filter(Boolean).join("")}:${node.children.length}ch]`;
3312
+ stats.cascadeNodes += (stats.cascadeNodes ? " " : "") + entry;
3313
+ }
3314
+ }
3315
+ if (_cellDbg && (_coversCellNow || _coversCellPrev)) {
3316
+ const id = props.id ?? node.type;
3317
+ const depth = _getNodeDepth(node);
3318
+ const prev = node.prevLayout;
3319
+ const flags = [
3320
+ isDirty(node.dirtyBits, node.dirtyEpoch, 1) && "C",
3321
+ isDirty(node.dirtyBits, node.dirtyEpoch, 2) && "P",
3322
+ layoutChanged && "L",
3323
+ isDirty(node.dirtyBits, node.dirtyEpoch, 16) && "S",
3324
+ isDirty(node.dirtyBits, node.dirtyEpoch, 8) && "Ch",
3325
+ childPositionChanged && "CP",
3326
+ isDirty(node.dirtyBits, node.dirtyEpoch, 4) && "B"
3327
+ ].filter(Boolean).join(",");
3328
+ const msg = `RENDER ${id}@${depth} rect=${layout.x},${screenY} ${layout.width}x${layout.height} prev=${prev ? `${prev.x},${prev.y - scrollOffset} ${prev.width}x${prev.height}` : "null"} flags=[${flags}] hasPrev=${hasPrevBuffer} ancClr=${ancestorCleared} caa=${contentAreaAffected} prc=${contentRegionCleared} prm=${childrenNeedFreshRender} coversNow=${_coversCellNow} coversPrev=${_coversCellPrev} bg=${props.backgroundColor ?? "none"}`;
3329
+ _cellDbg.log.push(msg);
3330
+ cellLog.debug?.(msg);
3331
+ }
3332
+ }
3333
+ /**
3334
+ * Handle all region clearing before rendering own content.
3335
+ *
3336
+ * Three clearing paths:
3337
+ * 1. contentRegionCleared: clear the node's region with inherited bg (no own bg)
3338
+ * 2. Excess area: clear stale pixels when a node shrank (even without contentRegionCleared)
3339
+ * 3. Descendant overflow: clear areas where descendants previously overflowed this node's rect
3340
+ *
3341
+ * All clearing runs BEFORE renderBox/renderText so borders drawn later are not overwritten.
3342
+ */
3343
+ function executeRegionClearing(node, buffer, layout, scrollOffset, clipBounds, bufferIsCloned, layoutChanged, contentRegionCleared, descendantOverflowChanged, instrumentEnabled, stats, threadedInheritedBg) {
3344
+ if (contentRegionCleared) {
3345
+ if (instrumentEnabled) stats.clearOps++;
3346
+ clearNodeRegion(node, buffer, layout, scrollOffset, clipBounds, layoutChanged, threadedInheritedBg);
3347
+ } else if (bufferIsCloned && layoutChanged && node.prevLayout) clearExcessArea(node, buffer, layout, scrollOffset, clipBounds, layoutChanged, threadedInheritedBg);
3348
+ if (descendantOverflowChanged) clearDescendantOverflowRegions(node, buffer, layout, scrollOffset, clipBounds, threadedInheritedBg);
3349
+ }
3350
+ /**
3351
+ * Render this node's own content (box background/border or text).
3352
+ *
3353
+ * For boxes: computes inherited bg for border rendering and calls renderBox.
3354
+ * For text: computes inherited bg/fg for text rendering and calls renderText.
3355
+ *
3356
+ * @returns The boxInheritedBg color (needed by outline rendering after children).
3357
+ */
3358
+ function renderOwnContent(node, buffer, layout, props, nodeState, skipBgFill, instrumentEnabled, stats, ctx, bgOnlyChange = false, useTextStyleFastPath = false) {
3359
+ const boxInheritedBg = node.type === "silvery-box" && !getEffectiveBg(props) ? nodeState.inheritedBg.color : void 0;
3360
+ if (node.type === "silvery-box") {
3361
+ if (instrumentEnabled) stats.boxNodes++;
3362
+ renderBox(node, buffer, layout, props, nodeState, skipBgFill, boxInheritedBg, bgOnlyChange);
3363
+ } else if (node.type === "silvery-text") {
3364
+ if (instrumentEnabled) stats.textNodes++;
3365
+ const textInheritedBg = nodeState.inheritedBg.color;
3366
+ const textInheritedFg = nodeState.inheritedFg;
3367
+ if (useTextStyleFastPath) {
3368
+ const style = getTextStyle(props);
3369
+ if (style.fg === null && textInheritedFg !== void 0) style.fg = textInheritedFg;
3370
+ const effectiveBg = style.bg !== null ? style.bg : textInheritedBg ?? null;
3371
+ const { x, width, height } = layout;
3372
+ const y = layout.y - nodeState.scrollOffset;
3373
+ buffer.restyleRegion(x, y, width, height, {
3374
+ fg: style.fg,
3375
+ bg: effectiveBg,
3376
+ underlineColor: style.underlineColor ?? null,
3377
+ attrs: style.attrs
3378
+ });
3379
+ } else renderText(node, buffer, layout, props, nodeState, textInheritedBg, textInheritedFg, ctx);
3380
+ }
3381
+ return boxInheritedBg;
3382
+ }
3383
+ /**
3384
+ * Determine the scroll tier strategy for this frame.
3385
+ *
3386
+ * Pure function -- no side effects, no node access beyond the inputs.
3387
+ *
3388
+ * Three-tier strategy:
3389
+ * 1. **shift**: Only scroll offset changed, no sticky children. Buffer contents
3390
+ * shifted by scroll delta; only newly visible edges re-render.
3391
+ * 2. **clear**: Children restructured, visible range changed with scroll, or
3392
+ * parent region changed. Entire viewport cleared and all children re-render.
3393
+ * 3. **subtree-only**: Only some descendants changed. Children use hasPrevBuffer=true
3394
+ * and skip via fast-path if clean. With sticky children, forces all first-pass
3395
+ * items to re-render (stickyForceRefresh).
3396
+ */
3397
+ function planScrollRender(inputs) {
3398
+ const { scrollOffsetChanged, visibleRangeChanged, hasStickyChildren, childrenNeedFreshRender, childrenDirty, hasPrevBuffer, ancestorCleared, contentRegionCleared, scrollBg } = inputs;
3399
+ const scrollOnly = hasPrevBuffer && scrollOffsetChanged && !childrenDirty && !childrenNeedFreshRender && !hasStickyChildren && !visibleRangeChanged;
3400
+ const needsViewportClear = hasPrevBuffer && !scrollOnly && (scrollOffsetChanged || childrenDirty || childrenNeedFreshRender || visibleRangeChanged);
3401
+ const stickyForceRefresh = hasStickyChildren && hasPrevBuffer && !needsViewportClear;
3402
+ const reasons = [];
3403
+ if (scrollOnly) reasons.push("SHIFT");
3404
+ if (needsViewportClear) {
3405
+ if (scrollOffsetChanged) reasons.push("scrollOffset");
3406
+ if (childrenDirty) reasons.push("childrenDirty");
3407
+ if (childrenNeedFreshRender) reasons.push("childrenNeedFreshRender");
3408
+ if (visibleRangeChanged) reasons.push("visibleRangeChanged");
3409
+ }
3410
+ if (stickyForceRefresh) reasons.push("stickyForceRefresh");
3411
+ return {
3412
+ tier: scrollOnly ? "shift" : needsViewportClear ? "clear" : "subtree-only",
3413
+ clearBg: scrollOnly || needsViewportClear ? scrollBg : null,
3414
+ childHasPrev: needsViewportClear ? false : hasPrevBuffer,
3415
+ childAncestorCleared: needsViewportClear ? true : ancestorCleared || contentRegionCleared,
3416
+ stickyForceRefresh,
3417
+ reasons
3418
+ };
3419
+ }
3420
+ /**
3421
+ * Render children of a scroll container with proper clipping and offset.
3422
+ */
3423
+ function renderScrollContainerChildren(node, buffer, props, nodeState, contentRegionCleared = false, childrenNeedFreshRender = false, ctx) {
3424
+ const { clipBounds, hasPrevBuffer, ancestorCleared, bufferIsCloned, ancestorLayoutChanged, inheritedBg, inheritedFg } = nodeState;
3425
+ const instr = resolveInstrumentation(ctx);
3426
+ const layout = node.boxRect;
3427
+ const ss = node.scrollState;
3428
+ if (!layout || !ss) return;
3429
+ const border = props.borderStyle ? getBorderSize(props) : {
3430
+ top: 0,
3431
+ bottom: 0,
3432
+ left: 0,
3433
+ right: 0
3434
+ };
3435
+ const padding = getPadding(props);
3436
+ const childClipBounds = computeChildClipBounds(layout, props, clipBounds, 0, false, true);
3437
+ const scrollOffsetChanged = ss.offset !== ss.prevOffset;
3438
+ const hasStickyChildren = !!(ss.stickyChildren && ss.stickyChildren.length > 0);
3439
+ const visibleRangeChanged = ss.firstVisibleChild !== ss.prevFirstVisibleChild || ss.lastVisibleChild !== ss.prevLastVisibleChild;
3440
+ const clearY = childClipBounds.top;
3441
+ const clearHeight = childClipBounds.bottom - childClipBounds.top;
3442
+ const contentX = layout.x + border.left + padding.left;
3443
+ const contentWidth = layout.width - border.left - border.right - padding.left - padding.right;
3444
+ const scrollBg = scrollOffsetChanged || isDirty(node.dirtyBits, node.dirtyEpoch, 8) || childrenNeedFreshRender || visibleRangeChanged ? getEffectiveBg(props) ? parseColor(getEffectiveBg(props)) : inheritedBg.color : null;
3445
+ const plan = planScrollRender({
3446
+ scrollOffsetChanged,
3447
+ visibleRangeChanged,
3448
+ hasStickyChildren,
3449
+ childrenNeedFreshRender,
3450
+ childrenDirty: isDirty(node.dirtyBits, node.dirtyEpoch, 8),
3451
+ hasPrevBuffer,
3452
+ ancestorCleared,
3453
+ contentRegionCleared,
3454
+ scrollBg
3455
+ });
3456
+ const { tier, stickyForceRefresh } = plan;
3457
+ const defaultChildHasPrev = plan.childHasPrev;
3458
+ const defaultChildAncestorCleared = plan.childAncestorCleared;
3459
+ if (instr.enabled) {
3460
+ instr.stats.scrollContainerCount++;
3461
+ if (tier !== "subtree-only" || stickyForceRefresh) {
3462
+ instr.stats.scrollViewportCleared++;
3463
+ const reasons = [...plan.reasons];
3464
+ if (scrollOffsetChanged) reasons.push(`scrollOffset(${ss.prevOffset}->${ss.offset})`);
3465
+ reasons.push(`vp=${ss.viewportHeight} content=${ss.contentHeight} vis=${ss.firstVisibleChild}-${ss.lastVisibleChild}`);
3466
+ instr.stats.scrollClearReason = reasons.join("+");
3467
+ }
3468
+ }
3469
+ if (process?.env?.SILVERY_STRICT && tier === "shift" && hasStickyChildren) throw new Error(`[SILVERY_STRICT] Scroll Tier 1 (buffer shift) activated with sticky children (node: ${props.id ?? node.type}, stickyCount: ${ss.stickyChildren?.length ?? 0})`);
3470
+ const scrollDelta = ss.offset - (ss.prevOffset ?? ss.offset);
3471
+ if (tier === "shift" && clearHeight > 0) {
3472
+ if (props.overflowIndicator === true && !border.top && !border.bottom) {
3473
+ const topIndicatorY = clearY;
3474
+ const bottomIndicatorY = clearY + clearHeight - 1;
3475
+ if (ss.prevOffset != null && ss.prevOffset > 0) buffer.fill(contentX, topIndicatorY, contentWidth, 1, {
3476
+ char: " ",
3477
+ bg: plan.clearBg
3478
+ });
3479
+ buffer.fill(contentX, bottomIndicatorY, contentWidth, 1, {
3480
+ char: " ",
3481
+ bg: plan.clearBg
3482
+ });
3483
+ }
3484
+ buffer.scrollRegion(contentX, clearY, contentWidth, clearHeight, scrollDelta, {
3485
+ char: " ",
3486
+ bg: plan.clearBg
3487
+ });
3488
+ }
3489
+ if (tier === "clear" && clearHeight > 0) buffer.fill(contentX, clearY, contentWidth, clearHeight, {
3490
+ char: " ",
3491
+ bg: plan.clearBg
3492
+ });
3493
+ if (stickyForceRefresh && clearHeight > 0) buffer.fill(contentX, clearY, contentWidth, clearHeight, {
3494
+ char: " ",
3495
+ bg: null
3496
+ });
3497
+ const childAncestorLayoutChanged = isCurrentEpoch(node.layoutChangedThisFrame) || !!ancestorLayoutChanged;
3498
+ const prevVisTop = ss.prevOffset ?? ss.offset;
3499
+ const prevVisBottom = prevVisTop + ss.viewportHeight;
3500
+ for (let i = 0; i < node.children.length; i++) {
3501
+ const child = node.children[i];
3502
+ if (!child) continue;
3503
+ if (child.props.position === "sticky") continue;
3504
+ if (i < ss.firstVisibleChild || i > ss.lastVisibleChild) continue;
3505
+ let thisChildHasPrev = defaultChildHasPrev;
3506
+ let thisChildAncestorCleared = defaultChildAncestorCleared;
3507
+ if (tier === "shift") {
3508
+ const childRect = child.boxRect;
3509
+ if (childRect) {
3510
+ const childTop = childRect.y - layout.y - border.top - padding.top;
3511
+ const childBottom = childTop + childRect.height;
3512
+ const wasFullyVisible = childTop >= prevVisTop && childBottom <= prevVisBottom;
3513
+ thisChildHasPrev = wasFullyVisible;
3514
+ thisChildAncestorCleared = wasFullyVisible ? ancestorCleared || contentRegionCleared : true;
3515
+ }
3516
+ }
3517
+ if (stickyForceRefresh && thisChildHasPrev) {
3518
+ thisChildHasPrev = false;
3519
+ thisChildAncestorCleared = false;
3520
+ }
3521
+ if (canSkipChildSubtree(child, thisChildHasPrev, childAncestorLayoutChanged)) continue;
3522
+ renderNodeToBuffer(child, buffer, {
3523
+ scrollOffset: ss.offset,
3524
+ clipBounds: childClipBounds,
3525
+ hasPrevBuffer: thisChildHasPrev,
3526
+ ancestorCleared: thisChildAncestorCleared,
3527
+ bufferIsCloned,
3528
+ ancestorLayoutChanged: childAncestorLayoutChanged,
3529
+ inheritedBg,
3530
+ inheritedFg
3531
+ }, ctx);
3532
+ }
3533
+ if (ss.stickyChildren) for (const sticky of ss.stickyChildren) {
3534
+ const child = node.children[sticky.index];
3535
+ if (!child?.boxRect) continue;
3536
+ renderNodeToBuffer(child, buffer, {
3537
+ scrollOffset: sticky.naturalTop - sticky.renderOffset,
3538
+ clipBounds: childClipBounds,
3539
+ hasPrevBuffer: false,
3540
+ ancestorCleared: false,
3541
+ bufferIsCloned,
3542
+ ancestorLayoutChanged: childAncestorLayoutChanged,
3543
+ inheritedBg,
3544
+ inheritedFg
3545
+ }, ctx);
3546
+ }
3547
+ }
3548
+ /**
3549
+ * Render children of a normal (non-scroll) container.
3550
+ */
3551
+ function renderNormalChildren(node, buffer, props, nodeState, childPositionChanged = false, contentRegionCleared = false, childrenNeedFreshRender = false, ctx) {
3552
+ const { scrollOffset, clipBounds, hasPrevBuffer, ancestorCleared, bufferIsCloned, ancestorLayoutChanged, inheritedBg, inheritedFg } = nodeState;
3553
+ const instr = resolveInstrumentation(ctx);
3554
+ const layout = node.boxRect;
3555
+ if (!layout) return;
3556
+ const clipX = (props.overflowX ?? props.overflow) === "hidden";
3557
+ const clipY = (props.overflowY ?? props.overflow) === "hidden";
3558
+ const effectiveClipBounds = clipX || clipY ? computeChildClipBounds(layout, props, clipBounds, scrollOffset, clipX, clipY) : clipBounds;
3559
+ const hasStickyChildren = !!(node.stickyChildren && node.stickyChildren.length > 0);
3560
+ const stickyForceRefresh = hasStickyChildren && hasPrevBuffer;
3561
+ if (stickyForceRefresh) {
3562
+ const border = props.borderStyle ? getBorderSize(props) : {
3563
+ top: 0,
3564
+ bottom: 0,
3565
+ left: 0,
3566
+ right: 0
3567
+ };
3568
+ const padding = getPadding(props);
3569
+ let clearX = layout.x + border.left + padding.left;
3570
+ let clearY = layout.y - scrollOffset + border.top + padding.top;
3571
+ let clearW = layout.width - border.left - border.right - padding.left - padding.right;
3572
+ let clearH = layout.height - border.top - border.bottom - padding.top - padding.bottom;
3573
+ if (clipBounds) {
3574
+ const clipTop = clipBounds.top;
3575
+ const clipBottom = clipBounds.bottom;
3576
+ if (clearY < clipTop) {
3577
+ clearH -= clipTop - clearY;
3578
+ clearY = clipTop;
3579
+ }
3580
+ if (clearY + clearH > clipBottom) clearH = clipBottom - clearY;
3581
+ if (clipBounds.left !== void 0 && clearX < clipBounds.left) {
3582
+ clearW -= clipBounds.left - clearX;
3583
+ clearX = clipBounds.left;
3584
+ }
3585
+ if (clipBounds.right !== void 0 && clearX + clearW > clipBounds.right) clearW = clipBounds.right - clearX;
3586
+ }
3587
+ if (clearW > 0 && clearH > 0) buffer.fill(clearX, clearY, clearW, clearH, {
3588
+ char: " ",
3589
+ bg: null
3590
+ });
3591
+ }
3592
+ const childrenNeedRepaint = isDirty(node.dirtyBits, node.dirtyEpoch, 8) || childPositionChanged || childrenNeedFreshRender;
3593
+ if (instr.enabled && childrenNeedRepaint && hasPrevBuffer) {
3594
+ instr.stats.normalChildrenRepaint++;
3595
+ const reasons = [];
3596
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 8)) reasons.push("childrenDirty");
3597
+ if (childPositionChanged) reasons.push("childPositionChanged");
3598
+ if (childrenNeedFreshRender) reasons.push("childrenNeedFreshRender");
3599
+ instr.stats.normalRepaintReason = reasons.join("+");
3600
+ }
3601
+ let childHasPrev = childrenNeedRepaint ? false : hasPrevBuffer;
3602
+ let childAncestorCleared = contentRegionCleared || ancestorCleared && !getEffectiveBg(props);
3603
+ const childAncestorLayoutChanged = isCurrentEpoch(node.layoutChangedThisFrame) || !!ancestorLayoutChanged;
3604
+ if (stickyForceRefresh) {
3605
+ childHasPrev = false;
3606
+ childAncestorCleared = false;
3607
+ }
3608
+ let hasAbsoluteChildren = false;
3609
+ for (const child of node.children) {
3610
+ const childProps = child.props;
3611
+ if (childProps.position === "absolute") {
3612
+ hasAbsoluteChildren = true;
3613
+ continue;
3614
+ }
3615
+ if (hasStickyChildren && childProps.position === "sticky") continue;
3616
+ if (canSkipChildSubtree(child, childHasPrev, childAncestorLayoutChanged)) continue;
3617
+ renderNodeToBuffer(child, buffer, {
3618
+ scrollOffset,
3619
+ clipBounds: effectiveClipBounds,
3620
+ hasPrevBuffer: childHasPrev,
3621
+ ancestorCleared: childAncestorCleared,
3622
+ bufferIsCloned,
3623
+ ancestorLayoutChanged: childAncestorLayoutChanged,
3624
+ inheritedBg,
3625
+ inheritedFg
3626
+ }, ctx);
3627
+ }
3628
+ if (node.stickyChildren) for (const sticky of node.stickyChildren) {
3629
+ const child = node.children[sticky.index];
3630
+ if (!child?.boxRect) continue;
3631
+ renderNodeToBuffer(child, buffer, {
3632
+ scrollOffset: sticky.naturalTop - sticky.renderOffset,
3633
+ clipBounds: effectiveClipBounds,
3634
+ hasPrevBuffer: false,
3635
+ ancestorCleared: false,
3636
+ bufferIsCloned,
3637
+ ancestorLayoutChanged: childAncestorLayoutChanged,
3638
+ inheritedBg,
3639
+ inheritedFg
3640
+ }, ctx);
3641
+ }
3642
+ if (hasAbsoluteChildren) for (const child of node.children) {
3643
+ if (child.props.position !== "absolute") continue;
3644
+ renderNodeToBuffer(child, buffer, {
3645
+ scrollOffset,
3646
+ clipBounds: effectiveClipBounds,
3647
+ hasPrevBuffer: false,
3648
+ ancestorCleared: false,
3649
+ bufferIsCloned,
3650
+ ancestorLayoutChanged: childAncestorLayoutChanged,
3651
+ inheritedBg,
3652
+ inheritedFg
3653
+ }, ctx);
3654
+ }
3655
+ }
3656
+ /**
3657
+ * O(1) pre-check: can we skip calling renderNodeToBuffer() on this child entirely?
3658
+ *
3659
+ * This is the Phase 4 "dirty set rendering" optimization. Instead of calling
3660
+ * renderNodeToBuffer() on every child (which checks canSkipEntireSubtree and
3661
+ * returns early for clean nodes), we skip the function call entirely for
3662
+ * subtrees with no dirty descendants.
3663
+ *
3664
+ * The pre-check is CONSERVATIVE: it only skips when we're certain the subtree
3665
+ * is clean. False negatives (calling renderNodeToBuffer unnecessarily) are
3666
+ * harmless — the existing canSkipEntireSubtree check inside handles them.
3667
+ *
3668
+ * Key insight: subtreeDirtyEpoch is propagated from every dirty node to the
3669
+ * root by markSubtreeDirty (reconciler) and propagateLayout (layout phase).
3670
+ * If subtreeDirtyEpoch !== currentEpoch, no descendant is dirty. Combined
3671
+ * with layoutChangedThisFrame (set by layout phase but NOT included in
3672
+ * subtreeDirtyEpoch on self — only on parent), this covers all dirty paths.
3673
+ */
3674
+ function canSkipChildSubtree(child, childHasPrev, childAncestorLayoutChanged) {
3675
+ if (!childHasPrev) return false;
3676
+ if (childAncestorLayoutChanged) return false;
3677
+ if (isDirty(child.dirtyBits, child.dirtyEpoch, 16)) return false;
3678
+ if (isCurrentEpoch(child.layoutChangedThisFrame)) return false;
3679
+ if (child.scrollState && child.scrollState.offset !== child.scrollState.prevOffset) return false;
3680
+ return true;
3681
+ }
3682
+ /**
3683
+ * Clear dirty flags on the current node only (no recursion).
3684
+ * Used after rendering a node to reset its flags.
3685
+ *
3686
+ * With epoch-stamped flags, this is only needed when a subtree is SKIPPED
3687
+ * by the fast path (clearDirtyFlags on skipped subtrees) or for the
3688
+ * render-phase-adapter. The normal render path relies on advanceRenderEpoch()
3689
+ * to expire all flags at once — O(1) instead of O(N).
3690
+ */
3691
+ function clearNodeDirtyFlags(node) {
3692
+ node.dirtyBits = 0;
3693
+ node.dirtyEpoch = -1;
3694
+ node.layoutChangedThisFrame = -1;
3695
+ }
3696
+ /**
3697
+ * Clear dirty flags on a subtree that was skipped during incremental rendering.
3698
+ */
3699
+ function clearDirtyFlags(node) {
3700
+ clearNodeDirtyFlags(node);
3701
+ for (const child of node.children) if (child.layoutNode) clearDirtyFlags(child);
3702
+ else clearVirtualTextFlags(child);
3703
+ }
3704
+ /**
3705
+ * Clear dirty flags on a virtual text node and its descendants.
3706
+ * Virtual text nodes (no layoutNode) are rendered by their parent layout
3707
+ * ancestor via collectTextContent(). Their dirty flags must be cleared
3708
+ * after the parent renders, otherwise stale subtreeDirty blocks
3709
+ * markSubtreeDirty() propagation on future updates.
3710
+ */
3711
+ function clearVirtualTextFlags(node) {
3712
+ clearNodeDirtyFlags(node);
3713
+ for (const child of node.children) clearVirtualTextFlags(child);
3714
+ }
3715
+ /**
3716
+ * Check if any child's position changed since last render (sibling shift).
3717
+ * Checked even when subtreeDirty=true because subtreeDirty only means
3718
+ * descendants are dirty, not that this container's gap regions need clearing.
3719
+ */
3720
+ function hasChildPositionChanged(node) {
3721
+ for (const child of node.children) if (child.boxRect && child.prevLayout) {
3722
+ if (child.boxRect.x !== child.prevLayout.x || child.boxRect.y !== child.prevLayout.y) return true;
3723
+ }
3724
+ return false;
3725
+ }
3726
+ /**
3727
+ * Check if any descendant has an explicit backgroundColor.
3728
+ *
3729
+ * Used by the bgOnlyChange fast path: fillBg() updates ALL cells in the region
3730
+ * with the parent's bg. If a descendant has its own bg, those cells would be
3731
+ * incorrectly overwritten (the descendant is clean and won't re-render to fix it).
3732
+ *
3733
+ * Only checks Box nodes with explicit backgroundColor or effective bg from theme.
3734
+ * Text nodes with backgroundColor are also checked since they render their own bg.
3735
+ * Stops at first match (early exit).
3736
+ *
3737
+ * Performance: walks the child tree, but only runs when bgOnlyChange is true
3738
+ * (bg changed, no other flags). This is the cursor-move hot path where trees
3739
+ * are typically small (card contents: ~5-20 nodes).
3740
+ */
3741
+ function hasDescendantWithBg(node) {
3742
+ for (const child of node.children) {
3743
+ if (getEffectiveBg(child.props)) return true;
3744
+ if (child.children.length > 0 && hasDescendantWithBg(child)) return true;
3745
+ }
3746
+ return false;
3747
+ }
3748
+ /**
3749
+ * Compute clip bounds for a container's children by insetting for border+padding,
3750
+ * then intersecting with parent clip bounds.
3751
+ */
3752
+ function computeChildClipBounds(layout, props, parentClip, scrollOffset = 0, horizontal = true, vertical = true) {
3753
+ const border = props.borderStyle ? getBorderSize(props) : {
3754
+ top: 0,
3755
+ bottom: 0,
3756
+ left: 0,
3757
+ right: 0
3758
+ };
3759
+ const padding = getPadding(props);
3760
+ const adjustedY = layout.y - scrollOffset;
3761
+ const nodeClip = vertical ? {
3762
+ top: adjustedY + border.top + padding.top,
3763
+ bottom: adjustedY + layout.height - border.bottom - padding.bottom
3764
+ } : {
3765
+ top: -Infinity,
3766
+ bottom: Infinity
3767
+ };
3768
+ if (horizontal) {
3769
+ nodeClip.left = layout.x + border.left + padding.left;
3770
+ nodeClip.right = layout.x + layout.width - border.right - padding.right;
3771
+ }
3772
+ if (!parentClip) return nodeClip;
3773
+ const result = {
3774
+ top: vertical ? Math.max(parentClip.top, nodeClip.top) : parentClip.top,
3775
+ bottom: vertical ? Math.min(parentClip.bottom, nodeClip.bottom) : parentClip.bottom
3776
+ };
3777
+ if (horizontal && nodeClip.left !== void 0 && nodeClip.right !== void 0) {
3778
+ result.left = Math.max(parentClip.left ?? 0, nodeClip.left);
3779
+ result.right = Math.min(parentClip.right ?? Infinity, nodeClip.right);
3780
+ } else if (parentClip.left !== void 0 && parentClip.right !== void 0) {
3781
+ result.left = parentClip.left;
3782
+ result.right = parentClip.right;
3783
+ }
3784
+ return result;
3785
+ }
3786
+ /**
3787
+ * Clear overflow regions: areas where children's prevLayouts extended beyond
3788
+ * this node's rect. Called when childOverflowChanged detected stale overflow.
3789
+ *
3790
+ * clearNodeRegion handles the node's own rect. This function handles the
3791
+ * overflow area — pixels that a child rendered OUTSIDE the parent's rect
3792
+ * in a previous frame (via overflow:visible behavior). When the child shrinks,
3793
+ * those pixels become stale in the cloned buffer.
3794
+ *
3795
+ * Clears each child's overflow extent, clipped to buffer bounds.
3796
+ */
3797
+ /**
3798
+ * Clear areas where descendants' previous layouts overflowed beyond THIS node's rect.
3799
+ * Only clears OUTSIDE the node's rect — interior clearing is handled by clearNodeRegion
3800
+ * and renderBox. Recursive: follows subtreeDirty paths to find all overflowing descendants.
3801
+ */
3802
+ function clearDescendantOverflowRegions(node, buffer, layout, scrollOffset, clipBounds, threadedInheritedBg) {
3803
+ const clearBg = threadedInheritedBg.color;
3804
+ const nodeRight = layout.x + layout.width;
3805
+ const nodeBottom = layout.y - scrollOffset + layout.height;
3806
+ const nodeLeft = layout.x;
3807
+ const nodeTop = layout.y - scrollOffset;
3808
+ _clearDescendantOverflow(node.children, buffer, nodeLeft, nodeTop, nodeRight, nodeBottom, scrollOffset, clipBounds, clearBg);
3809
+ }
3810
+ function _clearDescendantOverflow(children, buffer, nodeLeft, nodeTop, nodeRight, nodeBottom, scrollOffset, clipBounds, clearBg) {
3811
+ for (const child of children) {
3812
+ if (child.prevLayout && isCurrentEpoch(child.layoutChangedThisFrame)) {
3813
+ const prev = child.prevLayout;
3814
+ const prevRight = prev.x + prev.width;
3815
+ const prevBottom = prev.y - scrollOffset + prev.height;
3816
+ const prevTop = prev.y - scrollOffset;
3817
+ if (prevRight > nodeRight) {
3818
+ const overflowX = nodeRight;
3819
+ const overflowWidth = Math.min(prevRight, buffer.width) - overflowX;
3820
+ const overflowTop = Math.max(prevTop, clipBounds?.top ?? 0);
3821
+ const overflowBottom = Math.min(prevBottom, clipBounds?.bottom ?? buffer.height);
3822
+ if (overflowWidth > 0 && overflowBottom > overflowTop) buffer.fill(overflowX, overflowTop, overflowWidth, overflowBottom - overflowTop, {
3823
+ char: " ",
3824
+ bg: clearBg
3825
+ });
3826
+ }
3827
+ if (prevBottom > nodeBottom) {
3828
+ const overflowTop = Math.max(nodeBottom, clipBounds?.top ?? 0);
3829
+ const overflowBottom = Math.min(prevBottom, clipBounds?.bottom ?? buffer.height);
3830
+ const overflowX = Math.max(prev.x, clipBounds?.left ?? 0);
3831
+ const overflowWidth = Math.min(prevRight, clipBounds?.right ?? buffer.width) - overflowX;
3832
+ if (overflowWidth > 0 && overflowBottom > overflowTop) buffer.fill(overflowX, overflowTop, overflowWidth, overflowBottom - overflowTop, {
3833
+ char: " ",
3834
+ bg: clearBg
3835
+ });
3836
+ }
3837
+ if (prev.x < nodeLeft) {
3838
+ const overflowX = Math.max(prev.x, 0);
3839
+ const overflowWidth = Math.min(nodeLeft, buffer.width) - overflowX;
3840
+ const overflowTop = Math.max(prevTop, clipBounds?.top ?? 0);
3841
+ const overflowBottom = Math.min(prevBottom, clipBounds?.bottom ?? buffer.height);
3842
+ if (overflowWidth > 0 && overflowBottom > overflowTop) buffer.fill(overflowX, overflowTop, overflowWidth, overflowBottom - overflowTop, {
3843
+ char: " ",
3844
+ bg: clearBg
3845
+ });
3846
+ }
3847
+ if (prevTop < nodeTop) {
3848
+ const overflowTop = Math.max(prevTop, clipBounds?.top ?? 0);
3849
+ const overflowBottom = Math.min(nodeTop, clipBounds?.bottom ?? buffer.height);
3850
+ const overflowX = Math.max(prev.x, clipBounds?.left ?? 0);
3851
+ const overflowWidth = Math.min(prevRight, clipBounds?.right ?? buffer.width) - overflowX;
3852
+ if (overflowWidth > 0 && overflowBottom > overflowTop) buffer.fill(overflowX, overflowTop, overflowWidth, overflowBottom - overflowTop, {
3853
+ char: " ",
3854
+ bg: clearBg
3855
+ });
3856
+ }
3857
+ }
3858
+ if (isDirty(child.dirtyBits, child.dirtyEpoch, 16) && child.children !== void 0) _clearDescendantOverflow(child.children, buffer, nodeLeft, nodeTop, nodeRight, nodeBottom, scrollOffset, clipBounds, clearBg);
3859
+ }
3860
+ }
3861
+ /**
3862
+ * Clear a node's region with inherited bg when it has no backgroundColor.
3863
+ * Also clears excess area when the node shrank (previous layout was larger).
3864
+ *
3865
+ * Clipping: clips to parent's boxRect (prevents overflow) and to the
3866
+ * colored ancestor's bounds (prevents bg color bleeding into siblings).
3867
+ */
3868
+ function clearNodeRegion(node, buffer, layout, scrollOffset, clipBounds, layoutChanged, threadedInheritedBg) {
3869
+ const inherited = threadedInheritedBg;
3870
+ const clearBg = inherited.color;
3871
+ const screenY = layout.y - scrollOffset;
3872
+ const parentRect = node.parent?.boxRect;
3873
+ const parentBottom = parentRect ? parentRect.y - scrollOffset + parentRect.height : void 0;
3874
+ const clearY = clipBounds ? Math.max(screenY, clipBounds.top) : screenY;
3875
+ let clearBottom = clipBounds ? Math.min(screenY + layout.height, clipBounds.bottom) : screenY + layout.height;
3876
+ if (parentBottom !== void 0) clearBottom = Math.min(clearBottom, parentBottom);
3877
+ let clearX = layout.x;
3878
+ let clearWidth = layout.width;
3879
+ if (clipBounds?.left !== void 0 && clipBounds.right !== void 0) {
3880
+ if (clearX < clipBounds.left) {
3881
+ clearWidth -= clipBounds.left - clearX;
3882
+ clearX = clipBounds.left;
3883
+ }
3884
+ if (clearX + clearWidth > clipBounds.right) clearWidth = Math.max(0, clipBounds.right - clearX);
3885
+ }
3886
+ if (inherited.ancestorRect) {
3887
+ const ancestorRight = inherited.ancestorRect.x + inherited.ancestorRect.width;
3888
+ const ancestorLeft = inherited.ancestorRect.x;
3889
+ if (clearX < ancestorLeft) {
3890
+ clearWidth -= ancestorLeft - clearX;
3891
+ clearX = ancestorLeft;
3892
+ }
3893
+ if (clearX + clearWidth > ancestorRight) clearWidth = Math.max(0, ancestorRight - clearX);
3894
+ }
3895
+ const clearHeight = clearBottom - clearY;
3896
+ if (clearHeight > 0 && clearWidth > 0) {
3897
+ const _cellDbg2 = getCellDebug();
3898
+ if (_cellDbg2 && cellCoversPoint(_cellDbg2, clearX, clearY, clearWidth, clearHeight)) {
3899
+ const msg = `CLEAR_REGION ${node.props.id ?? node.type} fill=${clearX},${clearY} ${clearWidth}x${clearHeight} bg=${String(clearBg)} COVERS TARGET`;
3900
+ _cellDbg2.log.push(msg);
3901
+ cellLog.debug?.(msg);
3902
+ }
3903
+ buffer.fill(clearX, clearY, clearWidth, clearHeight, {
3904
+ char: " ",
3905
+ bg: clearBg
3906
+ });
3907
+ }
3908
+ clearExcessArea(node, buffer, layout, scrollOffset, clipBounds, layoutChanged, inherited);
3909
+ }
3910
+ /**
3911
+ * Clear the excess area when a node shrinks (old bounds were larger than new).
3912
+ *
3913
+ * This is separated from clearNodeRegion because excess area clearing must happen
3914
+ * even when contentRegionCleared is false. Key scenario: absolute-positioned overlays
3915
+ * (e.g., search dialog) that shrink while normal-flow siblings are dirty. The
3916
+ * forceRepaint path sets hasPrevBuffer=false + ancestorCleared=false, making
3917
+ * contentRegionCleared=false — but the cloned buffer still has stale pixels from
3918
+ * the old larger layout that must be cleared.
3919
+ *
3920
+ * Clips to the COLORED ANCESTOR's content area (not immediate parent's full rect)
3921
+ * to prevent inherited color from bleeding into sibling areas with different bg.
3922
+ *
3923
+ * IMPORTANT: Uses content area (inside border/padding), not full boxRect.
3924
+ * Without this, excess clearing of a child that previously filled the parent's
3925
+ * content area will extend into the parent's border row, overwriting border chars.
3926
+ */
3927
+ function clearExcessArea(node, buffer, layout, scrollOffset, clipBounds, layoutChanged, inherited) {
3928
+ if (!layoutChanged || !node.prevLayout) return;
3929
+ const prev = node.prevLayout;
3930
+ const _cellDbg3 = getCellDebug();
3931
+ const _prevCoversCell3 = _cellDbg3 && cellCoversPoint(_cellDbg3, prev.x, prev.y - scrollOffset, prev.width, prev.height);
3932
+ if (prev.width <= layout.width && prev.height <= layout.height) {
3933
+ if (_cellDbg3 && _prevCoversCell3) {
3934
+ const msg = `EXCESS_SKIP_NO_SHRINK ${node.props.id ?? node.type} prev=${prev.x},${prev.y - scrollOffset} ${prev.width}x${prev.height} now=${layout.x},${layout.y - scrollOffset} ${layout.width}x${layout.height}`;
3935
+ _cellDbg3.log.push(msg);
3936
+ cellLog.debug?.(msg);
3937
+ }
3938
+ return;
3939
+ }
3940
+ if (prev.x !== layout.x || prev.y !== layout.y) {
3941
+ if (_cellDbg3 && _prevCoversCell3) {
3942
+ const msg = `EXCESS_SKIP_MOVED ${node.props.id ?? node.type} prev=${prev.x},${prev.y - scrollOffset} ${prev.width}x${prev.height} now=${layout.x},${layout.y - scrollOffset} ${layout.width}x${layout.height} (dx=${layout.x - prev.x} dy=${layout.y - prev.y})`;
3943
+ _cellDbg3.log.push(msg);
3944
+ cellLog.debug?.(msg);
3945
+ }
3946
+ return;
3947
+ }
3948
+ const clearBg = inherited.color;
3949
+ const screenY = layout.y - scrollOffset;
3950
+ const prevScreenY = prev.y - scrollOffset;
3951
+ const clipRect = inherited.ancestorRect ?? node.parent?.boxRect;
3952
+ if (!clipRect) return;
3953
+ let clipRectBottom = clipRect.y - scrollOffset + clipRect.height;
3954
+ let clipRectRight = clipRect.x + clipRect.width;
3955
+ const parent = node.parent;
3956
+ if (parent?.boxRect) {
3957
+ const parentProps = parent.props;
3958
+ const border = getBorderSize(parentProps);
3959
+ const padding = getPadding(parentProps);
3960
+ const parentRight = parent.boxRect.x + parent.boxRect.width - border.right - padding.right;
3961
+ const parentBottom = parent.boxRect.y - scrollOffset + parent.boxRect.height - border.bottom - padding.bottom;
3962
+ clipRectRight = Math.min(clipRectRight, parentRight);
3963
+ clipRectBottom = Math.min(clipRectBottom, parentBottom);
3964
+ }
3965
+ if (prev.width > layout.width) {
3966
+ const excessX = layout.x + layout.width;
3967
+ let excessWidth = prev.width - layout.width;
3968
+ if (excessX + excessWidth > clipRectRight) excessWidth = Math.max(0, clipRectRight - excessX);
3969
+ if (excessWidth > 0) clippedFill(buffer, excessX, excessWidth, prevScreenY, prevScreenY + prev.height, clipBounds, clipRectBottom, clearBg);
3970
+ }
3971
+ if (prev.height > layout.height) {
3972
+ let bottomWidth = prev.width;
3973
+ if (layout.x + bottomWidth > clipRectRight) bottomWidth = Math.max(0, clipRectRight - layout.x);
3974
+ clippedFill(buffer, layout.x, bottomWidth, screenY + layout.height, prevScreenY + prev.height, clipBounds, clipRectBottom, clearBg);
3975
+ }
3976
+ }
3977
+ /** Fill a rectangular region, clipping to clipBounds and an outer bottom limit. */
3978
+ function clippedFill(buffer, x, width, top, bottom, clipBounds, outerBottom, bg) {
3979
+ const clippedTop = clipBounds ? Math.max(top, clipBounds.top) : top;
3980
+ const clippedBottom = Math.min(clipBounds ? Math.min(bottom, clipBounds.bottom) : bottom, outerBottom);
3981
+ let clippedX = x;
3982
+ let clippedWidth = width;
3983
+ if (clipBounds?.left !== void 0 && clipBounds.right !== void 0) {
3984
+ if (clippedX < clipBounds.left) {
3985
+ clippedWidth -= clipBounds.left - clippedX;
3986
+ clippedX = clipBounds.left;
3987
+ }
3988
+ if (clippedX + clippedWidth > clipBounds.right) clippedWidth = Math.max(0, clipBounds.right - clippedX);
3989
+ }
3990
+ const height = clippedBottom - clippedTop;
3991
+ if (height > 0 && clippedWidth > 0) buffer.fill(clippedX, clippedTop, clippedWidth, height, {
3992
+ char: " ",
3993
+ bg
3994
+ });
3995
+ }
3996
+ //#endregion
3997
+ //#region \0@oxc-project+runtime@0.122.0/helpers/usingCtx.js
3998
+ function _usingCtx() {
3999
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
4000
+ var n = Error();
4001
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
4002
+ }, e = {}, n = [];
4003
+ function using(r, e) {
4004
+ if (null != e) {
4005
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
4006
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
4007
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
4008
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
4009
+ t && (o = function o() {
4010
+ try {
4011
+ t.call(e);
4012
+ } catch (r) {
4013
+ return Promise.reject(r);
4014
+ }
4015
+ }), n.push({
4016
+ v: e,
4017
+ d: o,
4018
+ a: r
4019
+ });
4020
+ } else r && n.push({
4021
+ d: e,
4022
+ a: r
4023
+ });
4024
+ return e;
4025
+ }
4026
+ return {
4027
+ e,
4028
+ u: using.bind(null, !1),
4029
+ a: using.bind(null, !0),
4030
+ d: function d() {
4031
+ var o, t = this.e, s = 0;
4032
+ function next() {
4033
+ for (; o = n.pop();) try {
4034
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
4035
+ if (o.d) {
4036
+ var r = o.d.call(o.v);
4037
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
4038
+ } else s |= 1;
4039
+ } catch (r) {
4040
+ return err(r);
4041
+ }
4042
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
4043
+ if (t !== e) throw t;
4044
+ }
4045
+ function err(n) {
4046
+ return t = t !== e ? new r(n, t) : n, next();
4047
+ }
4048
+ return next();
4049
+ }
4050
+ };
4051
+ }
4052
+ //#endregion
4053
+ //#region packages/ag-term/src/ag.ts
4054
+ /**
4055
+ * Ag — tree + layout engine + renderer.
4056
+ *
4057
+ * Decomposes the opaque executeRender() into two independent phases:
4058
+ * - ag.layout(dims) — measure + flexbox → positions/sizes
4059
+ * - ag.render() — positioned tree → cell grid → TextFrame
4060
+ *
4061
+ * The output phase (buffer → ANSI) is NOT part of ag — it lives in term.paint().
4062
+ *
4063
+ * @example
4064
+ * ```ts
4065
+ * const ag = createAg(root, { measurer })
4066
+ * ag.layout({ cols: 80, rows: 24 })
4067
+ * const { frame, buffer } = ag.render()
4068
+ * const output = term.paint(buffer, prevBuffer)
4069
+ * ```
4070
+ */
4071
+ init_buffer();
4072
+ const log$1 = createLogger("silvery:render");
4073
+ const baseLog$1 = createLogger("@silvery/ag-react");
4074
+ function createAg(root, options) {
4075
+ const measurer = options?.measurer;
4076
+ const ctx = measurer ? { measurer } : void 0;
4077
+ let _prevBuffer = null;
4078
+ let hasScroll = false;
4079
+ let hasSticky = false;
4080
+ function doLayout(cols, rows, opts) {
4081
+ try {
4082
+ var _usingCtx$2 = _usingCtx();
4083
+ const prevRootLayout = root.boxRect;
4084
+ if (!(prevRootLayout && (prevRootLayout.width !== cols || prevRootLayout.height !== rows)) && !hasLayoutDirty() && !hasScrollDirty()) {
4085
+ log$1.debug?.("layout: skipped (no layoutDirty, no scrollDirty, dimensions unchanged)");
4086
+ return {
4087
+ tMeasure: 0,
4088
+ tLayout: 0,
4089
+ tScroll: 0,
4090
+ tScrollRect: 0,
4091
+ tNotify: 0
4092
+ };
4093
+ }
4094
+ const render = _usingCtx$2.u(baseLog$1.span("pipeline", {
4095
+ width: cols,
4096
+ height: rows
4097
+ }));
4098
+ let tMeasure;
4099
+ try {
4100
+ var _usingCtx3 = _usingCtx();
4101
+ _usingCtx3.u(render.span("measure"));
4102
+ const t = performance.now();
4103
+ measurePhase(root, ctx);
4104
+ tMeasure = performance.now() - t;
4105
+ log$1.debug?.(`measure: ${tMeasure.toFixed(2)}ms`);
4106
+ } catch (_) {
4107
+ _usingCtx3.e = _;
4108
+ } finally {
4109
+ _usingCtx3.d();
4110
+ }
4111
+ let tLayout;
4112
+ try {
4113
+ var _usingCtx4 = _usingCtx();
4114
+ _usingCtx4.u(render.span("layout"));
4115
+ const t = performance.now();
4116
+ layoutPhase(root, cols, rows);
4117
+ tLayout = performance.now() - t;
4118
+ log$1.debug?.(`layout: ${tLayout.toFixed(2)}ms`);
4119
+ } catch (_) {
4120
+ _usingCtx4.e = _;
4121
+ } finally {
4122
+ _usingCtx4.d();
4123
+ }
4124
+ if (!hasScroll || !hasSticky) {
4125
+ const features = detectPipelineFeatures(root);
4126
+ if (features.hasScroll) hasScroll = true;
4127
+ if (features.hasSticky) hasSticky = true;
4128
+ }
4129
+ let tScroll;
4130
+ if (hasScroll) try {
4131
+ var _usingCtx5 = _usingCtx();
4132
+ _usingCtx5.u(render.span("scroll"));
4133
+ const t = performance.now();
4134
+ scrollPhase(root, { skipStateUpdates: opts?.skipScrollStateUpdates });
4135
+ tScroll = performance.now() - t;
4136
+ } catch (_) {
4137
+ _usingCtx5.e = _;
4138
+ } finally {
4139
+ _usingCtx5.d();
4140
+ }
4141
+ else tScroll = 0;
4142
+ if (hasSticky) stickyPhase(root);
4143
+ let tScrollRect;
4144
+ try {
4145
+ var _usingCtx6 = _usingCtx();
4146
+ _usingCtx6.u(render.span("scrollRect"));
4147
+ const t = performance.now();
4148
+ if (hasScroll || hasSticky) scrollrectPhase(root);
4149
+ else scrollrectPhaseSimple(root);
4150
+ tScrollRect = performance.now() - t;
4151
+ } catch (_) {
4152
+ _usingCtx6.e = _;
4153
+ } finally {
4154
+ _usingCtx6.d();
4155
+ }
4156
+ let tNotify = 0;
4157
+ if (!opts?.skipLayoutNotifications) try {
4158
+ var _usingCtx7 = _usingCtx();
4159
+ _usingCtx7.u(render.span("notify"));
4160
+ const t = performance.now();
4161
+ notifyLayoutSubscribers(root);
4162
+ tNotify = performance.now() - t;
4163
+ } catch (_) {
4164
+ _usingCtx7.e = _;
4165
+ } finally {
4166
+ _usingCtx7.d();
4167
+ }
4168
+ const acc = globalThis.__silvery_bench_phases;
4169
+ if (acc) {
4170
+ acc.measure += tMeasure;
4171
+ acc.layout += tLayout;
4172
+ acc.scroll += tScroll;
4173
+ acc.scrollRect += tScrollRect;
4174
+ acc.notify += tNotify;
4175
+ acc.layoutTotal += tMeasure + tLayout + tScroll + tScrollRect + tNotify;
4176
+ }
4177
+ return {
4178
+ tMeasure,
4179
+ tLayout,
4180
+ tScroll,
4181
+ tScrollRect,
4182
+ tNotify
4183
+ };
4184
+ } catch (_) {
4185
+ _usingCtx$2.e = _;
4186
+ } finally {
4187
+ _usingCtx$2.d();
4188
+ }
4189
+ }
4190
+ function doRender(opts) {
4191
+ clearBgConflictWarnings();
4192
+ const prevBuffer = opts?.fresh ? null : opts?.prevBuffer !== void 0 ? opts.prevBuffer : _prevBuffer;
4193
+ let tContent;
4194
+ let buffer;
4195
+ {
4196
+ const t = performance.now();
4197
+ buffer = renderPhase(root, prevBuffer, ctx);
4198
+ tContent = performance.now() - t;
4199
+ log$1.debug?.(`content: ${tContent.toFixed(2)}ms`);
4200
+ }
4201
+ if (!opts?.fresh) _prevBuffer = buffer;
4202
+ clearDirtyTracking();
4203
+ const acc = globalThis.__silvery_bench_phases;
4204
+ if (acc) {
4205
+ acc.content += tContent;
4206
+ acc.renderCalls += 1;
4207
+ }
4208
+ return {
4209
+ frame: createTextFrame(buffer),
4210
+ buffer,
4211
+ prevBuffer,
4212
+ tContent
4213
+ };
4214
+ }
4215
+ function agCreateNode(type, props) {
4216
+ return {
4217
+ type,
4218
+ props,
4219
+ children: [],
4220
+ parent: null,
4221
+ layoutNode: getLayoutEngine().createNode(),
4222
+ boxRect: null,
4223
+ scrollRect: null,
4224
+ screenRect: null,
4225
+ prevLayout: null,
4226
+ prevScrollRect: null,
4227
+ prevScreenRect: null,
4228
+ layoutChangedThisFrame: -1,
4229
+ layoutDirty: true,
4230
+ dirtyBits: 31,
4231
+ dirtyEpoch: getRenderEpoch(),
4232
+ layoutSubscribers: /* @__PURE__ */ new Set()
4233
+ };
4234
+ }
4235
+ function agInsertChild(parent, child, index) {
4236
+ if (child.parent) agRemoveChild(child.parent, child);
4237
+ parent.children.splice(index, 0, child);
4238
+ child.parent = parent;
4239
+ if (parent.layoutNode && child.layoutNode) {
4240
+ const layoutIndex = parent.children.slice(0, index).filter((c) => c.layoutNode !== null).length;
4241
+ parent.layoutNode.insertChild(child.layoutNode, layoutIndex);
4242
+ }
4243
+ }
4244
+ function agRemoveChild(parent, child) {
4245
+ const index = parent.children.indexOf(child);
4246
+ if (index === -1) return;
4247
+ parent.children.splice(index, 1);
4248
+ if (parent.layoutNode && child.layoutNode) {
4249
+ parent.layoutNode.removeChild(child.layoutNode);
4250
+ child.layoutNode.free();
4251
+ }
4252
+ child.parent = null;
4253
+ }
4254
+ return {
4255
+ root,
4256
+ layout(dims, options) {
4257
+ if (measurer) runWithMeasurer(measurer, () => doLayout(dims.cols, dims.rows, options));
4258
+ else doLayout(dims.cols, dims.rows, options);
4259
+ },
4260
+ render(options) {
4261
+ const result = measurer ? runWithMeasurer(measurer, () => doRender(options)) : doRender(options);
4262
+ return {
4263
+ frame: result.frame,
4264
+ buffer: result.buffer,
4265
+ prevBuffer: result.prevBuffer
4266
+ };
4267
+ },
4268
+ resetBuffer() {
4269
+ _prevBuffer = null;
4270
+ },
4271
+ createNode: agCreateNode,
4272
+ insertChild: agInsertChild,
4273
+ removeChild: agRemoveChild,
4274
+ updateProps(node, props, oldProps) {
4275
+ node.props = props;
4276
+ if (node.layoutNode) node.layoutNode.markDirty();
4277
+ },
4278
+ setText(node, text) {
4279
+ node.textContent = text;
4280
+ const epoch = getRenderEpoch();
4281
+ const bits = 3;
4282
+ node.dirtyBits = node.dirtyEpoch !== epoch ? bits : node.dirtyBits | bits;
4283
+ node.dirtyEpoch = epoch;
4284
+ if (node.layoutNode) node.layoutNode.markDirty();
4285
+ },
4286
+ toString() {
4287
+ return `[Ag root=${root.type} children=${root.children.length}]`;
4288
+ }
4289
+ };
4290
+ }
4291
+ //#endregion
4292
+ //#region packages/ag-term/src/pipeline/index.ts
4293
+ /**
4294
+ * Silvery Render Pipeline
4295
+ *
4296
+ * The 5-phase rendering architecture:
4297
+ *
4298
+ * Phase 0: RECONCILIATION (React)
4299
+ * React reconciliation builds the SilveryNode tree.
4300
+ * Components register layout constraints via props.
4301
+ *
4302
+ * Phase 1: MEASURE (for fit-content nodes)
4303
+ * Traverse nodes with width/height="fit-content"
4304
+ * Measure intrinsic content size
4305
+ * Set Yoga constraints based on measurement
4306
+ *
4307
+ * Phase 2: LAYOUT
4308
+ * Run yoga.calculateLayout()
4309
+ * Propagate computed dimensions to all nodes
4310
+ * Notify useBoxRect() subscribers
4311
+ *
4312
+ * Phase 3: CONTENT RENDER
4313
+ * Render each node to the TerminalBuffer
4314
+ * Handle text truncation, styling, borders
4315
+ *
4316
+ * Phase 4: DIFF & OUTPUT
4317
+ * Compare current buffer with previous
4318
+ * Emit minimal ANSI sequences for changes
4319
+ */
4320
+ const log = createLogger("silvery:render");
4321
+ createLogger("@silvery/ag-react");
4322
+ /**
4323
+ * Execute the full render pipeline.
4324
+ *
4325
+ * Pass null for prevBuffer on the first render; pass the returned buffer on
4326
+ * subsequent renders to enable incremental content rendering (<1ms vs 20-30ms).
4327
+ * SILVERY_DEV=1 warns at runtime if prevBuffer is null after the first frame.
4328
+ */
4329
+ function executeRender(root, width, height, prevBuffer, options = "fullscreen", config) {
4330
+ if (config?.measurer) return runWithMeasurer(config.measurer, () => {
4331
+ return executeRenderCore(root, width, height, prevBuffer, options, config);
4332
+ });
4333
+ return executeRenderCore(root, width, height, prevBuffer, options, config);
4334
+ }
4335
+ /** Internal: runs the full pipeline, delegating layout + render to createAg. */
4336
+ function executeRenderCore(root, width, height, prevBuffer, options = "fullscreen", config) {
4337
+ const { mode = "fullscreen", skipLayoutNotifications = false, skipScrollStateUpdates = false, scrollbackOffset = 0, termRows, cursorPos } = typeof options === "string" ? { mode: options } : options;
4338
+ if (process?.env?.SILVERY_DEV && prevBuffer === null && root.prevLayout !== null && !skipLayoutNotifications) log.warn?.("executeRender called with prevBuffer=null on frame 2+ — incremental content rendering is disabled (full render every frame). Track the returned buffer and pass it as prevBuffer on subsequent renders.");
4339
+ const start = performance.now();
4340
+ const ag = createAg(root, { measurer: config?.measurer });
4341
+ ag.layout({
4342
+ cols: width,
4343
+ rows: height
4344
+ }, {
4345
+ skipLayoutNotifications,
4346
+ skipScrollStateUpdates
4347
+ });
4348
+ const { buffer } = ag.render({ prevBuffer });
4349
+ const tLayout = performance.now() - start;
4350
+ let output;
4351
+ let tOutput;
4352
+ {
4353
+ const t4 = performance.now();
4354
+ const outputFn = config?.outputPhaseFn ?? outputPhase;
4355
+ try {
4356
+ output = outputFn(prevBuffer, buffer, mode, scrollbackOffset, termRows, cursorPos);
4357
+ } catch (e) {
4358
+ if (e instanceof Error) e.__silvery_buffer = buffer;
4359
+ throw e;
4360
+ }
4361
+ tOutput = performance.now() - t4;
4362
+ log.debug?.(`output: ${tOutput.toFixed(2)}ms (${output.length} bytes)`);
4363
+ }
4364
+ const total = performance.now() - start;
4365
+ globalThis.__silvery_last_pipeline = {
4366
+ layout: tLayout,
4367
+ output: tOutput,
4368
+ total,
4369
+ incremental: prevBuffer !== null
4370
+ };
4371
+ globalThis.__silvery_render_count = (globalThis.__silvery_render_count ?? 0) + 1;
4372
+ const acc = globalThis.__silvery_bench_phases;
4373
+ if (acc) {
4374
+ acc.output += tOutput;
4375
+ acc.total += total;
4376
+ acc.pipelineCalls += 1;
4377
+ }
4378
+ log.debug?.(`pipeline: layout+render=${tLayout.toFixed(1)}ms output=${tOutput.toFixed(1)}ms total=${total.toFixed(1)}ms`);
4379
+ return {
4380
+ output,
4381
+ buffer
4382
+ };
4383
+ }
4384
+ //#endregion
4385
+ export { signal as i, createAg as n, _usingCtx as r, executeRender as t };
4386
+
4387
+ //# sourceMappingURL=pipeline-DDOPrjuY.mjs.map