silvery 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/{Text-BRf-59j2.mjs → Text-Ci6UCSTL.mjs} +46 -46
  2. package/dist/Text-Ci6UCSTL.mjs.map +1 -0
  3. package/dist/{ag-BeFC4S2N.mjs → ag-q9lRlKlt.mjs} +42 -19
  4. package/dist/ag-q9lRlKlt.mjs.map +1 -0
  5. package/dist/{animation-N8MFybTk.mjs → animation-CZ4x2R7C.mjs} +1 -1
  6. package/dist/{animation-N8MFybTk.mjs.map → animation-CZ4x2R7C.mjs.map} +1 -1
  7. package/dist/{ansi-C6Qs1Wn2.mjs → ansi-CLmIMnop.mjs} +1 -1
  8. package/dist/{ansi-C6Qs1Wn2.mjs.map → ansi-CLmIMnop.mjs.map} +1 -1
  9. package/dist/{ansi-CBkam1ty.d.mts → ansi-zmNzgkPB.d.mts} +1 -1
  10. package/dist/{ansi-CBkam1ty.d.mts.map → ansi-zmNzgkPB.d.mts.map} +1 -1
  11. package/dist/backend-DSiSIc-h.mjs +13396 -0
  12. package/dist/backend-DSiSIc-h.mjs.map +1 -0
  13. package/dist/chain-bridge-CkYcMxlq.mjs +4149 -0
  14. package/dist/chain-bridge-CkYcMxlq.mjs.map +1 -0
  15. package/dist/{chunk-hT5z_Zn9.mjs → chunk-BEJ448es.mjs} +1 -1
  16. package/dist/cli-VNqUKlXw.mjs +4 -0
  17. package/dist/{context-BU5LkkIy.mjs → context-CEhuD1R_.mjs} +1 -1
  18. package/dist/{context-BU5LkkIy.mjs.map → context-CEhuD1R_.mjs.map} +1 -1
  19. package/dist/devtools-BF4IMumr.mjs +2 -0
  20. package/dist/{devtools-9zfhpFyG.mjs → devtools-td2AjFwc.mjs} +5 -4
  21. package/dist/{devtools-9zfhpFyG.mjs.map → devtools-td2AjFwc.mjs.map} +1 -1
  22. package/dist/{easing-B0oZKDki.d.mts → easing-C5MrdO7V.d.mts} +1 -1
  23. package/dist/{easing-B0oZKDki.d.mts.map → easing-C5MrdO7V.d.mts.map} +1 -1
  24. package/dist/{eta-DGOuC8yU.mjs → eta-BwYWEjfS.mjs} +1 -5
  25. package/dist/{eta-DGOuC8yU.mjs.map → eta-BwYWEjfS.mjs.map} +1 -1
  26. package/dist/{flexily-zero-adapter-D6hcFgrH.mjs → flexily-zero-adapter-DktX3HsX.mjs} +1 -1
  27. package/dist/{flexily-zero-adapter-CEJOcbNp.mjs → flexily-zero-adapter-VoBGHG14.mjs} +1 -1
  28. package/dist/{flexily-zero-adapter-CEJOcbNp.mjs.map → flexily-zero-adapter-VoBGHG14.mjs.map} +1 -1
  29. package/dist/hit-registry-D_nxOivS.d.mts +71 -0
  30. package/dist/hit-registry-D_nxOivS.d.mts.map +1 -0
  31. package/dist/{image-1nRyKa60.mjs → image-CIF75xMr.mjs} +24 -24
  32. package/dist/image-CIF75xMr.mjs.map +1 -0
  33. package/dist/{index-2E1jYgak.d.mts → index-377zdPLP.d.mts} +35 -12
  34. package/dist/index-377zdPLP.d.mts.map +1 -0
  35. package/dist/{index-DnuadDNL.d.mts → index-BcSr0RAQ.d.mts} +8590 -11173
  36. package/dist/index-BcSr0RAQ.d.mts.map +1 -0
  37. package/dist/{index-Dg1YaeJb.d.mts → index-DLnpxDAm.d.mts} +2 -2
  38. package/dist/{index-Dg1YaeJb.d.mts.map → index-DLnpxDAm.d.mts.map} +1 -1
  39. package/dist/{index-Bi4Jdz5g.d.mts → index-DlNmckTq.d.mts} +2 -2
  40. package/dist/{index-Bi4Jdz5g.d.mts.map → index-DlNmckTq.d.mts.map} +1 -1
  41. package/dist/index.d.mts +12 -8
  42. package/dist/index.d.mts.map +1 -1
  43. package/dist/index.mjs +18 -17
  44. package/dist/index.mjs.map +1 -1
  45. package/dist/{layout-engine-Ca_nbtfL.mjs → layout-engine-BgR58vL0.mjs} +3 -3
  46. package/dist/{layout-engine-Ca_nbtfL.mjs.map → layout-engine-BgR58vL0.mjs.map} +1 -1
  47. package/dist/{layout-engine-CgsoBRIn.mjs → layout-engine-DNjTha8f.mjs} +1 -1
  48. package/dist/{layout-signals-Dch2EiCy.mjs → layout-signals-CGYbHmxy.mjs} +2 -2
  49. package/dist/layout-signals-CGYbHmxy.mjs.map +1 -0
  50. package/dist/{bound-term-BumfuXXW.d.mts → mouse-events-B2z5tbsL.d.mts} +540 -401
  51. package/dist/mouse-events-B2z5tbsL.d.mts.map +1 -0
  52. package/dist/{mouse-events-hnbJZRwK.mjs → mouse-events-WQ2h-uqG.mjs} +14 -6
  53. package/dist/mouse-events-WQ2h-uqG.mjs.map +1 -0
  54. package/dist/{multi-progress-Bg4ngK80.d.mts → multi-progress-BaEFfHKP.d.mts} +2 -2
  55. package/dist/{multi-progress-Bg4ngK80.d.mts.map → multi-progress-BaEFfHKP.d.mts.map} +1 -1
  56. package/dist/{multi-progress-CaXTuL9G.mjs → multi-progress-zCWHEm9P.mjs} +3 -3
  57. package/dist/{multi-progress-CaXTuL9G.mjs.map → multi-progress-zCWHEm9P.mjs.map} +1 -1
  58. package/dist/{node-BiFu8I9Y.mjs → node-CJ3J_rW-.mjs} +4 -4
  59. package/dist/node-CJ3J_rW-.mjs.map +1 -0
  60. package/dist/{progress-B_UPy6zk.mjs → progress-6vJ7nxUg.mjs} +4 -4
  61. package/dist/{progress-B_UPy6zk.mjs.map → progress-6vJ7nxUg.mjs.map} +1 -1
  62. package/dist/{progress-bar-DmIMPdL0.mjs → progress-bar-BlFNsZL8.mjs} +4 -4
  63. package/dist/{progress-bar-DmIMPdL0.mjs.map → progress-bar-BlFNsZL8.mjs.map} +1 -1
  64. package/dist/{reconciler-NBDSEm8k.mjs → reconciler--_-biN23.mjs} +111 -150
  65. package/dist/reconciler--_-biN23.mjs.map +1 -0
  66. package/dist/{render-string-BntLj7Xq.mjs → render-string-DaghHpLN.mjs} +1 -1
  67. package/dist/{render-string-B4h4SmK7.mjs → render-string-DtHZP0cU.mjs} +15 -11
  68. package/dist/render-string-DtHZP0cU.mjs.map +1 -0
  69. package/dist/runtime.d.mts +3 -2
  70. package/dist/runtime.mjs +3 -2
  71. package/dist/{schemes-DYt2ushj.mjs → schemes-DWQPxXAi.mjs} +2 -2
  72. package/dist/{schemes-DYt2ushj.mjs.map → schemes-DWQPxXAi.mjs.map} +1 -1
  73. package/dist/{spinner-Py8_-hn9.mjs → spinner-B50hILqx.mjs} +3 -3
  74. package/dist/{spinner-Py8_-hn9.mjs.map → spinner-B50hILqx.mjs.map} +1 -1
  75. package/dist/{spinner-CLgzJ_QF.d.mts → spinner-DN10UfF0.d.mts} +2 -2
  76. package/dist/{spinner-CLgzJ_QF.d.mts.map → spinner-DN10UfF0.d.mts.map} +1 -1
  77. package/dist/{src-Oe6x5PrS.mjs → src-B8lf1kiR.mjs} +190 -83
  78. package/dist/src-B8lf1kiR.mjs.map +1 -0
  79. package/dist/{src-Dvq-s8iD.mjs → src-BOLdi3WG.mjs} +3 -3
  80. package/dist/{src-Dvq-s8iD.mjs.map → src-BOLdi3WG.mjs.map} +1 -1
  81. package/dist/{src-B9S_woYc.mjs → src-BXeTHHlL.mjs} +20289 -23262
  82. package/dist/src-BXeTHHlL.mjs.map +1 -0
  83. package/dist/{src-WeA_J4BV.mjs → src-BqeBFB3X.mjs} +90 -47
  84. package/dist/src-BqeBFB3X.mjs.map +1 -0
  85. package/dist/{src-CbhWmUnF.mjs → src-DbqcSrhy.mjs} +1400 -1427
  86. package/dist/src-DbqcSrhy.mjs.map +1 -0
  87. package/dist/{steps-DYrzCUCK.d.mts → steps-B2kBmFpb.d.mts} +1 -1
  88. package/dist/{steps-DYrzCUCK.d.mts.map → steps-B2kBmFpb.d.mts.map} +1 -1
  89. package/dist/{svg-DhxQkz-O.mjs → svg-NmV9dgB9.mjs} +1 -1
  90. package/dist/{svg-DhxQkz-O.mjs.map → svg-NmV9dgB9.mjs.map} +1 -1
  91. package/dist/{svg-Hk7lIl4F.d.mts → svg-VUr78l5d.d.mts} +1 -1
  92. package/dist/{svg-Hk7lIl4F.d.mts.map → svg-VUr78l5d.d.mts.map} +1 -1
  93. package/dist/term.d.mts +2 -2
  94. package/dist/term.mjs +3 -3
  95. package/dist/test.d.mts +673 -0
  96. package/dist/test.d.mts.map +1 -0
  97. package/dist/test.mjs +2829 -0
  98. package/dist/test.mjs.map +1 -0
  99. package/dist/theme.d.mts +2 -2
  100. package/dist/theme.d.mts.map +1 -1
  101. package/dist/theme.mjs +3 -3
  102. package/dist/{types-Bx-XZNbE.d.mts → types-D_E0FbqA.d.mts} +1 -1
  103. package/dist/{types-Bx-XZNbE.d.mts.map → types-D_E0FbqA.d.mts.map} +1 -1
  104. package/dist/ui/animation.d.mts +2 -2
  105. package/dist/ui/animation.mjs +1 -1
  106. package/dist/ui/ansi.d.mts +1 -1
  107. package/dist/ui/ansi.mjs +1 -1
  108. package/dist/ui/cli.d.mts +3 -3
  109. package/dist/ui/cli.mjs +5 -5
  110. package/dist/ui/image.d.mts +1 -1
  111. package/dist/ui/image.mjs +1 -1
  112. package/dist/ui/progress.d.mts +4 -4
  113. package/dist/ui/progress.mjs +4 -4
  114. package/dist/ui/react.d.mts +1 -1
  115. package/dist/ui/react.mjs +2 -2
  116. package/dist/ui/react.mjs.map +1 -1
  117. package/dist/ui/recording-chrome-react.d.mts +1 -1
  118. package/dist/ui/recording-chrome-react.mjs +2 -2
  119. package/dist/ui/recording-chrome.d.mts +1 -1
  120. package/dist/ui/recording-chrome.mjs +1 -1
  121. package/dist/ui/utils.mjs +1 -1
  122. package/dist/ui/wrappers.d.mts +2 -2
  123. package/dist/ui/wrappers.mjs +1 -1
  124. package/dist/ui.d.mts +6 -6
  125. package/dist/ui.mjs +7 -7
  126. package/dist/{ansi-OYRLxxZQ.mjs → unicode-CxyxVGmB.mjs} +7317 -6909
  127. package/dist/unicode-CxyxVGmB.mjs.map +1 -0
  128. package/dist/{useLatest-DC8i7guK.d.mts → useLatest-UOvWy0ZW.d.mts} +2 -2
  129. package/dist/{useLatest-DC8i7guK.d.mts.map → useLatest-UOvWy0ZW.d.mts.map} +1 -1
  130. package/dist/{useLayout-BKsQl2Or.mjs → useLayout-D5cE8OCI.mjs} +4 -4
  131. package/dist/{useLayout-BKsQl2Or.mjs.map → useLayout-D5cE8OCI.mjs.map} +1 -1
  132. package/dist/{with-text-input-DG4f7JII.d.mts → with-text-input-DLoSCBrm.d.mts} +3 -3
  133. package/dist/{with-text-input-DG4f7JII.d.mts.map → with-text-input-DLoSCBrm.d.mts.map} +1 -1
  134. package/dist/wrap-measurer-registration-IV2HtcCd.d.mts +3106 -0
  135. package/dist/wrap-measurer-registration-IV2HtcCd.d.mts.map +1 -0
  136. package/dist/{wrapper-D7gNSsgf.mjs → wrapper-CD29afOs.mjs} +17 -37
  137. package/dist/wrapper-CD29afOs.mjs.map +1 -0
  138. package/dist/{wrappers-CypAzrMO.mjs → wrappers-CInBJm44.mjs} +6 -6
  139. package/dist/{wrappers-CypAzrMO.mjs.map → wrappers-CInBJm44.mjs.map} +1 -1
  140. package/dist/{yoga-adapter-SsEIqMc1.mjs → yoga-adapter-9uab5Qn-.mjs} +2 -2
  141. package/dist/{yoga-adapter-SsEIqMc1.mjs.map → yoga-adapter-9uab5Qn-.mjs.map} +1 -1
  142. package/dist/yoga-adapter-yqTl1aXK.mjs +2 -0
  143. package/package.json +20 -15
  144. package/dist/Text-BRf-59j2.mjs.map +0 -1
  145. package/dist/ag-BeFC4S2N.mjs.map +0 -1
  146. package/dist/ansi-OYRLxxZQ.mjs.map +0 -1
  147. package/dist/bound-term-BumfuXXW.d.mts.map +0 -1
  148. package/dist/cli-DG6zsfsS.mjs +0 -4
  149. package/dist/devtools-nmPUmYU_.mjs +0 -2
  150. package/dist/image-1nRyKa60.mjs.map +0 -1
  151. package/dist/index-2E1jYgak.d.mts.map +0 -1
  152. package/dist/index-DnuadDNL.d.mts.map +0 -1
  153. package/dist/layout-signals-Dch2EiCy.mjs.map +0 -1
  154. package/dist/mouse-events-hnbJZRwK.mjs.map +0 -1
  155. package/dist/node-BiFu8I9Y.mjs.map +0 -1
  156. package/dist/reconciler-NBDSEm8k.mjs.map +0 -1
  157. package/dist/render-string-B4h4SmK7.mjs.map +0 -1
  158. package/dist/src-B9S_woYc.mjs.map +0 -1
  159. package/dist/src-CbhWmUnF.mjs.map +0 -1
  160. package/dist/src-Oe6x5PrS.mjs.map +0 -1
  161. package/dist/src-WeA_J4BV.mjs.map +0 -1
  162. package/dist/ui/display.d.mts +0 -35
  163. package/dist/ui/display.d.mts.map +0 -1
  164. package/dist/ui/display.mjs +0 -123
  165. package/dist/ui/display.mjs.map +0 -1
  166. package/dist/ui/input.d.mts +0 -184
  167. package/dist/ui/input.d.mts.map +0 -1
  168. package/dist/ui/input.mjs +0 -283
  169. package/dist/ui/input.mjs.map +0 -1
  170. package/dist/wrapper-D7gNSsgf.mjs.map +0 -1
  171. package/dist/yoga-adapter-1ex8r0ws.mjs +0 -2
