silvery 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +7 -13
  2. package/dist/{UPNG-AVSMjiFE.mjs → UPNG-DvKjM6wE.mjs} +1 -1
  3. package/dist/{UPNG-AVSMjiFE.mjs.map → UPNG-DvKjM6wE.mjs.map} +1 -1
  4. package/dist/{__vite-browser-external-2447137e-D3GdsvS_.mjs → __vite-browser-external-2447137e-DPKHHqQK.mjs} +1 -1
  5. package/dist/{__vite-browser-external-2447137e-D3GdsvS_.mjs.map → __vite-browser-external-2447137e-DPKHHqQK.mjs.map} +1 -1
  6. package/dist/{animation-C_PTO0uH.mjs → animation-DhINOJk8.mjs} +1 -1
  7. package/dist/{animation-C_PTO0uH.mjs.map → animation-DhINOJk8.mjs.map} +1 -1
  8. package/dist/{ansi-CXLE_pt1.mjs → ansi-C6Qs1Wn2.mjs} +1 -1
  9. package/dist/{ansi-CXLE_pt1.mjs.map → ansi-C6Qs1Wn2.mjs.map} +1 -1
  10. package/dist/{ansi-zmNzgkPB.d.mts → ansi-CsjnZtAw.d.mts} +1 -1
  11. package/dist/{ansi-zmNzgkPB.d.mts.map → ansi-CsjnZtAw.d.mts.map} +1 -1
  12. package/dist/apng-CvSlLBtc.mjs +3 -0
  13. package/dist/{apng-ENBAJk-H.mjs → apng-DFFVOItr.mjs} +3 -3
  14. package/dist/{apng-ENBAJk-H.mjs.map → apng-DFFVOItr.mjs.map} +1 -1
  15. package/dist/{backend-CkIkIHR-.mjs → backend-DU0Y938U.mjs} +1 -1
  16. package/dist/{backend-CkIkIHR-.mjs.map → backend-DU0Y938U.mjs.map} +1 -1
  17. package/dist/{backends-CkvbG3js.mjs → backends-BihMKFY_.mjs} +3 -3
  18. package/dist/{backends-CkvbG3js.mjs.map → backends-BihMKFY_.mjs.map} +1 -1
  19. package/dist/backends-Dk_5G_gC.mjs +3 -0
  20. package/dist/cli-GwJ0S2In.mjs +4 -0
  21. package/dist/{context-QreF3UHr.mjs → context-BjWgrikx.mjs} +1 -1
  22. package/dist/{context-QreF3UHr.mjs.map → context-BjWgrikx.mjs.map} +1 -1
  23. package/dist/{derive-D7bFJdfU.d.mts → derive-O_Kb1Bk_.d.mts} +3 -3
  24. package/dist/derive-O_Kb1Bk_.d.mts.map +1 -0
  25. package/dist/{devtools-owvUPfBi.mjs → devtools-CeO9X_uv.mjs} +4 -4
  26. package/dist/{devtools-owvUPfBi.mjs.map → devtools-CeO9X_uv.mjs.map} +1 -1
  27. package/dist/devtools-nX4tj6OH.mjs +2 -0
  28. package/dist/{eta-DLiVPaSD.mjs → eta-BnQSZcWf.mjs} +1 -1
  29. package/dist/{eta-DLiVPaSD.mjs.map → eta-BnQSZcWf.mjs.map} +1 -1
  30. package/dist/{flexily-zero-adapter-DmG4Ge8t.mjs → flexily-zero-adapter-BOM0cl8R.mjs} +61 -9
  31. package/dist/flexily-zero-adapter-BOM0cl8R.mjs.map +1 -0
  32. package/dist/{flexily-zero-adapter-GHwEW11s.mjs → flexily-zero-adapter-V8R3HQtK.mjs} +1 -1
  33. package/dist/{gif-Bp6fIyN3.mjs → gif-B9Uq4qZA.mjs} +3 -3
  34. package/dist/{gif-Bp6fIyN3.mjs.map → gif-B9Uq4qZA.mjs.map} +1 -1
  35. package/dist/gif-BdrLRBmM.mjs +3 -0
  36. package/dist/{gifenc-GiVCZ9-3.mjs → gifenc-DfhOb4xr.mjs} +1 -1
  37. package/dist/{gifenc-GiVCZ9-3.mjs.map → gifenc-DfhOb4xr.mjs.map} +1 -1
  38. package/dist/{image-Dx7gYjkq.mjs → image-B0zMbVUr.mjs} +136 -5
  39. package/dist/image-B0zMbVUr.mjs.map +1 -0
  40. package/dist/index-Bh3U1K09.d.mts +10823 -0
  41. package/dist/index-Bh3U1K09.d.mts.map +1 -0
  42. package/dist/{index-p-wBs_wH.d.mts → index-C4vrhbud.d.mts} +1 -1
  43. package/dist/{index-p-wBs_wH.d.mts.map → index-C4vrhbud.d.mts.map} +1 -1
  44. package/dist/{index-DCVL3jHo.d.mts → index-dehZ18K-.d.mts} +144 -99
  45. package/dist/index-dehZ18K-.d.mts.map +1 -0
  46. package/dist/index.d.mts +7 -7219
  47. package/dist/index.d.mts.map +1 -1
  48. package/dist/index.mjs +13 -9343
  49. package/dist/index.mjs.map +1 -1
  50. package/dist/{key-mapping-BsUHe_nk.mjs → key-mapping-7k2ufK2b.mjs} +1 -1
  51. package/dist/{key-mapping-DsyfLEdC.mjs → key-mapping-WLUmxjx1.mjs} +1 -1
  52. package/dist/{key-mapping-DsyfLEdC.mjs.map → key-mapping-WLUmxjx1.mjs.map} +1 -1
  53. package/dist/{layout-engine-D_lSR4i9.mjs → layout-engine--drvrWjD.mjs} +1 -1
  54. package/dist/{layout-engine-B3dsnVLU.mjs → layout-engine-Dr3cY5U4.mjs} +3 -3
  55. package/dist/{layout-engine-B3dsnVLU.mjs.map → layout-engine-Dr3cY5U4.mjs.map} +1 -1
  56. package/dist/{multi-progress-CQVB9lES.mjs → multi-progress-CcdqJFlf.mjs} +3 -3
  57. package/dist/{multi-progress-CQVB9lES.mjs.map → multi-progress-CcdqJFlf.mjs.map} +1 -1
  58. package/dist/{multi-progress-C0-rkn86.d.mts → multi-progress-DQ-uUzLf.d.mts} +2 -2
  59. package/dist/{multi-progress-C0-rkn86.d.mts.map → multi-progress-DQ-uUzLf.d.mts.map} +1 -1
  60. package/dist/{node-Dedx-6xF.mjs → node-CP5WChgr.mjs} +1 -1
  61. package/dist/{node-Dedx-6xF.mjs.map → node-CP5WChgr.mjs.map} +1 -1
  62. package/dist/{progress-bar-COPSBlT9.mjs → progress-bar-IrUjkLfU.mjs} +4 -4
  63. package/dist/{progress-bar-COPSBlT9.mjs.map → progress-bar-IrUjkLfU.mjs.map} +1 -1
  64. package/dist/{reconciler-B-NaZvbO.mjs → reconciler-B8uxQxaU.mjs} +57 -81
  65. package/dist/reconciler-B8uxQxaU.mjs.map +1 -0
  66. package/dist/{render-string-CZKpuKXo.mjs → render-string-BwLG7rIX.mjs} +1 -1
  67. package/dist/{pipeline-BmfaZb1O.mjs → render-string-DVfgc8xr.mjs} +836 -508
  68. package/dist/render-string-DVfgc8xr.mjs.map +1 -0
  69. package/dist/{resvg-js-V6oMi8CY.mjs → resvg-js-Cwipz-_J.mjs} +1 -1
  70. package/dist/{resvg-js-V6oMi8CY.mjs.map → resvg-js-Cwipz-_J.mjs.map} +1 -1
  71. package/dist/runtime.d.mts +2 -2
  72. package/dist/runtime.mjs +3 -3
  73. package/dist/{spinner-Cgej6Vnb.d.mts → spinner-BRkaJI0N.d.mts} +2 -2
  74. package/dist/{spinner-Cgej6Vnb.d.mts.map → spinner-BRkaJI0N.d.mts.map} +1 -1
  75. package/dist/{spinner-DSByknyx.mjs → spinner-BmldKx0M.mjs} +3 -3
  76. package/dist/{spinner-DSByknyx.mjs.map → spinner-BmldKx0M.mjs.map} +1 -1
  77. package/dist/{src-C9f3hiVG.mjs → src-C0sOQW-t.mjs} +402 -156
  78. package/dist/src-C0sOQW-t.mjs.map +1 -0
  79. package/dist/src-CJPXf3fC.mjs +18348 -0
  80. package/dist/src-CJPXf3fC.mjs.map +1 -0
  81. package/dist/{src-fJVbhdn-.mjs → src-D8kLrQBT.mjs} +1 -1
  82. package/dist/{src-fJVbhdn-.mjs.map → src-D8kLrQBT.mjs.map} +1 -1
  83. package/dist/{src-9B5k0JmY.mjs → src-D_BS-as7.mjs} +1130 -100
  84. package/dist/src-D_BS-as7.mjs.map +1 -0
  85. package/dist/theme.d.mts +45 -30
  86. package/dist/theme.d.mts.map +1 -1
  87. package/dist/theme.mjs +3 -3
  88. package/dist/{types-CDgkE-Rw.d.mts → types-B4A8Ebba.d.mts} +1 -1
  89. package/dist/{types-CDgkE-Rw.d.mts.map → types-B4A8Ebba.d.mts.map} +1 -1
  90. package/dist/types-e4dpfbSa.mjs +468 -0
  91. package/dist/types-e4dpfbSa.mjs.map +1 -0
  92. package/dist/ui/animation.d.mts +1 -1
  93. package/dist/ui/animation.mjs +1 -1
  94. package/dist/ui/ansi.d.mts +1 -1
  95. package/dist/ui/ansi.mjs +1 -1
  96. package/dist/ui/cli.d.mts +3 -3
  97. package/dist/ui/cli.mjs +5 -5
  98. package/dist/ui/display.d.mts +2 -2
  99. package/dist/ui/display.mjs +1 -1
  100. package/dist/ui/display.mjs.map +1 -1
  101. package/dist/ui/image.d.mts +1 -1
  102. package/dist/ui/image.mjs +1 -1
  103. package/dist/ui/input.d.mts +3 -3
  104. package/dist/ui/input.mjs +2 -2
  105. package/dist/ui/input.mjs.map +1 -1
  106. package/dist/ui/progress.d.mts +3 -3
  107. package/dist/ui/progress.mjs +4 -4
  108. package/dist/ui/progress.mjs.map +1 -1
  109. package/dist/ui/react.d.mts +3 -3
  110. package/dist/ui/react.mjs +4 -4
  111. package/dist/ui/react.mjs.map +1 -1
  112. package/dist/ui/utils.mjs +1 -1
  113. package/dist/ui/wrappers.d.mts +2 -2
  114. package/dist/ui/wrappers.mjs +1 -1
  115. package/dist/ui.d.mts +5 -5
  116. package/dist/ui.mjs +6 -6
  117. package/dist/{useLatest-BMIYXd6e.d.mts → useLatest-6xqnGIU6.d.mts} +1 -1
  118. package/dist/{useLatest-BMIYXd6e.d.mts.map → useLatest-6xqnGIU6.d.mts.map} +1 -1
  119. package/dist/{with-text-input-CmHf_9d6.d.mts → with-text-input-lUh9gYAG.d.mts} +3 -3
  120. package/dist/{with-text-input-CmHf_9d6.d.mts.map → with-text-input-lUh9gYAG.d.mts.map} +1 -1
  121. package/dist/{wrapper-Dqh0zi2W.mjs → wrapper-CE6GQ27z.mjs} +1 -1
  122. package/dist/{wrapper-Dqh0zi2W.mjs.map → wrapper-CE6GQ27z.mjs.map} +1 -1
  123. package/dist/{wrappers-hhL8EQ_n.mjs → wrappers-JrEYTuKA.mjs} +4 -4
  124. package/dist/wrappers-JrEYTuKA.mjs.map +1 -0
  125. package/dist/yoga-adapter-B8LZpQcE.mjs +2 -0
  126. package/dist/{yoga-adapter-BJ9SOhTY.mjs → yoga-adapter-Bc8XT9cN.mjs} +11 -2
  127. package/dist/yoga-adapter-Bc8XT9cN.mjs.map +1 -0
  128. package/package.json +20 -17
  129. package/dist/apng-DCWY913R.mjs +0 -3
  130. package/dist/backends-CyJqNLeK.mjs +0 -3
  131. package/dist/cli-B-k7Bm56.mjs +0 -4
  132. package/dist/derive-D7bFJdfU.d.mts.map +0 -1
  133. package/dist/devtools-DS9NseGT.mjs +0 -2
  134. package/dist/flexily-zero-adapter-DmG4Ge8t.mjs.map +0 -1
  135. package/dist/gif-BaJNREpP.mjs +0 -3
  136. package/dist/image-Dx7gYjkq.mjs.map +0 -1
  137. package/dist/index-CBcSpGSM.d.mts +0 -3416
  138. package/dist/index-CBcSpGSM.d.mts.map +0 -1
  139. package/dist/index-DCVL3jHo.d.mts.map +0 -1
  140. package/dist/pipeline-BmfaZb1O.mjs.map +0 -1
  141. package/dist/reconciler-B-NaZvbO.mjs.map +0 -1
  142. package/dist/render-string-Bvh1XzBv.mjs +0 -201
  143. package/dist/render-string-Bvh1XzBv.mjs.map +0 -1
  144. package/dist/runtime-PH2xY1DM.mjs +0 -8723
  145. package/dist/runtime-PH2xY1DM.mjs.map +0 -1
  146. package/dist/src-9B5k0JmY.mjs.map +0 -1
  147. package/dist/src-C9f3hiVG.mjs.map +0 -1
  148. package/dist/types-Bhj5QkIQ.mjs +0 -13
  149. package/dist/types-Bhj5QkIQ.mjs.map +0 -1
  150. package/dist/useLayout-BG2cGl15.mjs +0 -139
  151. package/dist/useLayout-BG2cGl15.mjs.map +0 -1
  152. package/dist/wrappers-hhL8EQ_n.mjs.map +0 -1
  153. package/dist/yoga-adapter-BJ9SOhTY.mjs.map +0 -1
  154. package/dist/yoga-adapter-Daq6-dw1.mjs +0 -2
