silvery 0.19.2 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +9 -4
  2. package/dist/Text-BRf-59j2.mjs +237 -0
  3. package/dist/Text-BRf-59j2.mjs.map +1 -0
  4. package/dist/ag-BeFC4S2N.mjs +8727 -0
  5. package/dist/ag-BeFC4S2N.mjs.map +1 -0
  6. package/dist/{animation-Cn64yepo.mjs → animation-N8MFybTk.mjs} +2 -2
  7. package/dist/animation-N8MFybTk.mjs.map +1 -0
  8. package/dist/{ansi-CLOitHKx.mjs → ansi-C6Qs1Wn2.mjs} +1 -1
  9. package/dist/{ansi-CLOitHKx.mjs.map → ansi-C6Qs1Wn2.mjs.map} +1 -1
  10. package/dist/{ansi-Cc33mW54.d.mts → ansi-CBkam1ty.d.mts} +1 -1
  11. package/dist/{ansi-Cc33mW54.d.mts.map → ansi-CBkam1ty.d.mts.map} +1 -1
  12. package/dist/ansi-OYRLxxZQ.mjs +10669 -0
  13. package/dist/ansi-OYRLxxZQ.mjs.map +1 -0
  14. package/dist/bound-term-BumfuXXW.d.mts +4902 -0
  15. package/dist/bound-term-BumfuXXW.d.mts.map +1 -0
  16. package/dist/{chunk-Vs_PY4HZ.mjs → chunk-hT5z_Zn9.mjs} +1 -3
  17. package/dist/cli-DG6zsfsS.mjs +4 -0
  18. package/dist/context-BU5LkkIy.mjs.map +1 -1
  19. package/dist/{devtools-DxkSLXDA.mjs → devtools-9zfhpFyG.mjs} +3 -3
  20. package/dist/{devtools-DxkSLXDA.mjs.map → devtools-9zfhpFyG.mjs.map} +1 -1
  21. package/dist/devtools-nmPUmYU_.mjs +2 -0
  22. package/dist/easing-B0oZKDki.d.mts +24 -0
  23. package/dist/easing-B0oZKDki.d.mts.map +1 -0
  24. package/dist/{eta-Bb3RH3wh.mjs → eta-DGOuC8yU.mjs} +5 -1
  25. package/dist/{eta-Bb3RH3wh.mjs.map → eta-DGOuC8yU.mjs.map} +1 -1
  26. package/dist/flexily-zero-adapter-CEJOcbNp.mjs +306 -0
  27. package/dist/flexily-zero-adapter-CEJOcbNp.mjs.map +1 -0
  28. package/dist/{flexily-zero-adapter-CMxXhdOL.mjs → flexily-zero-adapter-D6hcFgrH.mjs} +1 -1
  29. package/dist/image-1nRyKa60.mjs +5960 -0
  30. package/dist/image-1nRyKa60.mjs.map +1 -0
  31. package/dist/{index-D3saHouR.d.mts → index-2E1jYgak.d.mts} +1057 -1133
  32. package/dist/index-2E1jYgak.d.mts.map +1 -0
  33. package/dist/index-Bi4Jdz5g.d.mts +453 -0
  34. package/dist/index-Bi4Jdz5g.d.mts.map +1 -0
  35. package/dist/index-Dg1YaeJb.d.mts +336 -0
  36. package/dist/index-Dg1YaeJb.d.mts.map +1 -0
  37. package/dist/{index-BXslOebb.d.mts → index-DnuadDNL.d.mts} +5750 -4158
  38. package/dist/index-DnuadDNL.d.mts.map +1 -0
  39. package/dist/index.d.mts +8 -5
  40. package/dist/index.d.mts.map +1 -1
  41. package/dist/index.mjs +16 -12
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/layout-engine-Ca_nbtfL.mjs +67 -0
  44. package/dist/layout-engine-Ca_nbtfL.mjs.map +1 -0
  45. package/dist/{layout-engine-B6Cdz1yZ.mjs → layout-engine-CgsoBRIn.mjs} +1 -1
  46. package/dist/layout-signals-Dch2EiCy.mjs +1111 -0
  47. package/dist/layout-signals-Dch2EiCy.mjs.map +1 -0
  48. package/dist/mouse-events-hnbJZRwK.mjs +1071 -0
  49. package/dist/mouse-events-hnbJZRwK.mjs.map +1 -0
  50. package/dist/{multi-progress-DAQC7eap.d.mts → multi-progress-Bg4ngK80.d.mts} +2 -2
  51. package/dist/{multi-progress-DAQC7eap.d.mts.map → multi-progress-Bg4ngK80.d.mts.map} +1 -1
  52. package/dist/{multi-progress-Bq9Oi_WI.mjs → multi-progress-CaXTuL9G.mjs} +3 -3
  53. package/dist/{multi-progress-Bq9Oi_WI.mjs.map → multi-progress-CaXTuL9G.mjs.map} +1 -1
  54. package/dist/{node-BeWlnCPY.mjs → node-BiFu8I9Y.mjs} +4 -4
  55. package/dist/node-BiFu8I9Y.mjs.map +1 -0
  56. package/dist/progress-B_UPy6zk.mjs +675 -0
  57. package/dist/progress-B_UPy6zk.mjs.map +1 -0
  58. package/dist/{progress-bar-CXE5Qfkd.mjs → progress-bar-DmIMPdL0.mjs} +4 -4
  59. package/dist/{progress-bar-CXE5Qfkd.mjs.map → progress-bar-DmIMPdL0.mjs.map} +1 -1
  60. package/dist/reconciler-NBDSEm8k.mjs +2178 -0
  61. package/dist/reconciler-NBDSEm8k.mjs.map +1 -0
  62. package/dist/render-string-B4h4SmK7.mjs +211 -0
  63. package/dist/render-string-B4h4SmK7.mjs.map +1 -0
  64. package/dist/{render-string-CDCeYkS3.mjs → render-string-BntLj7Xq.mjs} +1 -1
  65. package/dist/runtime.d.mts +3 -2
  66. package/dist/runtime.mjs +3 -3
  67. package/dist/{src-B5GjfG7g.mjs → schemes-DYt2ushj.mjs} +23 -1812
  68. package/dist/schemes-DYt2ushj.mjs.map +1 -0
  69. package/dist/{spinner-CGo34vyR.d.mts → spinner-CLgzJ_QF.d.mts} +2 -2
  70. package/dist/{spinner-CGo34vyR.d.mts.map → spinner-CLgzJ_QF.d.mts.map} +1 -1
  71. package/dist/{spinner-CeOmcuw_.mjs → spinner-Py8_-hn9.mjs} +23 -8
  72. package/dist/spinner-Py8_-hn9.mjs.map +1 -0
  73. package/dist/src-B9S_woYc.mjs +25024 -0
  74. package/dist/src-B9S_woYc.mjs.map +1 -0
  75. package/dist/src-CbhWmUnF.mjs +3928 -0
  76. package/dist/src-CbhWmUnF.mjs.map +1 -0
  77. package/dist/src-Dvq-s8iD.mjs +939 -0
  78. package/dist/src-Dvq-s8iD.mjs.map +1 -0
  79. package/dist/src-Oe6x5PrS.mjs +4621 -0
  80. package/dist/src-Oe6x5PrS.mjs.map +1 -0
  81. package/dist/{types-Bk2yw9Qj.mjs → src-WeA_J4BV.mjs} +34 -94
  82. package/dist/src-WeA_J4BV.mjs.map +1 -0
  83. package/dist/steps-DYrzCUCK.d.mts +202 -0
  84. package/dist/steps-DYrzCUCK.d.mts.map +1 -0
  85. package/dist/svg-DhxQkz-O.mjs +255 -0
  86. package/dist/svg-DhxQkz-O.mjs.map +1 -0
  87. package/dist/svg-Hk7lIl4F.d.mts +82 -0
  88. package/dist/svg-Hk7lIl4F.d.mts.map +1 -0
  89. package/dist/term.d.mts +3 -0
  90. package/dist/term.mjs +4 -0
  91. package/dist/theme.d.mts +95 -2
  92. package/dist/theme.d.mts.map +1 -0
  93. package/dist/theme.mjs +4 -3
  94. package/dist/{types-BH_v3iMT.d.mts → types-Bx-XZNbE.d.mts} +2 -15
  95. package/dist/types-Bx-XZNbE.d.mts.map +1 -0
  96. package/dist/ui/animation.d.mts +2 -1
  97. package/dist/ui/animation.mjs +1 -1
  98. package/dist/ui/ansi.d.mts +1 -1
  99. package/dist/ui/ansi.mjs +1 -1
  100. package/dist/ui/cli.d.mts +3 -3
  101. package/dist/ui/cli.mjs +5 -5
  102. package/dist/ui/display.d.mts +1 -1
  103. package/dist/ui/display.mjs.map +1 -1
  104. package/dist/ui/image.d.mts +2 -2
  105. package/dist/ui/image.mjs +2 -2
  106. package/dist/ui/input.d.mts +1 -1
  107. package/dist/ui/input.mjs.map +1 -1
  108. package/dist/ui/progress.d.mts +5 -249
  109. package/dist/ui/progress.mjs +5 -858
  110. package/dist/ui/react.d.mts +1 -1
  111. package/dist/ui/react.mjs +2 -2
  112. package/dist/ui/react.mjs.map +1 -1
  113. package/dist/ui/recording-chrome-react.d.mts +21 -0
  114. package/dist/ui/recording-chrome-react.d.mts.map +1 -0
  115. package/dist/ui/recording-chrome-react.mjs +105 -0
  116. package/dist/ui/recording-chrome-react.mjs.map +1 -0
  117. package/dist/ui/recording-chrome.d.mts +2 -0
  118. package/dist/ui/recording-chrome.mjs +2 -0
  119. package/dist/ui/utils.mjs +1 -1
  120. package/dist/ui/wrappers.d.mts +3 -3
  121. package/dist/ui/wrappers.mjs +2 -2
  122. package/dist/ui.d.mts +7 -6
  123. package/dist/ui.mjs +8 -7
  124. package/dist/{useLatest-Bg2x4bfP.d.mts → useLatest-DC8i7guK.d.mts} +5 -25
  125. package/dist/useLatest-DC8i7guK.d.mts.map +1 -0
  126. package/dist/useLayout-BKsQl2Or.mjs +424 -0
  127. package/dist/useLayout-BKsQl2Or.mjs.map +1 -0
  128. package/dist/{with-text-input-CRfoiFFG.d.mts → with-text-input-DG4f7JII.d.mts} +4 -55
  129. package/dist/with-text-input-DG4f7JII.d.mts.map +1 -0
  130. package/dist/wrapper-D7gNSsgf.mjs +3589 -0
  131. package/dist/wrapper-D7gNSsgf.mjs.map +1 -0
  132. package/dist/{wrappers-UTADQkSY.mjs → wrappers-CypAzrMO.mjs} +19 -161
  133. package/dist/wrappers-CypAzrMO.mjs.map +1 -0
  134. package/dist/yoga-adapter-1ex8r0ws.mjs +2 -0
  135. package/dist/{yoga-adapter-8oRGRw8V.mjs → yoga-adapter-SsEIqMc1.mjs} +28 -2
  136. package/dist/yoga-adapter-SsEIqMc1.mjs.map +1 -0
  137. package/package.json +63 -12
  138. package/dist/animation-Cn64yepo.mjs.map +0 -1
  139. package/dist/cli-BKp0YtBD.mjs +0 -4
  140. package/dist/devtools-9QY4teqI.mjs +0 -2
  141. package/dist/flexily-zero-adapter-BlQa46nr.mjs +0 -3385
  142. package/dist/flexily-zero-adapter-BlQa46nr.mjs.map +0 -1
  143. package/dist/image-CTII5QWI.mjs +0 -477
  144. package/dist/image-CTII5QWI.mjs.map +0 -1
  145. package/dist/index-BXslOebb.d.mts.map +0 -1
  146. package/dist/index-BnA7mNpo.d.mts +0 -175
  147. package/dist/index-BnA7mNpo.d.mts.map +0 -1
  148. package/dist/index-D3saHouR.d.mts.map +0 -1
  149. package/dist/layout-engine-ClUgv6jB.mjs +0 -50
  150. package/dist/layout-engine-ClUgv6jB.mjs.map +0 -1
  151. package/dist/node-BeWlnCPY.mjs.map +0 -1
  152. package/dist/reconciler-Cwgm8hRR.mjs +0 -8459
  153. package/dist/reconciler-Cwgm8hRR.mjs.map +0 -1
  154. package/dist/render-string-Darrg7ku.mjs +0 -5529
  155. package/dist/render-string-Darrg7ku.mjs.map +0 -1
  156. package/dist/spinner-CeOmcuw_.mjs.map +0 -1
  157. package/dist/src-B5GjfG7g.mjs.map +0 -1
  158. package/dist/src-CChwjk0Z.mjs +0 -738
  159. package/dist/src-CChwjk0Z.mjs.map +0 -1
  160. package/dist/src-CF-6UN01.mjs +0 -19434
  161. package/dist/src-CF-6UN01.mjs.map +0 -1
  162. package/dist/src-NCKb8kE5.mjs +0 -2660
  163. package/dist/src-NCKb8kE5.mjs.map +0 -1
  164. package/dist/types-BH_v3iMT.d.mts.map +0 -1
  165. package/dist/types-Bk2yw9Qj.mjs.map +0 -1
  166. package/dist/ui/progress.d.mts.map +0 -1
  167. package/dist/ui/progress.mjs.map +0 -1
  168. package/dist/useLatest-Bg2x4bfP.d.mts.map +0 -1
  169. package/dist/with-text-input-CRfoiFFG.d.mts.map +0 -1
  170. package/dist/wrappers-UTADQkSY.mjs.map +0 -1
  171. package/dist/yoga-adapter-8oRGRw8V.mjs.map +0 -1
  172. package/dist/yoga-adapter-D_CcxSt5.mjs +0 -2