@@ -1,14 +1,14 @@
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";
1
+ import { C as findFocusableAncestor, D as isFocusHidden, E as getTabOrder$1, O as isFocusable, T as getExplicitFocusLink, b as setFocused, w as findSpatialTarget, x as findByTestID } from "./mouse-events-WQ2h-uqG.mjs";
2
+ import { f as syncFocusedSignal } from "./layout-signals-CGYbHmxy.mjs";
3
+ import { N as stripAnsi, Rt as queryKittyKeyboard, U as getInternalStreams, mt as createOutputPhase, r as createMeasurer } from "./unicode-CxyxVGmB.mjs";
4
+ import { j as createTerminalProfile } from "./src-B8lf1kiR.mjs";
5
+ import { C as isDirty } from "./reconciler--_-biN23.mjs";
6
+ import "./layout-engine-BgR58vL0.mjs";
7
+ import { a as setActiveColorLevel, i as parseColor } from "./ag-q9lRlKlt.mjs";
8
+ import { a as NodeContext } from "./context-CEhuD1R_.mjs";
9
+ import { u as useScrollRect } from "./useLayout-D5cE8OCI.mjs";
11
10
  import { createLogger } from "loggily";
11
+ import React, { createContext, useContext, useEffect, useLayoutEffect, useRef } from "react";
12
12
  //#region packages/ag/src/focus-manager.ts
13
13
  function createFocusManager(options) {
14
14
  const onFocusChange = options?.onFocusChange;
@@ -456,807 +456,301 @@ function createFocusManager(options) {
456
456
  };
457
457
  }
458
458
  //#endregion
459
- //#region packages/ag-term/src/history-buffer.ts
459
+ //#region packages/headless/src/selection.ts
460
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.
461
+ * Check if a character is a word character (not whitespace/punctuation).
462
+ * Word chars: letters, digits, underscore (matching \w).
468
463
  */
469
- function createHistoryItem(key, ansi, width) {
470
- const rows = ansi.split("\n");
464
+ function isWordChar(ch) {
465
+ return /\w/.test(ch);
466
+ }
467
+ /**
468
+ * Find word boundaries at a given column in the buffer row.
469
+ * Returns { startCol, endCol } inclusive of the word.
470
+ * If the position is on whitespace/punctuation, selects that single char.
471
+ */
472
+ function findWordBoundary(buffer, col, row) {
473
+ const width = buffer.width;
474
+ const ch = buffer.getCell(col, row).char;
475
+ if (isWordChar(ch)) {
476
+ let startCol = col;
477
+ while (startCol > 0 && isWordChar(buffer.getCell(startCol - 1, row).char)) startCol--;
478
+ let endCol = col;
479
+ while (endCol < width - 1 && isWordChar(buffer.getCell(endCol + 1, row).char)) endCol++;
480
+ return {
481
+ startCol,
482
+ endCol
483
+ };
484
+ }
471
485
  return {
472
- key,
473
- ansi,
474
- rows,
475
- plainTextRows: rows.map((r) => stripAnsi(r)),
476
- width
486
+ startCol: col,
487
+ endCol: col
477
488
  };
478
489
  }
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
- }
490
+ /**
491
+ * Find line boundaries for a given row.
492
+ * Returns { startCol, endCol } spanning from first content to last content.
493
+ * If the row is empty, returns { startCol: 0, endCol: width - 1 }.
494
+ */
495
+ function findLineBoundary(buffer, row) {
496
+ const width = buffer.width;
497
+ let endCol = width - 1;
498
+ while (endCol > 0 && buffer.getCell(endCol, row).char.trim() === "") endCol--;
499
+ let startCol = 0;
500
+ while (startCol < endCol && buffer.getCell(startCol, row).char.trim() === "") startCol++;
501
+ if (startCol >= endCol && buffer.getCell(startCol, row).char.trim() === "") return {
502
+ startCol: 0,
503
+ endCol: width - 1
504
+ };
505
+ return {
506
+ startCol,
507
+ endCol
508
+ };
509
+ }
510
+ /**
511
+ * Extend the head position according to the current granularity.
512
+ * For word granularity: snaps to word boundaries.
513
+ * For line granularity: snaps to line boundaries.
514
+ */
515
+ function extendByGranularity(col, row, anchor, granularity, buffer) {
516
+ if (granularity === "character" || !buffer) return {
517
+ col,
518
+ row
519
+ };
520
+ if (granularity === "word") {
521
+ const { startCol, endCol } = findWordBoundary(buffer, col, row);
522
+ return row > anchor.row || row === anchor.row && col >= anchor.col ? {
523
+ col: endCol,
524
+ row
525
+ } : {
526
+ col: startCol,
527
+ row
528
+ };
491
529
  }
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;
530
+ if (granularity === "line") {
531
+ const { startCol, endCol } = findLineBoundary(buffer, row);
532
+ return row > anchor.row || row === anchor.row && col >= anchor.col ? {
533
+ col: endCol,
534
+ row
535
+ } : {
536
+ col: startCol,
537
+ row
538
+ };
505
539
  }
506
540
  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
- }
541
+ col,
542
+ row
570
543
  };
571
544
  }
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;
545
+ function createTerminalSelectionState() {
546
+ return {
547
+ range: null,
548
+ selecting: false,
549
+ source: null,
550
+ granularity: "character",
551
+ scope: null
552
+ };
578
553
  }
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
- }
554
+ /**
555
+ * Clamp a position to a scope boundary.
556
+ */
557
+ function clampToScope(col, row, scope) {
558
+ if (!scope) return {
559
+ col,
560
+ row
561
+ };
587
562
  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
- }
563
+ col: Math.max(scope.left, Math.min(scope.right, col)),
564
+ row: Math.max(scope.top, Math.min(scope.bottom, row))
697
565
  };