@@ -1,9 +1,12 @@
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-B-NaZvbO.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";
1
+ import { c as signal, i as syncRectSignals, o as computed, t as rectEqual } from "./types-e4dpfbSa.mjs";
2
+ import { c as TermContext, o as StderrContext, s as StdoutContext } from "./context-BjWgrikx.mjs";
3
+ import { At as bufferToText, Dt as TerminalBuffer, Et as DEFAULT_BG, Ft as createTextFrame, H as ensureEmojiPresentation, I as canBreakAnywhere, It as init_buffer, K as graphemeWidth, Lt as isDefaultBg, Ot as ansi256ToRgb, Pt as createMutableCell, U as getActiveLineHeight, V as displayWidthAnsi, _t as wrapText, a as hostConfig, at as parseAnsiText, b as createTerm, c as clearDirtyTracking, d as collectPlainText, et as isWordBoundary, f as advanceRenderEpoch, ft as splitGraphemes, g as isDirty, h as isCurrentEpoch, kt as bufferToStyledText, l as hasScrollDirty, lt as sliceByWidth, m as isAnyDirty, ot as runWithMeasurer, p as getRenderEpoch, pt as splitGraphemesAnsiAware, q as hasAnsi, r as getContainerRoot, t as createContainer, u as measureStats, ut as sliceByWidthFromEnd } from "./reconciler-B8uxQxaU.mjs";
4
+ import { T as resolveThemeColor, k as blend, t as init_src, x as monoAttrsForColorString } from "./src-D_BS-as7.mjs";
5
+ import { A as pushContextTheme, D as getActiveTheme, E as getActiveColorLevel, O as init_state, k as popContextTheme, t as init_src$1, x as init_resolve } from "./src-C0sOQW-t.mjs";
6
+ import { i as isLayoutEngineInitialized, r as getLayoutEngine } from "./layout-engine-Dr3cY5U4.mjs";
7
+ import React, { act } from "react";
6
8
  import { createLogger } from "loggily";
9
+ import Reconciler from "react-reconciler";
7
10
  //#region packages/ag-term/src/pipeline/prepared-text.ts
8
11
  const MAX_FORMAT_ENTRIES = 4;
9
12
  /** Content-affecting flags that invalidate plain text. */
@@ -409,26 +412,29 @@ function measurePhase(root, ctx) {
409
412
  traverseTree$1(root, (node) => {
410
413
  if (!node.layoutNode) return;
411
414
  const props = node.props;
412
- const isFitContent = props.width === "fit-content" || props.height === "fit-content";
413
415
  const isSnugContent = props.width === "snug-content";
414
- if (isFitContent || isSnugContent) {
416
+ const isHeightFitContent = props.height === "fit-content";
417
+ if (isSnugContent || isHeightFitContent) {
415
418
  let availableWidth;
416
- const widthIsFixed = typeof props.width === "number";
417
- if (props.height === "fit-content" && widthIsFixed) {
419
+ let definiteUpperWidth = typeof props.width === "number" && isHeightFitContent ? props.width : typeof props.maxWidth === "number" ? props.maxWidth : void 0;
420
+ if (definiteUpperWidth === void 0) definiteUpperWidth = findAncestorDefiniteWidth(node);
421
+ if (definiteUpperWidth !== void 0) {
418
422
  const padding = getPadding(props);
419
- availableWidth = props.width - padding.left - padding.right;
423
+ availableWidth = definiteUpperWidth - padding.left - padding.right;
420
424
  if (props.borderStyle) {
421
425
  const border = getBorderSize(props);
422
426
  availableWidth -= border.left + border.right;
423
427
  }
424
428
  if (availableWidth < 1) availableWidth = 1;
425
429
  }
426
- const intrinsicSize = measureIntrinsicSize(node, ctx, availableWidth);
427
430
  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);
431
+ const shrunkWidth = computeSnugContentWidth(node, measureIntrinsicSize(node, ctx, availableWidth).width, ctx);
432
+ node.layoutNode.setMaxWidth(shrunkWidth);
433
+ }
434
+ if (isHeightFitContent) {
435
+ const intrinsicSize = measureIntrinsicSize(node, ctx, availableWidth);
436
+ node.layoutNode.setHeight(intrinsicSize.height);
437
+ }
432
438
  }
433
439
  });
434
440
  }
@@ -533,6 +539,39 @@ function computeSnugContentWidth(node, fitContentWidth, ctx) {
533
539
  return shrinkwrapWidth(analysis, contentWidth) + overhead;
534
540
  }
535
541
  /**
542
+ * Walk up the tree from a node to find the nearest ancestor with a definite
543
+ * width (a fixed number, not "fit-content" or "snug-content"). Returns the
544
+ * ancestor's inner content width (after subtracting its own padding and border).
545
+ * Returns undefined if no definite-width ancestor is found.
546
+ */
547
+ function findAncestorDefiniteWidth(node) {
548
+ let current = node.parent;
549
+ while (current) {
550
+ const p = current.props;
551
+ if (typeof p.width === "number") {
552
+ let inner = p.width;
553
+ const padding = getPadding(p);
554
+ inner -= padding.left + padding.right;
555
+ if (p.borderStyle) {
556
+ const border = getBorderSize(p);
557
+ inner -= border.left + border.right;
558
+ }
559
+ return inner > 0 ? inner : 1;
560
+ }
561
+ if (typeof p.maxWidth === "number") {
562
+ let inner = p.maxWidth;
563
+ const padding = getPadding(p);
564
+ inner -= padding.left + padding.right;
565
+ if (p.borderStyle) {
566
+ const border = getBorderSize(p);
567
+ inner -= border.left + border.right;
568
+ }
569
+ return inner > 0 ? inner : 1;
570
+ }
571
+ current = current.parent;
572
+ }
573
+ }
574
+ /**
536
575
  * Traverse tree in depth-first order.
537
576
  */