@@ -0,0 +1,3928 @@
1
+ import { a as NodeContext } from "./context-BU5LkkIy.mjs";
2
+ import { f as syncFocusedSignal } from "./layout-signals-Dch2EiCy.mjs";
3
+ import { y as isDirty } from "./reconciler-NBDSEm8k.mjs";
4
+ import { E as createOutputPhase, R as createMeasurer, mt as stripAnsi, r as getInternalStreams, zt as queryKittyKeyboard } from "./ansi-OYRLxxZQ.mjs";
5
+ import { C as findSpatialTarget, D as isFocusable, E as isFocusHidden, S as findFocusableAncestor, T as getTabOrder$1, b as findByTestID, w as getExplicitFocusLink, y as setFocused } from "./mouse-events-hnbJZRwK.mjs";
6
+ import { u as useScrollRect } from "./useLayout-BKsQl2Or.mjs";
7
+ import { j as createTerminalProfile } from "./src-Oe6x5PrS.mjs";
8
+ import "./layout-engine-Ca_nbtfL.mjs";
9
+ import { a as setActiveColorLevel, i as parseColor } from "./ag-BeFC4S2N.mjs";
10
+ import React, { createContext, useContext, useEffect, useLayoutEffect, useRef } from "react";
11
+ import { createLogger } from "loggily";
12
+ //#region packages/ag/src/focus-manager.ts
13
+ function createFocusManager(options) {
14
+ const onFocusChange = options?.onFocusChange;
15
+ let activeElement = null;
16
+ let activeId = null;
17
+ let previousElement = null;
18
+ let previousId = null;
19
+ let focusOrigin = null;
20
+ const scopeStack = [];
21
+ const scopeMemory = {};
22
+ let activeScopeId = null;
23
+ const hookFocusables = [];
24
+ let hookFocusEnabled = true;
25
+ const listeners = /* @__PURE__ */ new Set();
26
+ let snapshot = null;
27
+ /** Counter incremented on every notify(); used by activateScope to detect inner notifications. */
28
+ let notifyCount = 0;
29
+ function notify() {
30
+ snapshot = null;
31
+ notifyCount++;
32
+ for (const listener of listeners) listener();
33
+ }
34
+ function getTestID(node) {
35
+ const props = node.props;
36
+ return typeof props.testID === "string" ? props.testID : null;
37
+ }
38
+ function focus(node, origin = "programmatic") {
39
+ if (activeElement === node) {
40
+ if (focusOrigin !== origin) {
41
+ focusOrigin = origin;
42
+ notify();
43
+ }
44
+ return;
45
+ }
46
+ const oldElement = activeElement;
47
+ previousElement = activeElement;
48
+ previousId = activeId;
49
+ activeElement = node;
50
+ activeId = getTestID(node);
51
+ focusOrigin = origin;
52
+ if (oldElement) {
53
+ setFocused(oldElement, false);
54
+ syncFocusedSignal(oldElement, false);
55
+ }
56
+ setFocused(node, true);
57
+ syncFocusedSignal(node, true);
58
+ if (activeId && scopeStack.length > 0) scopeMemory[scopeStack[scopeStack.length - 1]] = activeId;
59
+ notify();
60
+ onFocusChange?.(oldElement, node, origin);
61
+ }
62
+ function focusById(id, root, origin = "programmatic") {
63
+ const node = findByTestID(root, id);
64
+ if (node) {
65
+ const focusable = findFocusableAncestor(node);
66
+ if (focusable) {
67
+ focus(focusable, origin);
68
+ return;
69
+ }
70
+ }
71
+ if (activeId === id && !activeElement) return;
72
+ const oldElement = activeElement;
73
+ previousElement = activeElement;
74
+ previousId = activeId;
75
+ activeElement = null;
76
+ activeId = id;
77
+ focusOrigin = origin;
78
+ if (oldElement) {
79
+ setFocused(oldElement, false);
80
+ syncFocusedSignal(oldElement, false);
81
+ }
82
+ notify();
83
+ onFocusChange?.(oldElement, null, origin);
84
+ }
85
+ function blur() {
86
+ if (!activeElement && !activeId) return;
87
+ const oldElement = activeElement;
88
+ previousElement = activeElement;
89
+ previousId = activeId;
90
+ activeElement = null;
91
+ activeId = null;
92
+ focusOrigin = null;
93
+ if (oldElement) {
94
+ setFocused(oldElement, false);
95
+ syncFocusedSignal(oldElement, false);
96
+ }
97
+ notify();
98
+ onFocusChange?.(oldElement, null, null);
99
+ }
100
+ /**
101
+ * Set the focused target to a hook-registered id directly (no tree lookup).
102
+ * activeElement becomes null (no backing node), activeId is the hook id.
103
+ */
104
+ function focusVirtualId(id, origin = "programmatic") {
105
+ if (activeId === id && !activeElement) {
106
+ if (focusOrigin !== origin) {
107
+ focusOrigin = origin;
108
+ notify();
109
+ }
110
+ return;
111
+ }
112
+ const oldElement = activeElement;
113
+ previousElement = activeElement;
114
+ previousId = activeId;
115
+ activeElement = null;
116
+ activeId = id;
117
+ focusOrigin = origin;
118
+ if (oldElement) {
119
+ setFocused(oldElement, false);
120
+ syncFocusedSignal(oldElement, false);
121
+ }
122
+ notify();
123
+ onFocusChange?.(oldElement, null, origin);
124
+ }
125
+ function unregisterHookFocusable(id) {
126
+ const idx = hookFocusables.findIndex((f) => f.id === id);
127
+ if (idx === -1) return;
128
+ hookFocusables.splice(idx, 1);
129
+ if (activeId === id && !activeElement) {
130
+ previousId = activeId;
131
+ activeId = null;
132
+ focusOrigin = null;
133
+ notify();
134
+ onFocusChange?.(null, null, null);
135
+ return;
136
+ }
137
+ notify();
138
+ }
139
+ function registerHookFocusable(id, opts = {}) {
140
+ const { isActive = true, autoFocus = false } = opts;
141
+ const existing = hookFocusables.findIndex((f) => f.id === id);
142
+ if (existing !== -1) {
143
+ if (hookFocusables[existing].isActive === isActive && !autoFocus) return () => unregisterHookFocusable(id);
144
+ hookFocusables[existing] = {
145
+ id,
146
+ isActive
147
+ };
148
+ } else hookFocusables.push({
149
+ id,
150
+ isActive
151
+ });
152
+ if (autoFocus && isActive && activeId === null) focusVirtualId(id, "programmatic");
153
+ else notify();
154
+ return () => unregisterHookFocusable(id);
155
+ }
156
+ function setHookFocusableActive(id, isActive) {
157
+ const entry = hookFocusables.find((f) => f.id === id);
158
+ if (!entry) return;
159
+ if (entry.isActive === isActive) return;
160
+ entry.isActive = isActive;
161
+ if (!isActive && activeId === id && !activeElement) {
162
+ previousId = activeId;
163
+ activeId = null;
164
+ focusOrigin = null;
165
+ notify();
166
+ onFocusChange?.(null, null, null);
167
+ return;
168
+ }
169
+ notify();
170
+ }
171
+ function setHookFocusEnabled(enabled) {
172
+ if (hookFocusEnabled === enabled) return;
173
+ hookFocusEnabled = enabled;
174
+ notify();
175
+ }
176
+ /**
177
+ * Check if a node is the given target or contains it as a descendant.
178
+ */
179
+ function subtreeContains(subtreeRoot, target) {
180
+ if (subtreeRoot === target) return true;
181
+ for (const child of subtreeRoot.children) if (subtreeContains(child, target)) return true;
182
+ return false;
183
+ }
184
+ function updateInvalidatesFocusedNode(updatedNode, focusedNode) {
185
+ if (!subtreeContains(updatedNode, focusedNode)) return false;
186
+ let current = focusedNode;
187
+ while (current) {
188
+ if (isFocusHidden(current)) return true;
189
+ if (current === updatedNode) break;
190
+ current = current.parent;
191
+ }
192
+ return updatedNode === focusedNode && !isFocusable(focusedNode);
193
+ }
194
+ /**
195
+ * Handle a subtree being removed from the tree. If the active or previous
196
+ * element lives within the removed subtree, clear the dangling reference.
197
+ * This prevents dead node retention and broken navigation (indexOf → -1).
198
+ */
199
+ function handleSubtreeRemoved(removedRoot) {
200
+ let changed = false;
201
+ if (activeElement && subtreeContains(removedRoot, activeElement)) {
202
+ const oldElement = activeElement;
203
+ setFocused(oldElement, false);
204
+ syncFocusedSignal(oldElement, false);
205
+ previousElement = activeElement;
206
+ previousId = activeId;
207
+ activeElement = null;
208
+ activeId = null;
209
+ focusOrigin = null;
210
+ changed = true;
211
+ onFocusChange?.(oldElement, null, null);
212
+ }
213
+ if (previousElement && subtreeContains(removedRoot, previousElement)) {
214
+ previousElement = null;
215
+ previousId = null;
216
+ changed = true;
217
+ }
218
+ if (changed) notify();
219
+ }
220
+ /**
221
+ * Handle a mounted node update. React can update a focused node from
222
+ * `focusable` to non-focusable without unmounting it; leaving that node as
223
+ * `activeElement` keeps the focused dispatch lane alive and routes later keys
224
+ * into an input owner the app has already deactivated.
225
+ */
226
+ function handleNodeUpdated(updatedNode) {
227
+ let changed = false;
228
+ if (activeElement && updateInvalidatesFocusedNode(updatedNode, activeElement)) {
229
+ const oldElement = activeElement;
230
+ setFocused(oldElement, false);
231
+ syncFocusedSignal(oldElement, false);
232
+ previousElement = activeElement;
233
+ previousId = activeId;
234
+ activeElement = null;
235
+ activeId = null;
236
+ focusOrigin = null;
237
+ changed = true;
238
+ onFocusChange?.(oldElement, null, null);
239
+ }
240
+ if (previousElement && updateInvalidatesFocusedNode(updatedNode, previousElement)) {
241
+ previousElement = null;
242
+ previousId = null;
243
+ changed = true;
244
+ }
245
+ if (changed) notify();
246
+ maybePromoteVirtualFocus(updatedNode);
247
+ }
248
+ /**
249
+ * Promote pending VIRTUAL focus (activeId set, activeElement null) to the
250
+ * real node when a matching focusable target exists under `searchRoot`.
251
+ * Never steals: a real activeElement short-circuits immediately. Resolution
252
+ * mirrors focusById exactly (findByTestID + nearest focusable ancestor), so
253
+ * promoted focus is indistinguishable from a direct hit.
254
+ */
255
+ function maybePromoteVirtualFocus(searchRoot) {
256
+ if (activeId === null || activeElement !== null) return;
257
+ const node = findByTestID(searchRoot, activeId);
258
+ if (!node) return;
259
+ const focusable = findFocusableAncestor(node);
260
+ if (!focusable) return;
261
+ focus(focusable, focusOrigin ?? "programmatic");
262
+ }
263
+ function handleSubtreeAttached(attachedRoot) {
264
+ maybePromoteVirtualFocus(attachedRoot);
265
+ }
266
+ function enterScope(scopeId) {
267
+ scopeStack.push(scopeId);
268
+ notify();
269
+ }
270
+ function exitScope() {
271
+ if (scopeStack.pop() === void 0) return;
272
+ notify();
273
+ }
274
+ function activateScope(scopeId, root) {
275
+ if (activeScopeId && activeId) scopeMemory[activeScopeId] = activeId;
276
+ activeScopeId = scopeId;
277
+ const countBefore = notifyCount;
278
+ const remembered = scopeMemory[scopeId];
279
+ if (remembered) focusById(remembered, root, "programmatic");
280
+ else {
281
+ const scopeNode = findByTestID(root, scopeId);
282
+ if (scopeNode) {
283
+ const order = getTabOrder$1(root, scopeNode);
284
+ if (order.length > 0) focus(order[0], "programmatic");
285
+ }
286
+ }
287
+ if (notifyCount === countBefore) notify();
288
+ }
289
+ function getFocusPath(root) {
290
+ if (!activeElement) return [];
291
+ const path = [];
292
+ let current = activeElement;
293
+ while (current && current !== root.parent) {
294
+ const id = getTestID(current);
295
+ if (id) path.push(id);
296
+ current = current.parent;
297
+ }
298
+ return path;
299
+ }
300
+ function hasFocusWithin(root, testID) {
301
+ if (!activeElement) return false;
302
+ const target = findByTestID(root, testID);
303
+ if (!target) return false;
304
+ let current = activeElement;
305
+ while (current) {
306
+ if (current === target) return true;
307
+ current = current.parent;
308
+ }
309
+ return false;
310
+ }
311
+ /**
312
+ * Resolve the effective scope node for tab navigation.
313
+ * If an explicit scope is provided, use it. Otherwise, if the scopeStack
314
+ * is non-empty, find the topmost scope node in the tree by testID.
315
+ */
316
+ function resolveScope(root, explicitScope) {
317
+ if (explicitScope) return explicitScope;
318
+ if (scopeStack.length > 0) {
319
+ const scopeId = scopeStack[scopeStack.length - 1];
320
+ const scopeNode = findByTestID(root, scopeId);
321
+ if (scopeNode) return scopeNode;
322
+ }
323
+ }
324
+ function buildTabEntries(root, scope) {
325
+ const effectiveScope = resolveScope(root, scope);
326
+ const nodes = getTabOrder$1(root, effectiveScope);
327
+ const entries = nodes.map((node) => ({
328
+ kind: "node",
329
+ node
330
+ }));
331
+ if (!effectiveScope && hookFocusEnabled) {
332
+ const treeIds = new Set(nodes.map((n) => n.props.testID).filter(Boolean));
333
+ for (const entry of hookFocusables) if (entry.isActive && !treeIds.has(entry.id)) entries.push({
334
+ kind: "hook",
335
+ id: entry.id
336
+ });
337
+ }
338
+ return entries;
339
+ }
340
+ function currentTabIndex(entries) {
341
+ if (activeElement) {
342
+ for (let i = 0; i < entries.length; i++) {
343
+ const e = entries[i];
344
+ if (e.kind === "node" && e.node === activeElement) return i;
345
+ }
346
+ return -1;
347
+ }
348
+ if (activeId) for (let i = 0; i < entries.length; i++) {
349
+ const e = entries[i];
350
+ if (e.kind === "hook" && e.id === activeId) return i;
351
+ }
352
+ return -1;
353
+ }
354
+ function focusTabEntry(entry, origin) {
355
+ if (entry.kind === "node") focus(entry.node, origin);
356
+ else focusVirtualId(entry.id, origin);
357
+ }
358
+ function focusNext(root, scope) {
359
+ const entries = buildTabEntries(root, scope);
360
+ if (entries.length === 0) return;
361
+ const currentIndex = currentTabIndex(entries);
362
+ if (currentIndex === -1) {
363
+ focusTabEntry(entries[0], "keyboard");
364
+ return;
365
+ }
366
+ focusTabEntry(entries[(currentIndex + 1) % entries.length], "keyboard");
367
+ }
368
+ function focusPrev(root, scope) {
369
+ const entries = buildTabEntries(root, scope);
370
+ if (entries.length === 0) return;
371
+ const currentIndex = currentTabIndex(entries);
372
+ if (currentIndex === -1) {
373
+ focusTabEntry(entries[entries.length - 1], "keyboard");
374
+ return;
375
+ }
376
+ focusTabEntry(entries[currentIndex <= 0 ? entries.length - 1 : currentIndex - 1], "keyboard");
377
+ }
378
+ function focusDirection(root, direction, layoutFn) {
379
+ if (!activeElement) return;
380
+ const explicitTarget = getExplicitFocusLink(activeElement, direction);
381
+ if (explicitTarget) {
382
+ focusById(explicitTarget, root, "keyboard");
383
+ return;
384
+ }
385
+ const candidates = getTabOrder$1(root);
386
+ const target = findSpatialTarget(activeElement, direction, candidates, layoutFn ?? ((node) => node.scrollRect));
387
+ if (target) focus(target, "keyboard");
388
+ }
389
+ function subscribe(listener) {
390
+ listeners.add(listener);
391
+ return () => {
392
+ listeners.delete(listener);
393
+ };
394
+ }
395
+ function getSnapshot() {
396
+ if (!snapshot) snapshot = {
397
+ activeId,
398
+ previousId,
399
+ focusOrigin,
400
+ scopeStack: [...scopeStack],
401
+ activeScopeId
402
+ };
403
+ return snapshot;
404
+ }
405
+ return {
406
+ get activeElement() {
407
+ return activeElement;
408
+ },
409
+ get activeId() {
410
+ return activeId;
411
+ },
412
+ get previousElement() {
413
+ return previousElement;
414
+ },
415
+ get previousId() {
416
+ return previousId;
417
+ },
418
+ get focusOrigin() {
419
+ return focusOrigin;
420
+ },
421
+ get scopeStack() {
422
+ return [...scopeStack];
423
+ },
424
+ get scopeMemory() {
425
+ return scopeMemory;
426
+ },
427
+ get activeScopeId() {
428
+ return activeScopeId;
429
+ },
430
+ focus,
431
+ focusById,
432
+ focusVirtualId,
433
+ blur,
434
+ handleSubtreeRemoved,
435
+ handleNodeUpdated,
436
+ handleSubtreeAttached,
437
+ enterScope,
438
+ exitScope,
439
+ activateScope,
440
+ getFocusPath,
441
+ hasFocusWithin,
442
+ focusNext,
443
+ focusPrev,
444
+ focusDirection,
445
+ subscribe,
446
+ getSnapshot,
447
+ registerHookFocusable,
448
+ setHookFocusableActive,
449
+ get hasHookFocusables() {
450
+ return hookFocusables.length > 0;
451
+ },
452
+ get hookFocusEnabled() {
453
+ return hookFocusEnabled;
454
+ },
455
+ setHookFocusEnabled
456
+ };
457
+ }
458
+ //#endregion
459
+ //#region packages/ag-term/src/history-buffer.ts
460
+ /**
461
+ * Ring buffer for frozen scrollback items.
462
+ *
463
+ * Each item represents a rendered list entry (card, row, etc.) with its
464
+ * ANSI snapshot, split rows, and plain-text rows for searching.
465
+ * When total rows or cached ANSI bytes exceed capacity, the oldest items are
466
+ * evicted. The byte count tracks the serialized string payload; it is a
467
+ * deterministic cache budget, not an exact process heap measurement.
468
+ */
469
+ function createHistoryItem(key, ansi, width) {
470
+ const rows = ansi.split("\n");
471
+ return {
472
+ key,
473
+ ansi,
474
+ rows,
475
+ plainTextRows: rows.map((r) => stripAnsi(r)),
476
+ width
477
+ };
478
+ }
479
+ function createHistoryBuffer(options = 1e4) {
480
+ const capacity = typeof options === "number" ? options : options.capacityRows ?? 1e4;
481
+ const capacityBytes = typeof options === "number" ? null : normalizeCapacityBytes(options.capacityBytes);
482
+ let items = [];
483
+ let _totalRows = 0;
484
+ let _totalBytes = 0;
485
+ function evict() {
486
+ while (items.length > 0 && (_totalRows > capacity || capacityBytes !== null && _totalBytes > capacityBytes)) {
487
+ const removed = items.shift();
488
+ _totalRows -= removed.rows.length;
489
+ _totalBytes -= estimateHistoryItemBytes(removed);
490
+ }
491
+ }
492
+ /** Walk items to find which item contains the given document row. */
493
+ function resolveRow(row) {
494
+ if (row < 0 || row >= _totalRows) return null;
495
+ let cumulative = 0;
496
+ for (let i = 0; i < items.length; i++) {
497
+ const itemRows = items[i].rows.length;
498
+ if (row < cumulative + itemRows) return {
499
+ itemIndex: i,
500
+ localRow: row - cumulative
501
+ };
502
+ cumulative += itemRows;
503
+ }
504
+ return null;
505
+ }
506
+ return {
507
+ push(item) {
508
+ items.push(item);
509
+ _totalRows += item.rows.length;
510
+ _totalBytes += estimateHistoryItemBytes(item);
511
+ evict();
512
+ },
513
+ get totalRows() {
514
+ return _totalRows;
515
+ },
516
+ get totalBytes() {
517
+ return _totalBytes;
518
+ },
519
+ get itemCount() {
520
+ return items.length;
521
+ },
522
+ get capacity() {
523
+ return capacity;
524
+ },
525
+ get capacityBytes() {
526
+ return capacityBytes;
527
+ },
528
+ getRows(startRow, count) {
529
+ const result = [];
530
+ for (let r = startRow; r < startRow + count; r++) {
531
+ const resolved = resolveRow(r);
532
+ if (resolved) result.push(items[resolved.itemIndex].rows[resolved.localRow]);
533
+ else result.push("");
534
+ }
535
+ return result;
536
+ },
537
+ getPlainTextRows(startRow, count) {
538
+ const result = [];
539
+ for (let r = startRow; r < startRow + count; r++) {
540
+ const resolved = resolveRow(r);
541
+ if (resolved) result.push(items[resolved.itemIndex].plainTextRows[resolved.localRow]);
542
+ else result.push("");
543
+ }
544
+ return result;
545
+ },
546
+ search(query) {
547
+ if (!query) return [];
548
+ const lowerQuery = query.toLowerCase();
549
+ const matches = [];
550
+ let rowOffset = 0;
551
+ for (const item of items) {
552
+ for (let r = 0; r < item.plainTextRows.length; r++) if (item.plainTextRows[r].toLowerCase().includes(lowerQuery)) matches.push(rowOffset + r);
553
+ rowOffset += item.rows.length;
554
+ }
555
+ return matches;
556
+ },
557
+ getItemAtRow(row) {
558
+ const resolved = resolveRow(row);
559
+ if (!resolved) return null;
560
+ return {
561
+ item: items[resolved.itemIndex],
562
+ localRow: resolved.localRow
563
+ };
564
+ },
565
+ clear() {
566
+ items = [];
567
+ _totalRows = 0;
568
+ _totalBytes = 0;
569
+ }
570
+ };
571
+ }
572
+ function normalizeCapacityBytes(value) {
573
+ return Number.isFinite(value) && value !== void 0 && value > 0 ? Math.floor(value) : null;
574
+ }
575
+ const utf8Encoder = new TextEncoder();
576
+ function estimateHistoryItemBytes(item) {
577
+ return utf8Encoder.encode(item.ansi).byteLength;
578
+ }
579
+ //#endregion
580
+ //#region packages/ag-term/src/list-document.ts
581
+ function createListDocument(history, getLiveItems) {
582
+ function liveRowCount() {
583
+ let total = 0;
584
+ for (const block of getLiveItems()) total += block.rows.length;
585
+ return total;
586
+ }
587
+ return {
588
+ get totalRows() {
589
+ return history.totalRows + liveRowCount();
590
+ },
591
+ get frozenRows() {
592
+ return history.totalRows;
593
+ },
594
+ get liveRows() {
595
+ return liveRowCount();
596
+ },
597
+ getRows(startRow, count) {
598
+ const frozen = history.totalRows;
599
+ const result = [];
600
+ for (let r = startRow; r < startRow + count; r++) if (r < 0 || r >= this.totalRows) result.push("");
601
+ else if (r < frozen) result.push(...history.getRows(r, 1));
602
+ else {
603
+ let liveRow = r - frozen;
604
+ let found = false;
605
+ for (const block of getLiveItems()) {
606
+ if (liveRow < block.rows.length) {
607
+ result.push(block.rows[liveRow]);
608
+ found = true;
609
+ break;
610
+ }
611
+ liveRow -= block.rows.length;
612
+ }
613
+ if (!found) result.push("");
614
+ }
615
+ return result;
616
+ },
617
+ getPlainTextRows(startRow, count) {
618
+ const frozen = history.totalRows;
619
+ const result = [];
620
+ for (let r = startRow; r < startRow + count; r++) if (r < 0 || r >= this.totalRows) result.push("");
621
+ else if (r < frozen) result.push(...history.getPlainTextRows(r, 1));
622
+ else {
623
+ let liveRow = r - frozen;
624
+ let found = false;
625
+ for (const block of getLiveItems()) {
626
+ if (liveRow < block.plainTextRows.length) {
627
+ result.push(block.plainTextRows[liveRow]);
628
+ found = true;
629
+ break;
630
+ }
631
+ liveRow -= block.plainTextRows.length;
632
+ }
633
+ if (!found) result.push("");
634
+ }
635
+ return result;
636
+ },
637
+ getSource(row) {
638
+ const frozen = history.totalRows;
639
+ if (row < 0 || row >= this.totalRows) return null;
640
+ if (row < frozen) {
641
+ const hit = history.getItemAtRow(row);
642
+ if (!hit) return null;
643
+ return {
644
+ type: "frozen",
645
+ itemKey: hit.item.key,
646
+ localRow: hit.localRow
647
+ };
648
+ }
649
+ const liveItems = getLiveItems();
650
+ let liveRow = row - frozen;
651
+ for (const block of liveItems) {
652
+ if (liveRow < block.rows.length) return {
653
+ type: "live",
654
+ itemIndex: block.itemIndex,
655
+ localRow: liveRow
656
+ };
657
+ liveRow -= block.rows.length;
658
+ }
659
+ return null;
660
+ },
661
+ search(query) {
662
+ if (!query) return [];
663
+ const lowerQuery = query.toLowerCase();
664
+ const matches = [];
665
+ const frozen = history.totalRows;
666
+ const frozenRowMatches = history.search(query);
667
+ for (const row of frozenRowMatches) {
668
+ const line = history.getPlainTextRows(row, 1)[0].toLowerCase();
669
+ let pos = line.indexOf(lowerQuery);
670
+ while (pos !== -1) {
671
+ matches.push({
672
+ row,
673
+ startCol: pos,
674
+ endCol: pos + query.length
675
+ });
676
+ pos = line.indexOf(lowerQuery, pos + 1);
677
+ }
678
+ }
679
+ let rowOffset = 0;
680
+ for (const block of getLiveItems()) {
681
+ for (let i = 0; i < block.plainTextRows.length; i++) {
682
+ const line = block.plainTextRows[i].toLowerCase();
683
+ let pos = line.indexOf(lowerQuery);
684
+ while (pos !== -1) {
685
+ matches.push({
686
+ row: frozen + rowOffset + i,
687
+ startCol: pos,
688
+ endCol: pos + query.length
689
+ });
690
+ pos = line.indexOf(lowerQuery, pos + 1);
691
+ }
692
+ }
693
+ rowOffset += block.plainTextRows.length;
694
+ }
695
+ return matches;
696
+ }
697
+ };
698
+ }
699
+ //#endregion
700
+ //#region packages/ag-term/src/text-surface.ts
701
+ function createTextSurface(config) {
702
+ const listeners = /* @__PURE__ */ new Set();
703
+ function notify() {
704
+ for (const fn of listeners) fn();
705
+ }
706
+ return {
707
+ get id() {
708
+ return config.id;
709
+ },
710
+ get document() {
711
+ return config.document;
712
+ },
713
+ getText(startRow, startCol, endRow, endCol) {
714
+ const rows = config.document.getRows(startRow, endRow - startRow + 1);
715
+ const lines = [];
716
+ for (let i = 0; i < rows.length; i++) {
717
+ const plain = stripAnsi(rows[i]);
718
+ const row = startRow + i;
719
+ if (row === startRow && row === endRow) lines.push(plain.slice(startCol, endCol));
720
+ else if (row === startRow) lines.push(plain.slice(startCol));
721
+ else if (row === endRow) lines.push(plain.slice(0, endCol));
722
+ else lines.push(plain);
723
+ }
724
+ return lines.join("\n");
725
+ },
726
+ search(query) {
727
+ return config.document.search(query);
728
+ },
729
+ hitTest(viewportRow, viewportCol) {
730
+ const docRow = config.viewportToDocument(viewportRow);
731
+ if (docRow < 0 || docRow >= config.document.totalRows) return null;
732
+ return {
733
+ row: docRow,
734
+ col: viewportCol
735
+ };
736
+ },
737
+ reveal(row) {
738
+ config.onReveal(row);
739
+ notify();
740
+ },
741
+ notifyContentChange() {
742
+ notify();
743
+ },
744
+ subscribe(listener) {
745
+ listeners.add(listener);
746
+ return () => {
747
+ listeners.delete(listener);
748
+ };
749
+ },
750
+ get capabilities() {
751
+ return config.capabilities;
752
+ }
753
+ };
754
+ }
755
+ //#endregion
756
+ //#region packages/ag-term/src/viewport-compositor.ts
757
+ function composeViewport(config) {
758
+ const { history, viewportHeight, scrollOffset } = config;
759
+ const totalHistory = history.totalRows;
760
+ if (scrollOffset <= 0 || totalHistory === 0) return {
761
+ overlayRows: [],
762
+ overlayRowCount: 0,
763
+ liveRowsVisible: viewportHeight,
764
+ isScrolledUp: false,
765
+ totalHeight: totalHistory + viewportHeight
766
+ };
767
+ const startRow = Math.max(0, totalHistory - Math.min(scrollOffset, totalHistory));
768
+ const rowsToShow = Math.min(viewportHeight, totalHistory - startRow);
769
+ return {
770
+ overlayRows: history.getRows(startRow, rowsToShow),
771
+ overlayRowCount: rowsToShow,
772
+ liveRowsVisible: viewportHeight - rowsToShow,
773
+ isScrolledUp: true,
774
+ totalHeight: totalHistory + viewportHeight
775
+ };
776
+ }
777
+ //#endregion
778
+ //#region packages/ag-term/src/search-overlay.ts
779
+ /**
780
+ * Find all case-insensitive occurrences of `query` inside `text` and return
781
+ * their character offsets. Empty query or empty text returns `[]`.
782
+ *
783
+ * This is the canonical silvery search-match algorithm. The same logic runs
784
+ * inside `ListView`'s registered Searchable (to find matches across items)
785
+ * and is exposed to consumers that render multi-segment items — a LogRow
786
+ * whose searchable text is a concatenation of field values, but whose visual
787
+ * rendering splits those fields across separate Text nodes, needs per-segment
788
+ * ranges to highlight without re-implementing the semantics.
789
+ *
790
+ * Ranges are returned in ascending `start` order; overlapping matches are
791
+ * not produced (`indexOf(..., start = last.start + 1)` advances past each
792
+ * match start, not past its full length, preserving overlapping runs of
793
+ * short queries — e.g. `"aa"` in `"aaaa"` yields `[0..2], [1..3], [2..4]`).
794
+ *
795
+ * Offsets are CHARACTER offsets into the input string. They are not column
796
+ * offsets — multi-column wide glyphs are counted as one character here, the
797
+ * same way the SearchProvider's match-row computation does.
798
+ */
799
+ function computeMatchRanges(text, query) {
800
+ if (query === "" || text === "") return [];
801
+ const lowerText = text.toLowerCase();
802
+ const lowerQuery = query.toLowerCase();
803
+ const ranges = [];
804
+ let cursor = 0;
805
+ while (cursor < lowerText.length) {
806
+ const found = lowerText.indexOf(lowerQuery, cursor);
807
+ if (found === -1) break;
808
+ ranges.push({
809
+ start: found,
810
+ end: found + query.length
811
+ });
812
+ cursor = found + 1;
813
+ }
814
+ return ranges;
815
+ }
816
+ function createSearchState() {
817
+ return {
818
+ active: false,
819
+ query: "",
820
+ matches: [],
821
+ currentMatch: -1,
822
+ cursorPosition: 0
823
+ };
824
+ }
825
+ /**
826
+ * Update search state. searchFn is called when query changes to find matches.
827
+ */
828
+ function searchUpdate(action, state, searchFn) {
829
+ switch (action.type) {
830
+ case "open": return [{
831
+ ...state,
832
+ active: true,
833
+ query: "",
834
+ matches: [],
835
+ currentMatch: -1,
836
+ cursorPosition: 0
837
+ }, [{ type: "render" }]];
838
+ case "close": return [{
839
+ ...state,
840
+ active: false,
841
+ query: "",
842
+ cursorPosition: 0
843
+ }, [{ type: "render" }]];
844
+ case "input": {
845
+ const query = state.query.slice(0, state.cursorPosition) + action.char + state.query.slice(state.cursorPosition);
846
+ const cursorPosition = state.cursorPosition + 1;
847
+ const matches = searchFn ? searchFn(query) : [];
848
+ const currentMatch = matches.length > 0 ? 0 : -1;
849
+ const effects = [{ type: "render" }];
850
+ if (currentMatch >= 0) effects.push({
851
+ type: "scrollTo",
852
+ row: matches[0].row
853
+ });
854
+ return [{
855
+ ...state,
856
+ query,
857
+ cursorPosition,
858
+ matches,
859
+ currentMatch
860
+ }, effects];
861
+ }
862
+ case "backspace": {
863
+ if (state.cursorPosition === 0) return [state, []];
864
+ const query = state.query.slice(0, state.cursorPosition - 1) + state.query.slice(state.cursorPosition);
865
+ const cursorPosition = state.cursorPosition - 1;
866
+ const matches = searchFn ? searchFn(query) : [];
867
+ const currentMatch = matches.length > 0 ? 0 : -1;
868
+ const effects = [{ type: "render" }];
869
+ if (currentMatch >= 0) effects.push({
870
+ type: "scrollTo",
871
+ row: matches[0].row
872
+ });
873
+ return [{
874
+ ...state,
875
+ query,
876
+ cursorPosition,
877
+ matches,
878
+ currentMatch
879
+ }, effects];
880
+ }
881
+ case "nextMatch": {
882
+ if (state.matches.length === 0) return [state, []];
883
+ const currentMatch = (state.currentMatch + 1) % state.matches.length;
884
+ return [{
885
+ ...state,
886
+ currentMatch
887
+ }, [{
888
+ type: "scrollTo",
889
+ row: state.matches[currentMatch].row
890
+ }, { type: "render" }]];
891
+ }
892
+ case "prevMatch": {
893
+ if (state.matches.length === 0) return [state, []];
894
+ const currentMatch = (state.currentMatch - 1 + state.matches.length) % state.matches.length;
895
+ return [{
896
+ ...state,
897
+ currentMatch
898
+ }, [{
899
+ type: "scrollTo",
900
+ row: state.matches[currentMatch].row
901
+ }, { type: "render" }]];
902
+ }
903
+ case "cursorLeft":
904
+ if (state.cursorPosition === 0) return [state, []];
905
+ return [{
906
+ ...state,
907
+ cursorPosition: state.cursorPosition - 1
908
+ }, []];
909
+ case "cursorRight":
910
+ if (state.cursorPosition >= state.query.length) return [state, []];
911
+ return [{
912
+ ...state,
913
+ cursorPosition: state.cursorPosition + 1
914
+ }, []];
915
+ case "cursorToStart":
916
+ if (state.cursorPosition === 0) return [state, []];
917
+ return [{
918
+ ...state,
919
+ cursorPosition: 0
920
+ }, []];
921
+ case "cursorToEnd":
922
+ if (state.cursorPosition >= state.query.length) return [state, []];
923
+ return [{
924
+ ...state,
925
+ cursorPosition: state.query.length
926
+ }, []];
927
+ case "deleteWordBack": {
928
+ if (state.cursorPosition === 0) return [state, []];
929
+ let pos = state.cursorPosition;
930
+ while (pos > 0 && /\s/.test(state.query[pos - 1] ?? "")) pos--;
931
+ while (pos > 0 && !/\s/.test(state.query[pos - 1] ?? "")) pos--;
932
+ const query = state.query.slice(0, pos) + state.query.slice(state.cursorPosition);
933
+ const cursorPosition = pos;
934
+ const matches = searchFn ? searchFn(query) : [];
935
+ const currentMatch = matches.length > 0 ? 0 : -1;
936
+ const effects = [{ type: "render" }];
937
+ if (currentMatch >= 0) effects.push({
938
+ type: "scrollTo",
939
+ row: matches[0].row
940
+ });
941
+ return [{
942
+ ...state,
943
+ query,
944
+ cursorPosition,
945
+ matches,
946
+ currentMatch
947
+ }, effects];
948
+ }
949
+ case "deleteToStart": {
950
+ if (state.cursorPosition === 0) return [state, []];
951
+ const query = state.query.slice(state.cursorPosition);
952
+ const cursorPosition = 0;
953
+ const matches = searchFn ? searchFn(query) : [];
954
+ const currentMatch = matches.length > 0 ? 0 : -1;
955
+ const effects = [{ type: "render" }];
956
+ if (currentMatch >= 0) effects.push({
957
+ type: "scrollTo",
958
+ row: matches[0].row
959
+ });
960
+ return [{
961
+ ...state,
962
+ query,
963
+ cursorPosition,
964
+ matches,
965
+ currentMatch
966
+ }, effects];
967
+ }
968
+ }
969
+ }
970
+ /**
971
+ * Render the search bar as a plain (no ANSI) string padded to `cols` width.
972
+ * Format: " / query [2/15]" or " / query [no matches]" — padded with spaces.
973
+ *
974
+ * Caller decides how to display it — wrap with `\x1b[7m...\x1b[27m` for the
975
+ * legacy overlay path, or stamp into a buffer's cells with the inverse
976
+ * attribute for the compose+apply path
977
+ * (see `applySearchBarToPaintBuffer` in `runtime/renderer.ts`).
978
+ */
979
+ function renderSearchBarPlain(state, cols) {
980
+ const prefix = " / ";
981
+ const matchInfo = state.matches.length > 0 ? ` [${state.currentMatch + 1}/${state.matches.length}]` : state.query ? " [no matches]" : "";
982
+ return (prefix + state.query + matchInfo).padEnd(cols);
983
+ }
984
+ //#endregion
985
+ //#region packages/ag-term/src/pane-manager.ts
986
+ /** Create a single-pane layout */
987
+ function createLeaf(id) {
988
+ return {
989
+ type: "leaf",
990
+ id
991
+ };
992
+ }
993
+ /** Get all leaf pane IDs in depth-first left-to-right order */
994
+ function getPaneIds(layout) {
995
+ if (layout.type === "leaf") return [layout.id];
996
+ return [...getPaneIds(layout.first), ...getPaneIds(layout.second)];
997
+ }
998
+ /** Find the next/previous pane in tab order (depth-first left-to-right) */
999
+ function getTabOrder(layout) {
1000
+ return getPaneIds(layout);
1001
+ }
1002
+ /**
1003
+ * Split a pane into two. Returns new layout tree with the target pane split.
1004
+ * The original pane becomes the first child; the new pane becomes the second.
1005
+ */
1006
+ function splitPane(layout, targetPaneId, direction, newPaneId, ratio = .5) {
1007
+ const clampedRatio = clampRatio(ratio);
1008
+ if (layout.type === "leaf") {
1009
+ if (layout.id === targetPaneId) return {
1010
+ type: "split",
1011
+ direction,
1012
+ ratio: clampedRatio,
1013
+ first: {
1014
+ type: "leaf",
1015
+ id: targetPaneId
1016
+ },
1017
+ second: {
1018
+ type: "leaf",
1019
+ id: newPaneId
1020
+ }
1021
+ };
1022
+ return layout;
1023
+ }
1024
+ const newFirst = splitPane(layout.first, targetPaneId, direction, newPaneId, ratio);
1025
+ const newSecond = splitPane(layout.second, targetPaneId, direction, newPaneId, ratio);
1026
+ if (newFirst === layout.first && newSecond === layout.second) return layout;
1027
+ return {
1028
+ ...layout,
1029
+ first: newFirst,
1030
+ second: newSecond
1031
+ };
1032
+ }
1033
+ /**
1034
+ * Remove a pane from the layout. The sibling takes the full space.
1035
+ * Returns null if the removed pane was the last one.
1036
+ */
1037
+ function removePane(layout, paneId) {
1038
+ if (layout.type === "leaf") return layout.id === paneId ? null : layout;
1039
+ if (layout.first.type === "leaf" && layout.first.id === paneId) return layout.second;
1040
+ if (layout.second.type === "leaf" && layout.second.id === paneId) return layout.first;
1041
+ const newFirst = removePane(layout.first, paneId);
1042
+ const newSecond = removePane(layout.second, paneId);
1043
+ if (newFirst === null) return newSecond;
1044
+ if (newSecond === null) return newFirst;
1045
+ if (newFirst === layout.first && newSecond === layout.second) return layout;
1046
+ return {
1047
+ ...layout,
1048
+ first: newFirst,
1049
+ second: newSecond
1050
+ };
1051
+ }
1052
+ /** Swap two panes' positions in the layout */
1053
+ function swapPanes(layout, paneId1, paneId2) {
1054
+ if (layout.type === "leaf") {
1055
+ if (layout.id === paneId1) return {
1056
+ type: "leaf",
1057
+ id: paneId2
1058
+ };
1059
+ if (layout.id === paneId2) return {
1060
+ type: "leaf",
1061
+ id: paneId1
1062
+ };
1063
+ return layout;
1064
+ }
1065
+ const newFirst = swapPanes(layout.first, paneId1, paneId2);
1066
+ const newSecond = swapPanes(layout.second, paneId1, paneId2);
1067
+ if (newFirst === layout.first && newSecond === layout.second) return layout;
1068
+ return {
1069
+ ...layout,
1070
+ first: newFirst,
1071
+ second: newSecond
1072
+ };
1073
+ }
1074
+ /**
1075
+ * Resize a split: adjust the ratio of the nearest ancestor split
1076
+ * that contains the target pane as its first child.
1077
+ * Positive delta = grow (first child gets more), negative = shrink.
1078
+ */
1079
+ function resizeSplit(layout, paneId, delta) {
1080
+ if (layout.type === "leaf") return layout;
1081
+ if (getPaneIds(layout.first).includes(paneId)) {
1082
+ const newRatio = clampRatio(layout.ratio + delta);
1083
+ if (newRatio === layout.ratio) return layout;
1084
+ return {
1085
+ ...layout,
1086
+ ratio: newRatio
1087
+ };
1088
+ }
1089
+ if (getPaneIds(layout.second).includes(paneId)) {
1090
+ const newRatio = clampRatio(layout.ratio - delta);
1091
+ if (newRatio === layout.ratio) return layout;
1092
+ return {
1093
+ ...layout,
1094
+ ratio: newRatio
1095
+ };
1096
+ }
1097
+ return layout;
1098
+ }
1099
+ /**
1100
+ * Find the pane adjacent to the given pane in a direction.
1101
+ *
1102
+ * For left/right: looks for siblings in horizontal splits.
1103
+ * For up/down: looks for siblings in vertical splits.
1104
+ *
1105
+ * Returns null if no adjacent pane exists in that direction.
1106
+ */
1107
+ function findAdjacentPane(layout, paneId, direction) {
1108
+ const path = findPath(layout, paneId);
1109
+ if (!path) return null;
1110
+ const splitDirection = direction === "left" || direction === "right" ? "horizontal" : "vertical";
1111
+ const goToSecond = direction === "right" || direction === "down";
1112
+ for (let i = path.length - 1; i >= 0; i--) {
1113
+ const step = path[i];
1114
+ if (step.node.type !== "split") continue;
1115
+ if (step.node.direction !== splitDirection) continue;
1116
+ if (step.side === "first" && goToSecond) return firstLeaf(step.node.second);
1117
+ if (step.side === "second" && !goToSecond) return lastLeaf(step.node.first);
1118
+ }
1119
+ return null;
1120
+ }
1121
+ function clampRatio(ratio) {
1122
+ return Math.max(.1, Math.min(.9, ratio));
1123
+ }
1124
+ /** Get the first (leftmost/topmost) leaf ID */
1125
+ function firstLeaf(node) {
1126
+ if (node.type === "leaf") return node.id;
1127
+ return firstLeaf(node.first);
1128
+ }
1129
+ /** Get the last (rightmost/bottommost) leaf ID */
1130
+ function lastLeaf(node) {
1131
+ if (node.type === "leaf") return node.id;
1132
+ return lastLeaf(node.second);
1133
+ }
1134
+ /** Find the path from root to a leaf, recording which side we took at each split */
1135
+ function findPath(layout, paneId) {
1136
+ if (layout.type === "leaf") return layout.id === paneId ? [] : null;
1137
+ const firstPath = findPath(layout.first, paneId);
1138
+ if (firstPath !== null) return [{
1139
+ node: layout,
1140
+ side: "first"
1141
+ }, ...firstPath];
1142
+ const secondPath = findPath(layout.second, paneId);
1143
+ if (secondPath !== null) return [{
1144
+ node: layout,
1145
+ side: "second"
1146
+ }, ...secondPath];
1147
+ return null;
1148
+ }
1149
+ //#endregion
1150
+ //#region packages/ag-term/src/scroll-region.ts
1151
+ /**
1152
+ * Terminal scroll region (DECSTBM) utilities.
1153
+ *
1154
+ * Scroll regions tell the terminal to natively scroll content within
1155
+ * a defined row range, which is faster than re-rendering all cells.
1156
+ *
1157
+ * DECSTBM (DEC Set Top and Bottom Margins) is supported by most modern
1158
+ * terminal emulators: xterm, iTerm2, Kitty, Ghostty, WezTerm, etc.
1159
+ */
1160
+ const ESC = "\x1B";
1161
+ /** Set terminal scroll region (1-indexed top and bottom rows). */
1162
+ function setScrollRegion(stdout, top, bottom) {
1163
+ stdout.write(`${ESC}[${top};${bottom}r`);
1164
+ }
1165
+ /** Reset scroll region to full terminal. */
1166
+ function resetScrollRegion(stdout) {
1167
+ stdout.write(`${ESC}[r`);
1168
+ }
1169
+ /** Scroll content up by N lines within the current scroll region. */
1170
+ function scrollUp(stdout, lines = 1) {
1171
+ stdout.write(`${ESC}[${lines}S`);
1172
+ }
1173
+ /** Scroll content down by N lines within the current scroll region. */
1174
+ function scrollDown(stdout, lines = 1) {
1175
+ stdout.write(`${ESC}[${lines}T`);
1176
+ }
1177
+ /** Move cursor to a specific position (1-indexed row and column). */
1178
+ function moveCursor(stdout, row, col) {
1179
+ stdout.write(`${ESC}[${row};${col}H`);
1180
+ }
1181
+ /**
1182
+ * Detect if the terminal likely supports DECSTBM scroll regions.
1183
+ *
1184
+ * Most modern terminals do (xterm, iTerm2, Kitty, Ghostty, WezTerm, etc.)
1185
+ * but some (e.g., Linux console) may not handle them correctly.
1186
+ *
1187
+ * Pass `emulator` (via `term.emulator` or a test fixture) when in scope.
1188
+ * Without it, this falls back to {@link createTerminalProfile} — the canonical
1189
+ * single-source-of-truth entry point in `@silvery/ansi/profile`. Direct
1190
+ * reads of terminal-signal env vars (TERM / TERM_PROGRAM / …) are banned
1191
+ * outside that module — see `scripts/lint-env-reads.ts`.
1192
+ */
1193
+ function supportsScrollRegions(emulator) {
1194
+ const resolved = emulator ?? createTerminalProfile().emulator;
1195
+ const term = resolved.TERM;
1196
+ const termProgram = resolved.program;
1197
+ if (termProgram === "iTerm.app" || termProgram === "WezTerm" || termProgram === "ghostty" || termProgram === "Ghostty" || termProgram === "vscode") return true;
1198
+ if (term.includes("xterm") || term.includes("screen") || term.includes("tmux") || term.includes("kitty")) return true;
1199
+ if (term === "linux") return false;
1200
+ return term !== "";
1201
+ }
1202
+ //#endregion
1203
+ //#region packages/ag-term/src/scroll-utils.ts
1204
+ /**
1205
+ * Scroll Utilities
1206
+ *
1207
+ * Shared functions for edge-based scrolling behavior across VirtualList,
1208
+ * HorizontalVirtualList, and other scroll-aware components.
1209
+ */
1210
+ /**
1211
+ * Calculate edge-based scroll offset.
1212
+ *
1213
+ * Only scrolls when cursor approaches the edge of the visible area.
1214
+ * This provides smoother scrolling by starting to scroll before hitting
1215
+ * the absolute edge, maintaining context around the selected item.
1216
+ *
1217
+ * ## Algorithm
1218
+ *
1219
+ * The viewport is divided into zones:
1220
+ * ```
1221
+ * |<padding>|<------ safe zone ------>|<padding>|
1222
+ * | scroll | no scroll needed | scroll |
1223
+ * | if < | | if > |
1224
+ * ```
1225
+ *
1226
+ * When the selected item enters a padding zone, the viewport scrolls
1227
+ * to keep the item visible with margin.
1228
+ *
1229
+ * ## Asymmetry Note
1230
+ *
1231
+ * The +1 in the "scroll down/right" case is intentional:
1232
+ * - Offset points to the TOP/LEFT of the viewport
1233
+ * - We want the selected item to be `padding` items from the BOTTOM/RIGHT
1234
+ * - Formula: `selectedIndex - visibleCount + padding + 1`
1235
+ *
1236
+ * Example: visibleCount=10, padding=2, selectedIndex=15
1237
+ * offset = 15 - 10 + 2 + 1 = 8
1238
+ * viewport shows items 8-17, selected item 15 is at position 7 (2 from bottom)
1239
+ *
1240
+ * @param selectedIndex - Currently selected item index
1241
+ * @param currentOffset - Current scroll offset (topmost/leftmost visible item)
1242
+ * @param visibleCount - Number of items visible in viewport
1243
+ * @param totalCount - Total number of items
1244
+ * @param padding - Items to keep visible before/after cursor (default: 1)
1245
+ * @returns New scroll offset
1246
+ */
1247
+ function calcEdgeBasedScrollOffset(selectedIndex, currentOffset, visibleCount, totalCount, padding = 1) {
1248
+ if (totalCount <= visibleCount) return 0;
1249
+ const effectivePadding = padding * 2 >= visibleCount ? 0 : padding;
1250
+ const visibleStart = currentOffset;
1251
+ const visibleEnd = currentOffset + visibleCount - 1;
1252
+ const paddedStart = visibleStart + effectivePadding;
1253
+ const paddedEnd = visibleEnd - effectivePadding;
1254
+ let newOffset = currentOffset;
1255
+ if (selectedIndex < paddedStart) newOffset = Math.max(0, selectedIndex - effectivePadding);
1256
+ else if (effectivePadding === 0 && selectedIndex === paddedStart && currentOffset > 0 && visibleCount > padding) newOffset = Math.max(0, selectedIndex - padding);
1257
+ else if (selectedIndex > paddedEnd) newOffset = Math.min(totalCount - visibleCount, selectedIndex - visibleCount + effectivePadding + 1);
1258
+ return Math.max(0, Math.min(newOffset, totalCount - visibleCount));
1259
+ }
1260
+ //#endregion
1261
+ //#region packages/ag-react/src/hooks/useCursor.ts
1262
+ /**
1263
+ * useCursor - Show and position the terminal's blinking cursor.
1264
+ *
1265
+ * Maps component-relative (col, row) to absolute terminal coordinates
1266
+ * using useScrollRect. Per-instance last-writer-wins: only one cursor
1267
+ * can be active at a time per silvery instance (the terminal has one hardware cursor).
1268
+ *
1269
+ * Cursor state is isolated per silvery instance via CursorContext. Each runtime
1270
+ * (run(), createApp(), test renderer) provides its own CursorProvider so
1271
+ * multiple silvery instances don't fight over cursor position.
1272
+ *
1273
+ * Falls back to module-level globals when no CursorProvider is present
1274
+ * (backward compatibility / deprecation path).
1275
+ */
1276
+ /**
1277
+ * Unique-id counter for `useCursor` instances. Module-level so the
1278
+ * counter is shared across ALL instances in the process — guarantees
1279
+ * uniqueness regardless of CursorProvider scope. Wraps at MAX_SAFE_INTEGER
1280
+ * (practically infinite for terminal-UI lifetimes).
1281
+ */
1282
+ let cursorInstanceIdCounter = 0;
1283
+ /**
1284
+ * Create an isolated cursor store. Each silvery instance gets one.
1285
+ * Returns the store (for CursorProvider) and accessors (for scheduler/output).
1286
+ */
1287
+ function createCursorStore() {
1288
+ const store = {
1289
+ state: null,
1290
+ listeners: /* @__PURE__ */ new Set(),
1291
+ accessors: null,
1292
+ setCursorState(s) {
1293
+ store.state = s;
1294
+ for (const listener of store.listeners) listener();
1295
+ }
1296
+ };
1297
+ store.accessors = {
1298
+ getCursorState: () => store.state,
1299
+ subscribeCursor: (listener) => {
1300
+ store.listeners.add(listener);
1301
+ return () => {
1302
+ store.listeners.delete(listener);
1303
+ };
1304
+ }
1305
+ };
1306
+ return store;
1307
+ }
1308
+ const CursorCtx = createContext(null);
1309
+ /**
1310
+ * Provider that gives its subtree an isolated cursor store.
1311
+ * Wrap your silvery app root in this to isolate cursor state per instance.
1312
+ */
1313
+ function CursorProvider({ store, children }) {
1314
+ return React.createElement(CursorCtx.Provider, { value: store }, children);
1315
+ }
1316
+ let _globalCursorState = null;
1317
+ let _globalCursorListeners = /* @__PURE__ */ new Set();
1318
+ function globalSetCursorState(state) {
1319
+ _globalCursorState = state;
1320
+ for (const listener of _globalCursorListeners) listener();
1321
+ }
1322
+ function globalGetCursorState() {
1323
+ return _globalCursorState;
1324
+ }
1325
+ /** For testing -- reset global fallback state between tests. */
1326
+ function resetCursorState() {
1327
+ _globalCursorState = null;
1328
+ _globalCursorListeners = /* @__PURE__ */ new Set();
1329
+ }
1330
+ /**
1331
+ * Show and position the terminal's blinking cursor within this component.
1332
+ *
1333
+ * The cursor position is relative to the component's screen position.
1334
+ * Only one cursor can be active per silvery instance -- last caller with visible=true wins.
1335
+ *
1336
+ * Uses CursorContext if a CursorProvider is present (per-instance isolation).
1337
+ * Falls back to module-level globals otherwise (backward compat).
1338
+ *
1339
+ * @deprecated Phase 2 of `km-silvery.view-as-layout-output` migrated cursor
1340
+ * positioning from a React-effect-chain (`useCursor` → `useScrollRect` →
1341
+ * `setCursorState`) to a layout-output prop (`<Box cursorOffset={…}>`). The
1342
+ * prop path resolves absolute coordinates synchronously during the layout
1343
+ * phase, so the very first frame after a conditional mount emits correct
1344
+ * cursor ANSI — fixing `km-silvercode.cursor-startup-position` end-to-end.
1345
+ *
1346
+ * Migrate consumers by deleting the `useCursor({col, row, visible})` call
1347
+ * and adding `cursorOffset={{col, row, visible}}` to the surrounding Box. The
1348
+ * layout phase applies border + padding offsets automatically, so consumers
1349
+ * no longer need to add `borderColOffset` / `borderRowOffset` manually.
1350
+ *
1351
+ * This hook remains as a back-compat wrapper that writes to the cursor
1352
+ * store; the scheduler still reads the store as a fallback when no
1353
+ * `cursorOffset` prop is set on any node. Slated for deletion once all
1354
+ * in-tree consumers migrate (TextArea + TextInput already migrated; Ink
1355
+ * compat goes through the store directly and is unaffected). See bead
1356
+ * `km-silvery.delete-cursor-globals`.
1357
+ */
1358
+ function useCursor(position) {
1359
+ const { col, row, visible = true, shape } = position;
1360
+ const store = useContext(CursorCtx);
1361
+ const node = useContext(NodeContext);
1362
+ const set = store ? store.setCursorState.bind(store) : globalSetCursorState;
1363
+ const get = store ? store.accessors.getCursorState : globalGetCursorState;
1364
+ const instanceIdRef = useRef(0);
1365
+ if (instanceIdRef.current === 0) {
1366
+ cursorInstanceIdCounter += 1;
1367
+ instanceIdRef.current = cursorInstanceIdCounter;
1368
+ }
1369
+ const props = node?.props;
1370
+ const padLeft = props?.paddingLeft ?? props?.paddingX ?? props?.padding ?? 0;
1371
+ const padTop = props?.paddingTop ?? props?.paddingY ?? props?.padding ?? 0;
1372
+ const borderLeft = props?.borderStyle ? 1 : 0;
1373
+ const borderTop = props?.borderStyle ? 1 : 0;
1374
+ const contentOffsetX = borderLeft + padLeft;
1375
+ const contentOffsetY = borderTop + padTop;
1376
+ const colRef = useRef(col);
1377
+ const rowRef = useRef(row);
1378
+ const visibleRef = useRef(visible);
1379
+ const shapeRef = useRef(shape);
1380
+ const setRef = useRef(set);
1381
+ const getRef = useRef(get);
1382
+ const lastRectRef = useRef(null);
1383
+ const contentOffsetXRef = useRef(contentOffsetX);
1384
+ const contentOffsetYRef = useRef(contentOffsetY);
1385
+ colRef.current = col;
1386
+ rowRef.current = row;
1387
+ visibleRef.current = visible;
1388
+ shapeRef.current = shape;
1389
+ setRef.current = set;
1390
+ getRef.current = get;
1391
+ contentOffsetXRef.current = contentOffsetX;
1392
+ contentOffsetYRef.current = contentOffsetY;
1393
+ const scrollRect = useScrollRect();
1394
+ const hasNode = node != null;
1395
+ useEffect(() => {
1396
+ if (!hasNode) return;
1397
+ lastRectRef.current = scrollRect;
1398
+ if (!visibleRef.current) return;
1399
+ setRef.current({
1400
+ x: scrollRect.x + contentOffsetXRef.current + colRef.current,
1401
+ y: scrollRect.y + contentOffsetYRef.current + rowRef.current,
1402
+ visible: true,
1403
+ shape: shapeRef.current,
1404
+ owner: instanceIdRef.current
1405
+ });
1406
+ }, [
1407
+ hasNode,
1408
+ scrollRect.x,
1409
+ scrollRect.y,
1410
+ scrollRect.width,
1411
+ scrollRect.height,
1412
+ scrollRect
1413
+ ]);
1414
+ const owns = () => {
1415
+ const current = getRef.current();
1416
+ if (!current) return false;
1417
+ if (current.owner === void 0) return true;
1418
+ return current.owner === instanceIdRef.current;
1419
+ };
1420
+ const ownsRef = useRef(owns);
1421
+ ownsRef.current = owns;
1422
+ useLayoutEffect(() => {
1423
+ const rect = lastRectRef.current;
1424
+ if (!rect) return;
1425
+ if (visible) set({
1426
+ x: rect.x + contentOffsetX + col,
1427
+ y: rect.y + contentOffsetY + row,
1428
+ visible: true,
1429
+ shape,
1430
+ owner: instanceIdRef.current
1431
+ });
1432
+ else if (ownsRef.current()) set(null);
1433
+ }, [
1434
+ col,
1435
+ row,
1436
+ contentOffsetX,
1437
+ contentOffsetY,
1438
+ shape,
1439
+ visible,
1440
+ set
1441
+ ]);
1442
+ useEffect(() => {
1443
+ return () => {
1444
+ if (ownsRef.current()) setRef.current(null);
1445
+ };
1446
+ }, []);
1447
+ }
1448
+ //#endregion
1449
+ //#region packages/ag-term/src/hit-registry-core.ts
1450
+ /**
1451
+ * Registry for managing hit regions.
1452
+ *
1453
+ * Components register their screen regions with targets, and the registry
1454
+ * resolves mouse clicks to the appropriate target based on position and z-index.
1455
+ *
1456
+ * @example
1457
+ * ```typescript
1458
+ * const registry = new HitRegistry();
1459
+ *
1460
+ * // Register a card region
1461
+ * registry.register('card-1', {
1462
+ * x: 10, y: 5, width: 30, height: 8,
1463
+ * target: { type: 'node', nodeId: 'abc123' },
1464
+ * zIndex: 10
1465
+ * });
1466
+ *
1467
+ * // Hit test a click
1468
+ * const target = registry.hitTest(15, 7);
1469
+ * // Returns { type: 'node', nodeId: 'abc123' }
1470
+ * ```
1471
+ */
1472
+ var HitRegistry = class {
1473
+ regions = /* @__PURE__ */ new Map();
1474
+ /**
1475
+ * Register a hit region with a unique ID.
1476
+ *
1477
+ * @param id - Unique identifier for the region (used for unregistration)
1478
+ * @param region - The region definition including position, size, target, and z-index
1479
+ */
1480
+ register(id, region) {
1481
+ this.regions.set(id, region);
1482
+ }
1483
+ /**
1484
+ * Unregister a hit region by ID.
1485
+ *
1486
+ * @param id - The ID used when registering the region
1487
+ */
1488
+ unregister(id) {
1489
+ this.regions.delete(id);
1490
+ }
1491
+ /**
1492
+ * Clear all registered regions.
1493
+ * Useful when the UI is completely redrawn.
1494
+ */
1495
+ clear() {
1496
+ this.regions.clear();
1497
+ }
1498
+ /**
1499
+ * Get the number of registered regions.
1500
+ * Useful for debugging.
1501
+ */
1502
+ get size() {
1503
+ return this.regions.size;
1504
+ }
1505
+ /**
1506
+ * Test a screen position and return the highest z-index matching target.
1507
+ *
1508
+ * @param screenX - X position on screen (0-indexed column)
1509
+ * @param screenY - Y position on screen (0-indexed row)
1510
+ * @returns The target of the highest z-index region containing the point, or null if none
1511
+ */
1512
+ hitTest(screenX, screenY) {
1513
+ let bestMatch = null;
1514
+ for (const region of this.regions.values()) if (screenX >= region.x && screenX < region.x + region.width && screenY >= region.y && screenY < region.y + region.height) {
1515
+ if (!bestMatch || region.zIndex > bestMatch.zIndex) bestMatch = region;
1516
+ }
1517
+ return bestMatch?.target ?? null;
1518
+ }
1519
+ /**
1520
+ * Get all regions that contain a point, sorted by z-index (highest first).
1521
+ * Useful for debugging or when you need to know all overlapping elements.
1522
+ *
1523
+ * @param screenX - X position on screen (0-indexed column)
1524
+ * @param screenY - Y position on screen (0-indexed row)
1525
+ * @returns Array of matching regions, sorted by z-index descending
1526
+ */
1527
+ hitTestAll(screenX, screenY) {
1528
+ const matches = [];
1529
+ for (const region of this.regions.values()) if (screenX >= region.x && screenX < region.x + region.width && screenY >= region.y && screenY < region.y + region.height) matches.push(region);
1530
+ return matches.sort((a, b) => b.zIndex - a.zIndex);
1531
+ }
1532
+ /**
1533
+ * Debug helper: get all registered regions.
1534
+ */
1535
+ getAllRegions() {
1536
+ return new Map(this.regions);
1537
+ }
1538
+ };
1539
+ /**
1540
+ * Generate a unique ID for hit region registration.
1541
+ */
1542
+ let hitRegionIdCounter = 0;
1543
+ function generateHitRegionId() {
1544
+ return `hit-${++hitRegionIdCounter}`;
1545
+ }
1546
+ /**
1547
+ * Reset the ID counter (useful for testing).
1548
+ */
1549
+ function resetHitRegionIdCounter() {
1550
+ hitRegionIdCounter = 0;
1551
+ }
1552
+ /**
1553
+ * Recommended z-index values for different UI layers.
1554
+ */
1555
+ const Z_INDEX = {
1556
+ /** Background elements */
1557
+ BACKGROUND: 0,
1558
+ /** Column headers */
1559
+ COLUMN_HEADER: 5,
1560
+ /** Cards in the main view */
1561
+ CARD: 10,
1562
+ /** Fold toggles (above cards for easier clicking) */
1563
+ FOLD_TOGGLE: 15,
1564
+ /** Links within cards */
1565
+ LINK: 20,
1566
+ /** Floating elements */
1567
+ FLOATING: 50,
1568
+ /** Modal dialogs */
1569
+ DIALOG: 100,
1570
+ /** Dropdown menus */
1571
+ DROPDOWN: 150,
1572
+ /** Tooltips */
1573
+ TOOLTIP: 200
1574
+ };
1575
+ createLogger("silvery:render");
1576
+ createLogger("@silvery/ag-react");
1577
+ //#endregion
1578
+ //#region packages/ag-term/src/adapters/canvas-adapter.ts
1579
+ const DEFAULT_CONFIG$1 = {
1580
+ fontSize: 14,
1581
+ fontFamily: "monospace",
1582
+ lineHeight: 1.2,
1583
+ backgroundColor: "#1e1e1e",
1584
+ foregroundColor: "#d4d4d4",
1585
+ monospace: true,
1586
+ dpr: 1
1587
+ };
1588
+ const BORDER_CHARS$1 = {
1589
+ single: {
1590
+ topLeft: "┌",
1591
+ topRight: "┐",
1592
+ bottomLeft: "└",
1593
+ bottomRight: "┘",
1594
+ horizontal: "─",
1595
+ vertical: "│"
1596
+ },
1597
+ double: {
1598
+ topLeft: "╔",
1599
+ topRight: "╗",
1600
+ bottomLeft: "╚",
1601
+ bottomRight: "╝",
1602
+ horizontal: "═",
1603
+ vertical: "║"
1604
+ },
1605
+ round: {
1606
+ topLeft: "╭",
1607
+ topRight: "╮",
1608
+ bottomLeft: "╰",
1609
+ bottomRight: "╯",
1610
+ horizontal: "─",
1611
+ vertical: "│"
1612
+ },
1613
+ bold: {
1614
+ topLeft: "┏",
1615
+ topRight: "┓",
1616
+ bottomLeft: "┗",
1617
+ bottomRight: "┛",
1618
+ horizontal: "━",
1619
+ vertical: "┃"
1620
+ }
1621
+ };
1622
+ /** Create a scratch canvas 2D context for text measurement. */
1623
+ function createMeasureContext(fontSize, fontFamily) {
1624
+ const canvas = typeof document !== "undefined" ? document.createElement("canvas") : typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(1, 1) : null;
1625
+ if (!canvas) throw new Error("Canvas not available for text measurement");
1626
+ const ctx = canvas.getContext("2d");
1627
+ if (!ctx) throw new Error("Could not get 2d context for measurement");
1628
+ ctx.font = `${fontSize}px ${fontFamily}`;
1629
+ return ctx;
1630
+ }
1631
+ function createCanvasMeasurer(config) {
1632
+ if (config.monospace) return {
1633
+ measureText(text, _style) {
1634
+ return {
1635
+ width: text.length,
1636
+ height: 1
1637
+ };
1638
+ },
1639
+ getLineHeight(_style) {
1640
+ return 1;
1641
+ }
1642
+ };
1643
+ const lineHeightPx = config.fontSize * config.lineHeight;
1644
+ const ctx = createMeasureContext(config.fontSize, config.fontFamily);
1645
+ return {
1646
+ measureText(text, _style) {
1647
+ return {
1648
+ width: ctx.measureText(text).width,
1649
+ height: lineHeightPx
1650
+ };
1651
+ },
1652
+ getLineHeight(_style) {
1653
+ return lineHeightPx;
1654
+ }
1655
+ };
1656
+ }
1657
+ const ANSI_COLORS$1 = {
1658
+ black: "#000000",
1659
+ red: "#cd0000",
1660
+ green: "#00cd00",
1661
+ yellow: "#cdcd00",
1662
+ blue: "#0000ee",
1663
+ magenta: "#cd00cd",
1664
+ cyan: "#00cdcd",
1665
+ white: "#e5e5e5",
1666
+ gray: "#7f7f7f",
1667
+ grey: "#7f7f7f",
1668
+ brightBlack: "#7f7f7f",
1669
+ brightRed: "#ff0000",
1670
+ brightGreen: "#00ff00",
1671
+ brightYellow: "#ffff00",
1672
+ brightBlue: "#5c5cff",
1673
+ brightMagenta: "#ff00ff",
1674
+ brightCyan: "#00ffff",
1675
+ brightWhite: "#ffffff"
1676
+ };
1677
+ function resolveColor$1(color, fallback) {
1678
+ if (!color) return fallback;
1679
+ if (color.startsWith("#") || color.startsWith("rgb")) return color;
1680
+ const named = ANSI_COLORS$1[color.toLowerCase()];
1681
+ if (named) return named;
1682
+ return color;
1683
+ }
1684
+ var CanvasRenderBuffer = class {
1685
+ width;
1686
+ height;
1687
+ canvas;
1688
+ ctx;
1689
+ config;
1690
+ charWidth;
1691
+ cellHeight;
1692
+ constructor(width, height, config) {
1693
+ this.width = width;
1694
+ this.height = height;
1695
+ this.config = config;
1696
+ const dpr = config.dpr;
1697
+ let cssWidth;
1698
+ let cssHeight;
1699
+ if (config.monospace) {
1700
+ this.charWidth = config.fontSize * .6;
1701
+ this.cellHeight = config.fontSize * config.lineHeight;
1702
+ cssWidth = width * this.charWidth;
1703
+ cssHeight = height * this.cellHeight;
1704
+ } else {
1705
+ this.charWidth = 1;
1706
+ this.cellHeight = 1;
1707
+ cssWidth = width;
1708
+ cssHeight = height;
1709
+ }
1710
+ const nativeWidth = Math.ceil(cssWidth * dpr);
1711
+ const nativeHeight = Math.ceil(cssHeight * dpr);
1712
+ if (typeof OffscreenCanvas !== "undefined") this.canvas = new OffscreenCanvas(nativeWidth, nativeHeight);
1713
+ else if (typeof document !== "undefined") {
1714
+ this.canvas = document.createElement("canvas");
1715
+ this.canvas.width = nativeWidth;
1716
+ this.canvas.height = nativeHeight;
1717
+ } else throw new Error("Canvas not available");
1718
+ const ctx = this.canvas.getContext("2d");
1719
+ if (!ctx) throw new Error("Could not get 2d context");
1720
+ this.ctx = ctx;
1721
+ if (dpr !== 1) this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1722
+ this.ctx.fillStyle = config.backgroundColor;
1723
+ this.ctx.fillRect(0, 0, cssWidth, cssHeight);
1724
+ }
1725
+ fillRect(x, y, width, height, style) {
1726
+ if (style.bg) {
1727
+ const px = x * this.charWidth;
1728
+ const py = y * this.cellHeight;
1729
+ const pw = width * this.charWidth;
1730
+ const ph = height * this.cellHeight;
1731
+ this.ctx.fillStyle = resolveColor$1(style.bg, this.config.backgroundColor);
1732
+ this.ctx.fillRect(px, py, pw, ph);
1733
+ }
1734
+ }
1735
+ drawText(x, y, text, style) {
1736
+ const px = x * this.charWidth;
1737
+ const attrs = style.attrs ?? {};
1738
+ const weight = attrs.bold ? "bold" : "normal";
1739
+ const fontStyle = attrs.italic ? "italic" : "normal";
1740
+ this.ctx.font = `${fontStyle} ${weight} ${this.config.fontSize}px ${this.config.fontFamily}`;
1741
+ let py = y * this.cellHeight;
1742
+ this.ctx.fillStyle = resolveColor$1(style.fg, this.config.foregroundColor);
1743
+ if (!this.config.monospace) {
1744
+ const metrics = this.ctx.measureText("Hg");
1745
+ const fontAscent = metrics.fontBoundingBoxAscent;
1746
+ const contentArea = fontAscent + metrics.fontBoundingBoxDescent;
1747
+ const halfLeading = (this.config.fontSize * this.config.lineHeight - contentArea) / 2;
1748
+ this.ctx.textBaseline = "alphabetic";
1749
+ py += halfLeading + fontAscent;
1750
+ } else this.ctx.textBaseline = "top";
1751
+ this.ctx.fillText(text, px, py);
1752
+ if (attrs.underline) this.drawUnderline(px, py, text, style);
1753
+ if (attrs.strikethrough) {
1754
+ const textWidth = this.ctx.measureText(text).width;
1755
+ const strikeY = py + this.config.fontSize * .5;
1756
+ this.ctx.strokeStyle = resolveColor$1(style.fg, this.config.foregroundColor);
1757
+ this.ctx.lineWidth = 1;
1758
+ this.ctx.beginPath();
1759
+ this.ctx.moveTo(px, strikeY);
1760
+ this.ctx.lineTo(px + textWidth, strikeY);
1761
+ this.ctx.stroke();
1762
+ }
1763
+ }
1764
+ /**
1765
+ * Draw underline decorations at pixel coordinates.
1766
+ * Note: px, py are already in pixel coordinates.
1767
+ */
1768
+ drawUnderline(px, py, text, style) {
1769
+ const attrs = style.attrs ?? {};
1770
+ const textWidth = this.ctx.measureText(text).width;
1771
+ const underlineY = py + this.config.fontSize * .9;
1772
+ const underlineColor = resolveColor$1(attrs.underlineColor ?? style.fg, this.config.foregroundColor);
1773
+ this.ctx.strokeStyle = underlineColor;
1774
+ this.ctx.lineWidth = 1;
1775
+ switch (attrs.underlineStyle ?? "single") {
1776
+ case "double":
1777
+ this.ctx.beginPath();
1778
+ this.ctx.moveTo(px, underlineY - 1);
1779
+ this.ctx.lineTo(px + textWidth, underlineY - 1);
1780
+ this.ctx.moveTo(px, underlineY + 1);
1781
+ this.ctx.lineTo(px + textWidth, underlineY + 1);
1782
+ this.ctx.stroke();
1783
+ break;
1784
+ case "curly":
1785
+ this.ctx.beginPath();
1786
+ this.ctx.moveTo(px, underlineY);
1787
+ const waveLength = 4;
1788
+ const amplitude = 2;
1789
+ for (let wx = 0; wx < textWidth; wx += waveLength * 2) {
1790
+ this.ctx.quadraticCurveTo(px + wx + waveLength / 2, underlineY - amplitude, px + wx + waveLength, underlineY);
1791
+ this.ctx.quadraticCurveTo(px + wx + waveLength * 3 / 2, underlineY + amplitude, px + wx + waveLength * 2, underlineY);
1792
+ }
1793
+ this.ctx.stroke();
1794
+ break;
1795
+ case "dotted":
1796
+ this.ctx.setLineDash([2, 2]);
1797
+ this.ctx.beginPath();
1798
+ this.ctx.moveTo(px, underlineY);
1799
+ this.ctx.lineTo(px + textWidth, underlineY);
1800
+ this.ctx.stroke();
1801
+ this.ctx.setLineDash([]);
1802
+ break;
1803
+ case "dashed":
1804
+ this.ctx.setLineDash([4, 2]);
1805
+ this.ctx.beginPath();
1806
+ this.ctx.moveTo(px, underlineY);
1807
+ this.ctx.lineTo(px + textWidth, underlineY);
1808
+ this.ctx.stroke();
1809
+ this.ctx.setLineDash([]);
1810
+ break;
1811
+ default:
1812
+ this.ctx.beginPath();
1813
+ this.ctx.moveTo(px, underlineY);
1814
+ this.ctx.lineTo(px + textWidth, underlineY);
1815
+ this.ctx.stroke();
1816
+ }
1817
+ }
1818
+ drawChar(x, y, char, style) {
1819
+ this.drawText(x, y, char, style);
1820
+ }
1821
+ inBounds(x, y) {
1822
+ return x >= 0 && x < this.width && y >= 0 && y < this.height;
1823
+ }
1824
+ fillRoundedRect(x, y, width, height, radius, fill, stroke, lineWidth = 1) {
1825
+ const px = x * this.charWidth;
1826
+ const py = y * this.cellHeight;
1827
+ const pw = width * this.charWidth;
1828
+ const ph = height * this.cellHeight;
1829
+ const r = Math.min(radius, pw / 2, ph / 2);
1830
+ this.ctx.beginPath();
1831
+ this.ctx.moveTo(px + r, py);
1832
+ this.ctx.lineTo(px + pw - r, py);
1833
+ this.ctx.quadraticCurveTo(px + pw, py, px + pw, py + r);
1834
+ this.ctx.lineTo(px + pw, py + ph - r);
1835
+ this.ctx.quadraticCurveTo(px + pw, py + ph, px + pw - r, py + ph);
1836
+ this.ctx.lineTo(px + r, py + ph);
1837
+ this.ctx.quadraticCurveTo(px, py + ph, px, py + ph - r);
1838
+ this.ctx.lineTo(px, py + r);
1839
+ this.ctx.quadraticCurveTo(px, py, px + r, py);
1840
+ this.ctx.closePath();
1841
+ if (fill) {
1842
+ this.ctx.fillStyle = fill;
1843
+ this.ctx.fill();
1844
+ }
1845
+ if (stroke) {
1846
+ this.ctx.strokeStyle = stroke;
1847
+ this.ctx.lineWidth = lineWidth;
1848
+ this.ctx.stroke();
1849
+ }
1850
+ }
1851
+ };
1852
+ function createCanvasAdapter(config = {}) {
1853
+ const cfg = {
1854
+ ...DEFAULT_CONFIG$1,
1855
+ ...config
1856
+ };
1857
+ return {
1858
+ name: "canvas",
1859
+ measurer: createCanvasMeasurer(cfg),
1860
+ createBuffer(width, height) {
1861
+ return new CanvasRenderBuffer(width, height, cfg);
1862
+ },
1863
+ flush(_buffer, _prevBuffer) {},
1864
+ getBorderChars(style) {
1865
+ return BORDER_CHARS$1[style] ?? BORDER_CHARS$1.single;
1866
+ }
1867
+ };
1868
+ }
1869
+ //#endregion
1870
+ //#region packages/ag-term/src/adapters/dom-adapter.ts
1871
+ const DEFAULT_CONFIG = {
1872
+ fontSize: 14,
1873
+ fontFamily: "monospace",
1874
+ lineHeight: 1.2,
1875
+ backgroundColor: "#1e1e1e",
1876
+ foregroundColor: "#d4d4d4",
1877
+ classPrefix: "silvery"
1878
+ };
1879
+ const BORDER_CHARS = {
1880
+ single: {
1881
+ topLeft: "┌",
1882
+ topRight: "┐",
1883
+ bottomLeft: "└",
1884
+ bottomRight: "┘",
1885
+ horizontal: "─",
1886
+ vertical: "│"
1887
+ },
1888
+ double: {
1889
+ topLeft: "╔",
1890
+ topRight: "╗",
1891
+ bottomLeft: "╚",
1892
+ bottomRight: "╝",
1893
+ horizontal: "═",
1894
+ vertical: "║"
1895
+ },
1896
+ round: {
1897
+ topLeft: "╭",
1898
+ topRight: "╮",
1899
+ bottomLeft: "╰",
1900
+ bottomRight: "╯",
1901
+ horizontal: "─",
1902
+ vertical: "│"
1903
+ },
1904
+ bold: {
1905
+ topLeft: "┏",
1906
+ topRight: "┓",
1907
+ bottomLeft: "┗",
1908
+ bottomRight: "┛",
1909
+ horizontal: "━",
1910
+ vertical: "┃"
1911
+ }
1912
+ };
1913
+ const ANSI_COLORS = {
1914
+ black: "#000000",
1915
+ red: "#cd0000",
1916
+ green: "#00cd00",
1917
+ yellow: "#cdcd00",
1918
+ blue: "#0000ee",
1919
+ magenta: "#cd00cd",
1920
+ cyan: "#00cdcd",
1921
+ white: "#e5e5e5",
1922
+ gray: "#7f7f7f",
1923
+ grey: "#7f7f7f",
1924
+ brightblack: "#7f7f7f",
1925
+ brightred: "#ff0000",
1926
+ brightgreen: "#00ff00",
1927
+ brightyellow: "#ffff00",
1928
+ brightblue: "#5c5cff",
1929
+ brightmagenta: "#ff00ff",
1930
+ brightcyan: "#00ffff",
1931
+ brightwhite: "#ffffff"
1932
+ };
1933
+ function resolveColor(color, fallback) {
1934
+ if (!color) return fallback;
1935
+ if (color.startsWith("#") || color.startsWith("rgb")) return color;
1936
+ return ANSI_COLORS[color.toLowerCase()] ?? color;
1937
+ }
1938
+ function createDOMMeasurer(_config) {
1939
+ return {
1940
+ measureText(text, _style) {
1941
+ return {
1942
+ width: text.length,
1943
+ height: 1
1944
+ };
1945
+ },
1946
+ getLineHeight(_style) {
1947
+ return 1;
1948
+ }
1949
+ };
1950
+ }
1951
+ var DOMRenderBuffer = class {
1952
+ width;
1953
+ height;
1954
+ config;
1955
+ lines;
1956
+ backgrounds;
1957
+ charWidth;
1958
+ cellHeight;
1959
+ container = null;
1960
+ constructor(width, height, config) {
1961
+ this.width = width;
1962
+ this.height = height;
1963
+ this.config = config;
1964
+ this.lines = /* @__PURE__ */ new Map();
1965
+ this.backgrounds = /* @__PURE__ */ new Map();
1966
+ this.charWidth = config.fontSize * .6;
1967
+ this.cellHeight = config.fontSize * config.lineHeight;
1968
+ }
1969
+ /**
1970
+ * Set the container element for rendering.
1971
+ */
1972
+ setContainer(container) {
1973
+ this.container = container;
1974
+ }
1975
+ /**
1976
+ * Get the container element.
1977
+ */
1978
+ getContainer() {
1979
+ return this.container;
1980
+ }
1981
+ fillRect(x, y, width, height, style) {
1982
+ if (style.bg) {
1983
+ const key = `${x},${y},${width},${height}`;
1984
+ this.backgrounds.set(key, {
1985
+ x,
1986
+ y,
1987
+ w: width,
1988
+ h: height,
1989
+ color: resolveColor(style.bg, this.config.backgroundColor)
1990
+ });
1991
+ }
1992
+ }
1993
+ drawText(x, y, text, style) {
1994
+ if (!this.lines.has(y)) this.lines.set(y, []);
1995
+ this.lines.get(y).push({
1996
+ text,
1997
+ style,
1998
+ x
1999
+ });
2000
+ }
2001
+ drawChar(x, y, char, style) {
2002
+ this.drawText(x, y, char, style);
2003
+ }
2004
+ inBounds(x, y) {
2005
+ return x >= 0 && x < this.width && y >= 0 && y < this.height;
2006
+ }
2007
+ /**
2008
+ * Render the buffer to the container element.
2009
+ * Coordinates in the buffer are in cell units (cols/rows).
2010
+ * This method converts them to pixel coordinates for DOM positioning.
2011
+ */
2012
+ render() {
2013
+ if (!this.container) throw new Error("DOMRenderBuffer: No container set. Call setContainer() first.");
2014
+ const container = this.container;
2015
+ const cw = this.charWidth;
2016
+ const ch = this.cellHeight;
2017
+ const containerWidthPx = this.width * cw;
2018
+ const containerHeightPx = this.height * ch;
2019
+ container.innerHTML = "";
2020
+ container.style.cssText = `
2021
+ position: relative;
2022
+ font-family: ${this.config.fontFamily};
2023
+ font-size: ${this.config.fontSize}px;
2024
+ line-height: ${this.config.lineHeight};
2025
+ background-color: ${this.config.backgroundColor};
2026
+ color: ${this.config.foregroundColor};
2027
+ white-space: pre;
2028
+ overflow: hidden;
2029
+ width: ${containerWidthPx}px;
2030
+ height: ${containerHeightPx}px;
2031
+ `;
2032
+ for (const bg of this.backgrounds.values()) {
2033
+ const bgDiv = document.createElement("div");
2034
+ bgDiv.className = `${this.config.classPrefix}-bg`;
2035
+ bgDiv.style.cssText = `
2036
+ position: absolute;
2037
+ left: ${bg.x * cw}px;
2038
+ top: ${bg.y * ch}px;
2039
+ width: ${bg.w * cw}px;
2040
+ height: ${bg.h * ch}px;
2041
+ background-color: ${bg.color};
2042
+ `;
2043
+ container.appendChild(bgDiv);
2044
+ }
2045
+ const sortedLines = Array.from(this.lines.entries()).sort((a, b) => a[0] - b[0]);
2046
+ for (const [y, runs] of sortedLines) {
2047
+ const lineDiv = document.createElement("div");
2048
+ lineDiv.className = `${this.config.classPrefix}-line`;
2049
+ lineDiv.style.cssText = `
2050
+ position: absolute;
2051
+ left: 0;
2052
+ top: ${y * ch}px;
2053
+ height: ${ch}px;
2054
+ white-space: pre;
2055
+ `;
2056
+ const sortedRuns = runs.sort((a, b) => a.x - b.x);
2057
+ for (const run of sortedRuns) {
2058
+ const span = document.createElement(run.style.hyperlink === void 0 ? "span" : "a");
2059
+ span.className = `${this.config.classPrefix}-text`;
2060
+ span.textContent = run.text;
2061
+ if (run.style.hyperlink !== void 0) span.setAttribute("href", run.style.hyperlink);
2062
+ const styles = [`position: absolute`, `left: ${run.x * cw}px`];
2063
+ if (run.style.fg) styles.push(`color: ${resolveColor(run.style.fg, this.config.foregroundColor)}`);
2064
+ if (run.style.bg) styles.push(`background-color: ${resolveColor(run.style.bg, this.config.backgroundColor)}`);
2065
+ const attrs = run.style.attrs;
2066
+ if (attrs) {
2067
+ if (attrs.bold) styles.push("font-weight: bold");
2068
+ if (attrs.dim) styles.push("opacity: 0.5");
2069
+ if (attrs.italic) styles.push("font-style: italic");
2070
+ if (attrs.underline || attrs.underlineStyle) {
2071
+ const underlineStyle = attrs.underlineStyle ?? "single";
2072
+ const underlineColor = attrs.underlineColor ? resolveColor(attrs.underlineColor, this.config.foregroundColor) : "currentColor";
2073
+ switch (underlineStyle) {
2074
+ case "double":
2075
+ styles.push(`text-decoration: underline double ${underlineColor}`);
2076
+ break;
2077
+ case "curly":
2078
+ styles.push(`text-decoration: underline wavy ${underlineColor}`);
2079
+ break;
2080
+ case "dotted":
2081
+ styles.push(`text-decoration: underline dotted ${underlineColor}`);
2082
+ break;
2083
+ case "dashed":
2084
+ styles.push(`text-decoration: underline dashed ${underlineColor}`);
2085
+ break;
2086
+ default: styles.push(`text-decoration: underline solid ${underlineColor}`);
2087
+ }
2088
+ }
2089
+ if (attrs.strikethrough) {
2090
+ const existing = styles.find((s) => s.startsWith("text-decoration:"));
2091
+ if (existing) {
2092
+ const idx = styles.indexOf(existing);
2093
+ styles[idx] = existing.replace("underline", "underline line-through");
2094
+ } else styles.push("text-decoration: line-through");
2095
+ }
2096
+ if (attrs.inverse) {
2097
+ const fg = run.style.fg ? resolveColor(run.style.fg, this.config.foregroundColor) : this.config.foregroundColor;
2098
+ const bg = run.style.bg ? resolveColor(run.style.bg, this.config.backgroundColor) : this.config.backgroundColor;
2099
+ styles.push(`color: ${bg}`, `background-color: ${fg}`);
2100
+ }
2101
+ }
2102
+ span.style.cssText = styles.join("; ");
2103
+ lineDiv.appendChild(span);
2104
+ }
2105
+ container.appendChild(lineDiv);
2106
+ }
2107
+ }
2108
+ /**
2109
+ * Clear the buffer.
2110
+ */
2111
+ clear() {
2112
+ this.lines.clear();
2113
+ this.backgrounds.clear();
2114
+ }
2115
+ };
2116
+ function createDOMAdapter(config = {}) {
2117
+ const cfg = {
2118
+ ...DEFAULT_CONFIG,
2119
+ ...config
2120
+ };
2121
+ return {
2122
+ name: "dom",
2123
+ measurer: createDOMMeasurer(cfg),
2124
+ createBuffer(width, height) {
2125
+ return new DOMRenderBuffer(width, height, cfg);
2126
+ },
2127
+ flush(buffer, _prevBuffer) {
2128
+ const domBuffer = buffer;
2129
+ if (domBuffer.getContainer()) domBuffer.render();
2130
+ },
2131
+ getBorderChars(style) {
2132
+ return BORDER_CHARS[style] ?? BORDER_CHARS.single;
2133
+ }
2134
+ };
2135
+ }
2136
+ let stylesInjected = false;
2137
+ /**
2138
+ * Inject global CSS styles for silvery DOM rendering.
2139
+ * Call once at application startup if you want default styling.
2140
+ */
2141
+ function injectDOMStyles(classPrefix = "silvery") {
2142
+ if (stylesInjected || typeof document === "undefined") return;
2143
+ const style = document.createElement("style");
2144
+ style.textContent = `
2145
+ .${classPrefix}-container {
2146
+ font-family: monospace;
2147
+ white-space: pre;
2148
+ overflow: hidden;
2149
+ }
2150
+ .${classPrefix}-line {
2151
+ white-space: pre;
2152
+ }
2153
+ .${classPrefix}-text {
2154
+ white-space: pre;
2155
+ }
2156
+ /* Selection styling */
2157
+ .${classPrefix}-text::selection {
2158
+ background-color: rgba(100, 150, 255, 0.3);
2159
+ }
2160
+ `;
2161
+ document.head.appendChild(style);
2162
+ stylesInjected = true;
2163
+ }
2164
+ //#endregion
2165
+ //#region packages/ag-term/src/caret-style.ts
2166
+ /**
2167
+ * Walk a tree and return the AgNode whose `cursorOffset` produced the active
2168
+ * caret rect (matching the precedence rules of `findActiveCursorRect`).
2169
+ *
2170
+ * Used by the scheduler to feed `resolveCaretStyle`. Re-walks the tree to
2171
+ * keep `findActiveCursorRect` returning a value-only (data) result rather
2172
+ * than coupling it to the AgNode reference.
2173
+ *
2174
+ * Returns null when no caret is active.
2175
+ */
2176
+ function findActiveCursorNode(root) {
2177
+ let focusedNode = null;
2178
+ let focusedSuppressesFallback = false;
2179
+ let fallbackNode = null;
2180
+ function walk(node) {
2181
+ for (const child of node.children) walk(child);
2182
+ const props = node.props;
2183
+ if (!props?.cursorOffset) return;
2184
+ const offset = props.cursorOffset;
2185
+ if (props.focused === true || node.interactiveState?.focused) {
2186
+ focusedSuppressesFallback = true;
2187
+ focusedNode = node;
2188
+ return;
2189
+ }
2190
+ if (offset.visible !== false) fallbackNode = node;
2191
+ }
2192
+ walk(root);
2193
+ return focusedNode ?? (focusedSuppressesFallback ? null : fallbackNode);
2194
+ }
2195
+ //#endregion
2196
+ //#region packages/ag-term/src/osc-markers.ts
2197
+ /**
2198
+ * OSC 133 Semantic Prompt Markers
2199
+ *
2200
+ * Shell integration protocol that marks prompts and commands in terminal output.
2201
+ * Terminals like iTerm2, Kitty, and WezTerm use these markers to provide
2202
+ * "jump to previous/next prompt" navigation (Cmd+Up/Cmd+Down in iTerm2).
2203
+ *
2204
+ * Protocol: OSC 133
2205
+ * - Prompt start: ESC ] 133 ; A BEL (before user input)
2206
+ * - Prompt end: ESC ] 133 ; B BEL (after user input, before command output)
2207
+ * - Command output start: ESC ] 133 ; C BEL (before command output)
2208
+ * - Command output end: ESC ] 133 ; D ; N BEL (after command output, N = exit code)
2209
+ *
2210
+ * For a chat-style app, each "exchange" (user prompt + assistant response) maps to:
2211
+ * - 133;A before the user's message
2212
+ * - 133;B after the user's message
2213
+ * - 133;C before the assistant's response
2214
+ * - 133;D;0 after the assistant's response
2215
+ *
2216
+ * Supported by: iTerm2, Kitty, WezTerm, foot, Ghostty
2217
+ */
2218
+ const OSC133 = {
2219
+ /** Mark prompt start (before user input) */
2220
+ promptStart: "\x1B]133;A\x07",
2221
+ /** Mark prompt end (after user input, before command output) */
2222
+ promptEnd: "\x1B]133;B\x07",
2223
+ /** Mark command output start */
2224
+ commandStart: "\x1B]133;C\x07",
2225
+ /** Mark command output end (exit code defaults to 0 = success) */
2226
+ commandEnd: (exitCode) => `\x1b]133;D;${exitCode ?? 0}\x07`
2227
+ };
2228
+ //#endregion
2229
+ //#region packages/ag-term/src/kitty-detect.ts
2230
+ /**
2231
+ * Kitty keyboard protocol detection.
2232
+ *
2233
+ * Sends CSI ? u and parses the response to determine whether the terminal
2234
+ * supports the Kitty keyboard protocol and which flags it reports.
2235
+ */
2236
+ /** Regex to match a Kitty keyboard query response: CSI ? <flags> u */
2237
+ const KITTY_RESPONSE_RE = /\x1b\[\?(\d+)u/;
2238
+ /**
2239
+ * Detect Kitty keyboard protocol support.
2240
+ *
2241
+ * Sends CSI ? u to the terminal and waits for a response.
2242
+ * Supported terminals respond with CSI ? flags u.
2243
+ * Unsupported terminals either ignore the query or echo it.
2244
+ *
2245
+ * @param write Function to write to stdout
2246
+ * @param read Function to read a chunk from stdin (should resolve with data or null on timeout)
2247
+ * @param timeoutMs How long to wait for response (default: 200ms)
2248
+ */
2249
+ async function detectKittySupport(write, read, timeoutMs = 200) {
2250
+ write(queryKittyKeyboard());
2251
+ const data = await read(timeoutMs);
2252
+ if (data == null) return {
2253
+ supported: false,
2254
+ flags: 0
2255
+ };
2256
+ const match = KITTY_RESPONSE_RE.exec(data);
2257
+ if (!match) return {
2258
+ supported: false,
2259
+ flags: 0,
2260
+ buffered: data
2261
+ };
2262
+ return {
2263
+ supported: true,
2264
+ flags: parseInt(match[1], 10),
2265
+ buffered: data.slice(0, match.index) + data.slice(match.index + match[0].length) || void 0
2266
+ };
2267
+ }
2268
+ /**
2269
+ * Detect Kitty support using real stdin/stdout.
2270
+ * Convenience wrapper around detectKittySupport.
2271
+ */
2272
+ async function detectKittyFromStdio(stdout, stdin, timeoutMs = 200) {
2273
+ const otherListeners = stdin.listenerCount("data") > 0;
2274
+ const wasRaw = stdin.isRaw;
2275
+ let didSetRaw = false;
2276
+ if (!wasRaw && !otherListeners) {
2277
+ stdin.setRawMode(true);
2278
+ didSetRaw = true;
2279
+ }
2280
+ try {
2281
+ const write = (s) => {
2282
+ stdout.write(s);
2283
+ };
2284
+ const read = (ms) => new Promise((resolve) => {
2285
+ const timer = setTimeout(() => {
2286
+ stdin.removeListener("data", onData);
2287
+ resolve(null);
2288
+ }, ms);
2289
+ function onData(chunk) {
2290
+ clearTimeout(timer);
2291
+ stdin.removeListener("data", onData);
2292
+ resolve(chunk.toString());
2293
+ }
2294
+ stdin.on("data", onData);
2295
+ });
2296
+ return await detectKittySupport(write, read, timeoutMs);
2297
+ } finally {
2298
+ if (didSetRaw) stdin.setRawMode(false);
2299
+ }
2300
+ }
2301
+ //#endregion
2302
+ //#region packages/ag-term/src/termtest.ts
2303
+ /**
2304
+ * Terminal Capability Test
2305
+ *
2306
+ * Renders labeled test patterns for each terminal feature.
2307
+ * Run in any terminal to visually verify what it supports.
2308
+ *
2309
+ * Usage:
2310
+ * import { runTermtest } from "@silvery/ag-react"
2311
+ * runTermtest() // all sections
2312
+ * runTermtest({ sections: ["emoji", "colors"] }) // specific sections
2313
+ *
2314
+ * Compare output across terminals to build/verify profiles.
2315
+ */
2316
+ const CSI = `[`;
2317
+ const RESET = `${CSI}0m`;
2318
+ function sgr(...codes) {
2319
+ return `${CSI}${codes.join(";")}m`;
2320
+ }
2321
+ function sectionHeader(title) {
2322
+ return `\n${sgr(1)}═══ ${title} ═══${RESET}\n`;
2323
+ }
2324
+ function row(label, content) {
2325
+ return ` ${label.padEnd(24)} ${content}${RESET}`;
2326
+ }
2327
+ /** Available test sections */
2328
+ const TERMTEST_SECTIONS = [
2329
+ "sgr",
2330
+ "underline",
2331
+ "colors",
2332
+ "256",
2333
+ "truecolor",
2334
+ "unicode",
2335
+ "emoji",
2336
+ "borders",
2337
+ "inverse",
2338
+ "profile"
2339
+ ];
2340
+ /**
2341
+ * Run the terminal capability test.
2342
+ * Pass section names to filter: `runTermtest({ sections: ["emoji"] })`
2343
+ */
2344
+ function runTermtest(options) {
2345
+ const w = options?.output ?? process.stdout;
2346
+ const filter = options?.sections;
2347
+ const show = (s) => !filter || filter.length === 0 || filter.includes(s);
2348
+ const { caps, emulator } = createTerminalProfile();
2349
+ w.write(`\n${sgr(1)}Terminal Capability Test${RESET}\n`);
2350
+ w.write(` Program: ${emulator.program || "(unknown)"}\n`);
2351
+ w.write(` TERM: ${emulator.TERM || "(unknown)"}\n`);
2352
+ w.write(` COLORTERM: ${process.env.COLORTERM || "(unset)"}\n`);
2353
+ w.write(` Detected: color=${caps.colorLevel} maybe-dark=${caps.maybeDarkBackground} maybe-nerdfont=${caps.maybeNerdFont}\n`);
2354
+ w.write(` Underline: styles=[${caps.underlineStyles.join(", ")}] color=${caps.underlineColor}\n`);
2355
+ w.write(` Maybe wide emojis: ${caps.maybeWideEmojis}\n`);
2356
+ if (show("sgr")) {
2357
+ w.write(sectionHeader("SGR Text Attributes"));
2358
+ w.write(row("Bold", `${sgr(1)}The quick brown fox${RESET}`) + "\n");
2359
+ w.write(row("Dim", `${sgr(2)}The quick brown fox${RESET}`) + "\n");
2360
+ w.write(row("Italic", `${sgr(3)}The quick brown fox${RESET}`) + "\n");
2361
+ w.write(row("Underline", `${sgr(4)}The quick brown fox${RESET}`) + "\n");
2362
+ w.write(row("Strikethrough", `${sgr(9)}The quick brown fox${RESET}`) + "\n");
2363
+ w.write(row("Inverse", `${sgr(7)}The quick brown fox${RESET}`) + "\n");
2364
+ w.write(row("Blink", `${sgr(5)}The quick brown fox${RESET}`) + "\n");
2365
+ w.write(row("Bold+Italic", `${sgr(1, 3)}The quick brown fox${RESET}`) + "\n");
2366
+ }
2367
+ if (show("underline")) {
2368
+ w.write(sectionHeader("SGR 4:x Underline Styles (Terminal.app BREAKS here)"));
2369
+ w.write(row("4:1 Single", `${CSI}4:1mThe quick brown fox${RESET}`) + "\n");
2370
+ w.write(row("4:2 Double", `${CSI}4:2mThe quick brown fox${RESET}`) + "\n");
2371
+ w.write(row("4:3 Curly", `${CSI}4:3mThe quick brown fox${RESET}`) + "\n");
2372
+ w.write(row("4:4 Dotted", `${CSI}4:4mThe quick brown fox${RESET}`) + "\n");
2373
+ w.write(row("4:5 Dashed", `${CSI}4:5mThe quick brown fox${RESET}`) + "\n");
2374
+ w.write(row("After 4:x (clean?)", `${CSI}4:3m${RESET}This text should be normal — if garbled, 4:x corrupted SGR state`) + "\n");
2375
+ w.write(sectionHeader("SGR 58 Underline Color (Terminal.app BREAKS here)"));
2376
+ w.write(row("58;5;1 Red UL", `${sgr(4)}${CSI}58;5;1mThe quick brown fox${RESET}`) + "\n");
2377
+ w.write(row("58;5;2 Green UL", `${sgr(4)}${CSI}58;5;2mThe quick brown fox${RESET}`) + "\n");
2378
+ w.write(row("58;5;4 Blue UL", `${sgr(4)}${CSI}58;5;4mThe quick brown fox${RESET}`) + "\n");
2379
+ w.write(row("58;2;R;G;B TC UL", `${sgr(4)}${CSI}58;2;255;128;0mThe quick brown fox${RESET}`) + "\n");
2380
+ w.write(row("After SGR 58 (clean?)", `${sgr(4)}${CSI}58;5;1m${RESET}This text should be normal — if garbled, 58 corrupted SGR state`) + "\n");
2381
+ }
2382
+ if (show("colors")) {
2383
+ w.write(sectionHeader("ANSI 16 Colors"));
2384
+ const colorNames = [
2385
+ "Black",
2386
+ "Red",
2387
+ "Green",
2388
+ "Yellow",
2389
+ "Blue",
2390
+ "Magenta",
2391
+ "Cyan",
2392
+ "White"
2393
+ ];
2394
+ let fgLine = " FG: ";
2395
+ for (let i = 0; i < 8; i++) fgLine += `${sgr(30 + i)} ${colorNames[i]}${RESET}`;
2396
+ w.write(fgLine + "\n");
2397
+ let fgBrLine = " Br: ";
2398
+ for (let i = 0; i < 8; i++) fgBrLine += `${sgr(90 + i)} ${colorNames[i]}${RESET}`;
2399
+ w.write(fgBrLine + "\n");
2400
+ let bgLine = " BG: ";
2401
+ for (let i = 0; i < 8; i++) bgLine += `${sgr(40 + i)} ${colorNames[i]} ${RESET}`;
2402
+ w.write(bgLine + "\n");
2403
+ let bgBrLine = " BrBG:";
2404
+ for (let i = 0; i < 8; i++) bgBrLine += `${sgr(100 + i)} ${colorNames[i]} ${RESET}`;
2405
+ w.write(bgBrLine + "\n");
2406
+ }
2407
+ if (show("256")) {
2408
+ w.write(sectionHeader("256 Colors (indices 0-15, 16-231, 232-255)"));
2409
+ let stdLine = " 0-15: ";
2410
+ for (let i = 0; i < 16; i++) stdLine += `${CSI}48;5;${i}m ${RESET}`;
2411
+ w.write(stdLine + "\n");
2412
+ let cubeLine = " Cube: ";
2413
+ for (let i = 16; i < 52; i++) cubeLine += `${CSI}48;5;${i}m ${RESET}`;
2414
+ w.write(cubeLine + "\n");
2415
+ let grayLine = " Gray: ";
2416
+ for (let i = 232; i < 256; i++) grayLine += `${CSI}48;5;${i}m ${RESET}`;
2417
+ w.write(grayLine + "\n");
2418
+ }
2419
+ if (show("truecolor")) {
2420
+ w.write(sectionHeader("Truecolor (38;2;R;G;B / 48;2;R;G;B)"));
2421
+ let tcLine = " Gradient: ";
2422
+ for (let i = 0; i < 40; i++) {
2423
+ const r = Math.round(i / 39 * 255);
2424
+ const g = Math.round((39 - i) / 39 * 128);
2425
+ tcLine += `${CSI}48;2;${r};${g};80m ${RESET}`;
2426
+ }
2427
+ w.write(tcLine + "\n");
2428
+ w.write(row("If solid blocks →", "Truecolor NOT supported (256-color fallback)") + "\n");
2429
+ }
2430
+ if (show("unicode")) {
2431
+ w.write(sectionHeader("Unicode, Emoji, PUA (Nerd Fonts)"));
2432
+ w.write(row("ASCII", "Hello World! 0123456789") + "\n");
2433
+ w.write(row("Latin Extended", "àéîõü ñ ß ø å") + "\n");
2434
+ w.write(row("CJK", "你好世界 日本語 한국어") + "\n");
2435
+ w.write(row("Box Drawing", "┌─┬─┐ ╔═╦═╗ ╭─╮") + "\n");
2436
+ w.write(row("Block Elements", "▀▄█▌▐░▒▓") + "\n");
2437
+ w.write(row("Symbols", "● ○ ◉ ▶ ◀ ⚠ ✓ ✗ ⋮ §") + "\n");
2438
+ w.write(row("Emoji", "🎉 🚀 📁 📄 ⭐ 🔥 👍") + "\n");
2439
+ w.write(row("Nerd Font PUA", " folder  file  arrow  gear") + "\n");
2440
+ w.write(row("If PUA = boxes →", "Nerd Fonts not installed") + "\n");
2441
+ }
2442
+ if (show("emoji")) {
2443
+ w.write(sectionHeader("Emoji Width Alignment (_'s should align with ruler)"));
2444
+ w.write(" Ruler: |1234567890|\n");
2445
+ w.write(" ASCII 'A': |A_________| (w=1)\n");
2446
+ w.write(" CJK '你': |你________| (w=2)\n");
2447
+ w.write(" Flag 🇨🇦: |🇨🇦________| (w=2)\n");
2448
+ w.write(" Flag 🇺🇸: |🇺🇸________| (w=2)\n");
2449
+ w.write(" Emoji 📁: |📁________| (w=2)\n");
2450
+ w.write(" Emoji 📄: |📄________| (w=2)\n");
2451
+ w.write(" Emoji 📋: |📋________| (w=2)\n");
2452
+ w.write(" Emoji ⚠️: |⚠️________| (w=2)\n");
2453
+ w.write(" Text ⚠: |⚠_________| (w=1 text, 2 emoji)\n");
2454
+ w.write(" Text ⭐: |⭐________| (w=1 text, 2 emoji)\n");
2455
+ w.write(" Text ☑: |☑_________| (w=1 text, 2 emoji)\n");
2456
+ w.write(" Emoji 🏠: |🏠________| (w=2)\n");
2457
+ w.write(" Emoji 👓: |👓________| (w=2)\n");
2458
+ w.write(" ZWJ 👨🏻‍💻: |👨🏻‍💻________| (w=2)\n");
2459
+ w.write(" Arrow →: |→_________| (w=1)\n");
2460
+ w.write(" Arrow ▸: |▸_________| (w=1)\n");
2461
+ w.write(" Circle ○: |○_________| (w=1)\n");
2462
+ w.write(" Square □: |□_________| (w=1)\n");
2463
+ w.write(" Check ✓: |✓_________| (w=1)\n");
2464
+ }
2465
+ if (show("borders")) {
2466
+ w.write(sectionHeader("Box Drawing Borders"));
2467
+ w.write(" ┌──────────┐ ╔══════════╗ ╭──────────╮\n");
2468
+ w.write(" │ single │ ║ double ║ │ round │\n");
2469
+ w.write(" └──────────┘ ╚══════════╝ ╰──────────╯\n");
2470
+ }
2471
+ if (show("inverse")) {
2472
+ w.write(sectionHeader("Inverse + Background (potential artifact source)"));
2473
+ w.write(row("Red FG + Inverse", `${sgr(31, 7)}This should have red background${RESET}`) + "\n");
2474
+ w.write(row("Cyan BG + White FG", `${sgr(46, 37)}Cyan background, white text${RESET}`) + "\n");
2475
+ w.write(row("Black BG + White FG", `${sgr(40, 97)}Black bg, bright white text${RESET}`) + "\n");
2476
+ w.write(row("White BG + Black FG", `${sgr(107, 30)}White bg, black text${RESET}`) + "\n");
2477
+ w.write(sectionHeader("Reset Sanity Check"));
2478
+ w.write(" This line should be completely normal with no formatting artifacts.\n");
2479
+ w.write(" If you see colors, underlines, or other styling above, the terminal\n");
2480
+ w.write(" failed to process an SGR reset (\\x1b[0m) correctly.\n");
2481
+ }
2482
+ if (show("profile")) {
2483
+ w.write(sectionHeader("Detected Terminal Profile"));
2484
+ const entries = Object.entries(caps);
2485
+ for (const [key, value] of entries) {
2486
+ const indicator = value === true ? "✓" : value === false ? "✗" : String(value);
2487
+ w.write(` ${key.padEnd(20)} ${indicator}\n`);
2488
+ }
2489
+ }
2490
+ w.write("\n");
2491
+ }
2492
+ //#endregion
2493
+ //#region packages/ag-term/src/cursor-query.ts
2494
+ /**
2495
+ * CSI 6n Cursor Position Query
2496
+ *
2497
+ * Queries the terminal for the current cursor position using the standard
2498
+ * Device Status Report (DSR) mechanism.
2499
+ *
2500
+ * Protocol:
2501
+ * - Query: CSI 6 n (\x1b[6n)
2502
+ * - Response: CSI {row} ; {col} R (\x1b[{row};{col}R)
2503
+ *
2504
+ * Row and column are 1-indexed in the protocol response.
2505
+ *
2506
+ * Supported by: virtually all terminals (VT100+)
2507
+ */
2508
+ /** Regex to match a CPR response: CSI row ; col R */
2509
+ const CPR_RESPONSE_RE = /\x1b\[(\d+);(\d+)R/;
2510
+ /**
2511
+ * Query the terminal cursor position.
2512
+ *
2513
+ * Sends CSI 6n and parses the CPR response.
2514
+ * Returns 1-indexed row and column.
2515
+ *
2516
+ * @param write Function to write to stdout
2517
+ * @param read Function to read a chunk from stdin (resolves with data or null on timeout)
2518
+ * @param timeoutMs How long to wait for response (default: 200ms)
2519
+ */
2520
+ async function queryCursorPosition(write, read, timeoutMs = 200) {
2521
+ write("\x1B[6n");
2522
+ const data = await read(timeoutMs);
2523
+ if (data == null) return null;
2524
+ const match = CPR_RESPONSE_RE.exec(data);
2525
+ if (!match) return null;
2526
+ return {
2527
+ row: parseInt(match[1], 10),
2528
+ col: parseInt(match[2], 10)
2529
+ };
2530
+ }
2531
+ /**
2532
+ * Query cursor position using real stdin/stdout.
2533
+ * Convenience wrapper around queryCursorPosition.
2534
+ */
2535
+ async function queryCursorFromStdio(stdout, stdin, timeoutMs = 200) {
2536
+ const otherListeners = stdin.listenerCount("data") > 0;
2537
+ const wasRaw = stdin.isRaw;
2538
+ let didSetRaw = false;
2539
+ if (!wasRaw && !otherListeners) {
2540
+ stdin.setRawMode(true);
2541
+ didSetRaw = true;
2542
+ }
2543
+ try {
2544
+ const write = (s) => {
2545
+ stdout.write(s);
2546
+ };
2547
+ const read = (ms) => new Promise((resolve) => {
2548
+ const timer = setTimeout(() => {
2549
+ stdin.removeListener("data", onData);
2550
+ resolve(null);
2551
+ }, ms);
2552
+ function onData(chunk) {
2553
+ clearTimeout(timer);
2554
+ stdin.removeListener("data", onData);
2555
+ resolve(chunk.toString());
2556
+ }
2557
+ stdin.on("data", onData);
2558
+ });
2559
+ return await queryCursorPosition(write, read, timeoutMs);
2560
+ } finally {
2561
+ if (didSetRaw) stdin.setRawMode(false);
2562
+ }
2563
+ }
2564
+ //#endregion
2565
+ //#region packages/ag-term/src/ansi/width-detection.ts
2566
+ /**
2567
+ * DEC Width Mode Detection (xterm patch #407)
2568
+ *
2569
+ * Queries the terminal for its character width settings using DECRQM
2570
+ * (DEC Private Mode Request). This replaces guesswork with definitive
2571
+ * answers from the terminal itself.
2572
+ *
2573
+ * Modes:
2574
+ * - 1020: UTF-8 mode
2575
+ * - 1021: CJK ambiguous width (1 or 2 cells)
2576
+ * - 1022: Emoji width (1 or 2 cells)
2577
+ * - 1023: Private-use area width (1 or 2 cells)
2578
+ *
2579
+ * Protocol:
2580
+ * - Query: CSI ? {mode} $ p (DECRQM)
2581
+ * - Response: CSI ? {mode} ; {Ps} $ y (DECRPM)
2582
+ *
2583
+ * Where Ps is:
2584
+ * 1 = set (enabled / wide / 2-cell)
2585
+ * 2 = reset (disabled / narrow / 1-cell)
2586
+ * 0 = not recognized
2587
+ * 3 = permanently set
2588
+ * 4 = permanently reset
2589
+ *
2590
+ * @see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
2591
+ */
2592
+ /** Well-known xterm width DEC mode numbers. */
2593
+ const WidthMode = {
2594
+ /** UTF-8 mode */
2595
+ UTF8: 1020,
2596
+ /** CJK ambiguous character width */
2597
+ CJK_WIDTH: 1021,
2598
+ /** Emoji width */
2599
+ EMOJI_WIDTH: 1022,
2600
+ /** Private-use area width */
2601
+ PRIVATE_USE_WIDTH: 1023
2602
+ };
2603
+ /** Default configuration when detection fails or times out. */
2604
+ const DEFAULT_WIDTH_CONFIG = {
2605
+ utf8: true,
2606
+ cjkWidth: 1,
2607
+ emojiWidth: 2,
2608
+ privateUseWidth: 1
2609
+ };
2610
+ /** Regex for DECRPM response: CSI ? mode ; Ps $ y */
2611
+ const DECRPM_RE = /\x1b\[\?(\d+);(\d+)\$y/g;
2612
+ /**
2613
+ * Parse a DECRPM response value into a boolean (set/reset).
2614
+ * 1 and 3 (permanently set) = true, everything else = false.
2615
+ */
2616
+ function isSet(ps) {
2617
+ return ps === 1 || ps === 3;
2618
+ }
2619
+ /**
2620
+ * Query a single DEC width mode via DECRQM.
2621
+ * Returns the Ps value from the DECRPM response, or null on timeout.
2622
+ */
2623
+ function queryWidthMode(write, onData, mode, timeoutMs) {
2624
+ return new Promise((resolve) => {
2625
+ let timer = null;
2626
+ let unsubscribe = null;
2627
+ let buffer = "";
2628
+ function cleanup() {
2629
+ if (timer !== null) {
2630
+ clearTimeout(timer);
2631
+ timer = null;
2632
+ }
2633
+ if (unsubscribe !== null) {
2634
+ unsubscribe();
2635
+ unsubscribe = null;
2636
+ }
2637
+ }
2638
+ unsubscribe = onData((data) => {
2639
+ buffer += data;
2640
+ DECRPM_RE.lastIndex = 0;
2641
+ let match;
2642
+ while ((match = DECRPM_RE.exec(buffer)) !== null) if (parseInt(match[1], 10) === mode) {
2643
+ const ps = parseInt(match[2], 10);
2644
+ cleanup();
2645
+ resolve(ps);
2646
+ return;
2647
+ }
2648
+ });
2649
+ timer = setTimeout(() => {
2650
+ cleanup();
2651
+ resolve(null);
2652
+ }, timeoutMs);
2653
+ write(`\x1b[?${mode}$p`);
2654
+ });
2655
+ }
2656
+ /**
2657
+ * Apply detected width configuration to terminal capabilities.
2658
+ *
2659
+ * Post km-silvery.plateau-naming-polish (2026-04-23): `maybeWideEmojis` and
2660
+ * `textSizing` both live on caps, so the overlay returns a single updated
2661
+ * caps object. The mapping is:
2662
+ * - emojiWidth=2 → caps.maybeWideEmojis=true
2663
+ * - privateUseWidth=2 → caps.textSizing=true (PUA treated as 2-wide)
2664
+ *
2665
+ * CJK width and UTF-8 mode are informational — they don't yet map to caps
2666
+ * fields but are available in TerminalWidthConfig for consumers.
2667
+ */
2668
+ function applyWidthConfig(caps, config) {
2669
+ return {
2670
+ ...caps,
2671
+ textSizing: config.privateUseWidth === 2,
2672
+ maybeWideEmojis: config.emojiWidth === 2
2673
+ };
2674
+ }
2675
+ /**
2676
+ * Create a width detector that queries the terminal for DEC modes 1020-1023.
2677
+ *
2678
+ * @example
2679
+ * ```ts
2680
+ * const detector = createWidthDetector({
2681
+ * write: (data) => process.stdout.write(data),
2682
+ * onData: (handler) => {
2683
+ * process.stdin.on('data', (chunk) => handler(chunk.toString()))
2684
+ * return () => process.stdin.removeListener('data', handler)
2685
+ * },
2686
+ * })
2687
+ *
2688
+ * const config = await detector.detect()
2689
+ * console.log(config.emojiWidth) // 1 or 2
2690
+ * detector.dispose()
2691
+ * ```
2692
+ */
2693
+ function createWidthDetector(options) {
2694
+ const { write, onData, timeoutMs = 200 } = options;
2695
+ let config = null;
2696
+ let disposed = false;
2697
+ return {
2698
+ get config() {
2699
+ return config;
2700
+ },
2701
+ async detect() {
2702
+ if (disposed) return config ?? { ...DEFAULT_WIDTH_CONFIG };
2703
+ if (config !== null) return config;
2704
+ const utf8Ps = await queryWidthMode(write, onData, WidthMode.UTF8, timeoutMs);
2705
+ const cjkPs = await queryWidthMode(write, onData, WidthMode.CJK_WIDTH, timeoutMs);
2706
+ const emojiPs = await queryWidthMode(write, onData, WidthMode.EMOJI_WIDTH, timeoutMs);
2707
+ const puaPs = await queryWidthMode(write, onData, WidthMode.PRIVATE_USE_WIDTH, timeoutMs);
2708
+ config = {
2709
+ utf8: utf8Ps !== null ? isSet(utf8Ps) : DEFAULT_WIDTH_CONFIG.utf8,
2710
+ cjkWidth: cjkPs !== null ? isSet(cjkPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.cjkWidth,
2711
+ emojiWidth: emojiPs !== null ? isSet(emojiPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.emojiWidth,
2712
+ privateUseWidth: puaPs !== null ? isSet(puaPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.privateUseWidth
2713
+ };
2714
+ return config;
2715
+ },
2716
+ dispose() {
2717
+ disposed = true;
2718
+ }
2719
+ };
2720
+ }
2721
+ //#endregion
2722
+ //#region packages/ag-term/src/device-attrs.ts
2723
+ /**
2724
+ * Device Attributes (DA1/DA2/DA3) + XTVERSION Queries
2725
+ *
2726
+ * Provides functions to query terminal identity and capabilities using
2727
+ * the standard VT device attribute escape sequences.
2728
+ *
2729
+ * Protocols:
2730
+ *
2731
+ * DA1 (Primary Device Attributes):
2732
+ * Query: CSI c (\x1b[c)
2733
+ * Response: CSI ? Ps ; Ps ; ... c
2734
+ *
2735
+ * DA2 (Secondary Device Attributes):
2736
+ * Query: CSI > c (\x1b[>c)
2737
+ * Response: CSI > Pt ; Pv ; Pc c
2738
+ * Where Pt=terminal type, Pv=firmware version, Pc=ROM cartridge id
2739
+ *
2740
+ * DA3 (Tertiary Device Attributes):
2741
+ * Query: CSI = c (\x1b[=c)
2742
+ * Response: DCS ! | hex-encoded-id ST (\x1bP!|{hex}\x1b\\)
2743
+ *
2744
+ * XTVERSION (Terminal Name + Version):
2745
+ * Query: CSI > 0 q (\x1b[>0q)
2746
+ * Response: DCS > | name(version) ST (\x1bP>|{text}\x1b\\)
2747
+ *
2748
+ * Supported by: xterm, Ghostty, Kitty, WezTerm, foot, VTE-based terminals
2749
+ */
2750
+ /** Regex for DA1 response: CSI ? params c */
2751
+ const DA1_RESPONSE_RE = /\x1b\[\?([\d;]+)c/;
2752
+ /** Regex for DA2 response: CSI > params c */
2753
+ const DA2_RESPONSE_RE = /\x1b\[>([\d;]+)c/;
2754
+ /** Regex for DA3 response: DCS ! | hex ST */
2755
+ const DA3_RESPONSE_RE = /\x1bP!\|([0-9a-fA-F]*)\x1b\\/;
2756
+ /** Regex for XTVERSION response: DCS > | text ST */
2757
+ const XTVERSION_RESPONSE_RE = /\x1bP>\|([^\x1b]*)\x1b\\/;
2758
+ /**
2759
+ * Query primary device attributes (DA1).
2760
+ *
2761
+ * Returns the list of attribute parameters the terminal reports.
2762
+ * Common params: 1=132-cols, 4=sixel, 6=selective-erase, 22=ANSI-color
2763
+ *
2764
+ * @param write Function to write to stdout
2765
+ * @param read Function to read a chunk from stdin
2766
+ * @param timeoutMs How long to wait for response (default: 200ms)
2767
+ */
2768
+ async function queryPrimaryDA(write, read, timeoutMs = 200) {
2769
+ write("\x1B[c");
2770
+ const data = await read(timeoutMs);
2771
+ if (data == null) return null;
2772
+ const match = DA1_RESPONSE_RE.exec(data);
2773
+ if (!match) return null;
2774
+ return { params: match[1].split(";").map((s) => parseInt(s, 10)) };
2775
+ }
2776
+ /**
2777
+ * Query secondary device attributes (DA2).
2778
+ *
2779
+ * Returns terminal type, firmware version, and ROM cartridge id.
2780
+ * Common type values: 0=VT100, 1=VT220, 41=xterm, 65=VT500
2781
+ *
2782
+ * @param write Function to write to stdout
2783
+ * @param read Function to read a chunk from stdin
2784
+ * @param timeoutMs How long to wait for response (default: 200ms)
2785
+ */
2786
+ async function querySecondaryDA(write, read, timeoutMs = 200) {
2787
+ write("\x1B[>c");
2788
+ const data = await read(timeoutMs);
2789
+ if (data == null) return null;
2790
+ const match = DA2_RESPONSE_RE.exec(data);
2791
+ if (!match) return null;
2792
+ const parts = match[1].split(";");
2793
+ if (parts.length < 3) return null;
2794
+ return {
2795
+ type: parseInt(parts[0], 10),
2796
+ version: parseInt(parts[1], 10),
2797
+ id: parseInt(parts[2], 10)
2798
+ };
2799
+ }
2800
+ /**
2801
+ * Query tertiary device attributes (DA3).
2802
+ *
2803
+ * Returns a hex-encoded unit ID string. Decode with Buffer.from(hex, 'hex').
2804
+ *
2805
+ * @param write Function to write to stdout
2806
+ * @param read Function to read a chunk from stdin
2807
+ * @param timeoutMs How long to wait for response (default: 200ms)
2808
+ */
2809
+ async function queryTertiaryDA(write, read, timeoutMs = 200) {
2810
+ write("\x1B[=c");
2811
+ const data = await read(timeoutMs);
2812
+ if (data == null) return null;
2813
+ const match = DA3_RESPONSE_RE.exec(data);
2814
+ if (!match) return null;
2815
+ return match[1];
2816
+ }
2817
+ /**
2818
+ * Query the terminal name and version via XTVERSION.
2819
+ *
2820
+ * Returns the version string as reported by the terminal, e.g.:
2821
+ * - "xterm(388)"
2822
+ * - "tmux 3.4"
2823
+ * - "WezTerm 20230712-072601-f4abf8fd"
2824
+ *
2825
+ * @param write Function to write to stdout
2826
+ * @param read Function to read a chunk from stdin
2827
+ * @param timeoutMs How long to wait for response (default: 200ms)
2828
+ */
2829
+ async function queryTerminalVersion(write, read, timeoutMs = 200) {
2830
+ write("\x1B[>0q");
2831
+ const data = await read(timeoutMs);
2832
+ if (data == null) return null;
2833
+ const match = XTVERSION_RESPONSE_RE.exec(data);
2834
+ if (!match) return null;
2835
+ return match[1];
2836
+ }
2837
+ /**
2838
+ * Query all device attributes: DA1, DA2, and XTVERSION.
2839
+ *
2840
+ * Convenience wrapper that queries all three sequentially.
2841
+ * DA3 is omitted from the combined query as it's rarely needed.
2842
+ *
2843
+ * @param stdout Writable stream (e.g., process.stdout)
2844
+ * @param stdin Readable stream (e.g., process.stdin)
2845
+ * @param timeoutMs Per-query timeout (default: 200ms)
2846
+ */
2847
+ async function queryDeviceAttributes(stdout, stdin, timeoutMs = 200) {
2848
+ const otherListeners = stdin.listenerCount("data") > 0;
2849
+ const wasRaw = stdin.isRaw;
2850
+ let didSetRaw = false;
2851
+ if (!wasRaw && !otherListeners) {
2852
+ stdin.setRawMode(true);
2853
+ didSetRaw = true;
2854
+ }
2855
+ try {
2856
+ const write = (s) => {
2857
+ stdout.write(s);
2858
+ };
2859
+ const read = (ms) => new Promise((resolve) => {
2860
+ const timer = setTimeout(() => {
2861
+ stdin.removeListener("data", onData);
2862
+ resolve(null);
2863
+ }, ms);
2864
+ function onData(chunk) {
2865
+ clearTimeout(timer);
2866
+ stdin.removeListener("data", onData);
2867
+ resolve(chunk.toString());
2868
+ }
2869
+ stdin.on("data", onData);
2870
+ });
2871
+ return {
2872
+ da1: await queryPrimaryDA(write, read, timeoutMs),
2873
+ da2: await querySecondaryDA(write, read, timeoutMs),
2874
+ version: await queryTerminalVersion(write, read, timeoutMs)
2875
+ };
2876
+ } finally {
2877
+ if (didSetRaw) stdin.setRawMode(false);
2878
+ }
2879
+ }
2880
+ //#endregion
2881
+ //#region packages/ag-term/src/mode-query.ts
2882
+ /**
2883
+ * DECRQM — DEC Private Mode Query
2884
+ *
2885
+ * Queries the terminal for the state of DEC private modes.
2886
+ *
2887
+ * Protocol:
2888
+ * - Query: CSI ? {mode} $ p
2889
+ * - Response: CSI ? {mode} ; {Ps} $ y
2890
+ *
2891
+ * Where Ps is:
2892
+ * 1 = set (mode is enabled)
2893
+ * 2 = reset (mode is disabled)
2894
+ * 0 = not recognized (unknown mode)
2895
+ * 3 = permanently set
2896
+ * 4 = permanently reset
2897
+ *
2898
+ * We normalize 3→"set" and 4→"reset" for simplicity.
2899
+ *
2900
+ * Supported by: xterm, Ghostty, Kitty, WezTerm, foot, VTE-based terminals
2901
+ */
2902
+ /** Regex for DECRPM response: CSI ? mode ; Ps $ y */
2903
+ const DECRPM_RESPONSE_RE = /\x1b\[\?(\d+);(\d+)\$y/;
2904
+ /** Well-known DEC private mode constants. */
2905
+ const DecMode = {
2906
+ /** DEC cursor visible (DECTCEM) */
2907
+ CURSOR_VISIBLE: 25,
2908
+ /** Alternate screen buffer (DECSET 1049) */
2909
+ ALT_SCREEN: 1049,
2910
+ /** Normal mouse tracking (X10) */
2911
+ MOUSE_TRACKING: 1e3,
2912
+ /** Bracketed paste mode */
2913
+ BRACKETED_PASTE: 2004,
2914
+ /** Synchronized output */
2915
+ SYNC_OUTPUT: 2026,
2916
+ /** Focus reporting */
2917
+ FOCUS_REPORTING: 1004
2918
+ };
2919
+ /**
2920
+ * Query the state of a single DEC private mode.
2921
+ *
2922
+ * @param write Function to write to stdout
2923
+ * @param read Function to read a chunk from stdin
2924
+ * @param mode DEC private mode number (e.g., DecMode.ALT_SCREEN)
2925
+ * @param timeoutMs How long to wait for response (default: 200ms)
2926
+ * @returns "set", "reset", or "unknown"
2927
+ */
2928
+ async function queryMode(write, read, mode, timeoutMs = 200) {
2929
+ write(`\x1b[?${mode}$p`);
2930
+ const data = await read(timeoutMs);
2931
+ if (data == null) return "unknown";
2932
+ const match = DECRPM_RESPONSE_RE.exec(data);
2933
+ if (!match) return "unknown";
2934
+ if (parseInt(match[1], 10) !== mode) return "unknown";
2935
+ switch (parseInt(match[2], 10)) {
2936
+ case 1:
2937
+ case 3: return "set";
2938
+ case 2:
2939
+ case 4: return "reset";
2940
+ default: return "unknown";
2941
+ }
2942
+ }
2943
+ /**
2944
+ * Query the state of multiple DEC private modes.
2945
+ *
2946
+ * Queries each mode sequentially and returns a Map of results.
2947
+ *
2948
+ * @param write Function to write to stdout
2949
+ * @param read Function to read a chunk from stdin
2950
+ * @param modes Array of DEC private mode numbers
2951
+ * @param timeoutMs Per-query timeout (default: 200ms)
2952
+ */
2953
+ async function queryModes(write, read, modes, timeoutMs = 200) {
2954
+ const results = /* @__PURE__ */ new Map();
2955
+ for (const mode of modes) {
2956
+ const state = await queryMode(write, read, mode, timeoutMs);
2957
+ results.set(mode, state);
2958
+ }
2959
+ return results;
2960
+ }
2961
+ //#endregion
2962
+ //#region packages/ag-term/src/pixel-size.ts
2963
+ /**
2964
+ * CSI 14t/18t — Terminal Pixel and Text Area Size Queries
2965
+ *
2966
+ * Queries the terminal for window dimensions in pixels and characters.
2967
+ *
2968
+ * Protocols:
2969
+ *
2970
+ * Text Area Pixels (CSI 14t):
2971
+ * Query: CSI 14 t
2972
+ * Response: CSI 4 ; height ; width t
2973
+ *
2974
+ * Text Area Size in Characters (CSI 18t):
2975
+ * Query: CSI 18 t
2976
+ * Response: CSI 8 ; rows ; cols t
2977
+ *
2978
+ * Cell size can be derived by dividing pixel dimensions by character dimensions.
2979
+ *
2980
+ * Supported by: xterm, Ghostty, Kitty, WezTerm, foot, iTerm2
2981
+ */
2982
+ /** Regex for CSI 4 ; height ; width t (pixel size response) */
2983
+ const PIXEL_RESPONSE_RE = /\x1b\[4;(\d+);(\d+)t/;
2984
+ /** Regex for CSI 8 ; rows ; cols t (text area size response) */
2985
+ const TEXT_AREA_RESPONSE_RE = /\x1b\[8;(\d+);(\d+)t/;
2986
+ /**
2987
+ * Query the terminal text area size in pixels.
2988
+ *
2989
+ * @param write Function to write to stdout
2990
+ * @param read Function to read a chunk from stdin
2991
+ * @param timeoutMs How long to wait for response (default: 200ms)
2992
+ * @returns Width and height in pixels, or null on timeout/unsupported
2993
+ */
2994
+ async function queryTextAreaPixels(write, read, timeoutMs = 200) {
2995
+ write("\x1B[14t");
2996
+ const data = await read(timeoutMs);
2997
+ if (data == null) return null;
2998
+ const match = PIXEL_RESPONSE_RE.exec(data);
2999
+ if (!match) return null;
3000
+ return {
3001
+ height: parseInt(match[1], 10),
3002
+ width: parseInt(match[2], 10)
3003
+ };
3004
+ }
3005
+ /**
3006
+ * Query the terminal text area size in characters (rows x columns).
3007
+ *
3008
+ * @param write Function to write to stdout
3009
+ * @param read Function to read a chunk from stdin
3010
+ * @param timeoutMs How long to wait for response (default: 200ms)
3011
+ * @returns Rows and columns, or null on timeout/unsupported
3012
+ */
3013
+ async function queryTextAreaSize(write, read, timeoutMs = 200) {
3014
+ write("\x1B[18t");
3015
+ const data = await read(timeoutMs);
3016
+ if (data == null) return null;
3017
+ const match = TEXT_AREA_RESPONSE_RE.exec(data);
3018
+ if (!match) return null;
3019
+ return {
3020
+ rows: parseInt(match[1], 10),
3021
+ cols: parseInt(match[2], 10)
3022
+ };
3023
+ }
3024
+ /**
3025
+ * Query the terminal cell size in pixels by querying both pixel
3026
+ * dimensions and character dimensions, then dividing.
3027
+ *
3028
+ * @param write Function to write to stdout
3029
+ * @param read Function to read a chunk from stdin
3030
+ * @param timeoutMs Per-query timeout (default: 200ms)
3031
+ * @returns Cell width and height in pixels, or null if either query fails
3032
+ */
3033
+ async function queryCellSize(write, read, timeoutMs = 200) {
3034
+ const pixels = await queryTextAreaPixels(write, read, timeoutMs);
3035
+ if (pixels == null) return null;
3036
+ const size = await queryTextAreaSize(write, read, timeoutMs);
3037
+ if (size == null) return null;
3038
+ if (size.cols === 0 || size.rows === 0) return null;
3039
+ return {
3040
+ width: pixels.width / size.cols,
3041
+ height: pixels.height / size.rows
3042
+ };
3043
+ }
3044
+ //#endregion
3045
+ //#region packages/ag-term/src/term-def.ts
3046
+ /**
3047
+ * Default dimensions when not detectable.
3048
+ */
3049
+ const DEFAULT_WIDTH = 80;
3050
+ const DEFAULT_HEIGHT = 24;
3051
+ /**
3052
+ * Check if a value is a Term instance (duck typing).
3053
+ *
3054
+ * Post km-silvery.caps-restructure Phase 7: the legacy hasCursor/hasInput/
3055
+ * hasColor methods were removed — caps/profile/write/size are the canonical
3056
+ * Term surface.
3057
+ */
3058
+ function isTerm(value) {
3059
+ if (!value || typeof value !== "object" && typeof value !== "function") return false;
3060
+ const obj = value;
3061
+ return typeof obj.caps === "object" && obj.caps !== null && typeof obj.write === "function" && typeof obj.size === "object";
3062
+ }
3063
+ /**
3064
+ * Check if a value is a TermDef (not a Term).
3065
+ */
3066
+ function isTermDef(value) {
3067
+ if (!value || typeof value !== "object") return false;
3068
+ return value.caps === void 0;
3069
+ }
3070
+ /**
3071
+ * Resolve a TermDef into concrete values.
3072
+ *
3073
+ * @param def - TermDef to resolve
3074
+ * @returns Resolved values ready for rendering
3075
+ */
3076
+ function resolveTermDef(def) {
3077
+ const width = def.width ?? def.stdout?.columns ?? DEFAULT_WIDTH;
3078
+ const height = def.height ?? def.stdout?.rows ?? DEFAULT_HEIGHT;
3079
+ let colors;
3080
+ if (def.colors === true) colors = detectColorLevel(def.stdout);
3081
+ else if (def.colors === false || def.colors === null) colors = "mono";
3082
+ else if (def.colors) colors = def.colors;
3083
+ else colors = detectColorLevel(def.stdout);
3084
+ let events = null;
3085
+ if (def.events) events = def.events;
3086
+ else if (def.stdin) events = createInputEvents(def.stdin);
3087
+ return {
3088
+ stdout: def.stdout ?? null,
3089
+ width,
3090
+ height,
3091
+ colors,
3092
+ events,
3093
+ isStatic: events === null
3094
+ };
3095
+ }
3096
+ /**
3097
+ * Resolve a Term instance into ResolvedTermDef.
3098
+ *
3099
+ * @param term - Term instance
3100
+ * @returns Resolved values
3101
+ */
3102
+ function resolveFromTerm(term) {
3103
+ const { stdin, stdout } = getInternalStreams(term);
3104
+ return {
3105
+ stdout,
3106
+ width: term.cols ?? DEFAULT_WIDTH,
3107
+ height: term.rows ?? DEFAULT_HEIGHT,
3108
+ colors: term.profile.colorLevel,
3109
+ events: createInputEvents(stdin),
3110
+ isStatic: false
3111
+ };
3112
+ }
3113
+ /**
3114
+ * Detect the color tier from a stdout stream.
3115
+ *
3116
+ * Phase 3 of km-silvery.terminal-profile-plateau: this is now a single-line
3117
+ * delegate into `createTerminalProfile` — no more local detection copy.
3118
+ */
3119
+ function detectColorLevel(stdout) {
3120
+ return createTerminalProfile({ stdout: stdout ?? { isTTY: false } }).colorLevel;
3121
+ }
3122
+ /**
3123
+ * Create an async iterable of input events from a stdin stream.
3124
+ *
3125
+ * This enables interactive mode by providing a source of keyboard events.
3126
+ */
3127
+ function createInputEvents(stdin) {
3128
+ return { [Symbol.asyncIterator]() {
3129
+ const buffer = [];
3130
+ let resolveNext = null;
3131
+ let done = false;
3132
+ const handleData = (chunk) => {
3133
+ const data = typeof chunk === "string" ? chunk : chunk.toString("utf8");
3134
+ for (const char of data) {
3135
+ const event = {
3136
+ type: "key",
3137
+ key: char,
3138
+ ctrl: char.charCodeAt(0) < 32 && char !== "\r" && char !== "\n" && char !== " "
3139
+ };
3140
+ if (resolveNext) {
3141
+ resolveNext({
3142
+ value: event,
3143
+ done: false
3144
+ });
3145
+ resolveNext = null;
3146
+ } else buffer.push(event);
3147
+ }
3148
+ };
3149
+ const handleEnd = () => {
3150
+ done = true;
3151
+ if (resolveNext) {
3152
+ resolveNext({
3153
+ value: void 0,
3154
+ done: true
3155
+ });
3156
+ resolveNext = null;
3157
+ }
3158
+ };
3159
+ if (stdin.isTTY && typeof stdin.setRawMode === "function") {
3160
+ stdin.setEncoding("utf8");
3161
+ stdin.on("data", handleData);
3162
+ stdin.on("end", handleEnd);
3163
+ }
3164
+ return {
3165
+ next() {
3166
+ const buffered = buffer.shift();
3167
+ if (buffered) return Promise.resolve({
3168
+ value: buffered,
3169
+ done: false
3170
+ });
3171
+ if (done) return Promise.resolve({
3172
+ value: void 0,
3173
+ done: true
3174
+ });
3175
+ return new Promise((resolve) => {
3176
+ resolveNext = resolve;
3177
+ });
3178
+ },
3179
+ return() {
3180
+ done = true;
3181
+ stdin.off("data", handleData);
3182
+ stdin.off("end", handleEnd);
3183
+ return Promise.resolve({
3184
+ value: void 0,
3185
+ done: true
3186
+ });
3187
+ }
3188
+ };
3189
+ } };
3190
+ }
3191
+ //#endregion
3192
+ //#region packages/ag-term/src/non-tty.ts
3193
+ /**
3194
+ * Non-TTY Mode Support for Silvery
3195
+ *
3196
+ * Provides detection and rendering modes for non-interactive environments:
3197
+ * - Piped output (process.stdout.isTTY === false)
3198
+ * - CI environments
3199
+ * - TERM=dumb
3200
+ *
3201
+ * When in non-TTY mode, silvery avoids cursor positioning codes that garble
3202
+ * output in non-interactive environments.
3203
+ *
3204
+ * Modes:
3205
+ * - 'auto': Detect based on environment (default)
3206
+ * - 'tty': Force TTY mode (normal cursor positioning)
3207
+ * - 'line-by-line': Simple newline-separated output, no cursor movement
3208
+ * - 'static': Single output at end (no updates)
3209
+ * - 'plain': Strip all ANSI codes
3210
+ */
3211
+ /**
3212
+ * Check if the environment is a TTY.
3213
+ *
3214
+ * Returns false if:
3215
+ * - stdout.isTTY is false or undefined
3216
+ * - TERM=dumb
3217
+ * - CI environment variables are set
3218
+ *
3219
+ * Pass `emulator` (from `term.emulator` or a TerminalEmulator fixture) when
3220
+ * available to avoid a redundant env read. Without it, the TERM=dumb check
3221
+ * delegates to {@link createTerminalProfile} — the canonical single-source-
3222
+ * of-truth entry in `@silvery/ansi/profile`. CI env vars remain read
3223
+ * directly because they are orthogonal to terminal capabilities (a CI
3224
+ * runner's TTY status doesn't describe what the terminal can render).
3225
+ */
3226
+ function isTTY(stdout = process.stdout, emulator) {
3227
+ if (!stdout.isTTY) return false;
3228
+ if ((emulator?.TERM ?? createTerminalProfile().emulator.TERM) === "dumb") return false;
3229
+ if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.JENKINS_URL || process.env.BUILDKITE || process.env.CIRCLECI || process.env.TRAVIS) return false;
3230
+ return true;
3231
+ }
3232
+ /**
3233
+ * Resolve the non-TTY mode based on options and environment.
3234
+ *
3235
+ * When mode is 'auto':
3236
+ * - If TTY detected: returns 'tty'
3237
+ * - If non-TTY detected: returns 'line-by-line'
3238
+ */
3239
+ function resolveNonTTYMode(options = {}) {
3240
+ const { mode = "auto", stdout = process.stdout } = options;
3241
+ if (mode !== "auto") return mode;
3242
+ return isTTY(stdout) ? "tty" : "line-by-line";
3243
+ }
3244
+ /**
3245
+ * Convert buffer output to line-by-line format.
3246
+ *
3247
+ * Instead of using cursor positioning, outputs each line with a simple
3248
+ * carriage return and clear-to-end-of-line sequence.
3249
+ *
3250
+ * @param content The rendered content (may contain ANSI codes but no cursor positioning)
3251
+ * @param prevLineCount Number of lines in the previous frame (for clearing)
3252
+ * @returns Output string suitable for non-TTY rendering
3253
+ */
3254
+ function toLineByLineOutput(content, prevLineCount) {
3255
+ const lines = content.split("\n");
3256
+ let output = "";
3257
+ if (prevLineCount > 0) {
3258
+ output += "\r";
3259
+ if (prevLineCount > 1) output += `\x1b[${prevLineCount - 1}A`;
3260
+ }
3261
+ for (let i = 0; i < lines.length; i++) {
3262
+ if (i > 0) output += "\n";
3263
+ output += lines[i];
3264
+ output += "\x1B[K";
3265
+ }
3266
+ const extraLines = prevLineCount - lines.length;
3267
+ if (extraLines > 0) {
3268
+ for (let i = 0; i < extraLines; i++) output += "\n\x1B[K";
3269
+ output += `\x1b[${extraLines}A`;
3270
+ }
3271
+ return output;
3272
+ }
3273
+ /**
3274
+ * Convert buffer output to plain text format.
3275
+ *
3276
+ * Strips all ANSI codes and outputs simple newline-separated text.
3277
+ * No cursor movement or clearing.
3278
+ *
3279
+ * @param content The rendered content
3280
+ * @param prevLineCount Number of lines in the previous frame (unused in plain mode)
3281
+ * @returns Plain text output
3282
+ */
3283
+ function toPlainOutput(content, _prevLineCount) {
3284
+ const lines = stripAnsi(content).split("\n").map((line) => line.trimEnd());
3285
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
3286
+ return lines.join("\n");
3287
+ }
3288
+ /**
3289
+ * Create an output transformer based on the non-TTY mode.
3290
+ *
3291
+ * @param mode The resolved non-TTY mode
3292
+ * @returns A function that transforms output based on the mode
3293
+ */
3294
+ function createOutputTransformer(mode) {
3295
+ switch (mode) {
3296
+ case "tty": return (content) => content;
3297
+ case "line-by-line": return toLineByLineOutput;
3298
+ case "static": return () => "";
3299
+ case "plain": return toPlainOutput;
3300
+ }
3301
+ }
3302
+ /**
3303
+ * Count the number of lines in a string.
3304
+ */
3305
+ function countLines(str) {
3306
+ if (!str) return 0;
3307
+ return str.split("\n").length;
3308
+ }
3309
+ //#endregion
3310
+ //#region packages/ag-term/src/inspector.ts
3311
+ let inspectorEnabled = false;
3312
+ let inspectorOutput = process.stderr;
3313
+ /** Enable the silvery inspector. */
3314
+ function enableInspector(options) {
3315
+ inspectorEnabled = true;
3316
+ if (options?.logFile) inspectorOutput = process.getBuiltinModule("node:fs").createWriteStream(options.logFile, { flags: "a" });
3317
+ else if (options?.output) inspectorOutput = options.output;
3318
+ else inspectorOutput = process.stderr;
3319
+ }
3320
+ /** Disable the inspector. */
3321
+ function disableInspector() {
3322
+ inspectorEnabled = false;
3323
+ }
3324
+ /** Check if inspector is active. */
3325
+ function isInspectorEnabled() {
3326
+ return inspectorEnabled;
3327
+ }
3328
+ /**
3329
+ * Log render stats after each frame.
3330
+ *
3331
+ * Called by the scheduler after doRender completes. When the inspector
3332
+ * is disabled this is a no-op (zero overhead).
3333
+ */
3334
+ function inspectFrame(stats) {
3335
+ if (!inspectorEnabled) return;
3336
+ const line = `[silvery] frame #${stats.renderCount} ${stats.lastRenderTime.toFixed(1)}ms avg=${stats.avgRenderTime.toFixed(1)}ms skipped=${stats.skippedCount}\n`;
3337
+ inspectorOutput.write(line);
3338
+ }
3339
+ /**
3340
+ * Dump the component tree structure as indented text.
3341
+ *
3342
+ * Walks the SilveryNode tree and formats each node with its type, testID,
3343
+ * layout rect, and dirty flags.
3344
+ */
3345
+ function inspectTree(rootNode, options) {
3346
+ const maxDepth = options?.depth ?? 10;
3347
+ const showLayout = options?.showLayout ?? true;
3348
+ const lines = [];
3349
+ function walk(node, indent) {
3350
+ if (indent > maxDepth) return;
3351
+ const prefix = " ".repeat(indent);
3352
+ const type = node.type;
3353
+ const testID = node.props?.testID;
3354
+ const idStr = testID ? ` #${testID}` : "";
3355
+ let rectStr = "";
3356
+ if (showLayout) {
3357
+ if (node.boxRect) {
3358
+ const r = node.boxRect;
3359
+ rectStr = ` [${r.x},${r.y} ${r.width}x${r.height}]`;
3360
+ } else if (node.layoutNode) {
3361
+ const ln = node.layoutNode;
3362
+ rectStr = ` [${ln.getComputedLeft()},${ln.getComputedTop()} ${ln.getComputedWidth()}x${ln.getComputedHeight()}]`;
3363
+ }
3364
+ }
3365
+ const dirtyFlags = [];
3366
+ if (node.layoutNode?.isDirty()) dirtyFlags.push("layout");
3367
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 1)) dirtyFlags.push("content");
3368
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 2)) dirtyFlags.push("paint");
3369
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 4)) dirtyFlags.push("bg");
3370
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 16)) dirtyFlags.push("subtree");
3371
+ if (isDirty(node.dirtyBits, node.dirtyEpoch, 8)) dirtyFlags.push("children");
3372
+ const dirtyStr = dirtyFlags.length > 0 ? ` dirty=[${dirtyFlags.join(",")}]` : "";
3373
+ const textStr = node.textContent ? ` "${node.textContent.slice(0, 30)}${node.textContent.length > 30 ? "..." : ""}"` : "";
3374
+ lines.push(`${prefix}${type}${idStr}${rectStr}${dirtyStr}${textStr}`);
3375
+ for (const child of node.children) walk(child, indent + 1);
3376
+ }
3377
+ walk(rootNode, 0);
3378
+ return lines.join("\n");
3379
+ }
3380
+ /**
3381
+ * Auto-enable if SILVERY_DEV=1 is set.
3382
+ *
3383
+ * Call this at startup to respect the environment variable convention.
3384
+ */
3385
+ function autoEnableInspector() {
3386
+ if (process.env.SILVERY_DEV === "1" || process.env.SILVERY_DEV === "true") {
3387
+ const logFile = process.env.SILVERY_DEV_LOG;
3388
+ enableInspector(logFile ? { logFile } : void 0);
3389
+ }
3390
+ }
3391
+ //#endregion
3392
+ //#region packages/ag-term/src/measurer.ts
3393
+ /**
3394
+ * Extend a Term with measurement capabilities.
3395
+ *
3396
+ * Creates a width measurer from the term's caps and adds measurement
3397
+ * methods (displayWidth, graphemeWidth, wrapText, etc.) to the term.
3398
+ */
3399
+ function withMeasurer(term) {
3400
+ const caps = term.caps;
3401
+ const measurer = createMeasurer(caps ?? {});
3402
+ return Object.create(term, {
3403
+ maybeWideEmojis: {
3404
+ get: () => measurer.maybeWideEmojis,
3405
+ enumerable: true
3406
+ },
3407
+ textSizing: {
3408
+ get: () => measurer.textSizing,
3409
+ enumerable: true
3410
+ },
3411
+ displayWidth: {
3412
+ value: measurer.displayWidth.bind(measurer),
3413
+ enumerable: true
3414
+ },
3415
+ displayWidthAnsi: {
3416
+ value: measurer.displayWidthAnsi.bind(measurer),
3417
+ enumerable: true
3418
+ },
3419
+ graphemeWidth: {
3420
+ value: measurer.graphemeWidth.bind(measurer),
3421
+ enumerable: true
3422
+ },
3423
+ wrapText: {
3424
+ value: measurer.wrapText.bind(measurer),
3425
+ enumerable: true
3426
+ },
3427
+ sliceByWidth: {
3428
+ value: measurer.sliceByWidth.bind(measurer),
3429
+ enumerable: true
3430
+ },
3431
+ sliceByWidthFromEnd: {
3432
+ value: measurer.sliceByWidthFromEnd.bind(measurer),
3433
+ enumerable: true
3434
+ }
3435
+ });
3436
+ }
3437
+ /**
3438
+ * Create a pipeline configuration from caps and/or measurer.
3439
+ *
3440
+ * This is the single factory for PipelineConfig -- use it instead of
3441
+ * manually constructing { measurer, outputPhaseFn }.
3442
+ *
3443
+ * @param options.caps - Terminal capabilities (for output phase SGR generation).
3444
+ * Reads `caps.maybeWideEmojis` (text-presentation emoji width) and
3445
+ * `caps.textSizing` for measurement.
3446
+ * @param options.measurer - Explicit measurer (if omitted, created from caps)
3447
+ */
3448
+ function createPipeline(options = {}) {
3449
+ const { caps, measurer: explicitMeasurer } = options;
3450
+ const measurer = explicitMeasurer ?? createMeasurer(caps ?? {});
3451
+ const outputPhaseFn = createOutputPhase(caps ? {
3452
+ underlineStyles: caps.underlineStyles,
3453
+ underlineColor: caps.underlineColor,
3454
+ overline: caps.overline,
3455
+ colorLevel: caps.colorLevel
3456
+ } : {}, measurer);
3457
+ if (caps?.colorLevel) setActiveColorLevel(caps.colorLevel);
3458
+ return {
3459
+ measurer,
3460
+ outputPhaseFn
3461
+ };
3462
+ }
3463
+ //#endregion
3464
+ //#region packages/headless/src/selection.ts
3465
+ /**
3466
+ * Check if a character is a word character (not whitespace/punctuation).
3467
+ * Word chars: letters, digits, underscore (matching \w).
3468
+ */
3469
+ function isWordChar(ch) {
3470
+ return /\w/.test(ch);
3471
+ }
3472
+ /**
3473
+ * Find word boundaries at a given column in the buffer row.
3474
+ * Returns { startCol, endCol } inclusive of the word.
3475
+ * If the position is on whitespace/punctuation, selects that single char.
3476
+ */
3477
+ function findWordBoundary(buffer, col, row) {
3478
+ const width = buffer.width;
3479
+ const ch = buffer.getCell(col, row).char;
3480
+ if (isWordChar(ch)) {
3481
+ let startCol = col;
3482
+ while (startCol > 0 && isWordChar(buffer.getCell(startCol - 1, row).char)) startCol--;
3483
+ let endCol = col;
3484
+ while (endCol < width - 1 && isWordChar(buffer.getCell(endCol + 1, row).char)) endCol++;
3485
+ return {
3486
+ startCol,
3487
+ endCol
3488
+ };
3489
+ }
3490
+ return {
3491
+ startCol: col,
3492
+ endCol: col
3493
+ };
3494
+ }
3495
+ /**
3496
+ * Find line boundaries for a given row.
3497
+ * Returns { startCol, endCol } spanning from first content to last content.
3498
+ * If the row is empty, returns { startCol: 0, endCol: width - 1 }.
3499
+ */
3500
+ function findLineBoundary(buffer, row) {
3501
+ const width = buffer.width;
3502
+ let endCol = width - 1;
3503
+ while (endCol > 0 && buffer.getCell(endCol, row).char.trim() === "") endCol--;
3504
+ let startCol = 0;
3505
+ while (startCol < endCol && buffer.getCell(startCol, row).char.trim() === "") startCol++;
3506
+ if (startCol >= endCol && buffer.getCell(startCol, row).char.trim() === "") return {
3507
+ startCol: 0,
3508
+ endCol: width - 1
3509
+ };
3510
+ return {
3511
+ startCol,
3512
+ endCol
3513
+ };
3514
+ }
3515
+ /**
3516
+ * Extend the head position according to the current granularity.
3517
+ * For word granularity: snaps to word boundaries.
3518
+ * For line granularity: snaps to line boundaries.
3519
+ */
3520
+ function extendByGranularity(col, row, anchor, granularity, buffer) {
3521
+ if (granularity === "character" || !buffer) return {
3522
+ col,
3523
+ row
3524
+ };
3525
+ if (granularity === "word") {
3526
+ const { startCol, endCol } = findWordBoundary(buffer, col, row);
3527
+ return row > anchor.row || row === anchor.row && col >= anchor.col ? {
3528
+ col: endCol,
3529
+ row
3530
+ } : {
3531
+ col: startCol,
3532
+ row
3533
+ };
3534
+ }
3535
+ if (granularity === "line") {
3536
+ const { startCol, endCol } = findLineBoundary(buffer, row);
3537
+ return row > anchor.row || row === anchor.row && col >= anchor.col ? {
3538
+ col: endCol,
3539
+ row
3540
+ } : {
3541
+ col: startCol,
3542
+ row
3543
+ };
3544
+ }
3545
+ return {
3546
+ col,
3547
+ row
3548
+ };
3549
+ }
3550
+ function createTerminalSelectionState() {
3551
+ return {
3552
+ range: null,
3553
+ selecting: false,
3554
+ source: null,
3555
+ granularity: "character",
3556
+ scope: null
3557
+ };
3558
+ }
3559
+ /**
3560
+ * Clamp a position to a scope boundary.
3561
+ */
3562
+ function clampToScope(col, row, scope) {
3563
+ if (!scope) return {
3564
+ col,
3565
+ row
3566
+ };
3567
+ return {
3568
+ col: Math.max(scope.left, Math.min(scope.right, col)),
3569
+ row: Math.max(scope.top, Math.min(scope.bottom, row))
3570
+ };
3571
+ }
3572
+ function terminalSelectionUpdate(action, state) {
3573
+ switch (action.type) {
3574
+ case "start": {
3575
+ const scope = action.scope ?? null;
3576
+ const pos = clampToScope(action.col, action.row, scope);
3577
+ return [{
3578
+ range: {
3579
+ anchor: pos,
3580
+ head: pos
3581
+ },
3582
+ selecting: true,
3583
+ source: action.source ?? "mouse",
3584
+ granularity: "character",
3585
+ scope
3586
+ }, [{ type: "render" }]];
3587
+ }
3588
+ case "startWord": {
3589
+ const scope = action.scope ?? null;
3590
+ const { startCol, endCol } = findWordBoundary(action.buffer, action.col, action.row);
3591
+ return [{
3592
+ range: {
3593
+ anchor: clampToScope(startCol, action.row, scope),
3594
+ head: clampToScope(endCol, action.row, scope)
3595
+ },
3596
+ selecting: true,
3597
+ source: action.source ?? "mouse",
3598
+ granularity: "word",
3599
+ scope
3600
+ }, [{ type: "render" }]];
3601
+ }
3602
+ case "startLine": {
3603
+ const scope = action.scope ?? null;
3604
+ const { startCol, endCol } = findLineBoundary(action.buffer, action.row);
3605
+ return [{
3606
+ range: {
3607
+ anchor: clampToScope(startCol, action.row, scope),
3608
+ head: clampToScope(endCol, action.row, scope)
3609
+ },
3610
+ selecting: true,
3611
+ source: action.source ?? "mouse",
3612
+ granularity: "line",
3613
+ scope
3614
+ }, [{ type: "render" }]];
3615
+ }
3616
+ case "extend": {
3617
+ if (!state.selecting) return [state, []];
3618
+ const scope = "scope" in action ? action.scope ?? null : state.scope;
3619
+ const extended = extendByGranularity(action.col, action.row, state.range.anchor, state.granularity, action.buffer);
3620
+ const head = clampToScope(extended.col, extended.row, scope);
3621
+ return [{
3622
+ ...state,
3623
+ range: {
3624
+ anchor: state.range.anchor,
3625
+ head
3626
+ },
3627
+ scope,
3628
+ selecting: true
3629
+ }, [{ type: "render" }]];
3630
+ }
3631
+ case "finish":
3632
+ if (!state.range) return [{
3633
+ ...state,
3634
+ selecting: false
3635
+ }, []];
3636
+ return [{
3637
+ ...state,
3638
+ range: state.range,
3639
+ selecting: false
3640
+ }, []];
3641
+ case "clear": {
3642
+ const hadRange = state.range !== null;
3643
+ return [createTerminalSelectionState(), hadRange ? [{ type: "render" }] : []];
3644
+ }
3645
+ }
3646
+ }
3647
+ function normalizeRange(range) {
3648
+ const { anchor, head } = range;
3649
+ if (anchor.row < head.row || anchor.row === head.row && anchor.col <= head.col) return {
3650
+ startRow: anchor.row,
3651
+ startCol: anchor.col,
3652
+ endRow: head.row,
3653
+ endCol: head.col
3654
+ };
3655
+ return {
3656
+ startRow: head.row,
3657
+ startCol: head.col,
3658
+ endRow: anchor.row,
3659
+ endCol: anchor.col
3660
+ };
3661
+ }
3662
+ /**
3663
+ * Extract text from a buffer within a selection range.
3664
+ *
3665
+ * Handles:
3666
+ * - Soft-wrap joining (via RowMetadata.softWrapped) — rejoins wrapped visual
3667
+ * rows into their logical line, reinserting the word-wrap break space
3668
+ * (RowMetadata.wrapJoinSpace) that trim-mode rendering stripped
3669
+ * - Trailing fill exclusion (clamp to RowMetadata.lastContentCol; falls back
3670
+ * to a trailing-whitespace trim when a row carries no metadata)
3671
+ * - Blank line preservation within selection
3672
+ * - Wide-char continuation cell skipping
3673
+ * - SELECTABLE_FLAG filtering (when respectSelectableFlag is true)
3674
+ * - Contain-scope clipping (when options.scope is set)
3675
+ */
3676
+ function extractText(buffer, range, options) {
3677
+ const { startRow, startCol, endRow, endCol } = normalizeRange(range);
3678
+ const respectSelectable = options?.respectSelectableFlag ?? true;
3679
+ const rowMeta = options?.rowMetadata;
3680
+ const scope = options?.scope;
3681
+ const parts = [];
3682
+ for (let row = startRow; row <= endRow; row++) {
3683
+ const meta = rowMeta?.[row];
3684
+ let colStart = row === startRow ? startCol : 0;
3685
+ let colEnd = row === endRow ? endCol : buffer.width - 1;
3686
+ if (scope) {
3687
+ colStart = Math.max(colStart, scope.left);
3688
+ colEnd = Math.min(colEnd, scope.right);
3689
+ if (colStart > colEnd) {
3690
+ if (!(meta?.softWrapped && row < endRow)) {
3691
+ parts.push("");
3692
+ if (row < endRow) parts.push("\n");
3693
+ }
3694
+ continue;
3695
+ }
3696
+ }
3697
+ if (meta && meta.lastContentCol >= 0) colEnd = Math.min(colEnd, meta.lastContentCol);
3698
+ let line = "";
3699
+ for (let col = colStart; col <= colEnd; col++) {
3700
+ if (buffer.isCellContinuation(col, row)) continue;
3701
+ if (respectSelectable && !buffer.isCellSelectable(col, row)) continue;
3702
+ line += buffer.getCellChar(col, row);
3703
+ }
3704
+ if (!meta || meta.lastContentCol < 0) line = line.replace(/\s+$/, "");
3705
+ if (meta?.softWrapped && row < endRow) {
3706
+ parts.push(line);
3707
+ if (meta.wrapJoinSpace) parts.push(" ");
3708
+ } else {
3709
+ parts.push(line);
3710
+ if (row < endRow) parts.push("\n");
3711
+ }
3712
+ }
3713
+ return parts.join("");
3714
+ }
3715
+ //#endregion
3716
+ //#region packages/ag-term/src/selection-renderer.ts
3717
+ function normalizeSelectionColor(color) {
3718
+ if (color === void 0) return void 0;
3719
+ if (typeof color === "string") return parseColor(color);
3720
+ return color;
3721
+ }
3722
+ /**
3723
+ * Compute selection style changes for all cells within a selection range.
3724
+ *
3725
+ * Returns a sparse list of cell changes (fg/bg only). The caller applies these
3726
+ * to the buffer before the output phase diffs it. This approach:
3727
+ * - Composes correctly with existing cell styles
3728
+ * - Handles already-inverted content (swaps back to normal instead of double-inverting)
3729
+ * - Respects SELECTABLE_FLAG per cell (skip non-selectable)
3730
+ * - Works with the normal diff/output renderer (no separate ANSI pass)
3731
+ *
3732
+ * @param buffer The rendered buffer (post-render-phase)
3733
+ * @param selection Current selection range, or null
3734
+ * @param theme Optional selection theme colors
3735
+ * @param respectSelectableFlag When true, skip cells without SELECTABLE_FLAG
3736
+ */
3737
+ function composeSelectionCells(buffer, selection, theme, respectSelectableFlag = false, scope) {
3738
+ if (!selection) return [];
3739
+ const { startRow, startCol, endRow, endCol } = normalizeRange(selection);
3740
+ const changes = [];
3741
+ for (let row = startRow; row <= endRow; row++) {
3742
+ let colStart = row === startRow ? startCol : 0;
3743
+ let colEnd = row === endRow ? endCol : buffer.width - 1;
3744
+ const rowMeta = buffer.getRowMeta(row);
3745
+ if (rowMeta.lastContentCol >= 0) colEnd = Math.min(colEnd, rowMeta.lastContentCol);
3746
+ if (colStart > colEnd) continue;
3747
+ if (scope) {
3748
+ colStart = Math.max(colStart, scope.left);
3749
+ colEnd = Math.min(colEnd, scope.right);
3750
+ if (colStart > colEnd) continue;
3751
+ }
3752
+ const firstContentCol = findFirstContentCol(buffer, row, colStart, colEnd, respectSelectableFlag);
3753
+ if (firstContentCol === null) continue;
3754
+ colStart = Math.max(colStart, firstContentCol);
3755
+ for (let col = colStart; col <= colEnd; col++) {
3756
+ if (buffer.isCellContinuation(col, row)) continue;
3757
+ if (respectSelectableFlag && !buffer.isCellSelectable(col, row)) continue;
3758
+ const cellFg = buffer.getCellFg(col, row);
3759
+ const cellBg = buffer.getCellBg(col, row);
3760
+ let newFg;
3761
+ let newBg;
3762
+ let inverseAttr = false;
3763
+ if (theme?.selectionBg != null) {
3764
+ newFg = theme.selectionFg ?? cellFg;
3765
+ newBg = theme.selectionBg;
3766
+ } else if (cellFg == null || cellBg == null) {
3767
+ newFg = cellFg;
3768
+ newBg = cellBg;
3769
+ inverseAttr = true;
3770
+ } else {
3771
+ newFg = cellBg;
3772
+ newBg = cellFg;
3773
+ }
3774
+ changes.push({
3775
+ col,
3776
+ row,
3777
+ fg: newFg,
3778
+ bg: newBg,
3779
+ inverseAttr
3780
+ });
3781
+ }
3782
+ }
3783
+ return changes;
3784
+ }
3785
+ function findFirstContentCol(buffer, row, colStart, colEnd, respectSelectableFlag) {
3786
+ for (let col = colStart; col <= colEnd; col++) {
3787
+ if (buffer.isCellContinuation(col, row)) continue;
3788
+ if (respectSelectableFlag && !buffer.isCellSelectable(col, row)) continue;
3789
+ if (buffer.getCell(col, row).char.trim().length > 0) return col;
3790
+ }
3791
+ return null;
3792
+ }
3793
+ /**
3794
+ * Apply selection style changes to a buffer.
3795
+ *
3796
+ * Modifies the buffer in-place by setting fg/bg on affected cells.
3797
+ * Call this after the render phase and before the output phase.
3798
+ *
3799
+ * @param buffer The rendered buffer to modify
3800
+ * @param changes Cell changes from composeSelectionCells
3801
+ */
3802
+ function applySelectionToBuffer(buffer, changes) {
3803
+ for (const change of changes) {
3804
+ const cell = buffer.getCell(change.col, change.row);
3805
+ if (change.inverseAttr) buffer.setCell(change.col, change.row, {
3806
+ ...cell,
3807
+ attrs: {
3808
+ ...cell.attrs,
3809
+ inverse: !cell.attrs.inverse
3810
+ }
3811
+ });
3812
+ else buffer.setCell(change.col, change.row, {
3813
+ ...cell,
3814
+ fg: normalizeSelectionColor(change.fg) ?? null,
3815
+ bg: normalizeSelectionColor(change.bg) ?? null
3816
+ });
3817
+ }
3818
+ }
3819
+ /**
3820
+ * Compute search-highlight style changes for a list of highlight ranges.
3821
+ *
3822
+ * Returns a sparse list of cell changes (fg/bg only) — apply them with
3823
+ * `applySelectionToBuffer` (the apply step is style-source-agnostic — it
3824
+ * just sets fg/bg or toggles inverse on the cells you give it).
3825
+ *
3826
+ * Default behaviour mirrors the legacy `renderSearchHighlights` that wrote
3827
+ * `\x1b[7m...\x1b[27m` past the buffer: cells with default fg/bg get the
3828
+ * inverse-attr toggle (so the terminal handles the SGR 7 swap at display).
3829
+ * Cells with explicit fg/bg get a direct fg↔bg swap. A theme can override
3830
+ * with explicit `selectionFg` / `selectionBg` tokens.
3831
+ *
3832
+ * Tracking: km-silvery.delete-search-overlay-ansi
3833
+ */
3834
+ function composeSearchHighlightCells(buffer, highlights, theme) {
3835
+ if (highlights.length === 0) return [];
3836
+ const changes = [];
3837
+ for (const h of highlights) {
3838
+ const row = h.screenRow;
3839
+ if (row < 0 || row >= buffer.height) continue;
3840
+ const colStart = Math.max(0, h.startCol);
3841
+ const colEnd = Math.min(buffer.width - 1, h.endCol);
3842
+ if (colStart > colEnd) continue;
3843
+ for (let col = colStart; col <= colEnd; col++) {
3844
+ if (buffer.isCellContinuation(col, row)) continue;
3845
+ const cellFg = buffer.getCellFg(col, row);
3846
+ const cellBg = buffer.getCellBg(col, row);
3847
+ let newFg;
3848
+ let newBg;
3849
+ let inverseAttr = false;
3850
+ if (theme?.selectionBg != null) {
3851
+ newFg = theme.selectionFg ?? cellFg;
3852
+ newBg = theme.selectionBg;
3853
+ } else if (cellFg == null || cellBg == null) {
3854
+ newFg = cellFg;
3855
+ newBg = cellBg;
3856
+ inverseAttr = true;
3857
+ } else {
3858
+ newFg = cellBg;
3859
+ newBg = cellFg;
3860
+ }
3861
+ changes.push({
3862
+ col,
3863
+ row,
3864
+ fg: newFg,
3865
+ bg: newBg,
3866
+ inverseAttr
3867
+ });
3868
+ }
3869
+ }
3870
+ return changes;
3871
+ }
3872
+ //#endregion
3873
+ //#region packages/ag-term/src/virtual-scrollback.ts
3874
+ /**
3875
+ * Virtual scrollback buffer for storing historical rendered content.
3876
+ *
3877
+ * Used by virtual inline mode to maintain scrollable history while
3878
+ * rendering in altscreen (which normally has no scrollback).
3879
+ *
3880
+ * Implementation uses a circular buffer for O(1) push and bounded memory.
3881
+ */
3882
+ function createVirtualScrollback(options) {
3883
+ const maxLines = options?.maxLines ?? 1e4;
3884
+ const ansiLines = new Array(maxLines);
3885
+ const plainLines = new Array(maxLines);
3886
+ let head = 0;
3887
+ let count = 0;
3888
+ return {
3889
+ push(lines) {
3890
+ for (const line of lines) {
3891
+ ansiLines[head] = line;
3892
+ plainLines[head] = stripAnsi(line);
3893
+ head = (head + 1) % maxLines;
3894
+ if (count < maxLines) count++;
3895
+ }
3896
+ },
3897
+ getVisibleRows(offset, rowCount) {
3898
+ const result = [];
3899
+ for (let i = 0; i < rowCount; i++) {
3900
+ const logicalIndex = count - offset - rowCount + i;
3901
+ if (logicalIndex < 0 || logicalIndex >= count) result.push("");
3902
+ else {
3903
+ const physical = (head - count + logicalIndex + maxLines) % maxLines;
3904
+ result.push(ansiLines[physical]);
3905
+ }
3906
+ }
3907
+ return result;
3908
+ },
3909
+ get totalLines() {
3910
+ return count;
3911
+ },
3912
+ search(query) {
3913
+ if (!query) return [];
3914
+ const lowerQuery = query.toLowerCase();
3915
+ const matches = [];
3916
+ for (let i = 0; i < count; i++) if (plainLines[(head - count + i + maxLines) % maxLines].toLowerCase().includes(lowerQuery)) matches.push(i);
3917
+ return matches;
3918
+ },
3919
+ clear() {
3920
+ head = 0;
3921
+ count = 0;
3922
+ }
3923
+ };
3924
+ }
3925
+ //#endregion
3926
+ export { generateHitRegionId as $, queryModes as A, TERMTEST_SECTIONS as B, resolveFromTerm as C, searchUpdate as Ct, queryTextAreaSize as D, createHistoryBuffer as Dt, queryTextAreaPixels as E, createListDocument as Et, queryTertiaryDA as F, findActiveCursorNode as G, detectKittyFromStdio as H, applyWidthConfig as I, injectDOMStyles as J, DOMRenderBuffer as K, createWidthDetector as L, queryPrimaryDA as M, querySecondaryDA as N, DecMode as O, createHistoryItem as Ot, queryTerminalVersion as P, Z_INDEX as Q, queryCursorFromStdio as R, isTermDef as S, renderSearchBarPlain as St, queryCellSize as T, createTextSurface as Tt, detectKittySupport as U, runTermtest as V, OSC133 as W, createCanvasAdapter as X, CanvasRenderBuffer as Y, HitRegistry as Z, createOutputTransformer as _, resizeSplit as _t, createTerminalSelectionState as a, calcEdgeBasedScrollOffset as at, createInputEvents as b, computeMatchRanges as bt, createPipeline as c, scrollDown as ct, disableInspector as d, supportsScrollRegions as dt, resetHitRegionIdCounter as et, enableInspector as f, createLeaf as ft, countLines as g, removePane as gt, isInspectorEnabled as h, getTabOrder as ht, composeSelectionCells as i, useCursor as it, queryDeviceAttributes as j, queryMode as k, createFocusManager as kt, withMeasurer as l, scrollUp as lt, inspectTree as m, getPaneIds as mt, applySelectionToBuffer as n, createCursorStore as nt, extractText as o, moveCursor as ot, inspectFrame as p, findAdjacentPane as pt, createDOMAdapter as q, composeSearchHighlightCells as r, resetCursorState as rt, terminalSelectionUpdate as s, resetScrollRegion as st, createVirtualScrollback as t, CursorProvider as tt, autoEnableInspector as u, setScrollRegion as ut, isTTY as v, splitPane as vt, resolveTermDef as w, composeViewport as wt, isTerm as x, createSearchState as xt, resolveNonTTYMode as y, swapPanes as yt, queryCursorPosition as z };
3927
+
3928
+ //# sourceMappingURL=src-CbhWmUnF.mjs.map