698
566
  }
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;
567
+ function terminalSelectionUpdate(action, state) {
568
+ switch (action.type) {
569
+ case "start": {
570
+ const scope = action.scope ?? null;
571
+ const pos = clampToScope(action.col, action.row, scope);
572
+ return [{
573
+ range: {
574
+ anchor: pos,
575
+ head: pos
576
+ },
577
+ selecting: true,
578
+ source: action.source ?? "mouse",
579
+ granularity: "character",
580
+ scope
581
+ }, [{ type: "render" }]];
752
582
  }
753
- };
583
+ case "startWord": {
584
+ const scope = action.scope ?? null;
585
+ const { startCol, endCol } = findWordBoundary(action.buffer, action.col, action.row);
586
+ return [{
587
+ range: {
588
+ anchor: clampToScope(startCol, action.row, scope),
589
+ head: clampToScope(endCol, action.row, scope)
590
+ },
591
+ selecting: true,
592
+ source: action.source ?? "mouse",
593
+ granularity: "word",
594
+ scope
595
+ }, [{ type: "render" }]];
596
+ }
597
+ case "startLine": {
598
+ const scope = action.scope ?? null;
599
+ const { startCol, endCol } = findLineBoundary(action.buffer, action.row);
600
+ return [{
601
+ range: {
602
+ anchor: clampToScope(startCol, action.row, scope),
603
+ head: clampToScope(endCol, action.row, scope)
604
+ },
605
+ selecting: true,
606
+ source: action.source ?? "mouse",
607
+ granularity: "line",
608
+ scope
609
+ }, [{ type: "render" }]];
610
+ }
611
+ case "extend": {
612
+ if (!state.selecting) return [state, []];
613
+ const scope = "scope" in action ? action.scope ?? null : state.scope;
614
+ const extended = extendByGranularity(action.col, action.row, state.range.anchor, state.granularity, action.buffer);
615
+ const head = clampToScope(extended.col, extended.row, scope);
616
+ return [{
617
+ ...state,
618
+ range: {
619
+ anchor: state.range.anchor,
620
+ head
621
+ },
622
+ scope,
623
+ selecting: true
624
+ }, [{ type: "render" }]];
625
+ }
626
+ case "finish":
627
+ if (!state.range) return [{
628
+ ...state,
629
+ selecting: false
630
+ }, []];
631
+ return [{
632
+ ...state,
633
+ range: state.range,
634
+ selecting: false
635
+ }, []];
636
+ case "clear": {
637
+ const hadRange = state.range !== null;
638
+ return [createTerminalSelectionState(), hadRange ? [{ type: "render" }] : []];
639
+ }
640
+ }
754
641
  }
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
642
+ function normalizeRange(range) {
643
+ const { anchor, head } = range;
644
+ if (anchor.row < head.row || anchor.row === head.row && anchor.col <= head.col) return {
645
+ startRow: anchor.row,
646
+ startCol: anchor.col,
647
+ endRow: head.row,
648
+ endCol: head.col
766
649
  };
767
- const startRow = Math.max(0, totalHistory - Math.min(scrollOffset, totalHistory));
768
- const rowsToShow = Math.min(viewportHeight, totalHistory - startRow);
769
650
  return {
770
- overlayRows: history.getRows(startRow, rowsToShow),
771
- overlayRowCount: rowsToShow,
772
- liveRowsVisible: viewportHeight - rowsToShow,
773
- isScrolledUp: true,
774
- totalHeight: totalHistory + viewportHeight
651
+ startRow: head.row,
652
+ startCol: head.col,
653
+ endRow: anchor.row,
654
+ endCol: anchor.col
775
655
  };
776
656
  }
777
- //#endregion
778
- //#region packages/ag-term/src/search-overlay.ts
779
657
  /**
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]`).
658
+ * Extract text from a buffer within a selection range.
794
659
  *
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.
660
+ * Handles:
661
+ * - Soft-wrap joining (via RowMetadata.softWrapped) rejoins wrapped visual
662
+ * rows into their logical line, reinserting the word-wrap break space
663
+ * (RowMetadata.wrapJoinSpace) that trim-mode rendering stripped
664
+ * - Trailing fill exclusion (clamp to RowMetadata.lastContentCol; falls back
665
+ * to a trailing-whitespace trim when a row carries no metadata)
666
+ * - Blank line preservation within selection
667
+ * - Wide-char continuation cell skipping
668
+ * - SELECTABLE_FLAG filtering (when respectSelectableFlag is true)
669
+ * - Contain-scope clipping (when options.scope is set)
798
670
  */
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" }]];
671
+ function extractText(buffer, range, options) {
672
+ const { startRow, startCol, endRow, endCol } = normalizeRange(range);
673
+ const respectSelectable = options?.respectSelectableFlag ?? true;
674
+ const rowMeta = options?.rowMetadata;
675
+ const scope = options?.scope;
676
+ const parts = [];
677
+ for (let row = startRow; row <= endRow; row++) {
678
+ const meta = rowMeta?.[row];
679
+ let colStart = row === startRow ? startCol : 0;
680
+ let colEnd = row === endRow ? endCol : buffer.width - 1;
681
+ if (scope) {
682
+ colStart = Math.max(colStart, scope.left);
683
+ colEnd = Math.min(colEnd, scope.right);
684
+ if (colStart > colEnd) {
685
+ if (!(meta?.softWrapped && row < endRow)) {
686
+ parts.push("");
687
+ if (row < endRow) parts.push("\n");
688
+ }
689
+ continue;
690
+ }
902
691
  }
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];
692
+ if (meta && meta.lastContentCol >= 0) colEnd = Math.min(colEnd, meta.lastContentCol);
693
+ let line = "";
694
+ for (let col = colStart; col <= colEnd; col++) {
695
+ if (buffer.isCellContinuation(col, row)) continue;
696
+ if (respectSelectable && !buffer.isCellSelectable(col, row)) continue;
697
+ line += buffer.getCellChar(col, row);
948
698
  }
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];
699
+ if (!meta || meta.lastContentCol < 0) line = line.replace(/\s+$/, "");
700
+ if (meta?.softWrapped && row < endRow) {
701
+ parts.push(line);
702
+ if (meta.wrapJoinSpace) parts.push(" ");
703
+ } else {
704
+ parts.push(line);
705
+ if (row < endRow) parts.push("\n");
967
706
  }
968
707
  }
708
+ return parts.join("");
969
709
  }
710
+ //#endregion
711
+ //#region packages/ag-term/src/drag-events.ts
970
712
  /**
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`).
713
+ * Create a DragEvent payload.
978
714
  */
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) {
715
+ function createDragEvent(source, position, dropTarget) {
988
716
  return {
989
- type: "leaf",
990
- id
717
+ source,
718
+ position,
719
+ dropTarget
991
720
  };
992
721
  }
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
722
  /**
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.
723
+ * Create initial DragState when a drag starts.
1005
724
  */
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;
725
+ function createDragState(source, startPos) {
1027
726
  return {
1028
- ...layout,
1029
- first: newFirst,
1030
- second: newSecond
727
+ active: true,
728
+ source,
729
+ startPos,
730
+ currentPos: startPos,
731
+ dropTarget: null
1031
732
  };
1032
733
  }
1033
734
  /**
1034
- * Remove a pane from the layout. The sibling takes the full space.
1035
- * Returns null if the removed pane was the last one.
735
+ * Check if a node accepts drops (has any drag event handler).
1036
736
  */
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
- };
737
+ function isDropTarget(node) {
738
+ if (!node) return false;
739
+ const props = node.props;
740
+ return !!(props.onDragEnter || props.onDragLeave || props.onDragOver || props.onDrop);
1073
741
  }
1074
742
  /**
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.
743
+ * Find the nearest ancestor (including self) that is a drop target.
744
+ * Returns null if no ancestor accepts drops.
1106
745
  */
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);
746
+ function findDropTarget(node) {
747
+ let current = node;
748
+ while (current) {
749
+ if (isDropTarget(current)) return current;
750
+ current = current.parent;
1118
751
  }
1119
752
  return null;
1120
753
  }
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
754
  //#endregion
1261
755
  //#region packages/ag-react/src/hooks/useCursor.ts
1262
756
  /**
@@ -1445,135 +939,8 @@ function useCursor(position) {
1445
939
  };
1446
940
  }, []);
1447
941
  }
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");
942
+ createLogger("silvery:render");
943
+ createLogger("@silvery/ag-react");
1577
944
  //#endregion
1578
945
  //#region packages/ag-term/src/adapters/canvas-adapter.ts
1579
946
  const DEFAULT_CONFIG$1 = {
@@ -2216,23 +1583,13 @@ function findActiveCursorNode(root) {
2216
1583
  * Supported by: iTerm2, Kitty, WezTerm, foot, Ghostty
2217
1584
  */
2218
1585
  const OSC133 = {
2219
- /** Mark prompt start (before user input) */
2220
1586
  promptStart: "\x1B]133;A\x07",
2221
- /** Mark prompt end (after user input, before command output) */
2222
1587
  promptEnd: "\x1B]133;B\x07",
2223
- /** Mark command output start */
2224
1588
  commandStart: "\x1B]133;C\x07",
2225
- /** Mark command output end (exit code defaults to 0 = success) */
2226
1589
  commandEnd: (exitCode) => `\x1b]133;D;${exitCode ?? 0}\x07`
2227
1590
  };
2228
1591
  //#endregion
2229
1592
  //#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
1593
  /** Regex to match a Kitty keyboard query response: CSI ? <flags> u */
2237
1594
  const KITTY_RESPONSE_RE = /\x1b\[\?(\d+)u/;
2238
1595
  /**
@@ -2253,16 +1610,45 @@ async function detectKittySupport(write, read, timeoutMs = 200) {
2253
1610
  supported: false,
2254
1611
  flags: 0
2255
1612
  };
2256
- const match = KITTY_RESPONSE_RE.exec(data);
2257
- if (!match) return {
1613
+ const parsed = parseKittyResponse(data);
1614
+ if (!parsed) return {
2258
1615
  supported: false,
2259
1616
  flags: 0,
2260
1617
  buffered: data
2261
1618
  };
1619
+ const buffered = data.slice(0, parsed.start) + data.slice(parsed.end);
1620
+ return {
1621
+ ...parsed.result,
1622
+ buffered: buffered || void 0
1623
+ };
1624
+ }
1625
+ /** Query Kitty support through the session's canonical stdin owner. */
1626
+ async function detectKittyWithProbe(input, timeoutMs = 200) {
1627
+ return await input.probe({
1628
+ query: queryKittyKeyboard(),
1629
+ timeoutMs,
1630
+ parse: (acc) => {
1631
+ const parsed = parseKittyResponse(acc);
1632
+ return parsed ? {
1633
+ result: parsed.result,
1634
+ consumed: parsed.end
1635
+ } : null;
1636
+ }
1637
+ }) ?? {
1638
+ supported: false,
1639
+ flags: 0
1640
+ };
1641
+ }
1642
+ function parseKittyResponse(data) {
1643
+ const match = KITTY_RESPONSE_RE.exec(data);
1644
+ if (!match) return null;
2262
1645
  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
1646
+ result: {
1647
+ supported: true,
1648
+ flags: parseInt(match[1], 10)
1649
+ },
1650
+ start: match.index,
1651
+ end: match.index + match[0].length
2266
1652
  };
2267
1653
  }
2268
1654
  /**
@@ -2563,41 +1949,11 @@ async function queryCursorFromStdio(stdout, stdin, timeoutMs = 200) {
2563
1949
  }
2564
1950
  //#endregion
2565
1951
  //#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
1952
  /** Well-known xterm width DEC mode numbers. */
2593
1953
  const WidthMode = {
2594
- /** UTF-8 mode */
2595
1954
  UTF8: 1020,
2596
- /** CJK ambiguous character width */
2597
1955
  CJK_WIDTH: 1021,
2598
- /** Emoji width */
2599
1956
  EMOJI_WIDTH: 1022,
2600
- /** Private-use area width */
2601
1957
  PRIVATE_USE_WIDTH: 1023
2602
1958
  };
2603
1959
  /** Default configuration when detection fails or times out. */
@@ -2617,41 +1973,42 @@ function isSet(ps) {
2617
1973
  return ps === 1 || ps === 3;
2618
1974
  }