538
577
  function traverseTree$1(node, callback) {
@@ -554,7 +593,7 @@ function getTextWidth$1(text, ctx) {
554
593
  *
555
594
  * Run Yoga layout calculation and propagate dimensions to all nodes.
556
595
  */
557
- const log$3 = createLogger("silvery:layout");
596
+ const log$2 = createLogger("silvery:layout");
558
597
  /**
559
598
  * Run Yoga layout calculation and propagate dimensions to all nodes.
560
599
  *
@@ -565,15 +604,17 @@ const log$3 = createLogger("silvery:layout");
565
604
  function layoutPhase(root, width, height) {
566
605
  const prevLayout = root.boxRect;
567
606
  const dimensionsChanged = prevLayout && (prevLayout.width !== width || prevLayout.height !== height);
568
- if (!dimensionsChanged && !hasLayoutDirty()) return;
569
- clearLayoutDirtyTracking();
607
+ if (!dimensionsChanged && !root.layoutNode?.isDirty()) {
608
+ if (isDirty(root.dirtyBits, root.dirtyEpoch, 16)) propagateCascadeInputs(root);
609
+ return;
610
+ }
570
611
  if (root.layoutNode) {
571
612
  const nodeCount = countNodes(root);
572
613
  measureStats.reset();
573
614
  const t0 = Date.now();
574
615
  root.layoutNode.calculateLayout(width, height);
575
616
  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}`);
617
+ log$2.debug?.(`calculateLayout: ${elapsed}ms (${nodeCount} nodes) measure: calls=${measureStats.calls} hits=${measureStats.cacheHits} collects=${measureStats.textCollects} displayWidth=${measureStats.displayWidthCalls}`);
577
618
  }
578
619
  propagateLayout(root, 0, 0, !dimensionsChanged);
579
620
  }
@@ -599,8 +640,8 @@ function countNodes(node) {
599
640
  * subtrees whose inputs didn't change
600
641
  * - If the parent's rect matches, all descendants' rects also match
601
642
  * (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
643
+ * - prevLayout and layoutChangedThisFrame (stale epoch, won't match
644
+ * current) all retain correct values
604
645
  *
605
646
  * @param node The node to process
606
647
  * @param parentX Absolute X position of parent
@@ -616,7 +657,6 @@ function propagateLayout(node, parentX, parentY, incrementalSkip) {
616
657
  width: 0,
617
658
  height: 0
618
659
  };
619
- node.layoutDirty = false;
620
660
  for (const child of node.children) propagateLayout(child, parentX, parentY, incrementalSkip);
621
661
  return;
622
662
  }
@@ -626,12 +666,11 @@ function propagateLayout(node, parentX, parentY, incrementalSkip) {
626
666
  width: node.layoutNode.getComputedWidth(),
627
667
  height: node.layoutNode.getComputedHeight()
628
668
  };
629
- if (incrementalSkip && node.boxRect && !node.layoutDirty && !isDirty(node.dirtyBits, node.dirtyEpoch, 16) && !isDirty(node.dirtyBits, node.dirtyEpoch, 8)) {
669
+ if (incrementalSkip && node.boxRect && !isDirty(node.dirtyBits, node.dirtyEpoch, 16) && !isDirty(node.dirtyBits, node.dirtyEpoch, 8)) {
630
670
  if (rect.x === node.boxRect.x && rect.y === node.boxRect.y && rect.width === node.boxRect.width && rect.height === node.boxRect.height) return;
631
671
  }
632
672
  node.prevLayout = node.boxRect;
633
673
  node.boxRect = rect;
634
- node.layoutDirty = false;
635
674
  node.layoutChangedThisFrame = !!(node.prevLayout && !rectEqual(node.prevLayout, node.boxRect)) ? getRenderEpoch() : -1;
636
675
  if (process?.env?.SILVERY_STRICT && isCurrentEpoch(node.layoutChangedThisFrame)) {
637
676
  if (rectEqual(node.prevLayout, node.boxRect)) {
@@ -665,6 +704,34 @@ function propagateLayout(node, parentX, parentY, incrementalSkip) {
665
704
  } else if (node.dirtyEpoch === getRenderEpoch()) node.dirtyBits &= -97;
666
705
  }
667
706
  /**
707
+ * Lightweight cascade input caching when the layout phase skips.
708
+ *
709
+ * When no layout nodes are dirty and dimensions haven't changed,
710
+ * `layoutPhase` returns early and `propagateLayout` never runs.
711
+ * But structural changes (absolute child mount/unmount, descendant overflow)
712
+ * still need cascade input bits (ABS_CHILD_BIT, DESC_OVERFLOW_BIT) to be
713
+ * computed for the render phase.
714
+ *
715
+ * This traversal follows only subtreeDirty paths (O(changed) not O(N))
716
+ * and computes the same cascade inputs as propagateLayout's caching block.
717
+ * No layout changes, no prevLayout updates, no layoutChangedThisFrame.
718
+ */
719
+ function propagateCascadeInputs(node) {
720
+ if (!isDirty(node.dirtyBits, node.dirtyEpoch, 16)) return;
721
+ if (!node.children || node.children.length === 0) return;
722
+ for (const child of node.children) if (isDirty(child.dirtyBits, child.dirtyEpoch, 16)) propagateCascadeInputs(child);
723
+ const epoch = getRenderEpoch();
724
+ const absChild = _hasAbsoluteChildMutated(node.children);
725
+ const descOverflow = node.boxRect ? _hasDescendantOverflowChanged(node, node.boxRect) : false;
726
+ let bits = node.dirtyBits;
727
+ if (absChild) bits |= 32;
728
+ else bits &= -33;
729
+ if (descOverflow) bits |= 64;
730
+ else bits &= -65;
731
+ node.dirtyBits = bits;
732
+ node.dirtyEpoch = epoch;
733
+ }
734
+ /**
668
735
  * Check if any direct child is position="absolute" and had structural changes.
669
736
  */
670
737
  function _hasAbsoluteChildMutated(children) {
@@ -702,7 +769,7 @@ function _checkDescendantOverflow(children, nodeLeft, nodeTop, nodeRight, nodeBo
702
769
  /**
703
770
  * Notify all layout subscribers of dimension changes.
704
771
  *
705
- * Called from executeRender AFTER scrollrectPhase completes,
772
+ * Called by the pipeline AFTER scrollrectPhase completes,
706
773
  * so useScrollRect can read correct screen positions.
707
774
  *
708
775
  * Notifies when EITHER boxRect, scrollRect, or screenRect changed.
@@ -712,13 +779,64 @@ function _checkDescendantOverflow(children, nodeLeft, nodeTop, nodeRight, nodeBo
712
779
  * offset changes even when scrollRect stays the same.
713
780
  */
714
781
  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();
782
+ rectEqual(node.prevLayout, node.boxRect);
783
+ rectEqual(node.prevScrollRect, node.scrollRect);
784
+ rectEqual(node.prevScreenRect, node.screenRect);
785
+ syncRectSignals(node);
719
786
  for (const child of node.children) notifyLayoutSubscribers(child);
720
787
  }
721
788
  /**
789
+ * Verify that no child's boxRect.width exceeds its parent's inner content width.
790
+ *
791
+ * This catches fit-content/snug-content bugs at the source — any measure-phase
792
+ * or correction-pass error fires immediately.
793
+ *
794
+ * - SILVERY_STRICT=1: console.warn on violation
795
+ * - SILVERY_STRICT=2: throw on violation
796
+ *
797
+ * Exceptions:
798
+ * - Parent has overflow: "scroll" or "hidden" (overflow is allowed)
799
+ * - Child has position: "absolute" (absolute nodes can overflow)
800
+ */
801
+ function strictLayoutOverflowCheck(root) {
802
+ const strict = process?.env?.SILVERY_STRICT;
803
+ if (!strict) return;
804
+ const shouldThrow = strict === "2";
805
+ function walk(node) {
806
+ for (const child of node.children) {
807
+ if (child.boxRect && node.boxRect) {
808
+ const childProps = child.props;
809
+ if (childProps.position === "absolute") {
810
+ walk(child);
811
+ continue;
812
+ }
813
+ const parentProps = node.props;
814
+ if (parentProps.overflow === "scroll" || parentProps.overflow === "hidden") {
815
+ walk(child);
816
+ continue;
817
+ }
818
+ const border = parentProps.borderStyle ? getBorderSize(parentProps) : {
819
+ top: 0,
820
+ bottom: 0,
821
+ left: 0,
822
+ right: 0
823
+ };
824
+ const padding = getPadding(parentProps);
825
+ const parentInnerWidth = node.boxRect.width - padding.left - padding.right - border.left - border.right;
826
+ if (child.boxRect.width > parentInnerWidth) {
827
+ const childId = childProps.id ?? child.type;
828
+ const parentId = parentProps.id ?? node.type;
829
+ const msg = `[SILVERY_STRICT] Layout overflow: child "${childId}" width ${child.boxRect.width} exceeds parent "${parentId}" inner width ${parentInnerWidth} (parent box: ${node.boxRect.width}, border: ${border.left}+${border.right}, padding: ${padding.left}+${padding.right})`;
830
+ if (shouldThrow) throw new Error(msg);
831
+ else console.warn(msg);
832
+ }
833
+ }
834
+ walk(child);
835
+ }
836
+ }
837
+ walk(root);
838
+ }
839
+ /**
722
840
  * Calculate scroll state for all overflow='scroll' containers.
723
841
  *
724
842
  * This phase runs after layout to determine which children are visible
@@ -1095,6 +1213,7 @@ function detectPipelineFeatures(root) {
1095
1213
  init_buffer();
1096
1214
  init_state();
1097
1215
  init_resolve();
1216
+ init_src();
1098
1217
  const namedColors = {
1099
1218
  black: 0,
1100
1219
  red: 1,
@@ -1132,7 +1251,7 @@ function blendColors(c1, c2, t) {
1132
1251
  * Supports: mix(c1,c2,amount), $token (theme), named colors, hex (#rgb, #rrggbb), rgb(r,g,b)
1133
1252
  */
1134
1253
  function parseColor(color) {
1135
- if (color === "inherit") return null;
1254
+ if (color === "inherit" || color === "currentColor") return null;
1136
1255
  if (color === "$default") return DEFAULT_BG;
1137
1256
  if (color.startsWith("mix(") && color.endsWith(")")) {
1138
1257
  const inner = color.slice(4, -1);
@@ -1158,6 +1277,7 @@ function parseColor(color) {
1158
1277
  }
1159
1278
  }
1160
1279
  if (color.startsWith("$")) {
1280
+ if (getActiveColorLevel() === "none") return null;
1161
1281
  const resolved = resolveThemeColor(color, getActiveTheme());
1162
1282
  if (resolved && resolved !== color) return parseColor(resolved);
1163
1283
  return null;
@@ -1270,24 +1390,61 @@ function getBorderChars(style) {
1270
1390
  return borders[style ?? "single"];
1271
1391
  }
1272
1392
  /**
1393
+ * Collect monochrome attrs from a color string (`"$primary"` → `["bold"]`).
1394
+ *
1395
+ * At mono tier, `parseColor` strips the color (returns `null`). The hierarchy
1396
+ * signal lives in the attrs bag. This helper merges the mapped attrs from
1397
+ * `DEFAULT_MONO_ATTRS` into a mutable accumulator. Called per color-carrying
1398
+ * prop in `getTextStyle`.
1399
+ *
1400
+ * No-op when the color is not a `$token` — non-token hex / named colors
1401
+ * pass through with no attrs (spec: "apps that hardcoded #FF0000 get nothing").
1402
+ */
1403
+ function collectMonoAttrs(color, into) {
1404
+ if (!color) return;
1405
+ const attrs = monoAttrsForColorString(color, getActiveTheme());
1406
+ if (!attrs) return;
1407
+ for (const a of attrs) into.add(a);
1408
+ }
1409
+ /**
1273
1410
  * Get text style from props.
1274
1411
  */
1275
1412
  function getTextStyle(props) {
1276
1413
  let underlineStyle;
1277
1414
  if (props.underlineStyle !== void 0) underlineStyle = props.underlineStyle;
1278
1415
  else if (props.underline) underlineStyle = "single";
1416
+ let bold = props.bold;
1417
+ let dim = props.dim || props.dimColor;
1418
+ let italic = props.italic;
1419
+ let underline = props.underline || !!underlineStyle;
1420
+ let strikethrough = props.strikethrough;
1421
+ let inverse = props.inverse;
1422
+ if (getActiveColorLevel() === "none") {
1423
+ const monoAttrs = /* @__PURE__ */ new Set();
1424
+ collectMonoAttrs(props.color, monoAttrs);
1425
+ collectMonoAttrs(props.backgroundColor, monoAttrs);
1426
+ if (monoAttrs.has("bold")) bold = true;
1427
+ if (monoAttrs.has("dim")) dim = true;
1428
+ if (monoAttrs.has("italic")) italic = true;
1429
+ if (monoAttrs.has("underline")) {
1430
+ underline = true;
1431
+ if (!underlineStyle) underlineStyle = "single";
1432
+ }
1433
+ if (monoAttrs.has("strikethrough")) strikethrough = true;
1434
+ if (monoAttrs.has("inverse")) inverse = true;
1435
+ }
1279
1436
  return {
1280
1437
  fg: props.color ? parseColor(props.color) : null,
1281
1438
  bg: props.backgroundColor ? parseColor(props.backgroundColor) : null,
1282
1439
  underlineColor: props.underlineColor ? parseColor(props.underlineColor) : null,
1283
1440
  attrs: {
1284
- bold: props.bold,
1285
- dim: props.dim || props.dimColor,
1286
- italic: props.italic,
1287
- underline: props.underline || !!underlineStyle,
1441
+ bold,
1442
+ dim,
1443
+ italic,
1444
+ underline,
1288
1445
  underlineStyle,
1289
- strikethrough: props.strikethrough,
1290
- inverse: props.inverse
1446
+ strikethrough,
1447
+ inverse
1291
1448
  }
1292
1449
  };
1293
1450
  }
@@ -1306,7 +1463,7 @@ function getTextWidth(text, ctx) {
1306
1463
  //#endregion
1307
1464
  //#region packages/ag-term/src/pipeline/render-text.ts
1308
1465
  init_buffer();
1309
- const log$2 = createLogger("silvery:content");
1466
+ const log$1 = createLogger("silvery:content");
1310
1467
  /** Cached bg conflict mode. Read from env once at module load. */
1311
1468
  let bgConflictMode = (() => {
1312
1469
  const env = typeof process !== "undefined" ? process.env.SILVERY_BG_CONFLICT?.toLowerCase() : void 0;
@@ -1386,9 +1543,12 @@ function styleToAnsi(style) {
1386
1543
  }[style.underlineStyle] ?? "4");
1387
1544
  else if (style.underline) parts.push("4");
1388
1545
  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}`);
1546
+ const underlineSource = style.underlineColor === "currentColor" || style.underlineColor === "inherit" ? style.color : style.underlineColor;
1547
+ if (underlineSource) {
1548
+ const ulColor = parseColor(underlineSource);
1549
+ if (ulColor !== null) if (typeof ulColor === "number") parts.push(`58;5;${ulColor}`);
1550
+ else parts.push(`58;2;${ulColor.r};${ulColor.g};${ulColor.b}`);
1551
+ }
1392
1552
  }
1393
1553
  if (style.inverse) parts.push("7");
1394
1554
  if (style.strikethrough) parts.push("9");
@@ -1401,7 +1561,7 @@ function styleToAnsi(style) {
1401
1561
  */
1402
1562
  function mergeStyleContext(parent, childProps) {
1403
1563
  return {
1404
- color: childProps.color ?? parent.color,
1564
+ color: (childProps.color === "inherit" || childProps.color === "currentColor" ? parent.color : childProps.color) ?? parent.color,
1405
1565
  backgroundColor: childProps.backgroundColor ?? parent.backgroundColor,
1406
1566
  bold: childProps.bold ?? parent.bold,
1407
1567
  dim: childProps.dim ?? childProps.dimColor ?? parent.dim,
@@ -1829,7 +1989,7 @@ function renderAnsiTextLineReturn(buffer, x, y, text, baseStyle, maxCol, inherit
1829
1989
  const key = `${JSON.stringify(existingBufBg)}-${segment.bg}-${preview}`;
1830
1990
  if (!effectiveWarnedBgConflicts.has(key)) {
1831
1991
  effectiveWarnedBgConflicts.add(key);
1832
- log$2.warn?.(msg);
1992
+ log$1.warn?.(msg);
1833
1993
  }
1834
1994
  }
1835
1995
  }
@@ -1970,13 +2130,25 @@ function renderText(node, buffer, layout, props, nodeState, inheritedBg, inherit
1970
2130
  let text;
1971
2131
  let bgSegments;
1972
2132
  let childSpans;
2133
+ const rootContext = {
2134
+ color: props.color,
2135
+ backgroundColor: props.backgroundColor,
2136
+ bold: props.bold,
2137
+ dim: props.dim || props.dimColor,
2138
+ italic: props.italic,
2139
+ underline: !!(props.underline || props.underlineStyle),
2140
+ underlineStyle: props.underlineStyle,
2141
+ underlineColor: props.underlineColor,
2142
+ inverse: props.inverse,
2143
+ strikethrough: props.strikethrough
2144
+ };
1973
2145
  const cachedCollected = getCachedCollectedText(node, maxDisplayWidth);
1974
2146
  if (cachedCollected) {
1975
2147
  text = cachedCollected.text;
1976
2148
  bgSegments = cachedCollected.bgSegments;
1977
2149
  childSpans = cachedCollected.childSpans;
1978
2150
  } else {
1979
- const collected = collectTextWithBg(node, {}, 0, maxDisplayWidth, ctx);
2151
+ const collected = collectTextWithBg(node, rootContext, 0, maxDisplayWidth, ctx);
1980
2152
  text = collected.text;
1981
2153
  bgSegments = collected.bgSegments;
1982
2154
  childSpans = collected.childSpans;
@@ -1984,6 +2156,7 @@ function renderText(node, buffer, layout, props, nodeState, inheritedBg, inherit
1984
2156
  }
1985
2157
  const style = getTextStyle(props);
1986
2158
  if (style.fg === null && inheritedFg !== void 0) style.fg = inheritedFg;
2159
+ if (props.underlineColor === "currentColor" || props.underlineColor === "inherit") style.underlineColor = style.fg;
1987
2160
  const trim = !(style.bg !== null || bgSegments.length > 0 || inheritedBg !== void 0 && inheritedBg !== null);
1988
2161
  const internalTransform = props.internal_transform;
1989
2162
  let lines;
@@ -2084,7 +2257,7 @@ function getEffectiveBg(props) {
2084
2257
  /**
2085
2258
  * Render a Box node.
2086
2259
  */
2087
- function renderBox(_node, buffer, layout, props, nodeState, skipBgFill = false, inheritedBg, bgOnlyChange = false) {
2260
+ function renderBox(_node, buffer, layout, props, nodeState, skipBgFill = false, inheritedBg, bgOnlyChange = false, inheritedFg) {
2088
2261
  const { scrollOffset, clipBounds } = nodeState;
2089
2262
  const { x, width, height } = layout;
2090
2263
  const y = layout.y - scrollOffset;
@@ -2111,14 +2284,16 @@ function renderBox(_node, buffer, layout, props, nodeState, skipBgFill = false,
2111
2284
  } else if (bgOnlyChange) buffer.fillBg(x, y, width, height, bg);
2112
2285
  else buffer.fill(x, y, width, height, { bg });
2113
2286
  }
2114
- if (props.borderStyle) renderBorder(buffer, x, y, width, height, props, clipBounds, inheritedBg);
2287
+ if (props.borderStyle) renderBorder(buffer, x, y, width, height, props, clipBounds, inheritedBg, inheritedFg);
2115
2288
  }
2116
2289
  /**
2117
2290
  * Render a border around a box.
2118
2291
  */
2119
- function renderBorder(buffer, x, y, width, height, props, clipBounds, inheritedBg) {
2292
+ function renderBorder(buffer, x, y, width, height, props, clipBounds, inheritedBg, inheritedFg) {
2120
2293
  const chars = getBorderChars(props.borderStyle ?? "single");
2121
- const color = props.borderColor ? parseColor(props.borderColor) : null;
2294
+ let color;
2295
+ if (props.borderColor === "currentColor" || props.borderColor === "inherit") color = props.color ? parseColor(props.color) : inheritedFg ?? null;
2296
+ else color = props.borderColor ? parseColor(props.borderColor) : null;
2122
2297
  const baseBg = props.backgroundColor ? parseColor(props.backgroundColor) : inheritedBg ?? null;
2123
2298
  const borderBgStr = props.borderBackgroundColor;
2124
2299
  const borderBgBase = borderBgStr ? parseColor(borderBgStr) : baseBg;
@@ -2203,14 +2378,21 @@ function renderBorder(buffer, x, y, width, height, props, clipBounds, inheritedB
2203
2378
  * Render an outline around a box.
2204
2379
  *
2205
2380
  * 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.
2381
+ * characters OUTSIDE the box one cell beyond each edge, in the gap/margin
2382
+ * space between siblings. This matches CSS `outline` semantics.
2383
+ *
2384
+ * The outline occupies cells at (x-1, y-1) through (x+width, y+height) —
2385
+ * entirely outside the box's own rect. Content is never overlapped.
2208
2386
  */
2209
2387
  function renderOutline(buffer, x, y, width, height, props, clipBounds, inheritedBg) {
2210
2388
  const chars = getBorderChars(props.outlineStyle ?? "single");
2211
2389
  const color = props.outlineColor ? parseColor(props.outlineColor) : null;
2212
2390
  const bg = props.backgroundColor ? parseColor(props.backgroundColor) : inheritedBg ?? null;
2213
2391
  const attrs = props.outlineDimColor ? { dim: true } : {};
2392
+ const ox = x - 1;
2393
+ const oy = y - 1;
2394
+ const ow = width + 2;
2395
+ const oh = height + 2;
2214
2396
  const isRowVisible = (row) => {
2215
2397
  if (!clipBounds) return row >= 0 && row < buffer.height;
2216
2398
  return row >= clipBounds.top && row < clipBounds.bottom && row < buffer.height;
@@ -2223,20 +2405,20 @@ function renderOutline(buffer, x, y, width, height, props, clipBounds, inherited
2223
2405
  const showBottom = props.outlineBottom !== false;
2224
2406
  const showLeft = props.outlineLeft !== false;
2225
2407
  const showRight = props.outlineRight !== false;
2226
- if (showTop && isRowVisible(y)) {
2227
- if (showLeft && isColVisible(x)) buffer.setCell(x, y, {
2408
+ if (showTop && isRowVisible(oy)) {
2409
+ if (showLeft && isColVisible(ox)) buffer.setCell(ox, oy, {
2228
2410
  char: chars.topLeft,
2229
2411
  fg: color,
2230
2412
  bg,
2231
2413
  attrs
2232
2414
  });
2233
- for (let col = x + 1; col < x + width - 1 && col < buffer.width; col++) if (isColVisible(col)) buffer.setCell(col, y, {
2415
+ for (let col = ox + 1; col < ox + ow - 1 && col < buffer.width; col++) if (isColVisible(col)) buffer.setCell(col, oy, {
2234
2416
  char: chars.horizontal,
2235
2417
  fg: color,
2236
2418
  bg,
2237
2419
  attrs
2238
2420
  });
2239
- if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, y, {
2421
+ if (showRight && ox + ow - 1 < buffer.width && isColVisible(ox + ow - 1)) buffer.setCell(ox + ow - 1, oy, {
2240
2422
  char: chars.topRight,
2241
2423
  fg: color,
2242
2424
  bg,
@@ -2244,17 +2426,17 @@ function renderOutline(buffer, x, y, width, height, props, clipBounds, inherited
2244
2426
  });
2245
2427
  }
2246
2428
  const outlineRightVertical = chars.rightVertical ?? chars.vertical;
2247
- const sideStart = showTop ? y + 1 : y;
2248
- const sideEnd = showBottom ? y + height - 1 : y + height;
2429
+ const sideStart = showTop ? oy + 1 : oy;
2430
+ const sideEnd = showBottom ? oy + oh - 1 : oy + oh;
2249
2431
  for (let row = sideStart; row < sideEnd; row++) {
2250
2432
  if (!isRowVisible(row)) continue;
2251
- if (showLeft && isColVisible(x)) buffer.setCell(x, row, {
2433
+ if (showLeft && isColVisible(ox)) buffer.setCell(ox, row, {
2252
2434
  char: chars.vertical,
2253
2435
  fg: color,
2254
2436
  bg,
2255
2437
  attrs
2256
2438
  });
2257
- if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, row, {
2439
+ if (showRight && ox + ow - 1 < buffer.width && isColVisible(ox + ow - 1)) buffer.setCell(ox + ow - 1, row, {
2258
2440
  char: outlineRightVertical,
2259
2441
  fg: color,
2260
2442
  bg,
@@ -2262,21 +2444,21 @@ function renderOutline(buffer, x, y, width, height, props, clipBounds, inherited
2262
2444
  });
2263
2445
  }
2264
2446
  const outlineBottomHorizontal = chars.bottomHorizontal ?? chars.horizontal;
2265
- const bottomY = y + height - 1;
2447
+ const bottomY = oy + oh - 1;
2266
2448
  if (showBottom && isRowVisible(bottomY)) {
2267
- if (showLeft && isColVisible(x)) buffer.setCell(x, bottomY, {
2449
+ if (showLeft && isColVisible(ox)) buffer.setCell(ox, bottomY, {
2268
2450
  char: chars.bottomLeft,
2269
2451
  fg: color,
2270
2452
  bg,
2271
2453
  attrs
2272
2454
  });
2273
- for (let col = x + 1; col < x + width - 1 && col < buffer.width; col++) if (isColVisible(col)) buffer.setCell(col, bottomY, {
2455
+ for (let col = ox + 1; col < ox + ow - 1 && col < buffer.width; col++) if (isColVisible(col)) buffer.setCell(col, bottomY, {
2274
2456
  char: outlineBottomHorizontal,
2275
2457
  fg: color,
2276
2458
  bg,
2277
2459
  attrs
2278
2460
  });
2279
- if (showRight && x + width - 1 < buffer.width && isColVisible(x + width - 1)) buffer.setCell(x + width - 1, bottomY, {
2461
+ if (showRight && ox + ow - 1 < buffer.width && isColVisible(ox + ow - 1)) buffer.setCell(ox + ow - 1, bottomY, {
2280
2462
  char: chars.bottomRight,
2281
2463
  fg: color,
2282
2464
  bg,
@@ -2350,6 +2532,196 @@ function padCenter(text, width) {
2350
2532
  return " ".repeat(leftPad) + text + " ".repeat(rightPad);
2351
2533
  }
2352
2534
  //#endregion
2535
+ //#region packages/ag-term/src/pipeline/decoration-phase.ts
2536
+ /**
2537
+ * Restore cells at previously-drawn outline positions to their pre-outline
2538
+ * state. Called at the start of each incremental render, before the content
2539
+ * phase, on the cloned buffer. No-op when there are no previous snapshots
2540
+ * (fresh render or no outlines last frame).
2541
+ */
2542
+ function clearPreviousOutlines(buffer) {
2543
+ const snapshots = buffer.outlineSnapshots;
2544
+ if (!snapshots || snapshots.length === 0) return;
2545
+ for (const snap of snapshots) buffer.setCell(snap.x, snap.y, snap.cell);
2546
+ buffer.outlineSnapshots = [];
2547
+ }
2548
+ /**
2549
+ * Walk the node tree, drawing outlines for every node with `outlineStyle`.
2550
+ * Captures per-cell snapshots so the next frame can restore these positions.
2551
+ *
2552
+ * Called AFTER the content render phase on every frame (both fresh and
2553
+ * incremental). Mirrors `renderNodeToBuffer`'s state threading for scroll
2554
+ * offsets, clip bounds, and inherited background — but does nothing except
2555
+ * visit the tree and draw outlines.
2556
+ */
2557
+ function renderDecorationPass(buffer, root) {
2558
+ const snapshots = [];
2559
+ walk(root, buffer, 0, void 0, { color: null }, snapshots);
2560
+ buffer.outlineSnapshots = snapshots;
2561
+ }
2562
+ /**
2563
+ * Recursive tree walk. Each invocation corresponds to the state at a single
2564
+ * node — scroll offset, clip bounds, inherited background — matching what
2565
+ * `renderNodeToBuffer` would have threaded through its `NodeRenderState`.
2566
+ */
2567
+ function walk(node, buffer, scrollOffset, clipBounds, inheritedBg, snapshots) {
2568
+ if (!node.layoutNode) return;
2569
+ const layout = node.boxRect;
2570
+ if (!layout) return;
2571
+ if (node.hidden) return;
2572
+ const props = node.props;
2573
+ if (props.display === "none") return;
2574
+ const y = layout.y - scrollOffset;
2575
+ if (y >= buffer.height || y + layout.height <= 0) return;
2576
+ const effectiveBg = getEffectiveBg(props);
2577
+ const theme = props.theme;
2578
+ const childInheritedBg = effectiveBg ? { color: parseColor(effectiveBg) } : theme ? { color: parseColor(theme.bg) } : inheritedBg;
2579
+ if (node.type === "silvery-box" && props.outlineStyle) {
2580
+ const boxInheritedBg = effectiveBg ? void 0 : inheritedBg.color;
2581
+ const positions = collectOutlineCells(layout.x, y, layout.width, layout.height, props, clipBounds, buffer);
2582
+ for (const pos of positions) snapshots.push({
2583
+ x: pos.x,
2584
+ y: pos.y,
2585
+ cell: buffer.getCell(pos.x, pos.y)
2586
+ });
2587
+ renderOutline(buffer, layout.x, y, layout.width, layout.height, props, clipBounds, boxInheritedBg);
2588
+ }
2589
+ if (node.children.length === 0) return;
2590
+ const isScrollContainer = props.overflow === "scroll" && node.scrollState;
2591
+ const clipX = (props.overflowX ?? props.overflow) === "hidden";
2592
+ const clipY = (props.overflowY ?? props.overflow) === "hidden";
2593
+ if (isScrollContainer) {
2594
+ const ss = node.scrollState;
2595
+ const childClip = computeChildClip(layout, props, clipBounds, 0, false, true);
2596
+ for (let i = 0; i < node.children.length; i++) {
2597
+ const child = node.children[i];
2598
+ if (!child) continue;
2599
+ if (child.props.position === "sticky") continue;
2600
+ if (i < ss.firstVisibleChild || i > ss.lastVisibleChild) continue;
2601
+ walk(child, buffer, ss.offset, childClip, childInheritedBg, snapshots);
2602
+ }
2603
+ if (ss.stickyChildren) for (const sticky of ss.stickyChildren) {
2604
+ const child = node.children[sticky.index];
2605
+ if (!child) continue;
2606
+ walk(child, buffer, sticky.naturalTop - sticky.renderOffset, childClip, childInheritedBg, snapshots);
2607
+ }
2608
+ } else {
2609
+ const childClip = clipX || clipY ? computeChildClip(layout, props, clipBounds, scrollOffset, clipX, clipY) : clipBounds;
2610
+ for (const child of node.children) walk(child, buffer, scrollOffset, childClip, childInheritedBg, snapshots);
2611
+ }
2612
+ }
2613
+ /**
2614
+ * Compute the cells an outline would write, given the same inputs
2615
+ * `renderOutline` uses. Kept in lockstep with render-box.ts — any change to
2616
+ * the outline geometry (e.g., new outline styles, per-side toggles) must be
2617
+ * mirrored here. If they drift, the snapshots won't cover every cell the
2618
+ * next `renderOutline` call writes, and stale pixels will leak through.
2619
+ *
2620
+ * Uses the SAME visibility checks as `renderOutline` so the snapshot set is
2621
+ * an exact match for the cell set the renderer will overwrite.
2622
+ */
2623
+ function collectOutlineCells(x, y, width, height, props, clipBounds, buffer) {
2624
+ const out = [];
2625
+ const ox = x - 1;
2626
+ const oy = y - 1;
2627
+ const ow = width + 2;
2628
+ const oh = height + 2;
2629
+ const isRowVisible = (row) => {
2630
+ if (!clipBounds) return row >= 0 && row < buffer.height;
2631
+ return row >= clipBounds.top && row < clipBounds.bottom && row < buffer.height;
2632
+ };
2633
+ const isColVisible = (col) => {
2634
+ if (clipBounds?.left === void 0 || clipBounds.right === void 0) return col >= 0 && col < buffer.width;
2635
+ return col >= clipBounds.left && col < clipBounds.right && col < buffer.width;
2636
+ };
2637
+ const showTop = props.outlineTop !== false;
2638
+ const showBottom = props.outlineBottom !== false;
2639
+ const showLeft = props.outlineLeft !== false;
2640
+ const showRight = props.outlineRight !== false;
2641
+ if (showTop && isRowVisible(oy)) {
2642
+ if (showLeft && isColVisible(ox)) out.push({
2643
+ x: ox,
2644
+ y: oy
2645
+ });
2646
+ for (let col = ox + 1; col < ox + ow - 1 && col < buffer.width; col++) if (isColVisible(col)) out.push({
2647
+ x: col,
2648
+ y: oy
2649
+ });
2650
+ if (showRight && ox + ow - 1 < buffer.width && isColVisible(ox + ow - 1)) out.push({
2651
+ x: ox + ow - 1,
2652
+ y: oy
2653
+ });
2654
+ }
2655
+ const sideStart = showTop ? oy + 1 : oy;
2656
+ const sideEnd = showBottom ? oy + oh - 1 : oy + oh;
2657
+ for (let row = sideStart; row < sideEnd; row++) {
2658
+ if (!isRowVisible(row)) continue;
2659
+ if (showLeft && isColVisible(ox)) out.push({
2660
+ x: ox,
2661
+ y: row
2662
+ });
2663
+ if (showRight && ox + ow - 1 < buffer.width && isColVisible(ox + ow - 1)) out.push({
2664
+ x: ox + ow - 1,
2665
+ y: row
2666
+ });
2667
+ }
2668
+ const bottomY = oy + oh - 1;
2669
+ if (showBottom && isRowVisible(bottomY)) {
2670
+ if (showLeft && isColVisible(ox)) out.push({
2671
+ x: ox,
2672
+ y: bottomY
2673
+ });
2674
+ for (let col = ox + 1; col < ox + ow - 1 && col < buffer.width; col++) if (isColVisible(col)) out.push({
2675
+ x: col,
2676
+ y: bottomY
2677
+ });
2678
+ if (showRight && ox + ow - 1 < buffer.width && isColVisible(ox + ow - 1)) out.push({
2679
+ x: ox + ow - 1,
2680
+ y: bottomY
2681
+ });
2682
+ }
2683
+ return out;
2684
+ }
2685
+ /**
2686
+ * Local copy of `computeChildClipBounds` from render-phase.ts. The decoration
2687
+ * walker can't import private render-phase helpers without tangling modules,
2688
+ * so we reproduce the same computation here. Must stay in sync.
2689
+ */
2690
+ function computeChildClip(layout, props, parentClip, scrollOffset, horizontal, vertical) {
2691
+ const border = props.borderStyle ? getBorderSize(props) : {
2692
+ top: 0,
2693
+ bottom: 0,
2694
+ left: 0,
2695
+ right: 0
2696
+ };
2697
+ const padding = getPadding(props);
2698
+ const adjustedY = layout.y - scrollOffset;
2699
+ const nodeClip = vertical ? {
2700
+ top: adjustedY + border.top + padding.top,
2701
+ bottom: adjustedY + layout.height - border.bottom - padding.bottom
2702
+ } : {
2703
+ top: -Infinity,
2704
+ bottom: Infinity
2705
+ };
2706
+ if (horizontal) {
2707
+ nodeClip.left = layout.x + border.left + padding.left;
2708
+ nodeClip.right = layout.x + layout.width - border.right - padding.right;
2709
+ }
2710
+ if (!parentClip) return nodeClip;
2711
+ const result = {
2712
+ top: vertical ? Math.max(parentClip.top, nodeClip.top) : parentClip.top,
2713
+ bottom: vertical ? Math.min(parentClip.bottom, nodeClip.bottom) : parentClip.bottom
2714
+ };
2715
+ if (horizontal && nodeClip.left !== void 0 && nodeClip.right !== void 0) {
2716
+ result.left = Math.max(parentClip.left ?? 0, nodeClip.left);
2717
+ result.right = Math.min(parentClip.right ?? Infinity, nodeClip.right);
2718
+ } else if (parentClip.left !== void 0 && parentClip.right !== void 0) {
2719
+ result.left = parentClip.left;
2720
+ result.right = parentClip.right;
2721
+ }
2722
+ return result;
2723
+ }
2724
+ //#endregion
2353
2725
  //#region packages/ag-term/src/pipeline/cascade-predicates.ts
2354
2726
  /**
2355
2727
  * Compute all cascade predicate values from boolean inputs.
@@ -2374,350 +2746,6 @@ function computeCascade(inputs) {
2374
2746
  };
2375
2747
  }
2376
2748
  //#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
2749
  //#region packages/ag-term/src/pipeline/reactive-node.ts
2722
2750
  /**
2723
2751
  * Reactive Node State — alien-signals wrappers for cascade derivations.
@@ -2926,6 +2954,7 @@ function renderPhase(root, prevBuffer, ctx) {
2926
2954
  const buffer = hasPrevBuffer ? prevBuffer.clone() : new TerminalBuffer(layout.width, layout.height);
2927
2955
  const tClone = instr.enabled ? performance.now() - t0 : 0;
2928
2956
  buffer.setSelectableMode(true);
2957
+ clearPreviousOutlines(buffer);
2929
2958
  const t1 = instr.enabled ? performance.now() : 0;
2930
2959
  renderNodeToBuffer(root, buffer, {
2931
2960
  scrollOffset: 0,
@@ -2941,6 +2970,7 @@ function renderPhase(root, prevBuffer, ctx) {
2941
2970
  inheritedFg: null
2942
2971
  }, ctx);
2943
2972
  const tRender = instr.enabled ? performance.now() - t1 : 0;
2973
+ renderDecorationPass(buffer, root);
2944
2974
  if (instr.enabled) emitRenderPhaseStats(instr.stats, instr.nodeTrace, instr.nodeTraceEnabled, tClone, tRender);
2945
2975
  syncPrevLayout(root, isCurrentEpoch(root.layoutChangedThisFrame) || isDirty(root.dirtyBits, root.dirtyEpoch, 16) || !hasPrevBuffer);
2946
2976
  advanceRenderEpoch();
@@ -2964,7 +2994,7 @@ function renderPhase(root, prevBuffer, ctx) {
2964
2994
  * Previously walked ALL nodes O(N) every frame. Now only visits nodes
2965
2995
  * with layoutChangedThisFrame (set by propagateLayout in layout phase).
2966
2996
  * Falls back to full walk when layout phase ran (dimensions changed or
2967
- * layoutDirty) since any node may have moved.
2997
+ * Flexily isDirty) since any node may have moved.
2968
2998
  *
2969
2999
  * For cursor move (no layout change): O(1) — no nodes to sync.
2970
3000
  * For resize: O(N) — all nodes may have moved (same as before).
@@ -3212,7 +3242,7 @@ function renderNodeToBuffer(node, buffer, nodeState, ctx) {
3212
3242
  const useTextStyleFastPath = false;
3213
3243
  executeRegionClearing(node, buffer, layout, scrollOffset, clipBounds, bufferIsCloned, layoutChanged, contentRegionCleared, descendantOverflowChanged, instr.enabled, instr.stats, nodeState.inheritedBg);
3214
3244
  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;
3245
+ node.type === "silvery-box" && !getEffectiveBg(props) && nodeState.inheritedBg.color;
3216
3246
  if (needsOwnRepaint) renderOwnContent(node, buffer, layout, props, nodeState, skipBgFill, instr.enabled, instr.stats, ctx, cascade.bgOnlyChange, useTextStyleFastPath);
3217
3247
  const effectiveBg = getEffectiveBg(props);
3218
3248
  const childInheritedBg = effectiveBg ? {
@@ -3222,7 +3252,7 @@ function renderNodeToBuffer(node, buffer, nodeState, ctx) {
3222
3252
  color: parseColor(nodeTheme.bg),
3223
3253
  ancestorRect: node.boxRect
3224
3254
  } : nodeState.inheritedBg;
3225
- const childInheritedFg = props.color ? parseColor(props.color) : nodeTheme ? parseColor(nodeTheme.fg) : nodeState.inheritedFg;
3255
+ const childInheritedFg = props.color === "inherit" || props.color === "currentColor" ? nodeState.inheritedFg : props.color ? parseColor(props.color) : nodeTheme ? parseColor(nodeTheme.fg) : nodeState.inheritedFg;
3226
3256
  const childState = {
3227
3257
  ...nodeState,
3228
3258
  inheritedBg: childInheritedBg,
@@ -3232,10 +3262,6 @@ function renderNodeToBuffer(node, buffer, nodeState, ctx) {
3232
3262
  renderScrollContainerChildren(node, buffer, props, childState, contentRegionCleared, childrenNeedFreshRender, ctx);
3233
3263
  renderScrollIndicators(node, buffer, layout, props, node.scrollState, ctx);
3234
3264
  } 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
3265
  clearNodeDirtyFlags(node);
3240
3266
  } finally {
3241
3267
  if (nodeTheme) popContextTheme();
@@ -3359,7 +3385,7 @@ function renderOwnContent(node, buffer, layout, props, nodeState, skipBgFill, in
3359
3385
  const boxInheritedBg = node.type === "silvery-box" && !getEffectiveBg(props) ? nodeState.inheritedBg.color : void 0;
3360
3386
  if (node.type === "silvery-box") {
3361
3387
  if (instrumentEnabled) stats.boxNodes++;
3362
- renderBox(node, buffer, layout, props, nodeState, skipBgFill, boxInheritedBg, bgOnlyChange);
3388
+ renderBox(node, buffer, layout, props, nodeState, skipBgFill, boxInheritedBg, bgOnlyChange, nodeState.inheritedFg);
3363
3389
  } else if (node.type === "silvery-text") {
3364
3390
  if (instrumentEnabled) stats.textNodes++;
3365
3391
  const textInheritedBg = nodeState.inheritedBg.color;
@@ -3994,6 +4020,189 @@ function clippedFill(buffer, x, width, top, bottom, clipBounds, outerBottom, bg)
3994
4020
  });
3995
4021
  }
3996
4022
  //#endregion
4023
+ //#region packages/ag-term/src/pipeline/backdrop-phase.ts
4024
+ init_src$1();
4025
+ init_buffer();
4026
+ const FADE_ATTR = "data-backdrop-fade";
4027
+ const FADE_EXCLUDE_ATTR = "data-backdrop-fade-excluded";
4028
+ /**
4029
+ * Apply backdrop-fade to the buffer based on tree markers.
4030
+ *
4031
+ * Returns `true` if at least one region was modified; `false` if nothing
4032
+ * changed (no markers found, or colorLevel is `none`).
4033
+ */
4034
+ /**
4035
+ * Quick check: does the tree contain any backdrop markers? Used as a gate so
4036
+ * we don't clone the buffer every frame when no fade is active. Walks the
4037
+ * full tree once (O(N)) — the alternative (tracking dirty markers in the
4038
+ * reconciler) is more complex and the walk is cheap compared to the pass.
4039
+ */
4040
+ function hasBackdropMarkers(root) {
4041
+ const props = root.props;
4042
+ if (props[FADE_ATTR] !== void 0 || props[FADE_EXCLUDE_ATTR] !== void 0) return true;
4043
+ for (const child of root.children) if (hasBackdropMarkers(child)) return true;
4044
+ return false;
4045
+ }
4046
+ function applyBackdropFade(root, buffer, options) {
4047
+ const colorLevel = options?.colorLevel ?? "truecolor";
4048
+ if (colorLevel === "none") return false;
4049
+ const includes = [];
4050
+ const excludes = [];
4051
+ collectBackdropMarkers(root, includes, excludes);
4052
+ if (includes.length === 0 && excludes.length === 0) return false;
4053
+ const strategy = colorLevel === "basic" ? "dim" : "blend";
4054
+ let modified = false;
4055
+ for (const { rect, amount } of includes) {
4056
+ if (amount <= 0) continue;
4057
+ if (fadeRect(buffer, rect, amount, strategy)) modified = true;
4058
+ }
4059
+ if (excludes.length > 0) {
4060
+ const fullRect = {
4061
+ x: 0,
4062
+ y: 0,
4063
+ width: buffer.width,
4064
+ height: buffer.height
4065
+ };
4066
+ for (const { rect, amount } of excludes) {
4067
+ if (amount <= 0) continue;
4068
+ if (fadeRectExcluding(buffer, fullRect, rect, amount, strategy)) modified = true;
4069
+ }
4070
+ }
4071
+ return modified;
4072
+ }
4073
+ function collectBackdropMarkers(node, includes, excludes) {
4074
+ const props = node.props;
4075
+ const includeRaw = props[FADE_ATTR];
4076
+ const excludeRaw = props[FADE_EXCLUDE_ATTR];
4077
+ if (includeRaw !== void 0 || excludeRaw !== void 0) {
4078
+ const rect = node.screenRect ?? node.scrollRect ?? node.boxRect;
4079
+ if (rect && rect.width > 0 && rect.height > 0) {
4080
+ const inc = parseFade(includeRaw);
4081
+ if (inc !== null) includes.push({
4082
+ rect,
4083
+ amount: inc
4084
+ });
4085
+ const exc = parseFade(excludeRaw);
4086
+ if (exc !== null) excludes.push({
4087
+ rect,
4088
+ amount: exc
4089
+ });
4090
+ }
4091
+ }
4092
+ for (const child of node.children) collectBackdropMarkers(child, includes, excludes);
4093
+ }
4094
+ function parseFade(raw) {
4095
+ if (raw === void 0 || raw === null) return null;
4096
+ const n = typeof raw === "number" ? raw : Number(raw);
4097
+ if (!Number.isFinite(n)) return null;
4098
+ if (n <= 0) return null;
4099
+ return n > 1 ? 1 : n;
4100
+ }
4101
+ function fadeRect(buffer, rect, amount, strategy) {
4102
+ const x0 = Math.max(0, rect.x);
4103
+ const y0 = Math.max(0, rect.y);
4104
+ const x1 = Math.min(buffer.width, rect.x + rect.width);
4105
+ const y1 = Math.min(buffer.height, rect.y + rect.height);
4106
+ if (x0 >= x1 || y0 >= y1) return false;
4107
+ let any = false;
4108
+ for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) if (fadeCell(buffer, x, y, amount, strategy)) any = true;
4109
+ return any;
4110
+ }
4111
+ function fadeRectExcluding(buffer, outer, inner, amount, strategy) {
4112
+ const ox0 = Math.max(0, outer.x);
4113
+ const oy0 = Math.max(0, outer.y);
4114
+ const ox1 = Math.min(buffer.width, outer.x + outer.width);
4115
+ const oy1 = Math.min(buffer.height, outer.y + outer.height);
4116
+ const ix0 = Math.max(ox0, inner.x);
4117
+ const iy0 = Math.max(oy0, inner.y);
4118
+ const ix1 = Math.min(ox1, inner.x + inner.width);
4119
+ const iy1 = Math.min(oy1, inner.y + inner.height);
4120
+ const innerValid = ix0 < ix1 && iy0 < iy1;
4121
+ let any = false;
4122
+ for (let y = oy0; y < oy1; y++) for (let x = ox0; x < ox1; x++) {
4123
+ if (innerValid && x >= ix0 && x < ix1 && y >= iy0 && y < iy1) continue;
4124
+ if (fadeCell(buffer, x, y, amount, strategy)) any = true;
4125
+ }
4126
+ return any;
4127
+ }
4128
+ /**
4129
+ * Fade a single cell. Returns true if the cell was modified.
4130
+ *
4131
+ * - `blend` strategy: mix fg toward bg in OKLab. When either color is null or
4132
+ * the default-bg sentinel, also stamps `dim`.
4133
+ * - `dim` strategy: stamp `dim` attribute.
4134
+ *
4135
+ * Wide-char continuation cells are skipped — they share styling with the
4136
+ * leading cell and modifying them separately would desync.
4137
+ */
4138
+ function fadeCell(buffer, x, y, amount, strategy) {
4139
+ if (buffer.isCellContinuation(x, y)) return false;
4140
+ const cell = buffer.getCell(x, y);
4141
+ if (strategy === "dim") {
4142
+ if (cell.attrs.dim) return false;
4143
+ buffer.setCell(x, y, {
4144
+ ...cell,
4145
+ attrs: {
4146
+ ...cell.attrs,
4147
+ dim: true
4148
+ }
4149
+ });
4150
+ return true;
4151
+ }
4152
+ const fgHex = colorToHex(cell.fg);
4153
+ const bgHex = colorToHex(cell.bg);
4154
+ if (fgHex && bgHex) {
4155
+ const blendedRgb = hexToRgb(blend(fgHex, bgHex, amount));
4156
+ if (!blendedRgb) return false;
4157
+ buffer.setCell(x, y, {
4158
+ ...cell,
4159
+ fg: blendedRgb
4160
+ });
4161
+ return true;
4162
+ }
4163
+ if (cell.attrs.dim) return false;
4164
+ buffer.setCell(x, y, {
4165
+ ...cell,
4166
+ attrs: {
4167
+ ...cell.attrs,
4168
+ dim: true
4169
+ }
4170
+ });
4171
+ return true;
4172
+ }
4173
+ /** Convert a buffer Color to a `#rrggbb` hex string, or null if unresolvable. */
4174
+ function colorToHex(color) {
4175
+ if (color === null) return null;
4176
+ if (typeof color === "number") {
4177
+ const rgb = ansi256ToRgb(color);
4178
+ return rgbToHex(rgb.r, rgb.g, rgb.b);
4179
+ }
4180
+ if (isDefaultBg(color)) return null;
4181
+ return rgbToHex(color.r, color.g, color.b);
4182
+ }
4183
+ function rgbToHex(r, g, b) {
4184
+ const clamp = (n) => {
4185
+ return Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
4186
+ };
4187
+ return `#${clamp(r)}${clamp(g)}${clamp(b)}`;
4188
+ }
4189
+ function hexToRgb(hex) {
4190
+ if (typeof hex !== "string") return null;
4191
+ let s = hex;
4192
+ if (s.startsWith("#")) s = s.slice(1);
4193
+ if (s.length === 3) s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2];
4194
+ if (s.length !== 6) return null;
4195
+ const r = parseInt(s.slice(0, 2), 16);
4196
+ const g = parseInt(s.slice(2, 4), 16);
4197
+ const b = parseInt(s.slice(4, 6), 16);
4198
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
4199
+ return {
4200
+ r,
4201
+ g,
4202
+ b
4203
+ };
4204
+ }
4205
+ //#endregion
3997
4206
  //#region \0@oxc-project+runtime@0.122.0/helpers/usingCtx.js
3998
4207
  function _usingCtx() {
3999
4208
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
@@ -4054,7 +4263,7 @@ function _usingCtx() {
4054
4263
  /**
4055
4264
  * Ag — tree + layout engine + renderer.
4056
4265
  *
4057
- * Decomposes the opaque executeRender() into two independent phases:
4266
+ * The sole pipeline entry point. Two independent phases:
4058
4267
  * - ag.layout(dims) — measure + flexbox → positions/sizes
4059
4268
  * - ag.render() — positioned tree → cell grid → TextFrame
4060
4269
  *
@@ -4069,20 +4278,22 @@ function _usingCtx() {
4069
4278
  * ```
4070
4279
  */
4071
4280
  init_buffer();
4072
- const log$1 = createLogger("silvery:render");
4073
- const baseLog$1 = createLogger("@silvery/ag-react");
4281
+ const log = createLogger("silvery:render");
4282
+ const baseLog = createLogger("@silvery/ag-react");
4074
4283
  function createAg(root, options) {
4075
4284
  const measurer = options?.measurer;
4285
+ const colorLevel = options?.colorLevel ?? "truecolor";
4076
4286
  const ctx = measurer ? { measurer } : void 0;
4077
4287
  let _prevBuffer = null;
4078
4288
  let hasScroll = false;
4079
4289
  let hasSticky = false;
4080
4290
  function doLayout(cols, rows, opts) {
4081
4291
  try {
4082
- var _usingCtx$2 = _usingCtx();
4292
+ var _usingCtx$1 = _usingCtx();
4083
4293
  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)");
4294
+ if (!(prevRootLayout && (prevRootLayout.width !== cols || prevRootLayout.height !== rows)) && !root.layoutNode?.isDirty() && !hasScrollDirty()) {
4295
+ log.debug?.("layout: skipped (Flexily clean, no scrollDirty, dimensions unchanged)");
4296
+ layoutPhase(root, cols, rows);
4086
4297
  return {
4087
4298
  tMeasure: 0,
4088
4299
  tLayout: 0,
@@ -4091,7 +4302,7 @@ function createAg(root, options) {
4091
4302
  tNotify: 0
4092
4303
  };
4093
4304
  }
4094
- const render = _usingCtx$2.u(baseLog$1.span("pipeline", {
4305
+ const render = _usingCtx$1.u(baseLog.span("pipeline", {
4095
4306
  width: cols,
4096
4307
  height: rows
4097
4308
  }));
@@ -4102,7 +4313,7 @@ function createAg(root, options) {
4102
4313
  const t = performance.now();
4103
4314
  measurePhase(root, ctx);
4104
4315
  tMeasure = performance.now() - t;
4105
- log$1.debug?.(`measure: ${tMeasure.toFixed(2)}ms`);
4316
+ log.debug?.(`measure: ${tMeasure.toFixed(2)}ms`);
4106
4317
  } catch (_) {
4107
4318
  _usingCtx3.e = _;
4108
4319
  } finally {
@@ -4115,12 +4326,13 @@ function createAg(root, options) {
4115
4326
  const t = performance.now();
4116
4327
  layoutPhase(root, cols, rows);
4117
4328
  tLayout = performance.now() - t;
4118
- log$1.debug?.(`layout: ${tLayout.toFixed(2)}ms`);
4329
+ log.debug?.(`layout: ${tLayout.toFixed(2)}ms`);
4119
4330
  } catch (_) {
4120
4331
  _usingCtx4.e = _;
4121
4332
  } finally {
4122
4333
  _usingCtx4.d();
4123
4334
  }
4335
+ strictLayoutOverflowCheck(root);
4124
4336
  if (!hasScroll || !hasSticky) {
4125
4337
  const features = detectPipelineFeatures(root);
4126
4338
  if (features.hasScroll) hasScroll = true;
@@ -4182,9 +4394,9 @@ function createAg(root, options) {
4182
4394
  tNotify
4183
4395
  };
4184
4396
  } catch (_) {
4185
- _usingCtx$2.e = _;
4397
+ _usingCtx$1.e = _;
4186
4398
  } finally {
4187
- _usingCtx$2.d();
4399
+ _usingCtx$1.d();
4188
4400
  }
4189
4401
  }
4190
4402
  function doRender(opts) {
@@ -4196,9 +4408,17 @@ function createAg(root, options) {
4196
4408
  const t = performance.now();
4197
4409
  buffer = renderPhase(root, prevBuffer, ctx);
4198
4410
  tContent = performance.now() - t;
4199
- log$1.debug?.(`content: ${tContent.toFixed(2)}ms`);
4411
+ log.debug?.(`content: ${tContent.toFixed(2)}ms`);
4412
+ }
4413
+ let carryForwardBuffer;
4414
+ if (hasBackdropMarkers(root)) {
4415
+ carryForwardBuffer = buffer.clone();
4416
+ if (!opts?.fresh) _prevBuffer = carryForwardBuffer;
4417
+ applyBackdropFade(root, buffer, { colorLevel });
4418
+ } else {
4419
+ carryForwardBuffer = buffer;
4420
+ if (!opts?.fresh) _prevBuffer = buffer;
4200
4421
  }
4201
- if (!opts?.fresh) _prevBuffer = buffer;
4202
4422
  clearDirtyTracking();
4203
4423
  const acc = globalThis.__silvery_bench_phases;
4204
4424
  if (acc) {
@@ -4208,6 +4428,7 @@ function createAg(root, options) {
4208
4428
  return {
4209
4429
  frame: createTextFrame(buffer),
4210
4430
  buffer,
4431
+ carryForwardBuffer,
4211
4432
  prevBuffer,
4212
4433
  tContent
4213
4434
  };
@@ -4226,10 +4447,8 @@ function createAg(root, options) {
4226
4447
  prevScrollRect: null,
4227
4448
  prevScreenRect: null,
4228
4449
  layoutChangedThisFrame: -1,
4229
- layoutDirty: true,
4230
4450
  dirtyBits: 31,
4231
- dirtyEpoch: getRenderEpoch(),
4232
- layoutSubscribers: /* @__PURE__ */ new Set()
4451
+ dirtyEpoch: getRenderEpoch()
4233
4452
  };
4234
4453
  }
4235
4454
  function agInsertChild(parent, child, index) {
@@ -4262,6 +4481,7 @@ function createAg(root, options) {
4262
4481
  return {
4263
4482
  frame: result.frame,
4264
4483
  buffer: result.buffer,
4484
+ carryForwardBuffer: result.carryForwardBuffer,
4265
4485
  prevBuffer: result.prevBuffer
4266
4486
  };
4267
4487
  },
@@ -4289,99 +4509,207 @@ function createAg(root, options) {
4289
4509
  };
4290
4510
  }
4291
4511
  //#endregion
4292
- //#region packages/ag-term/src/pipeline/index.ts
4512
+ //#region packages/ag-react/src/reconciler/string-reconciler.ts
4293
4513
  /**
4294
- * Silvery Render Pipeline
4514
+ * Separate React reconciler instance for renderStringSync.
4295
4515
  *
4296
- * The 5-phase rendering architecture:
4516
+ * renderStringSync may be called from within React effects (e.g., useScrollback
4517
+ * freezing items to scrollback). If it uses the same reconciler singleton as the
4518
+ * main render tree, this causes re-entrancy: the nested reconciliation interferes
4519
+ * with the outer one, producing empty output.
4297
4520
  *
4298
- * Phase 0: RECONCILIATION (React)
4299
- * React reconciliation builds the SilveryNode tree.
4300
- * Components register layout constraints via props.
4521
+ * By using a dedicated reconciler instance, renderStringSync operates on an
4522
+ * independent fiber tree with no shared reconciler state.
4523
+ */
4524
+ /**
4525
+ * Dedicated reconciler for string rendering.
4301
4526
  *
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
4527
+ * Uses the same host config functions but overrides isPrimaryRenderer to false,
4528
+ * since this is a secondary renderer used only for one-shot string rendering.
4529
+ * This avoids conflicts with the main reconciler's hook ownership.
4530
+ */
4531
+ const stringReconciler = Reconciler({
4532
+ ...hostConfig,
4533
+ isPrimaryRenderer: false
4534
+ });
4535
+ //#endregion
4536
+ //#region packages/ag-react/src/render-string.tsx
4537
+ /**
4538
+ * renderString - Static one-shot rendering to string
4306
4539
  *
4307
- * Phase 2: LAYOUT
4308
- * Run yoga.calculateLayout()
4309
- * Propagate computed dimensions to all nodes
4310
- * Notify useBoxRect() subscribers
4540
+ * Renders a React element to a string without needing a terminal.
4541
+ * Use for:
4542
+ * - CI output (no cursor control needed)
4543
+ * - Piped output
4544
+ * - One-shot reports/summaries
4545
+ * - Testing component output
4311
4546
  *
4312
- * Phase 3: CONTENT RENDER
4313
- * Render each node to the TerminalBuffer
4314
- * Handle text truncation, styling, borders
4547
+ * @example
4548
+ * ```tsx
4549
+ * import { renderString, Box, Text } from '@silvery/ag-react'
4315
4550
  *
4316
- * Phase 4: DIFF & OUTPUT
4317
- * Compare current buffer with previous
4318
- * Emit minimal ANSI sequences for changes
4551
+ * // Basic usage
4552
+ * const output = renderString(<Summary stats={stats} />)
4553
+ * console.log(output)
4554
+ *
4555
+ * // Custom width
4556
+ * const wide = renderString(<Report />, { width: 120 })
4557
+ *
4558
+ * // Plain text (no ANSI)
4559
+ * const plain = renderString(<Report />, { plain: true })
4560
+ * ```
4319
4561
  */
4320
- const log = createLogger("silvery:render");
4321
- createLogger("@silvery/ag-react");
4562
+ init_buffer();
4563
+ let engineInitialized = false;
4564
+ async function ensureLayoutEngine() {
4565
+ if (engineInitialized || isLayoutEngineInitialized()) return;
4566
+ const { ensureDefaultLayoutEngine } = await import("./layout-engine--drvrWjD.mjs");
4567
+ await ensureDefaultLayoutEngine();
4568
+ engineInitialized = true;
4569
+ }
4322
4570
  /**
4323
- * Execute the full render pipeline.
4571
+ * Render a React element to a string (async version).
4572
+ *
4573
+ * Automatically initializes the layout engine if needed.
4574
+ * Use this when you're not sure if the layout engine is ready.
4324
4575
  *
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.
4576
+ * @param element - React element to render
4577
+ * @param options - Render options (width, height, plain)
4578
+ * @returns Rendered string (with or without ANSI codes)
4579
+ *
4580
+ * @example
4581
+ * ```tsx
4582
+ * const output = await renderString(<Summary stats={stats} />)
4583
+ * console.log(output)
4584
+ * ```
4328
4585
  */
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
4586
+ async function renderString(element, options = {}) {
4587
+ await ensureLayoutEngine();
4588
+ return renderStringSync(element, options);
4589
+ }
4590
+ /**
4591
+ * Render a React element to a string (sync version).
4592
+ *
4593
+ * Requires the layout engine to be already initialized.
4594
+ * Throws if the layout engine is not ready.
4595
+ *
4596
+ * @param element - React element to render
4597
+ * @param options - Render options (width, height, plain)
4598
+ * @returns Rendered string (with or without ANSI codes)
4599
+ *
4600
+ * @example
4601
+ * ```tsx
4602
+ * // After layout engine is initialized
4603
+ * const output = renderStringSync(<Summary stats={stats} />)
4604
+ * console.log(output)
4605
+ * ```
4606
+ */
4607
+ function renderStringSync(element, options = {}) {
4608
+ if (!isLayoutEngineInitialized()) throw new Error("Layout engine not initialized. Use renderString() (async) or initialize with setLayoutEngine().");
4609
+ const { width = 80, height = 24, plain = false, pipelineConfig, trimTrailingWhitespace = true, trimEmptyLines = true, onContentHeight, alwaysStyled = false } = options;
4610
+ let hadReactCommit = false;
4611
+ const container = createContainer(() => {
4612
+ hadReactCommit = true;
4347
4613
  });
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
4614
+ let uncaughtError = null;
4615
+ const onUncaughtError = (error) => {
4616
+ uncaughtError = error;
4370
4617
  };
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
4618
+ const fiberRoot = stringReconciler.createContainer(container, 1, null, false, null, "", onUncaughtError, () => {}, () => {}, null);
4619
+ const mockStdout = {
4620
+ columns: width,
4621
+ rows: height,
4622
+ write: () => true,
4623
+ isTTY: false,
4624
+ on: () => mockStdout,
4625
+ off: () => mockStdout,
4626
+ once: () => mockStdout,
4627
+ removeListener: () => mockStdout,
4628
+ addListener: () => mockStdout
4382
4629
  };
4630
+ const mockTerm = createTerm({ color: plain ? null : "truecolor" });
4631
+ const wrapped = React.createElement(TermContext.Provider, { value: mockTerm }, React.createElement(StdoutContext.Provider, { value: {
4632
+ stdout: mockStdout,
4633
+ write: () => {}
4634
+ } }, React.createElement(StderrContext.Provider, { value: {
4635
+ stderr: process.stderr,
4636
+ write: (data) => {
4637
+ process.stderr.write(data);
4638
+ }
4639
+ } }, element)));
4640
+ withActEnvironment(() => {
4641
+ act(() => {
4642
+ stringReconciler.updateContainerSync(wrapped, fiberRoot, null, null);
4643
+ stringReconciler.flushSyncWork();
4644
+ });
4645
+ });
4646
+ if (uncaughtError) throw uncaughtError instanceof Error ? uncaughtError : new Error(String(uncaughtError));
4647
+ let buffer;
4648
+ let rootNode;
4649
+ const MAX_ITERATIONS = 5;
4650
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
4651
+ hadReactCommit = false;
4652
+ withActEnvironment(() => {
4653
+ act(() => {
4654
+ const root = getContainerRoot(container);
4655
+ rootNode = root;
4656
+ const measurer = pipelineConfig?.measurer;
4657
+ const doRender = () => {
4658
+ const ag = createAg(root, { measurer });
4659
+ ag.layout({
4660
+ cols: width,
4661
+ rows: height
4662
+ });
4663
+ return ag.render();
4664
+ };
4665
+ buffer = (measurer ? runWithMeasurer(measurer, doRender) : doRender()).buffer;
4666
+ });
4667
+ if (!hadReactCommit) act(() => {
4668
+ stringReconciler.flushSyncWork();
4669
+ });
4670
+ });
4671
+ if (!hadReactCommit) break;
4672
+ }
4673
+ if (onContentHeight && rootNode) {
4674
+ let maxBottom = 0;
4675
+ let hasChildren = false;
4676
+ for (const child of rootNode.children) if (child.boxRect) {
4677
+ hasChildren = true;
4678
+ const props = child.props;
4679
+ const mb = props.marginBottom ?? props.marginY ?? props.margin ?? 0;
4680
+ const childBottom = child.boxRect.y + child.boxRect.height + mb;
4681
+ if (childBottom > maxBottom) maxBottom = childBottom;
4682
+ }
4683
+ onContentHeight(hasChildren ? maxBottom : 0);
4684
+ }
4685
+ withActEnvironment(() => {
4686
+ act(() => {
4687
+ stringReconciler.updateContainerSync(null, fiberRoot, null, null);
4688
+ stringReconciler.flushSyncWork();
4689
+ });
4690
+ });
4691
+ return plain && !alwaysStyled ? bufferToText(buffer, {
4692
+ trimTrailingWhitespace,
4693
+ trimEmptyLines
4694
+ }) : bufferToStyledText(buffer, {
4695
+ trimTrailingWhitespace,
4696
+ trimEmptyLines
4697
+ });
4698
+ }
4699
+ /**
4700
+ * Run a function with IS_REACT_ACT_ENVIRONMENT temporarily set to true.
4701
+ * This ensures act() captures forceUpdate/setState from layout notifications.
4702
+ */
4703
+ function withActEnvironment(fn) {
4704
+ const prev = globalThis.IS_REACT_ACT_ENVIRONMENT;
4705
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
4706
+ try {
4707
+ fn();
4708
+ } finally {
4709
+ globalThis.IS_REACT_ACT_ENVIRONMENT = prev;
4710
+ }
4383
4711
  }
4384
4712
  //#endregion
4385
- export { signal as i, createAg as n, _usingCtx as r, executeRender as t };
4713
+ export { _usingCtx as i, renderStringSync as n, createAg as r, renderString as t };
4386
4714
 
4387
- //# sourceMappingURL=pipeline-BmfaZb1O.mjs.map
4715
+ //# sourceMappingURL=render-string-DVfgc8xr.mjs.map