2619
1975
  /**
2620
- * Query a single DEC width mode via DECRQM.
2621
- * Returns the Ps value from the DECRPM response, or null on timeout.
1976
+ * Query width modes through the session's canonical stdin owner.
1977
+ *
1978
+ * Unlike `createWidthDetector`'s standalone transport adapter, this path
1979
+ * never attaches another `data` listener: responses are consumed by the
1980
+ * owner's probe lane before normal key/mouse parsing.
2622
1981
  */
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
- });
1982
+ function detectWidthConfigWithProbe(input, timeoutMs = 200) {
1983
+ return detectWidthConfig((mode) => input.probe({
1984
+ query: `\x1b[?${mode}$p`,
1985
+ timeoutMs,
1986
+ parse: (acc) => parseWidthModeResponse(acc, mode)
1987
+ }));
1988
+ }
1989
+ function parseWidthModeResponse(acc, expectedMode) {
1990
+ DECRPM_RE.lastIndex = 0;
1991
+ let match;
1992
+ while ((match = DECRPM_RE.exec(acc)) !== null) {
1993
+ if (parseInt(match[1], 10) !== expectedMode) continue;
1994
+ return {
1995
+ result: parseInt(match[2], 10),
1996
+ consumed: match.index + match[0].length
1997
+ };
1998
+ }
1999
+ return null;
2000
+ }
2001
+ async function detectWidthConfig(query) {
2002
+ const utf8Ps = await query(WidthMode.UTF8);
2003
+ const cjkPs = await query(WidthMode.CJK_WIDTH);
2004
+ const emojiPs = await query(WidthMode.EMOJI_WIDTH);
2005
+ const puaPs = await query(WidthMode.PRIVATE_USE_WIDTH);
2006
+ return {
2007
+ utf8: utf8Ps !== null ? isSet(utf8Ps) : DEFAULT_WIDTH_CONFIG.utf8,
2008
+ cjkWidth: cjkPs !== null ? isSet(cjkPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.cjkWidth,
2009
+ emojiWidth: emojiPs !== null ? isSet(emojiPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.emojiWidth,
2010
+ privateUseWidth: puaPs !== null ? isSet(puaPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.privateUseWidth
2011
+ };
2655
2012
  }
2656
2013
  /**
2657
2014
  * Apply detected width configuration to terminal capabilities.
@@ -2672,52 +2029,6 @@ function applyWidthConfig(caps, config) {
2672
2029
  maybeWideEmojis: config.emojiWidth === 2
2673
2030
  };
2674
2031
  }
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
2032
  //#endregion
2722
2033
  //#region packages/ag-term/src/device-attrs.ts
2723
2034
  /**
@@ -2903,17 +2214,11 @@ async function queryDeviceAttributes(stdout, stdin, timeoutMs = 200) {
2903
2214
  const DECRPM_RESPONSE_RE = /\x1b\[\?(\d+);(\d+)\$y/;
2904
2215
  /** Well-known DEC private mode constants. */
2905
2216
  const DecMode = {
2906
- /** DEC cursor visible (DECTCEM) */
2907
2217
  CURSOR_VISIBLE: 25,
2908
- /** Alternate screen buffer (DECSET 1049) */
2909
2218
  ALT_SCREEN: 1049,
2910
- /** Normal mouse tracking (X10) */
2911
2219
  MOUSE_TRACKING: 1e3,
2912
- /** Bracketed paste mode */
2913
2220
  BRACKETED_PASTE: 2004,
2914
- /** Synchronized output */
2915
2221
  SYNC_OUTPUT: 2026,
2916
- /** Focus reporting */
2917
2222
  FOCUS_REPORTING: 1004
2918
2223
  };
2919
2224
  /**
@@ -3189,6 +2494,124 @@ function createInputEvents(stdin) {
3189
2494
  } };
3190
2495
  }
3191
2496
  //#endregion
2497
+ //#region packages/ag-term/src/hit-registry-core.ts
2498
+ /**
2499
+ * Registry for managing hit regions.
2500
+ *
2501
+ * Components register their screen regions with targets, and the registry
2502
+ * resolves mouse clicks to the appropriate target based on position and z-index.
2503
+ *
2504
+ * @example
2505
+ * ```typescript
2506
+ * const registry = new HitRegistry();
2507
+ *
2508
+ * // Register a card region
2509
+ * registry.register('card-1', {
2510
+ * x: 10, y: 5, width: 30, height: 8,
2511
+ * target: { type: 'node', nodeId: 'abc123' },
2512
+ * zIndex: 10
2513
+ * });
2514
+ *
2515
+ * // Hit test a click
2516
+ * const target = registry.hitTest(15, 7);
2517
+ * // Returns { type: 'node', nodeId: 'abc123' }
2518
+ * ```
2519
+ */
2520
+ var HitRegistry = class {
2521
+ regions = /* @__PURE__ */ new Map();
2522
+ /**
2523
+ * Register a hit region with a unique ID.
2524
+ *
2525
+ * @param id - Unique identifier for the region (used for unregistration)
2526
+ * @param region - The region definition including position, size, target, and z-index
2527
+ */
2528
+ register(id, region) {
2529
+ this.regions.set(id, region);
2530
+ }
2531
+ /**
2532
+ * Unregister a hit region by ID.
2533
+ *
2534
+ * @param id - The ID used when registering the region
2535
+ */
2536
+ unregister(id) {
2537
+ this.regions.delete(id);
2538
+ }
2539
+ /**
2540
+ * Clear all registered regions.
2541
+ * Useful when the UI is completely redrawn.
2542
+ */
2543
+ clear() {
2544
+ this.regions.clear();
2545
+ }
2546
+ /**
2547
+ * Get the number of registered regions.
2548
+ * Useful for debugging.
2549
+ */
2550
+ get size() {
2551
+ return this.regions.size;
2552
+ }
2553
+ /**
2554
+ * Test a screen position and return the highest z-index matching target.
2555
+ *
2556
+ * @param screenX - X position on screen (0-indexed column)
2557
+ * @param screenY - Y position on screen (0-indexed row)
2558
+ * @returns The target of the highest z-index region containing the point, or null if none
2559
+ */
2560
+ hitTest(screenX, screenY) {
2561
+ let bestMatch = null;
2562
+ for (const region of this.regions.values()) if (screenX >= region.x && screenX < region.x + region.width && screenY >= region.y && screenY < region.y + region.height) {
2563
+ if (!bestMatch || region.zIndex > bestMatch.zIndex) bestMatch = region;
2564
+ }
2565
+ return bestMatch?.target ?? null;
2566
+ }
2567
+ /**
2568
+ * Get all regions that contain a point, sorted by z-index (highest first).
2569
+ * Useful for debugging or when you need to know all overlapping elements.
2570
+ *
2571
+ * @param screenX - X position on screen (0-indexed column)
2572
+ * @param screenY - Y position on screen (0-indexed row)
2573
+ * @returns Array of matching regions, sorted by z-index descending
2574
+ */
2575
+ hitTestAll(screenX, screenY) {
2576
+ const matches = [];
2577
+ 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);
2578
+ return matches.sort((a, b) => b.zIndex - a.zIndex);
2579
+ }
2580
+ /**
2581
+ * Debug helper: get all registered regions.
2582
+ */
2583
+ getAllRegions() {
2584
+ return new Map(this.regions);
2585
+ }
2586
+ };
2587
+ /**
2588
+ * Generate a unique ID for hit region registration.
2589
+ */
2590
+ let hitRegionIdCounter = 0;
2591
+ function generateHitRegionId() {
2592
+ return `hit-${++hitRegionIdCounter}`;
2593
+ }
2594
+ /**
2595
+ * Reset the ID counter (useful for testing).
2596
+ */
2597
+ function resetHitRegionIdCounter() {
2598
+ hitRegionIdCounter = 0;
2599
+ }
2600
+ /**
2601
+ * Recommended z-index values for different UI layers.
2602
+ */
2603
+ const Z_INDEX = {
2604
+ BACKGROUND: 0,
2605
+ COLUMN_HEADER: 5,
2606
+ CARD: 10,
2607
+ FOLD_TOGGLE: 15,
2608
+ LINK: 20,
2609
+ FLOATING: 50,
2610
+ DIALOG: 100,
2611
+ DROPDOWN: 150,
2612
+ TOOLTIP: 200
2613
+ };
2614
+ //#endregion
3192
2615
  //#region packages/ag-term/src/non-tty.ts
3193
2616
  /**
3194
2617
  * Non-TTY Mode Support for Silvery
@@ -3461,468 +2884,1018 @@ function createPipeline(options = {}) {
3461
2884
  };
3462
2885
  }
3463
2886
  //#endregion
3464
- //#region packages/headless/src/selection.ts
2887
+ //#region packages/ag-term/src/scroll-utils.ts
3465
2888
  /**
3466
- * Check if a character is a word character (not whitespace/punctuation).
3467
- * Word chars: letters, digits, underscore (matching \w).
2889
+ * Scroll Utilities
2890
+ *
2891
+ * Shared functions for edge-based scrolling behavior across VirtualList,
2892
+ * HorizontalVirtualList, and other scroll-aware components.
3468
2893
  */
3469
- function isWordChar(ch) {
3470
- return /\w/.test(ch);
2894
+ /**
2895
+ * Calculate edge-based scroll offset.
2896
+ *
2897
+ * Only scrolls when cursor approaches the edge of the visible area.
2898
+ * This provides smoother scrolling by starting to scroll before hitting
2899
+ * the absolute edge, maintaining context around the selected item.
2900
+ *
2901
+ * ## Algorithm
2902
+ *
2903
+ * The viewport is divided into zones:
2904
+ * ```
2905
+ * |<padding>|<------ safe zone ------>|<padding>|
2906
+ * | scroll | no scroll needed | scroll |
2907
+ * | if < | | if > |
2908
+ * ```
2909
+ *
2910
+ * When the selected item enters a padding zone, the viewport scrolls
2911
+ * to keep the item visible with margin.
2912
+ *
2913
+ * ## Asymmetry Note
2914
+ *
2915
+ * The +1 in the "scroll down/right" case is intentional:
2916
+ * - Offset points to the TOP/LEFT of the viewport
2917
+ * - We want the selected item to be `padding` items from the BOTTOM/RIGHT
2918
+ * - Formula: `selectedIndex - visibleCount + padding + 1`
2919
+ *
2920
+ * Example: visibleCount=10, padding=2, selectedIndex=15
2921
+ * offset = 15 - 10 + 2 + 1 = 8
2922
+ * viewport shows items 8-17, selected item 15 is at position 7 (2 from bottom)
2923
+ *
2924
+ * @param selectedIndex - Currently selected item index
2925
+ * @param currentOffset - Current scroll offset (topmost/leftmost visible item)
2926
+ * @param visibleCount - Number of items visible in viewport
2927
+ * @param totalCount - Total number of items
2928
+ * @param padding - Items to keep visible before/after cursor (default: 1)
2929
+ * @returns New scroll offset
2930
+ */
2931
+ function calcEdgeBasedScrollOffset(selectedIndex, currentOffset, visibleCount, totalCount, padding = 1) {
2932
+ if (totalCount <= visibleCount) return 0;
2933
+ const effectivePadding = padding * 2 >= visibleCount ? 0 : padding;
2934
+ const visibleStart = currentOffset;
2935
+ const visibleEnd = currentOffset + visibleCount - 1;
2936
+ const paddedStart = visibleStart + effectivePadding;
2937
+ const paddedEnd = visibleEnd - effectivePadding;
2938
+ let newOffset = currentOffset;
2939
+ if (selectedIndex < paddedStart) newOffset = Math.max(0, selectedIndex - effectivePadding);
2940
+ else if (effectivePadding === 0 && selectedIndex === paddedStart && currentOffset > 0 && visibleCount > padding) newOffset = Math.max(0, selectedIndex - padding);
2941
+ else if (selectedIndex > paddedEnd) newOffset = Math.min(totalCount - visibleCount, selectedIndex - visibleCount + effectivePadding + 1);
2942
+ return Math.max(0, Math.min(newOffset, totalCount - visibleCount));
3471
2943
  }
2944
+ //#endregion
2945
+ //#region packages/ag-term/src/scroll-region.ts
3472
2946
  /**
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.
2947
+ * Terminal scroll region (DECSTBM) utilities.
2948
+ *
2949
+ * Scroll regions tell the terminal to natively scroll content within
2950
+ * a defined row range, which is faster than re-rendering all cells.
2951
+ *
2952
+ * DECSTBM (DEC Set Top and Bottom Margins) is supported by most modern
2953
+ * terminal emulators: xterm, iTerm2, Kitty, Ghostty, WezTerm, etc.
3476
2954
  */
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
- };
2955
+ const ESC = "\x1B";
2956
+ /** Set terminal scroll region (1-indexed top and bottom rows). */
2957
+ function setScrollRegion(stdout, top, bottom) {
2958
+ stdout.write(`${ESC}[${top};${bottom}r`);
2959
+ }
2960
+ /** Reset scroll region to full terminal. */
2961
+ function resetScrollRegion(stdout) {
2962
+ stdout.write(`${ESC}[r`);
2963
+ }
2964
+ /** Scroll content up by N lines within the current scroll region. */
2965
+ function scrollUp(stdout, lines = 1) {
2966
+ stdout.write(`${ESC}[${lines}S`);
2967
+ }
2968
+ /** Scroll content down by N lines within the current scroll region. */
2969
+ function scrollDown(stdout, lines = 1) {
2970
+ stdout.write(`${ESC}[${lines}T`);
2971
+ }
2972
+ /** Move cursor to a specific position (1-indexed row and column). */
2973
+ function moveCursor(stdout, row, col) {
2974
+ stdout.write(`${ESC}[${row};${col}H`);
3494
2975
  }
3495
2976
  /**
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 }.
2977
+ * Detect if the terminal likely supports DECSTBM scroll regions.
2978
+ *
2979
+ * Most modern terminals do (xterm, iTerm2, Kitty, Ghostty, WezTerm, etc.)
2980
+ * but some (e.g., Linux console) may not handle them correctly.
2981
+ *
2982
+ * Pass `emulator` (via `term.emulator` or a test fixture) when in scope.
2983
+ * Without it, this falls back to {@link createTerminalProfile} — the canonical
2984
+ * single-source-of-truth entry point in `@silvery/ansi/profile`. Direct
2985
+ * reads of terminal-signal env vars (TERM / TERM_PROGRAM / …) are banned
2986
+ * outside that module — see `scripts/lint-env-reads.ts`.
3499
2987
  */
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
- };
2988
+ function supportsScrollRegions(emulator) {
2989
+ const resolved = emulator ?? createTerminalProfile().emulator;
2990
+ const term = resolved.TERM;
2991
+ const termProgram = resolved.program;
2992
+ if (termProgram === "iTerm.app" || termProgram === "WezTerm" || termProgram === "ghostty" || termProgram === "Ghostty" || termProgram === "vscode") return true;
2993
+ if (term.includes("xterm") || term.includes("screen") || term.includes("tmux") || term.includes("kitty")) return true;
2994
+ if (term === "linux") return false;
2995
+ return term !== "";
2996
+ }
2997
+ //#endregion
2998
+ //#region packages/ag-term/src/pane-manager.ts
2999
+ /** Create a single-pane layout */
3000
+ function createLeaf(id) {
3510
3001
  return {
3511
- startCol,
3512
- endCol
3002
+ type: "leaf",
3003
+ id
3513
3004
  };
3514
3005
  }
3006
+ /** Get all leaf pane IDs in depth-first left-to-right order */
3007
+ function getPaneIds(layout) {
3008
+ if (layout.type === "leaf") return [layout.id];
3009
+ return [...getPaneIds(layout.first), ...getPaneIds(layout.second)];
3010
+ }
3011
+ /** Find the next/previous pane in tab order (depth-first left-to-right) */
3012
+ function getTabOrder(layout) {
3013
+ return getPaneIds(layout);
3014
+ }
3515
3015
  /**
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.
3016
+ * Split a pane into two. Returns new layout tree with the target pane split.
3017
+ * The original pane becomes the first child; the new pane becomes the second.
3519
3018
  */
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
3019
+ function splitPane(layout, targetPaneId, direction, newPaneId, ratio = .5) {
3020
+ const clampedRatio = clampRatio(ratio);
3021
+ if (layout.type === "leaf") {
3022
+ if (layout.id === targetPaneId) return {
3023
+ type: "split",
3024
+ direction,
3025
+ ratio: clampedRatio,
3026
+ first: {
3027
+ type: "leaf",
3028
+ id: targetPaneId
3029
+ },
3030
+ second: {
3031
+ type: "leaf",
3032
+ id: newPaneId
3033
+ }
3543
3034
  };
3035
+ return layout;
3544
3036
  }
3037
+ const newFirst = splitPane(layout.first, targetPaneId, direction, newPaneId, ratio);
3038
+ const newSecond = splitPane(layout.second, targetPaneId, direction, newPaneId, ratio);
3039
+ if (newFirst === layout.first && newSecond === layout.second) return layout;
3545
3040
  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
3041
+ ...layout,
3042
+ first: newFirst,
3043
+ second: newSecond
3557
3044
  };
3558
3045
  }
3559
3046
  /**
3560
- * Clamp a position to a scope boundary.
3047
+ * Remove a pane from the layout. The sibling takes the full space.
3048
+ * Returns null if the removed pane was the last one.
3561
3049
  */
3562
- function clampToScope(col, row, scope) {
3563
- if (!scope) return {
3564
- col,
3565
- row
3050
+ function removePane(layout, paneId) {
3051
+ if (layout.type === "leaf") return layout.id === paneId ? null : layout;
3052
+ if (layout.first.type === "leaf" && layout.first.id === paneId) return layout.second;
3053
+ if (layout.second.type === "leaf" && layout.second.id === paneId) return layout.first;
3054
+ const newFirst = removePane(layout.first, paneId);
3055
+ const newSecond = removePane(layout.second, paneId);
3056
+ if (newFirst === null) return newSecond;
3057
+ if (newSecond === null) return newFirst;
3058
+ if (newFirst === layout.first && newSecond === layout.second) return layout;
3059
+ return {
3060
+ ...layout,
3061
+ first: newFirst,
3062
+ second: newSecond
3566
3063
  };
3064
+ }
3065
+ /** Swap two panes' positions in the layout */
3066
+ function swapPanes(layout, paneId1, paneId2) {
3067
+ if (layout.type === "leaf") {
3068
+ if (layout.id === paneId1) return {
3069
+ type: "leaf",
3070
+ id: paneId2
3071
+ };
3072
+ if (layout.id === paneId2) return {
3073
+ type: "leaf",
3074
+ id: paneId1
3075
+ };
3076
+ return layout;
3077
+ }
3078
+ const newFirst = swapPanes(layout.first, paneId1, paneId2);
3079
+ const newSecond = swapPanes(layout.second, paneId1, paneId2);
3080
+ if (newFirst === layout.first && newSecond === layout.second) return layout;
3567
3081
  return {
3568
- col: Math.max(scope.left, Math.min(scope.right, col)),
3569
- row: Math.max(scope.top, Math.min(scope.bottom, row))
3082
+ ...layout,
3083
+ first: newFirst,
3084
+ second: newSecond
3570
3085
  };
3571
3086
  }
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
- }
3087
+ /**
3088
+ * Resize a split: adjust the ratio of the nearest ancestor split
3089
+ * that contains the target pane as its first child.
3090
+ * Positive delta = grow (first child gets more), negative = shrink.
3091
+ */
3092
+ function resizeSplit(layout, paneId, delta) {
3093
+ if (layout.type === "leaf") return layout;
3094
+ if (getPaneIds(layout.first).includes(paneId)) {
3095
+ const newRatio = clampRatio(layout.ratio + delta);
3096
+ if (newRatio === layout.ratio) return layout;
3097
+ return {
3098
+ ...layout,
3099
+ ratio: newRatio
3100
+ };
3101
+ }
3102
+ if (getPaneIds(layout.second).includes(paneId)) {
3103
+ const newRatio = clampRatio(layout.ratio - delta);
3104
+ if (newRatio === layout.ratio) return layout;
3105
+ return {
3106
+ ...layout,
3107
+ ratio: newRatio
3108
+ };
3645
3109
  }
3110
+ return layout;
3646
3111
  }
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
- };
3112
+ /**
3113
+ * Find the pane adjacent to the given pane in a direction.
3114
+ *
3115
+ * For left/right: looks for siblings in horizontal splits.
3116
+ * For up/down: looks for siblings in vertical splits.
3117
+ *
3118
+ * Returns null if no adjacent pane exists in that direction.
3119
+ */
3120
+ function findAdjacentPane(layout, paneId, direction) {
3121
+ const path = findPath(layout, paneId);
3122
+ if (!path) return null;
3123
+ const splitDirection = direction === "left" || direction === "right" ? "horizontal" : "vertical";
3124
+ const goToSecond = direction === "right" || direction === "down";
3125
+ for (let i = path.length - 1; i >= 0; i--) {
3126
+ const step = path[i];
3127
+ if (step.node.type !== "split") continue;
3128
+ if (step.node.direction !== splitDirection) continue;
3129
+ if (step.side === "first" && goToSecond) return firstLeaf(step.node.second);
3130
+ if (step.side === "second" && !goToSecond) return lastLeaf(step.node.first);
3131
+ }
3132
+ return null;
3133
+ }
3134
+ function clampRatio(ratio) {
3135
+ return Math.max(.1, Math.min(.9, ratio));
3136
+ }
3137
+ /** Get the first (leftmost/topmost) leaf ID */
3138
+ function firstLeaf(node) {
3139
+ if (node.type === "leaf") return node.id;
3140
+ return firstLeaf(node.first);
3141
+ }
3142
+ /** Get the last (rightmost/bottommost) leaf ID */
3143
+ function lastLeaf(node) {
3144
+ if (node.type === "leaf") return node.id;
3145
+ return lastLeaf(node.second);
3146
+ }
3147
+ /** Find the path from root to a leaf, recording which side we took at each split */
3148
+ function findPath(layout, paneId) {
3149
+ if (layout.type === "leaf") return layout.id === paneId ? [] : null;
3150
+ const firstPath = findPath(layout.first, paneId);
3151
+ if (firstPath !== null) return [{
3152
+ node: layout,
3153
+ side: "first"
3154
+ }, ...firstPath];
3155
+ const secondPath = findPath(layout.second, paneId);
3156
+ if (secondPath !== null) return [{
3157
+ node: layout,
3158
+ side: "second"
3159
+ }, ...secondPath];
3160
+ return null;
3161
+ }
3162
+ //#endregion
3163
+ //#region packages/ag-term/src/selection-renderer.ts
3164
+ function normalizeSelectionColor(color) {
3165
+ if (color === void 0) return void 0;
3166
+ if (typeof color === "string") return parseColor(color);
3167
+ return color;
3661
3168
  }
3662
3169
  /**
3663
- * Extract text from a buffer within a selection range.
3170
+ * Compute selection style changes for all cells within a selection range.
3664
3171
  *
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)
3172
+ * Returns a sparse list of cell changes (fg/bg only). The caller applies these
3173
+ * to the buffer before the output phase diffs it. This approach:
3174
+ * - Composes correctly with existing cell styles
3175
+ * - Handles already-inverted content (swaps back to normal instead of double-inverting)
3176
+ * - Respects SELECTABLE_FLAG per cell (skip non-selectable)
3177
+ * - Works with the normal diff/output renderer (no separate ANSI pass)
3178
+ *
3179
+ * @param buffer The rendered buffer (post-render-phase)
3180
+ * @param selection Current selection range, or null
3181
+ * @param theme Optional selection theme colors
3182
+ * @param respectSelectableFlag When true, skip cells without SELECTABLE_FLAG
3675
3183
  */
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 = [];
3184
+ function composeSelectionCells(buffer, selection, theme, respectSelectableFlag = false, scope) {
3185
+ if (!selection) return [];
3186
+ const { startRow, startCol, endRow, endCol } = normalizeRange(selection);
3187
+ const changes = [];
3682
3188
  for (let row = startRow; row <= endRow; row++) {
3683
- const meta = rowMeta?.[row];
3684
3189
  let colStart = row === startRow ? startCol : 0;
3685
3190
  let colEnd = row === endRow ? endCol : buffer.width - 1;
3191
+ const rowMeta = buffer.getRowMeta(row);
3192
+ if (rowMeta.lastContentCol >= 0) colEnd = Math.min(colEnd, rowMeta.lastContentCol);
3193
+ if (colStart > colEnd) continue;
3686
3194
  if (scope) {
3687
3195
  colStart = Math.max(colStart, scope.left);
3688
3196
  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");
3197
+ if (colStart > colEnd) continue;
3198
+ }
3199
+ const firstContentCol = findFirstContentCol(buffer, row, colStart, colEnd, respectSelectableFlag);
3200
+ if (firstContentCol === null) continue;
3201
+ colStart = Math.max(colStart, firstContentCol);
3202
+ for (let col = colStart; col <= colEnd; col++) {
3203
+ if (buffer.isCellContinuation(col, row)) continue;
3204
+ if (respectSelectableFlag && !buffer.isCellSelectable(col, row)) continue;
3205
+ const cellFg = buffer.getCellFg(col, row);
3206
+ const cellBg = buffer.getCellBg(col, row);
3207
+ let newFg;
3208
+ let newBg;
3209
+ let inverseAttr = false;
3210
+ if (theme?.selectionBg != null) {
3211
+ newFg = theme.selectionFg ?? cellFg;
3212
+ newBg = theme.selectionBg;
3213
+ } else if (cellFg == null || cellBg == null) {
3214
+ newFg = cellFg;
3215
+ newBg = cellBg;
3216
+ inverseAttr = true;
3217
+ } else {
3218
+ newFg = cellBg;
3219
+ newBg = cellFg;
3220
+ }
3221
+ changes.push({
3222
+ col,
3223
+ row,
3224
+ fg: newFg,
3225
+ bg: newBg,
3226
+ inverseAttr
3227
+ });
3228
+ }
3229
+ }
3230
+ return changes;
3231
+ }
3232
+ function findFirstContentCol(buffer, row, colStart, colEnd, respectSelectableFlag) {
3233
+ for (let col = colStart; col <= colEnd; col++) {
3234
+ if (buffer.isCellContinuation(col, row)) continue;
3235
+ if (respectSelectableFlag && !buffer.isCellSelectable(col, row)) continue;
3236
+ if (buffer.getCell(col, row).char.trim().length > 0) return col;
3237
+ }
3238
+ return null;
3239
+ }
3240
+ /**
3241
+ * Apply selection style changes to a buffer.
3242
+ *
3243
+ * Modifies the buffer in-place by setting fg/bg on affected cells.
3244
+ * Call this after the render phase and before the output phase.
3245
+ *
3246
+ * @param buffer The rendered buffer to modify
3247
+ * @param changes Cell changes from composeSelectionCells
3248
+ */
3249
+ function applySelectionToBuffer(buffer, changes) {
3250
+ for (const change of changes) {
3251
+ const cell = buffer.getCell(change.col, change.row);
3252
+ if (change.inverseAttr) buffer.setCell(change.col, change.row, {
3253
+ ...cell,
3254
+ attrs: {
3255
+ ...cell.attrs,
3256
+ inverse: !cell.attrs.inverse
3257
+ }
3258
+ });
3259
+ else buffer.setCell(change.col, change.row, {
3260
+ ...cell,
3261
+ fg: normalizeSelectionColor(change.fg) ?? null,
3262
+ bg: normalizeSelectionColor(change.bg) ?? null
3263
+ });
3264
+ }
3265
+ }
3266
+ /**
3267
+ * Compute search-highlight style changes for a list of highlight ranges.
3268
+ *
3269
+ * Returns a sparse list of cell changes (fg/bg only) — apply them with
3270
+ * `applySelectionToBuffer` (the apply step is style-source-agnostic — it
3271
+ * just sets fg/bg or toggles inverse on the cells you give it).
3272
+ *
3273
+ * Default behaviour mirrors the legacy `renderSearchHighlights` that wrote
3274
+ * `\x1b[7m...\x1b[27m` past the buffer: cells with default fg/bg get the
3275
+ * inverse-attr toggle (so the terminal handles the SGR 7 swap at display).
3276
+ * Cells with explicit fg/bg get a direct fg↔bg swap. A theme can override
3277
+ * with explicit `selectionFg` / `selectionBg` tokens.
3278
+ *
3279
+ * Tracking: km-silvery.delete-search-overlay-ansi
3280
+ */
3281
+ function composeSearchHighlightCells(buffer, highlights, theme) {
3282
+ if (highlights.length === 0) return [];
3283
+ const changes = [];
3284
+ for (const h of highlights) {
3285
+ const row = h.screenRow;
3286
+ if (row < 0 || row >= buffer.height) continue;
3287
+ const colStart = Math.max(0, h.startCol);
3288
+ const colEnd = Math.min(buffer.width - 1, h.endCol);
3289
+ if (colStart > colEnd) continue;
3290
+ for (let col = colStart; col <= colEnd; col++) {
3291
+ if (buffer.isCellContinuation(col, row)) continue;
3292
+ const cellFg = buffer.getCellFg(col, row);
3293
+ const cellBg = buffer.getCellBg(col, row);
3294
+ let newFg;
3295
+ let newBg;
3296
+ let inverseAttr = false;
3297
+ if (theme?.selectionBg != null) {
3298
+ newFg = theme.selectionFg ?? cellFg;
3299
+ newBg = theme.selectionBg;
3300
+ } else if (cellFg == null || cellBg == null) {
3301
+ newFg = cellFg;
3302
+ newBg = cellBg;
3303
+ inverseAttr = true;
3304
+ } else {
3305
+ newFg = cellBg;
3306
+ newBg = cellFg;
3307
+ }
3308
+ changes.push({
3309
+ col,
3310
+ row,
3311
+ fg: newFg,
3312
+ bg: newBg,
3313
+ inverseAttr
3314
+ });
3315
+ }
3316
+ }
3317
+ return changes;
3318
+ }
3319
+ //#endregion
3320
+ //#region packages/ag-term/src/virtual-scrollback.ts
3321
+ /**
3322
+ * Virtual scrollback buffer for storing historical rendered content.
3323
+ *
3324
+ * Used by virtual inline mode to maintain scrollable history while
3325
+ * rendering in altscreen (which normally has no scrollback).
3326
+ *
3327
+ * Implementation uses a circular buffer for O(1) push and bounded memory.
3328
+ */
3329
+ function createVirtualScrollback(options) {
3330
+ const maxLines = options?.maxLines ?? 1e4;
3331
+ const ansiLines = new Array(maxLines);
3332
+ const plainLines = new Array(maxLines);
3333
+ let head = 0;
3334
+ let count = 0;
3335
+ return {
3336
+ push(lines) {
3337
+ for (const line of lines) {
3338
+ ansiLines[head] = line;
3339
+ plainLines[head] = stripAnsi(line);
3340
+ head = (head + 1) % maxLines;
3341
+ if (count < maxLines) count++;
3342
+ }
3343
+ },
3344
+ getVisibleRows(offset, rowCount) {
3345
+ const result = [];
3346
+ for (let i = 0; i < rowCount; i++) {
3347
+ const logicalIndex = count - offset - rowCount + i;
3348
+ if (logicalIndex < 0 || logicalIndex >= count) result.push("");
3349
+ else {
3350
+ const physical = (head - count + logicalIndex + maxLines) % maxLines;
3351
+ result.push(ansiLines[physical]);
3352
+ }
3353
+ }
3354
+ return result;
3355
+ },
3356
+ get totalLines() {
3357
+ return count;
3358
+ },
3359
+ search(query) {
3360
+ if (!query) return [];
3361
+ const lowerQuery = query.toLowerCase();
3362
+ const matches = [];
3363
+ for (let i = 0; i < count; i++) if (plainLines[(head - count + i + maxLines) % maxLines].toLowerCase().includes(lowerQuery)) matches.push(i);
3364
+ return matches;
3365
+ },
3366
+ clear() {
3367
+ head = 0;
3368
+ count = 0;
3369
+ }
3370
+ };
3371
+ }
3372
+ //#endregion
3373
+ //#region packages/ag-term/src/history-buffer.ts
3374
+ /**
3375
+ * Ring buffer for frozen scrollback items.
3376
+ *
3377
+ * Each item represents a rendered list entry (card, row, etc.) with its
3378
+ * ANSI snapshot, split rows, and plain-text rows for searching.
3379
+ * When total rows or cached ANSI bytes exceed capacity, the oldest items are
3380
+ * evicted. The byte count tracks the serialized string payload; it is a
3381
+ * deterministic cache budget, not an exact process heap measurement.
3382
+ */
3383
+ function createHistoryItem(key, ansi, width) {
3384
+ const rows = ansi.split("\n");
3385
+ return {
3386
+ key,
3387
+ ansi,
3388
+ rows,
3389
+ plainTextRows: rows.map((r) => stripAnsi(r)),
3390
+ width
3391
+ };
3392
+ }
3393
+ function createHistoryBuffer(options = 1e4) {
3394
+ const capacity = typeof options === "number" ? options : options.capacityRows ?? 1e4;
3395
+ const capacityBytes = typeof options === "number" ? null : normalizeCapacityBytes(options.capacityBytes);
3396
+ let items = [];
3397
+ let _totalRows = 0;
3398
+ let _totalBytes = 0;
3399
+ function evict() {
3400
+ while (items.length > 0 && (_totalRows > capacity || capacityBytes !== null && _totalBytes > capacityBytes)) {
3401
+ const removed = items.shift();
3402
+ _totalRows -= removed.rows.length;
3403
+ _totalBytes -= estimateHistoryItemBytes(removed);
3404
+ }
3405
+ }
3406
+ /** Walk items to find which item contains the given document row. */
3407
+ function resolveRow(row) {
3408
+ if (row < 0 || row >= _totalRows) return null;
3409
+ let cumulative = 0;
3410
+ for (let i = 0; i < items.length; i++) {
3411
+ const itemRows = items[i].rows.length;
3412
+ if (row < cumulative + itemRows) return {
3413
+ itemIndex: i,
3414
+ localRow: row - cumulative
3415
+ };
3416
+ cumulative += itemRows;
3417
+ }
3418
+ return null;
3419
+ }
3420
+ return {
3421
+ push(item) {
3422
+ items.push(item);
3423
+ _totalRows += item.rows.length;
3424
+ _totalBytes += estimateHistoryItemBytes(item);
3425
+ evict();
3426
+ },
3427
+ get totalRows() {
3428
+ return _totalRows;
3429
+ },
3430
+ get totalBytes() {
3431
+ return _totalBytes;
3432
+ },
3433
+ get itemCount() {
3434
+ return items.length;
3435
+ },
3436
+ get capacity() {
3437
+ return capacity;
3438
+ },
3439
+ get capacityBytes() {
3440
+ return capacityBytes;
3441
+ },
3442
+ getRows(startRow, count) {
3443
+ const result = [];
3444
+ for (let r = startRow; r < startRow + count; r++) {
3445
+ const resolved = resolveRow(r);
3446
+ if (resolved) result.push(items[resolved.itemIndex].rows[resolved.localRow]);
3447
+ else result.push("");
3448
+ }
3449
+ return result;
3450
+ },
3451
+ getPlainTextRows(startRow, count) {
3452
+ const result = [];
3453
+ for (let r = startRow; r < startRow + count; r++) {
3454
+ const resolved = resolveRow(r);
3455
+ if (resolved) result.push(items[resolved.itemIndex].plainTextRows[resolved.localRow]);
3456
+ else result.push("");
3457
+ }
3458
+ return result;
3459
+ },
3460
+ search(query) {
3461
+ if (!query) return [];
3462
+ const lowerQuery = query.toLowerCase();
3463
+ const matches = [];
3464
+ let rowOffset = 0;
3465
+ for (const item of items) {
3466
+ for (let r = 0; r < item.plainTextRows.length; r++) if (item.plainTextRows[r].toLowerCase().includes(lowerQuery)) matches.push(rowOffset + r);
3467
+ rowOffset += item.rows.length;
3468
+ }
3469
+ return matches;
3470
+ },
3471
+ getItemAtRow(row) {
3472
+ const resolved = resolveRow(row);
3473
+ if (!resolved) return null;
3474
+ return {
3475
+ item: items[resolved.itemIndex],
3476
+ localRow: resolved.localRow
3477
+ };
3478
+ },
3479
+ clear() {
3480
+ items = [];
3481
+ _totalRows = 0;
3482
+ _totalBytes = 0;
3483
+ }
3484
+ };
3485
+ }
3486
+ function normalizeCapacityBytes(value) {
3487
+ return Number.isFinite(value) && value !== void 0 && value > 0 ? Math.floor(value) : null;
3488
+ }
3489
+ const utf8Encoder = new TextEncoder();
3490
+ function estimateHistoryItemBytes(item) {
3491
+ return utf8Encoder.encode(item.ansi).byteLength;
3492
+ }
3493
+ //#endregion
3494
+ //#region packages/ag-term/src/viewport-compositor.ts
3495
+ function composeViewport(config) {
3496
+ const { history, viewportHeight, scrollOffset } = config;
3497
+ const totalHistory = history.totalRows;
3498
+ if (scrollOffset <= 0 || totalHistory === 0) return {
3499
+ overlayRows: [],
3500
+ overlayRowCount: 0,
3501
+ liveRowsVisible: viewportHeight,
3502
+ isScrolledUp: false,
3503
+ totalHeight: totalHistory + viewportHeight
3504
+ };
3505
+ const startRow = Math.max(0, totalHistory - Math.min(scrollOffset, totalHistory));
3506
+ const rowsToShow = Math.min(viewportHeight, totalHistory - startRow);
3507
+ return {
3508
+ overlayRows: history.getRows(startRow, rowsToShow),
3509
+ overlayRowCount: rowsToShow,
3510
+ liveRowsVisible: viewportHeight - rowsToShow,
3511
+ isScrolledUp: true,
3512
+ totalHeight: totalHistory + viewportHeight
3513
+ };
3514
+ }
3515
+ //#endregion
3516
+ //#region packages/ag-term/src/list-document.ts
3517
+ function createListDocument(history, getLiveItems) {
3518
+ function liveRowCount() {
3519
+ let total = 0;
3520
+ for (const block of getLiveItems()) total += block.rows.length;
3521
+ return total;
3522
+ }
3523
+ return {
3524
+ get totalRows() {
3525
+ return history.totalRows + liveRowCount();
3526
+ },
3527
+ get frozenRows() {
3528
+ return history.totalRows;
3529
+ },
3530
+ get liveRows() {
3531
+ return liveRowCount();
3532
+ },
3533
+ getRows(startRow, count) {
3534
+ const frozen = history.totalRows;
3535
+ const result = [];
3536
+ for (let r = startRow; r < startRow + count; r++) if (r < 0 || r >= this.totalRows) result.push("");
3537
+ else if (r < frozen) result.push(...history.getRows(r, 1));
3538
+ else {
3539
+ let liveRow = r - frozen;
3540
+ let found = false;
3541
+ for (const block of getLiveItems()) {
3542
+ if (liveRow < block.rows.length) {
3543
+ result.push(block.rows[liveRow]);
3544
+ found = true;
3545
+ break;
3546
+ }
3547
+ liveRow -= block.rows.length;
3548
+ }
3549
+ if (!found) result.push("");
3550
+ }
3551
+ return result;
3552
+ },
3553
+ getPlainTextRows(startRow, count) {
3554
+ const frozen = history.totalRows;
3555
+ const result = [];
3556
+ for (let r = startRow; r < startRow + count; r++) if (r < 0 || r >= this.totalRows) result.push("");
3557
+ else if (r < frozen) result.push(...history.getPlainTextRows(r, 1));
3558
+ else {
3559
+ let liveRow = r - frozen;
3560
+ let found = false;
3561
+ for (const block of getLiveItems()) {
3562
+ if (liveRow < block.plainTextRows.length) {
3563
+ result.push(block.plainTextRows[liveRow]);
3564
+ found = true;
3565
+ break;
3566
+ }
3567
+ liveRow -= block.plainTextRows.length;
3693
3568
  }
3694
- continue;
3569
+ if (!found) result.push("");
3695
3570
  }
3571
+ return result;
3572
+ },
3573
+ getSource(row) {
3574
+ const frozen = history.totalRows;
3575
+ if (row < 0 || row >= this.totalRows) return null;
3576
+ if (row < frozen) {
3577
+ const hit = history.getItemAtRow(row);
3578
+ if (!hit) return null;
3579
+ return {
3580
+ type: "frozen",
3581
+ itemKey: hit.item.key,
3582
+ localRow: hit.localRow
3583
+ };
3584
+ }
3585
+ const liveItems = getLiveItems();
3586
+ let liveRow = row - frozen;
3587
+ for (const block of liveItems) {
3588
+ if (liveRow < block.rows.length) return {
3589
+ type: "live",
3590
+ itemIndex: block.itemIndex,
3591
+ localRow: liveRow
3592
+ };
3593
+ liveRow -= block.rows.length;
3594
+ }
3595
+ return null;
3596
+ },
3597
+ search(query) {
3598
+ if (!query) return [];
3599
+ const lowerQuery = query.toLowerCase();
3600
+ const matches = [];
3601
+ const frozen = history.totalRows;
3602
+ const frozenRowMatches = history.search(query);
3603
+ for (const row of frozenRowMatches) {
3604
+ const line = history.getPlainTextRows(row, 1)[0].toLowerCase();
3605
+ let pos = line.indexOf(lowerQuery);
3606
+ while (pos !== -1) {
3607
+ matches.push({
3608
+ row,
3609
+ startCol: pos,
3610
+ endCol: pos + query.length
3611
+ });
3612
+ pos = line.indexOf(lowerQuery, pos + 1);
3613
+ }
3614
+ }
3615
+ let rowOffset = 0;
3616
+ for (const block of getLiveItems()) {
3617
+ for (let i = 0; i < block.plainTextRows.length; i++) {
3618
+ const line = block.plainTextRows[i].toLowerCase();
3619
+ let pos = line.indexOf(lowerQuery);
3620
+ while (pos !== -1) {
3621
+ matches.push({
3622
+ row: frozen + rowOffset + i,
3623
+ startCol: pos,
3624
+ endCol: pos + query.length
3625
+ });
3626
+ pos = line.indexOf(lowerQuery, pos + 1);
3627
+ }
3628
+ }
3629
+ rowOffset += block.plainTextRows.length;
3630
+ }
3631
+ return matches;
3696
3632
  }
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("");
3633
+ };
3714
3634
  }
3715
3635
  //#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;
3636
+ //#region packages/ag-term/src/text-surface.ts
3637
+ function createTextSurface(config) {
3638
+ const listeners = /* @__PURE__ */ new Set();
3639
+ function notify() {
3640
+ for (const fn of listeners) fn();
3641
+ }
3642
+ return {
3643
+ get id() {
3644
+ return config.id;
3645
+ },
3646
+ get document() {
3647
+ return config.document;
3648
+ },
3649
+ getText(startRow, startCol, endRow, endCol) {
3650
+ const rows = config.document.getRows(startRow, endRow - startRow + 1);
3651
+ const lines = [];
3652
+ for (let i = 0; i < rows.length; i++) {
3653
+ const plain = stripAnsi(rows[i]);
3654
+ const row = startRow + i;
3655
+ if (row === startRow && row === endRow) lines.push(plain.slice(startCol, endCol));
3656
+ else if (row === startRow) lines.push(plain.slice(startCol));
3657
+ else if (row === endRow) lines.push(plain.slice(0, endCol));
3658
+ else lines.push(plain);
3773
3659
  }
3774
- changes.push({
3775
- col,
3776
- row,
3777
- fg: newFg,
3778
- bg: newBg,
3779
- inverseAttr
3780
- });
3660
+ return lines.join("\n");
3661
+ },
3662
+ search(query) {
3663
+ return config.document.search(query);
3664
+ },
3665
+ hitTest(viewportRow, viewportCol) {
3666
+ const docRow = config.viewportToDocument(viewportRow);
3667
+ if (docRow < 0 || docRow >= config.document.totalRows) return null;
3668
+ return {
3669
+ row: docRow,
3670
+ col: viewportCol
3671
+ };
3672
+ },
3673
+ reveal(row) {
3674
+ config.onReveal(row);
3675
+ notify();
3676
+ },
3677
+ notifyContentChange() {
3678
+ notify();
3679
+ },
3680
+ subscribe(listener) {
3681
+ listeners.add(listener);
3682
+ return () => {
3683
+ listeners.delete(listener);
3684
+ };
3685
+ },
3686
+ get capabilities() {
3687
+ return config.capabilities;
3781
3688
  }
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;
3689
+ };
3792
3690
  }
3691
+ //#endregion
3692
+ //#region packages/ag-term/src/search-overlay.ts
3793
3693
  /**
3794
- * Apply selection style changes to a buffer.
3694
+ * Find all case-insensitive occurrences of `query` inside `text` and return
3695
+ * their character offsets. Empty query or empty text returns `[]`.
3795
3696
  *
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.
3697
+ * This is the canonical silvery search-match algorithm. The same logic runs
3698
+ * inside `ListView`'s registered Searchable (to find matches across items)
3699
+ * and is exposed to consumers that render multi-segment items — a LogRow
3700
+ * whose searchable text is a concatenation of field values, but whose visual
3701
+ * rendering splits those fields across separate Text nodes, needs per-segment
3702
+ * ranges to highlight without re-implementing the semantics.
3798
3703
  *
3799
- * @param buffer The rendered buffer to modify
3800
- * @param changes Cell changes from composeSelectionCells
3704
+ * Ranges are returned in ascending `start` order; overlapping matches are
3705
+ * not produced (`indexOf(..., start = last.start + 1)` advances past each
3706
+ * match start, not past its full length, preserving overlapping runs of
3707
+ * short queries — e.g. `"aa"` in `"aaaa"` yields `[0..2], [1..3], [2..4]`).
3708
+ *
3709
+ * Offsets are CHARACTER offsets into the input string. They are not column
3710
+ * offsets — multi-column wide glyphs are counted as one character here, the
3711
+ * same way the SearchProvider's match-row computation does.
3801
3712
  */
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
3713
+ function computeMatchRanges(text, query) {
3714
+ if (query === "" || text === "") return [];
3715
+ const lowerText = text.toLowerCase();
3716
+ const lowerQuery = query.toLowerCase();
3717
+ const ranges = [];
3718
+ let cursor = 0;
3719
+ while (cursor < lowerText.length) {
3720
+ const found = lowerText.indexOf(lowerQuery, cursor);
3721
+ if (found === -1) break;
3722
+ ranges.push({
3723
+ start: found,
3724
+ end: found + query.length
3816
3725
  });
3726
+ cursor = found + 1;
3817
3727
  }
3728
+ return ranges;
3729
+ }
3730
+ function createSearchState() {
3731
+ return {
3732
+ active: false,
3733
+ query: "",
3734
+ matches: [],
3735
+ currentMatch: -1,
3736
+ cursorPosition: 0
3737
+ };
3818
3738
  }
3819
3739
  /**
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
3740
+ * Update search state. searchFn is called when query changes to find matches.
3833
3741
  */
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
3742
+ function searchUpdate(action, state, searchFn) {
3743
+ switch (action.type) {
3744
+ case "open": return [{
3745
+ ...state,
3746
+ active: true,
3747
+ query: "",
3748
+ matches: [],
3749
+ currentMatch: -1,
3750
+ cursorPosition: 0
3751
+ }, [{ type: "render" }]];
3752
+ case "close": return [{
3753
+ ...state,
3754
+ active: false,
3755
+ query: "",
3756
+ cursorPosition: 0
3757
+ }, [{ type: "render" }]];
3758
+ case "input": {
3759
+ const query = state.query.slice(0, state.cursorPosition) + action.char + state.query.slice(state.cursorPosition);
3760
+ const cursorPosition = state.cursorPosition + 1;
3761
+ const matches = searchFn ? searchFn(query) : [];
3762
+ const currentMatch = matches.length > 0 ? 0 : -1;
3763
+ const effects = [{ type: "render" }];
3764
+ if (currentMatch >= 0) effects.push({
3765
+ type: "scrollTo",
3766
+ row: matches[0].row
3767
+ });
3768
+ return [{
3769
+ ...state,
3770
+ query,
3771
+ cursorPosition,
3772
+ matches,
3773
+ currentMatch
3774
+ }, effects];
3775
+ }
3776
+ case "backspace": {
3777
+ if (state.cursorPosition === 0) return [state, []];
3778
+ const query = state.query.slice(0, state.cursorPosition - 1) + state.query.slice(state.cursorPosition);
3779
+ const cursorPosition = state.cursorPosition - 1;
3780
+ const matches = searchFn ? searchFn(query) : [];
3781
+ const currentMatch = matches.length > 0 ? 0 : -1;
3782
+ const effects = [{ type: "render" }];
3783
+ if (currentMatch >= 0) effects.push({
3784
+ type: "scrollTo",
3785
+ row: matches[0].row
3786
+ });
3787
+ return [{
3788
+ ...state,
3789
+ query,
3790
+ cursorPosition,
3791
+ matches,
3792
+ currentMatch
3793
+ }, effects];
3794
+ }
3795
+ case "nextMatch": {
3796
+ if (state.matches.length === 0) return [state, []];
3797
+ const currentMatch = (state.currentMatch + 1) % state.matches.length;
3798
+ return [{
3799
+ ...state,
3800
+ currentMatch
3801
+ }, [{
3802
+ type: "scrollTo",
3803
+ row: state.matches[currentMatch].row
3804
+ }, { type: "render" }]];
3805
+ }
3806
+ case "prevMatch": {
3807
+ if (state.matches.length === 0) return [state, []];
3808
+ const currentMatch = (state.currentMatch - 1 + state.matches.length) % state.matches.length;
3809
+ return [{
3810
+ ...state,
3811
+ currentMatch
3812
+ }, [{
3813
+ type: "scrollTo",
3814
+ row: state.matches[currentMatch].row
3815
+ }, { type: "render" }]];
3816
+ }
3817
+ case "cursorLeft":
3818
+ if (state.cursorPosition === 0) return [state, []];
3819
+ return [{
3820
+ ...state,
3821
+ cursorPosition: state.cursorPosition - 1
3822
+ }, []];
3823
+ case "cursorRight":
3824
+ if (state.cursorPosition >= state.query.length) return [state, []];
3825
+ return [{
3826
+ ...state,
3827
+ cursorPosition: state.cursorPosition + 1
3828
+ }, []];
3829
+ case "cursorToStart":
3830
+ if (state.cursorPosition === 0) return [state, []];
3831
+ return [{
3832
+ ...state,
3833
+ cursorPosition: 0
3834
+ }, []];
3835
+ case "cursorToEnd":
3836
+ if (state.cursorPosition >= state.query.length) return [state, []];
3837
+ return [{
3838
+ ...state,
3839
+ cursorPosition: state.query.length
3840
+ }, []];
3841
+ case "deleteWordBack": {
3842
+ if (state.cursorPosition === 0) return [state, []];
3843
+ let pos = state.cursorPosition;
3844
+ while (pos > 0 && /\s/.test(state.query[pos - 1] ?? "")) pos--;
3845
+ while (pos > 0 && !/\s/.test(state.query[pos - 1] ?? "")) pos--;
3846
+ const query = state.query.slice(0, pos) + state.query.slice(state.cursorPosition);
3847
+ const cursorPosition = pos;
3848
+ const matches = searchFn ? searchFn(query) : [];
3849
+ const currentMatch = matches.length > 0 ? 0 : -1;
3850
+ const effects = [{ type: "render" }];
3851
+ if (currentMatch >= 0) effects.push({
3852
+ type: "scrollTo",
3853
+ row: matches[0].row
3854
+ });
3855
+ return [{
3856
+ ...state,
3857
+ query,
3858
+ cursorPosition,
3859
+ matches,
3860
+ currentMatch
3861
+ }, effects];
3862
+ }
3863
+ case "deleteToStart": {
3864
+ if (state.cursorPosition === 0) return [state, []];
3865
+ const query = state.query.slice(state.cursorPosition);
3866
+ const cursorPosition = 0;
3867
+ const matches = searchFn ? searchFn(query) : [];
3868
+ const currentMatch = matches.length > 0 ? 0 : -1;
3869
+ const effects = [{ type: "render" }];
3870
+ if (currentMatch >= 0) effects.push({
3871
+ type: "scrollTo",
3872
+ row: matches[0].row
3867
3873
  });
3874
+ return [{
3875
+ ...state,
3876
+ query,
3877
+ cursorPosition,
3878
+ matches,
3879
+ currentMatch
3880
+ }, effects];
3868
3881
  }
3869
3882
  }
3870
- return changes;
3871
3883
  }
3872
- //#endregion
3873
- //#region packages/ag-term/src/virtual-scrollback.ts
3874
3884
  /**
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).
3885
+ * Render the search bar as a plain (no ANSI) string padded to `cols` width.
3886
+ * Format: " / query [2/15]" or " / query [no matches]" — padded with spaces.
3879
3887
  *
3880
- * Implementation uses a circular buffer for O(1) push and bounded memory.
3888
+ * Caller decides how to display it wrap with `\x1b[7m...\x1b[27m` for the
3889
+ * legacy overlay path, or stamp into a buffer's cells with the inverse
3890
+ * attribute for the compose+apply path
3891
+ * (see `applySearchBarToPaintBuffer` in `runtime/renderer.ts`).
3881
3892
  */
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
- };
3893
+ function renderSearchBarPlain(state, cols) {
3894
+ const prefix = " / ";
3895
+ const matchInfo = state.matches.length > 0 ? ` [${state.currentMatch + 1}/${state.matches.length}]` : state.query ? " [no matches]" : "";
3896
+ return (prefix + state.query + matchInfo).padEnd(cols);
3924
3897
  }
3925
3898
  //#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 };
3899
+ export { DecMode as $, withMeasurer as A, createTerminalSelectionState as At, resolveNonTTYMode as B, resetScrollRegion as C, CursorProvider as Ct, supportsScrollRegions as D, createDragEvent as Dt, setScrollRegion as E, useCursor as Et, inspectTree as F, createInputEvents as G, Z_INDEX as H, isInspectorEnabled as I, resolveFromTerm as J, isTerm as K, countLines as L, disableInspector as M, terminalSelectionUpdate as Mt, enableInspector as N, createFocusManager as Nt, calcEdgeBasedScrollOffset as O, createDragState as Ot, inspectFrame as P, queryTextAreaSize as Q, createOutputTransformer as R, moveCursor as S, createCanvasAdapter as St, scrollUp as T, resetCursorState as Tt, generateHitRegionId as U, HitRegistry as V, resetHitRegionIdCounter as W, queryCellSize as X, resolveTermDef as Y, queryTextAreaPixels as Z, getTabOrder as _, findActiveCursorNode as _t, createTextSurface as a, queryTerminalVersion as at, splitPane as b, injectDOMStyles as bt, createHistoryBuffer as c, detectWidthConfigWithProbe as ct, applySelectionToBuffer as d, TERMTEST_SECTIONS as dt, queryMode as et, composeSearchHighlightCells as f, runTermtest as ft, getPaneIds as g, OSC133 as gt, findAdjacentPane as h, detectKittyWithProbe as ht, searchUpdate as i, querySecondaryDA as it, autoEnableInspector as j, extractText as jt, createPipeline as k, findDropTarget as kt, createHistoryItem as l, queryCursorFromStdio as lt, createLeaf as m, detectKittySupport as mt, createSearchState as n, queryDeviceAttributes as nt, createListDocument as o, queryTertiaryDA as ot, composeSelectionCells as p, detectKittyFromStdio as pt, isTermDef as q, renderSearchBarPlain as r, queryPrimaryDA as rt, composeViewport as s, applyWidthConfig as st, computeMatchRanges as t, queryModes as tt, createVirtualScrollback as u, queryCursorPosition as ut, removePane as v, DOMRenderBuffer as vt, scrollDown as w, createCursorStore as wt, swapPanes as x, CanvasRenderBuffer as xt, resizeSplit as y, createDOMAdapter as yt, isTTY as z };
3927
3900
 
3928
- //# sourceMappingURL=src-CbhWmUnF.mjs.map
3901
+ //# sourceMappingURL=src-DbqcSrhy.mjs.map