@deepseek-harness-tui/dsh-tui 0.8.7 → 0.9.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 (229) hide show
  1. package/README.md +29 -10
  2. package/bin/dsh-tui.js +1 -1
  3. package/lib/types/commands.d.ts.map +1 -1
  4. package/lib/types/commands.js +4 -2
  5. package/lib/types/components/ActivityPicker.d.ts +4 -1
  6. package/lib/types/components/ActivityPicker.d.ts.map +1 -1
  7. package/lib/types/components/ActivityPicker.js +2 -2
  8. package/lib/types/components/BtwPanel.d.ts.map +1 -1
  9. package/lib/types/components/BtwPanel.js +1 -1
  10. package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
  11. package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
  12. package/lib/types/components/Chat/SubagentMessage.js +11 -3
  13. package/lib/types/components/CommandSuggestions.d.ts +21 -4
  14. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  15. package/lib/types/components/CommandSuggestions.js +41 -14
  16. package/lib/types/components/EffortSlider.d.ts +4 -1
  17. package/lib/types/components/EffortSlider.d.ts.map +1 -1
  18. package/lib/types/components/EffortSlider.js +4 -2
  19. package/lib/types/components/ExtensionDialog.js +6 -2
  20. package/lib/types/components/FileSuggestions.d.ts +26 -5
  21. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  22. package/lib/types/components/FileSuggestions.js +53 -16
  23. package/lib/types/components/GoalTodoPanel.d.ts +26 -2
  24. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  25. package/lib/types/components/GoalTodoPanel.js +86 -17
  26. package/lib/types/components/HelpMenu.d.ts +7 -1
  27. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  28. package/lib/types/components/HelpMenu.js +12 -4
  29. package/lib/types/components/HistorySearchDialog.d.ts +4 -1
  30. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  31. package/lib/types/components/HistorySearchDialog.js +2 -2
  32. package/lib/types/components/LangPicker.d.ts +14 -0
  33. package/lib/types/components/LangPicker.d.ts.map +1 -0
  34. package/lib/types/components/LangPicker.js +17 -0
  35. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  36. package/lib/types/components/LoadedContextPanel.js +3 -1
  37. package/lib/types/components/LogoV2.d.ts +5 -1
  38. package/lib/types/components/LogoV2.d.ts.map +1 -1
  39. package/lib/types/components/LogoV2.js +12 -3
  40. package/lib/types/components/Markdown.d.ts.map +1 -1
  41. package/lib/types/components/Markdown.js +12 -3
  42. package/lib/types/components/MessageList.d.ts +43 -2
  43. package/lib/types/components/MessageList.d.ts.map +1 -1
  44. package/lib/types/components/MessageList.js +553 -41
  45. package/lib/types/components/ModelPicker.d.ts +4 -1
  46. package/lib/types/components/ModelPicker.d.ts.map +1 -1
  47. package/lib/types/components/ModelPicker.js +2 -2
  48. package/lib/types/components/PermissionsPicker.d.ts +21 -0
  49. package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
  50. package/lib/types/components/PermissionsPicker.js +27 -0
  51. package/lib/types/components/PlanPicker.d.ts +15 -0
  52. package/lib/types/components/PlanPicker.d.ts.map +1 -0
  53. package/lib/types/components/PlanPicker.js +18 -0
  54. package/lib/types/components/PresetPicker.d.ts +4 -1
  55. package/lib/types/components/PresetPicker.d.ts.map +1 -1
  56. package/lib/types/components/PresetPicker.js +2 -2
  57. package/lib/types/components/PromptInput.d.ts +4 -3
  58. package/lib/types/components/PromptInput.d.ts.map +1 -1
  59. package/lib/types/components/PromptInput.js +273 -50
  60. package/lib/types/components/RewindPicker.d.ts +12 -1
  61. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  62. package/lib/types/components/RewindPicker.js +6 -4
  63. package/lib/types/components/ScrollbarGutter.d.ts +28 -0
  64. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
  65. package/lib/types/components/ScrollbarGutter.js +68 -0
  66. package/lib/types/components/Select.d.ts +8 -1
  67. package/lib/types/components/Select.d.ts.map +1 -1
  68. package/lib/types/components/Select.js +4 -2
  69. package/lib/types/components/SkillsPicker.d.ts +4 -1
  70. package/lib/types/components/SkillsPicker.d.ts.map +1 -1
  71. package/lib/types/components/SkillsPicker.js +2 -2
  72. package/lib/types/components/SubagentCard.d.ts +2 -1
  73. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  74. package/lib/types/components/SubagentCard.js +6 -1
  75. package/lib/types/components/SubagentDashboard.d.ts +4 -0
  76. package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
  77. package/lib/types/components/SubagentDashboard.js +9 -1
  78. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  79. package/lib/types/components/SubagentDetailScene.js +5 -4
  80. package/lib/types/components/SuggestionCard.d.ts +63 -0
  81. package/lib/types/components/SuggestionCard.d.ts.map +1 -0
  82. package/lib/types/components/SuggestionCard.js +77 -0
  83. package/lib/types/components/ThemePicker.d.ts +4 -1
  84. package/lib/types/components/ThemePicker.d.ts.map +1 -1
  85. package/lib/types/components/ThemePicker.js +2 -2
  86. package/lib/types/components/ThinkingToggle.d.ts +4 -1
  87. package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
  88. package/lib/types/components/ThinkingToggle.js +2 -2
  89. package/lib/types/components/TimelineRail.d.ts +53 -0
  90. package/lib/types/components/TimelineRail.d.ts.map +1 -0
  91. package/lib/types/components/TimelineRail.js +190 -0
  92. package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
  93. package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
  94. package/lib/types/components/WorkspaceFlowPicker.js +4 -2
  95. package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
  96. package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
  97. package/lib/types/components/WorkspaceMenuPicker.js +19 -0
  98. package/lib/types/components/WorkspacePicker.d.ts +4 -1
  99. package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
  100. package/lib/types/components/WorkspacePicker.js +2 -2
  101. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  102. package/lib/types/components/approvals/ApprovalPanel.js +5 -1
  103. package/lib/types/components/design-system/ListItem.d.ts +8 -1
  104. package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
  105. package/lib/types/components/design-system/ListItem.js +8 -3
  106. package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
  107. package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
  108. package/lib/types/components/messages/AssistantTextMessage.js +6 -2
  109. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
  110. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  111. package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
  112. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
  113. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  114. package/lib/types/components/messages/AssistantToolUseMessage.js +4 -2
  115. package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
  116. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  117. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  118. package/lib/types/components/questions/AskUserQuestionPanel.js +34 -2
  119. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  120. package/lib/types/components/questions/PlanReviewPanel.js +13 -2
  121. package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
  122. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  123. package/lib/types/components/sessions/SessionListRow.js +4 -2
  124. package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
  125. package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
  126. package/lib/types/components/trajectory/HotspotView.js +8 -2
  127. package/lib/types/components/trajectory/Ledger.d.ts +9 -1
  128. package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
  129. package/lib/types/components/trajectory/Ledger.js +23 -5
  130. package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
  131. package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
  132. package/lib/types/components/trajectory/WaveBand.js +7 -2
  133. package/lib/types/dsh-adapter/channel.d.ts +27 -6
  134. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  135. package/lib/types/dsh-adapter/channel.js +432 -66
  136. package/lib/types/dsh-adapter/contract.d.ts +58 -18
  137. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  138. package/lib/types/dsh-adapter/contract.js +124 -36
  139. package/lib/types/dsh-adapter/index.d.ts +10 -2
  140. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  141. package/lib/types/dsh-adapter/index.js +10 -13
  142. package/lib/types/dsh-adapter/plugin.d.ts +0 -9
  143. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  144. package/lib/types/dsh-adapter/plugin.js +136 -6
  145. package/lib/types/i18n.d.ts +194 -16
  146. package/lib/types/i18n.d.ts.map +1 -1
  147. package/lib/types/i18n.js +73 -10
  148. package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
  149. package/lib/types/ink/components/AlternateScreen.js +13 -1
  150. package/lib/types/ink/components/App.d.ts +12 -1
  151. package/lib/types/ink/components/App.d.ts.map +1 -1
  152. package/lib/types/ink/components/App.js +101 -4
  153. package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
  154. package/lib/types/ink/components/ClockContext.js +2 -0
  155. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  156. package/lib/types/ink/components/ScrollBox.js +170 -91
  157. package/lib/types/ink/constants.d.ts +9 -0
  158. package/lib/types/ink/constants.d.ts.map +1 -1
  159. package/lib/types/ink/constants.js +9 -0
  160. package/lib/types/ink/events/click-event.d.ts +7 -16
  161. package/lib/types/ink/events/click-event.d.ts.map +1 -1
  162. package/lib/types/ink/events/click-event.js +16 -19
  163. package/lib/types/ink/events/event-handlers.d.ts +11 -1
  164. package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
  165. package/lib/types/ink/events/event-handlers.js +2 -0
  166. package/lib/types/ink/events/input-event.d.ts +9 -0
  167. package/lib/types/ink/events/input-event.d.ts.map +1 -1
  168. package/lib/types/ink/events/input-event.js +4 -0
  169. package/lib/types/ink/events/pointer-event.d.ts +61 -0
  170. package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
  171. package/lib/types/ink/events/pointer-event.js +61 -0
  172. package/lib/types/ink/events/wheel-event.d.ts +17 -0
  173. package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
  174. package/lib/types/ink/events/wheel-event.js +20 -0
  175. package/lib/types/ink/geometry-trace.d.ts +51 -0
  176. package/lib/types/ink/geometry-trace.d.ts.map +1 -0
  177. package/lib/types/ink/geometry-trace.js +83 -0
  178. package/lib/types/ink/hit-test.d.ts +56 -1
  179. package/lib/types/ink/hit-test.d.ts.map +1 -1
  180. package/lib/types/ink/hit-test.js +146 -8
  181. package/lib/types/ink/ink.d.ts +58 -1
  182. package/lib/types/ink/ink.d.ts.map +1 -1
  183. package/lib/types/ink/ink.js +268 -52
  184. package/lib/types/ink/node-cache.d.ts +5 -0
  185. package/lib/types/ink/node-cache.d.ts.map +1 -1
  186. package/lib/types/ink/parse-keypress.d.ts +8 -0
  187. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  188. package/lib/types/ink/parse-keypress.js +101 -12
  189. package/lib/types/ink/render-node-to-output.d.ts +16 -3
  190. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  191. package/lib/types/ink/render-node-to-output.js +144 -25
  192. package/lib/types/ink/selection.d.ts +29 -4
  193. package/lib/types/ink/selection.d.ts.map +1 -1
  194. package/lib/types/ink/selection.js +41 -5
  195. package/lib/types/ink/timeline-rail.d.ts +138 -0
  196. package/lib/types/ink/timeline-rail.d.ts.map +1 -0
  197. package/lib/types/ink/timeline-rail.js +179 -0
  198. package/lib/types/ink/wrap-text.d.ts.map +1 -1
  199. package/lib/types/ink/wrap-text.js +47 -5
  200. package/lib/types/screens/Chat.d.ts.map +1 -1
  201. package/lib/types/screens/Chat.js +819 -337
  202. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  203. package/lib/types/screens/SessionBrowser.js +56 -29
  204. package/lib/types/screens/Settings.d.ts.map +1 -1
  205. package/lib/types/screens/Settings.js +64 -22
  206. package/lib/types/screens/StatusLine.d.ts +2 -1
  207. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  208. package/lib/types/screens/StatusLine.js +17 -1
  209. package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
  210. package/lib/types/screens/TrajectoryScene.js +81 -14
  211. package/lib/types/screens/chatOverlay.d.ts +221 -0
  212. package/lib/types/screens/chatOverlay.d.ts.map +1 -0
  213. package/lib/types/screens/chatOverlay.js +114 -0
  214. package/lib/types/tips.d.ts.map +1 -1
  215. package/lib/types/tips.js +64 -10
  216. package/lib/types/tuiDisplayPrefs.d.ts +15 -0
  217. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  218. package/lib/types/tuiDisplayPrefs.js +9 -0
  219. package/lib/types/utils/debug.d.ts +10 -0
  220. package/lib/types/utils/debug.d.ts.map +1 -1
  221. package/lib/types/utils/debug.js +24 -0
  222. package/lib/types/utils/fileSuggestions.d.ts +19 -0
  223. package/lib/types/utils/fileSuggestions.d.ts.map +1 -0
  224. package/lib/types/utils/fileSuggestions.js +40 -0
  225. package/lib/types/utils/mentions.d.ts.map +1 -1
  226. package/lib/types/utils/mentions.js +3 -1
  227. package/lib/types/utils/paths.d.ts.map +1 -1
  228. package/lib/types/utils/paths.js +1 -0
  229. package/package.json +69 -66
@@ -610,7 +610,12 @@ export function parseMultipleKeypresses(prevState, input = '') {
610
610
  keys.push({ kind: 'response', sequence: token.value, response });
611
611
  }
612
612
  else {
613
- const mouse = parseMouseEvent(token.value);
613
+ // SGR first (1006); X10 (legacy 1000/1002 without SGR) as the
614
+ // compatibility fallback for clicks/drags. Wheel falls through
615
+ // to parseKeypress, which turns it into a wheel key WITH the
616
+ // pointer coordinates for position-based routing.
617
+ const mouse = parseMouseEvent(token.value) ??
618
+ parseX10MouseEvent(token.value);
614
619
  if (mouse) {
615
620
  keys.push(mouse);
616
621
  }
@@ -819,6 +824,8 @@ export const nonAlphanumericKeys = [
819
824
  'backspace',
820
825
  'wheelup',
821
826
  'wheeldown',
827
+ 'wheelleft',
828
+ 'wheelright',
822
829
  'mouse',
823
830
  ];
824
831
  const isShiftKey = (code) => {
@@ -955,6 +962,39 @@ function parseMouseEvent(s) {
955
962
  sequence: s,
956
963
  };
957
964
  }
965
+ /**
966
+ * Parse an X10 mouse sequence (CSI M + Cb/Cx/Cy raw bytes, each +32) into
967
+ * a ParsedMouse, or null for anything else. Serves terminals that honor
968
+ * DECSET 1000/1002 but ignore 1006 (SGR): clicks and button-drags become
969
+ * real ParsedMouse events so text selection works there too.
970
+ *
971
+ * Protocol limits (documented, accepted): X10 has no release events — a
972
+ * started selection finishes via App's lost-release recovery (next press /
973
+ * focus-out / no-button motion), and click dispatch (release-based) stays
974
+ * SGR-only. No-button motion (hover) is unrepresentable without the drag
975
+ * bit, so hover stays SGR-only. Wheel returns null (parseKeypress's wheel
976
+ * branch turns it into a key with coordinates).
977
+ */
978
+ function parseX10MouseEvent(s) {
979
+ if (s.length !== 6 || !s.startsWith('\x1b[M'))
980
+ return null;
981
+ const button = s.charCodeAt(3) - 32;
982
+ // Wheel (bit 6) → key path; no-button without drag bit → unsupported
983
+ // hover encoding, swallow upstream.
984
+ if ((button & 0x40) !== 0)
985
+ return null;
986
+ if ((button & 0x03) === 3 && (button & 0x20) === 0)
987
+ return null;
988
+ return {
989
+ kind: 'mouse',
990
+ button,
991
+ action: 'press',
992
+ // X10 coords are 1-indexed like SGR (charCode - 32).
993
+ col: s.charCodeAt(4) - 32,
994
+ row: s.charCodeAt(5) - 32,
995
+ sequence: s,
996
+ };
997
+ }
958
998
  function parseKeypress(s = '') {
959
999
  let parts;
960
1000
  const key = {
@@ -1020,26 +1060,53 @@ function parseKeypress(s = '') {
1020
1060
  // + direction while ignoring modifier bits (Shift=0x04, Meta=0x08,
1021
1061
  // Ctrl=0x10) — modified wheel events (e.g. Ctrl+scroll, button=80)
1022
1062
  // should still be recognized as wheelup/wheeldown.
1063
+ //
1064
+ // The SGR sequence carries the pointer position (CSI < btn;col;row M) —
1065
+ // preserved on the ParsedKey (mouseCol/mouseRow, 0-indexed) so wheel
1066
+ // routing can hit-test the ScrollBox under the pointer instead of
1067
+ // scrolling a hardcoded target. Buttons 66/67 are horizontal wheel
1068
+ // (wheelleft/wheelright); kept as keys with coords for future consumers.
1023
1069
  if ((match = SGR_MOUSE_RE.exec(s))) {
1024
1070
  const button = parseInt(match[1], 10);
1025
- if ((button & 0x43) === 0x40)
1026
- return createNavKey(s, 'wheelup', false);
1027
- if ((button & 0x43) === 0x41)
1028
- return createNavKey(s, 'wheeldown', false);
1071
+ const col = parseInt(match[2], 10) - 1;
1072
+ const row = parseInt(match[3], 10) - 1;
1073
+ const dir = button & 0x43;
1074
+ if (dir === 0x40)
1075
+ return createWheelKey(s, 'wheelup', col, row, button);
1076
+ if (dir === 0x41)
1077
+ return createWheelKey(s, 'wheeldown', col, row, button);
1078
+ if (dir === 0x42)
1079
+ return createWheelKey(s, 'wheelleft', col, row, button);
1080
+ if (dir === 0x43)
1081
+ return createWheelKey(s, 'wheelright', col, row, button);
1029
1082
  // Shouldn't reach here (parseMouseEvent catches non-wheel) but be safe
1030
1083
  return createNavKey(s, 'mouse', false);
1031
1084
  }
1032
1085
  // X10 mouse: CSI M + 3 raw bytes (Cb+32, Cx+32, Cy+32). Terminals that
1033
1086
  // ignore DECSET 1006 (SGR) but honor 1000/1002 emit this legacy encoding.
1034
- // Button bits match SGR: 0x40 = wheel, low bit = direction. Non-wheel
1035
- // X10 events (clicks/drags) are swallowed here — we only enable mouse
1036
- // tracking in alt-screen and only need wheel for ScrollBox.
1087
+ // Button bits match SGR: 0x40 = wheel, low bit = direction, 0x20 = drag.
1088
+ // Wheel events become wheel keys (with coordinates, like SGR); clicks and
1089
+ // drags become ParsedMouse so X10-only terminals get selection support.
1090
+ // X10 cannot report button release — click dispatch (release-based) stays
1091
+ // SGR-only; a started selection finishes via the lost-release recovery
1092
+ // paths (next press / focus-out / no-button motion).
1037
1093
  if (s.length === 6 && s.startsWith('\x1b[M')) {
1038
1094
  const button = s.charCodeAt(3) - 32;
1039
- if ((button & 0x43) === 0x40)
1040
- return createNavKey(s, 'wheelup', false);
1041
- if ((button & 0x43) === 0x41)
1042
- return createNavKey(s, 'wheeldown', false);
1095
+ const col = s.charCodeAt(4) - 32 - 1;
1096
+ const row = s.charCodeAt(5) - 32 - 1;
1097
+ const dir = button & 0x43;
1098
+ if (dir === 0x40)
1099
+ return createWheelKey(s, 'wheelup', col, row, button);
1100
+ if (dir === 0x41)
1101
+ return createWheelKey(s, 'wheeldown', col, row, button);
1102
+ if (dir === 0x42)
1103
+ return createWheelKey(s, 'wheelleft', col, row, button);
1104
+ if (dir === 0x43)
1105
+ return createWheelKey(s, 'wheelright', col, row, button);
1106
+ // Non-wheel X10 clicks/drags are intercepted by parseX10MouseEvent in
1107
+ // parseMultipleKeypresses before this function runs; reaching here means
1108
+ // a flush path bypassed it (no-button motion without the drag bit — X10
1109
+ // hover, unsupported). Swallow as before.
1043
1110
  return createNavKey(s, 'mouse', false);
1044
1111
  }
1045
1112
  if (s === '\r') {
@@ -1157,3 +1224,25 @@ function createNavKey(s, name, ctrl) {
1157
1224
  isPasted: false,
1158
1225
  };
1159
1226
  }
1227
+ /**
1228
+ * Build a wheel ParsedKey preserving the pointer position and modifier
1229
+ * bits from the protocol's button byte (Shift=0x04, Meta/Alt=0x08,
1230
+ * Ctrl=0x10 — same bit layout decodeModifier documents).
1231
+ */
1232
+ function createWheelKey(s, name, col, row, button) {
1233
+ return {
1234
+ kind: 'key',
1235
+ name,
1236
+ ctrl: (button & 0x10) !== 0,
1237
+ meta: (button & 0x08) !== 0,
1238
+ shift: (button & 0x04) !== 0,
1239
+ option: false,
1240
+ super: false,
1241
+ fn: false,
1242
+ sequence: s,
1243
+ raw: s,
1244
+ isPasted: false,
1245
+ mouseCol: col,
1246
+ mouseRow: row,
1247
+ };
1248
+ }
@@ -1,4 +1,5 @@
1
1
  import type { DOMElement } from './dom.js';
2
+ import type { Rectangle } from './layout/geometry.js';
2
3
  import type Output from './output.js';
3
4
  import type { Screen } from './screen.js';
4
5
  import { type StyledSegment } from './squash-text-nodes.js';
@@ -22,6 +23,15 @@ export type ScrollHint = {
22
23
  bottom: number;
23
24
  delta: number;
24
25
  };
26
+ export type AbsoluteHitEntry = {
27
+ node: DOMElement;
28
+ rect: Rectangle;
29
+ };
30
+ /**
31
+ * The current frame's absolute-positioned nodes in paint order.
32
+ * @returns read-only list; reverse-iterate for topmost-first hit-testing.
33
+ */
34
+ export declare function getAbsoluteHitList(): readonly AbsoluteHitEntry[];
25
35
  /** Reset the scroll hint for the next frame and rotate the absolute-rect buffers. */
26
36
  export declare function resetScrollHint(): void;
27
37
  /**
@@ -47,10 +57,13 @@ export type FollowScroll = {
47
57
  viewportBottom: number;
48
58
  };
49
59
  /**
50
- * Read and clear the follow-scroll event recorded this frame.
51
- * @returns the follow-scroll delta and viewport bounds, or null.
60
+ * Read and clear the follow-scroll events recorded this frame. At most one
61
+ * per ScrollBox (a box that follows AND drains reports only the follow —
62
+ * the follow branch clears pendingScrollDelta before the drain runs);
63
+ * several boxes may each report when they scroll in the same frame.
64
+ * @returns this frame's follow-scroll events; empty when none.
52
65
  */
53
- export declare function consumeFollowScroll(): FollowScroll | null;
66
+ export declare function consumeFollowScroll(): FollowScroll[];
54
67
  /**
55
68
  * Build a mapping from each character position in the plain text to its segment index.
56
69
  * Returns an array where charToSegment[i] is the segment index for character i.
@@ -1 +1 @@
1
- {"version":3,"file":"render-node-to-output.d.ts","sourceRoot":"","sources":["../../../src/ink/render-node-to-output.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAK1C,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAqBxC,6CAA6C;AAC7C,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAOD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAWvE,qFAAqF;AACrF,wBAAgB,eAAe,IAAI,IAAI,CAItC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AASD,8DAA8D;AAC9D,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,GAAG,IAAI,CAEtD;AAUD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAGD;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,YAAY,GAAG,IAAI,CAIzD;AAoFD;;;;;GAKG;AACH,iBAAS,qBAAqB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,MAAM,EAAE,CASlE;AAED;;;;;;;;;;;GAWG;AACH,iBAAS,wBAAwB,CAC/B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,aAAa,EAAE,EACzB,aAAa,EAAE,MAAM,EAAE,EACvB,aAAa,EAAE,MAAM,EACrB,WAAW,GAAE,OAAe,GAC3B,MAAM,CA0GR;AA+DD;;;;;;;;;GASG;AACH,iBAAS,kBAAkB,CACzB,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IAM9B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,wBAAwB,CAAC,EAAE,KAAK,CAAA;CACjC,GACA,IAAI,CA62BN;AAyOD,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,CAAA;AAE1D,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"render-node-to-output.d.ts","sourceRoot":"","sources":["../../../src/ink/render-node-to-output.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAGrD,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAqBxC,6CAA6C;AAC7C,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAOD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAkBvE,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAA;AAGpE;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,SAAS,gBAAgB,EAAE,CAEhE;AAED,qFAAqF;AACrF,wBAAgB,eAAe,IAAI,IAAI,CAKtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AASD,8DAA8D;AAC9D,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,GAAG,IAAI,CAEtD;AAgBD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAGD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,IAAI,YAAY,EAAE,CAIpD;AAoFD;;;;;GAKG;AACH,iBAAS,qBAAqB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,MAAM,EAAE,CASlE;AAED;;;;;;;;;;;GAWG;AACH,iBAAS,wBAAwB,CAC/B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,aAAa,EAAE,EACzB,aAAa,EAAE,MAAM,EAAE,EACvB,aAAa,EAAE,MAAM,EACrB,WAAW,GAAE,OAAe,GAC3B,MAAM,CA0GR;AA+DD;;;;;;;;;GASG;AACH,iBAAS,kBAAkB,CACzB,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IAM9B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,wBAAwB,CAAC,EAAE,KAAK,CAAA;CACjC,GACA,IAAI,CAq9BN;AA0OD,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,CAAA;AAE1D,eAAe,kBAAkB,CAAA"}
@@ -1,5 +1,6 @@
1
1
  import indentString from 'indent-string';
2
2
  import { applyTextStyles } from './colorize.js';
3
+ import { GEOMETRY_TRACE_ENABLED, noteScrollGeometry } from './geometry-trace.js';
3
4
  import getMaxWidth from './get-max-width.js';
4
5
  import { LayoutDisplay, LayoutEdge } from './layout/node.js';
5
6
  import { nodeCache, pendingClears } from './node-cache.js';
@@ -41,11 +42,20 @@ let scrollHint = null;
41
42
  // still have the rect.
42
43
  let absoluteRectsPrev = [];
43
44
  let absoluteRectsCur = [];
45
+ let absoluteHitList = [];
46
+ /**
47
+ * The current frame's absolute-positioned nodes in paint order.
48
+ * @returns read-only list; reverse-iterate for topmost-first hit-testing.
49
+ */
50
+ export function getAbsoluteHitList() {
51
+ return absoluteHitList;
52
+ }
44
53
  /** Reset the scroll hint for the next frame and rotate the absolute-rect buffers. */
45
54
  export function resetScrollHint() {
46
55
  scrollHint = null;
47
56
  absoluteRectsPrev = absoluteRectsCur;
48
57
  absoluteRectsCur = [];
58
+ absoluteHitList = [];
49
59
  }
50
60
  /**
51
61
  * The scroll hint captured this frame, or null.
@@ -71,14 +81,17 @@ export function resetScrollDrainNode() {
71
81
  export function getScrollDrainNode() {
72
82
  return scrollDrainNode;
73
83
  }
74
- let followScroll = null;
84
+ let followScrolls = [];
75
85
  /**
76
- * Read and clear the follow-scroll event recorded this frame.
77
- * @returns the follow-scroll delta and viewport bounds, or null.
86
+ * Read and clear the follow-scroll events recorded this frame. At most one
87
+ * per ScrollBox (a box that follows AND drains reports only the follow —
88
+ * the follow branch clears pendingScrollDelta before the drain runs);
89
+ * several boxes may each report when they scroll in the same frame.
90
+ * @returns this frame's follow-scroll events; empty when none.
78
91
  */
79
92
  export function consumeFollowScroll() {
80
- const f = followScroll;
81
- followScroll = null;
93
+ const f = followScrolls;
94
+ followScrolls = [];
82
95
  return f;
83
96
  }
84
97
  // ── Native terminal drain (iTerm2/Ghostty/etc. — proportional events) ──
@@ -356,18 +369,34 @@ function renderNodeToOutput(node, output, options) {
356
369
  let y = offsetY + yogaTop;
357
370
  const width = yogaNode.getComputedWidth();
358
371
  const height = yogaNode.getComputedHeight();
359
- // Absolute-positioned overlays (e.g. autocomplete menus with bottom='100%')
360
- // can compute negative screen y when they extend above the viewport. Without
361
- // clamping, setCellAt drops cells at y<0, clipping the TOP of the content
362
- // (best matches in an autocomplete). By clamping to 0, we shift the element
363
- // down so the top rows are visible and the bottom overflows below — the
364
- // opaque prop ensures it paints over whatever is underneath.
365
- if (y < 0 && node.style.position === 'absolute') {
366
- y = 0;
367
- }
372
+ // Absolute-positioned overlays anchored above their parent
373
+ // (bottom='100%') compute negative screen y when their content is
374
+ // taller than the space above the anchor. Do NOT clamp y to 0 here:
375
+ // shifting the node down breaks the bottom-anchoring contract — the
376
+ // panel's BOTTOM would overflow past the anchor and paint over the
377
+ // composer/status rows below it (small-terminal overlay reports).
378
+ // Instead let the y<0 rows clip naturally: setCellAt drops cells above
379
+ // row 0 and blitRegion clamps in screen space, so the panel keeps its
380
+ // bottom pinned at the anchor and only loses its topmost rows — the
381
+ // same semantics as CSS clipping above the containing block. Producers
382
+ // prevent the case entirely by maxHeight-ing the overlay to the actual
383
+ // space above the anchor (OverlayAbove), so clipping is the last
384
+ // resort, not the layout.
368
385
  // Check if we can skip this subtree (clean node with unchanged layout).
369
386
  // Blit cells from previous screen instead of re-rendering.
370
387
  const cached = nodeCache.get(node);
388
+ // The node's EFFECTIVE background = its own + the inherited one. When it
389
+ // changed since the last frame (hover on/off toggles a row's
390
+ // backgroundColor, a parent's bg swap changes every child's inherited
391
+ // value), prevScreen still holds the OLD background — a clean child
392
+ // blitting it would resurrect the stale color (stuck hover highlights:
393
+ // the row's fill is skipped once the bg is removed, its clean children
394
+ // then blit the previous bg'd cells, the frame equals prevScreen, the
395
+ // diff finds nothing, and the highlight never clears). Compare against
396
+ // the value recorded at the previous render and refuse the blit when it
397
+ // moved.
398
+ const effectiveBg = node.style.backgroundColor ?? inheritedBackgroundColor;
399
+ const bgChanged = cached?.bg !== effectiveBg;
371
400
  if (!node.dirty &&
372
401
  !skipSelfBlit &&
373
402
  node.pendingScrollDelta === undefined &&
@@ -376,6 +405,7 @@ function renderNodeToOutput(node, output, options) {
376
405
  cached.y === y &&
377
406
  cached.width === width &&
378
407
  cached.height === height &&
408
+ !bgChanged &&
379
409
  prevScreen) {
380
410
  const fx = Math.floor(x);
381
411
  const fy = Math.floor(y);
@@ -384,6 +414,7 @@ function renderNodeToOutput(node, output, options) {
384
414
  output.blit(prevScreen, fx, fy, fw, fh);
385
415
  if (node.style.position === 'absolute') {
386
416
  absoluteRectsCur.push(cached);
417
+ absoluteHitList.push({ node, rect: cached });
387
418
  }
388
419
  // Absolute descendants can paint outside this node's layout bounds
389
420
  // (e.g. a slash menu with position='absolute' bottom='100%' floats
@@ -590,7 +621,29 @@ function renderNodeToOutput(node, output, options) {
590
621
  // within the viewport (equal to the scroll container's
591
622
  // paddingTop), and innerHeight already subtracts padding, so
592
623
  // including it double-counts padding and inflates maxScroll.
593
- const scrollHeight = contentYoga?.getComputedHeight() ?? 0;
624
+ let scrollHeight = contentYoga?.getComputedHeight() ?? 0;
625
+ // Defensive extent floor: the wrapper's Yoga height can land ONE LINE
626
+ // short of its children's laid-out extent (the engine's flex-basis
627
+ // measure cache and the child's final subtree layout can disagree by
628
+ // a row — measured on resume of a long session: wrapper 286 while the
629
+ // last row's bottom sat at 287, stable across forced re-layouts). A
630
+ // short scrollHeight clamps maxScroll below the real bottom, so the
631
+ // sticky pin parks the viewport a line up and the tail line culls
632
+ // against the viewport edge — permanently invisible AND unreachable
633
+ // (no scroll position ever shows it; the user loses the newest
634
+ // message). The children's real extent is the authoritative floor.
635
+ // O(direct children) — the windowed list keeps this tiny, and the
636
+ // painter walks the same nodes right below.
637
+ if (content !== undefined) {
638
+ for (const child of content.childNodes) {
639
+ const childYoga = child.yogaNode;
640
+ if (childYoga === undefined)
641
+ continue;
642
+ const bottom = childYoga.getComputedTop() + childYoga.getComputedHeight();
643
+ if (bottom > scrollHeight)
644
+ scrollHeight = bottom;
645
+ }
646
+ }
594
647
  // Capture previous scroll bounds BEFORE overwriting — the at-bottom
595
648
  // follow check compares against last frame's max.
596
649
  const prevScrollHeight = node.scrollHeight ?? scrollHeight;
@@ -684,11 +737,11 @@ function renderNodeToOutput(node, output, options) {
684
737
  const followDelta = (node.scrollTop ?? 0) - scrollTopBeforeFollow;
685
738
  if (followDelta > 0) {
686
739
  const vpTop = node.scrollViewportTop ?? 0;
687
- followScroll = {
740
+ followScrolls.push({
688
741
  delta: followDelta,
689
742
  viewportTop: vpTop,
690
743
  viewportBottom: vpTop + innerHeight - 1,
691
- };
744
+ });
692
745
  }
693
746
  // Drain pendingScrollDelta. Native terminals (proportional burst
694
747
  // events) use proportional drain; xterm.js (VS Code, sparse events +
@@ -732,7 +785,17 @@ function renderNodeToOutput(node, output, options) {
732
785
  // Keep the pre-frame position on a shrink frame (measurement
733
786
  // artifact) instead of clamping to the shrunken maxScroll — the
734
787
  // clamp would persist the yank even after content grows back.
735
- let scrollTop = shrunk ? cur : Math.max(0, Math.min(cur, maxScroll));
788
+ // Exception: a STICKY view must stay pinned to the bottom. A width
789
+ // change clears MessageList's row-height cache, collapsing the
790
+ // estimated scrollHeight; freezing the pre-shrink scrollTop then
791
+ // parks it PAST the whole shrunken content — every child culls and
792
+ // the frame paints only the bottom chrome. With the resize erase
793
+ // (needsEraseBeforePaint) that lands as a pure blank screen with
794
+ // just the input box, and an idle app produces no follow-up frame
795
+ // to heal it (user-reported full-screen loss on pane-width jitter —
796
+ // issue #421; repro-resize-blank). Clamping a sticky view to the shrunken
797
+ // maxScroll is exactly its contract — show the bottom.
798
+ let scrollTop = shrunk && !sticky ? cur : Math.max(0, Math.min(cur, maxScroll));
736
799
  // Virtual-scroll clamp: if scrollTop raced past the currently-mounted
737
800
  // range (burst PageUp before React re-renders), render at the EDGE of
738
801
  // the mounted children instead of blank spacer. Do NOT write back to
@@ -741,7 +804,15 @@ function renderNodeToOutput(node, output, options) {
741
804
  // the right range. Not scheduling scrollDrainNode here keeps the
742
805
  // clamp passive — React's commit → resetAfterCommit → onRender will
743
806
  // paint again with fresh bounds.
744
- const clamped = Math.max(cMin ?? -Infinity, Math.min(scrollTop, cMax ?? Infinity));
807
+ // Cap both bounds at maxScroll: the clamp bounds come from
808
+ // MessageList's height ESTIMATES while maxScroll comes from the
809
+ // frame's actual Yoga height. A width change resets the estimates
810
+ // (row-height cache clear), and a stale bound can point PAST the
811
+ // shrunken content bottom — the painted viewport then sits entirely
812
+ // below the content and every child culls to blank (the sticky
813
+ // full-screen loss — issue #421; repro-resize-blank). Nothing renderable exists
814
+ // below maxScroll regardless of what the estimates claim.
815
+ const clamped = Math.max(Math.min(cMin ?? -Infinity, maxScroll), Math.min(scrollTop, Math.min(cMax ?? Infinity, maxScroll)));
745
816
  node.scrollTop = scrollTop;
746
817
  // Clamp hitting top/bottom consumes any remainder. Set drainPending
747
818
  // only after clamp so a wasted no-op frame isn't scheduled.
@@ -749,6 +820,51 @@ function renderNodeToOutput(node, output, options) {
749
820
  node.pendingScrollDelta = undefined;
750
821
  if (node.pendingScrollDelta !== undefined)
751
822
  scrollDrainNode = node;
823
+ // Geometry forensics (#421/#433): everything that decides where this
824
+ // viewport painted — captured AFTER all clamps so the trace shows the
825
+ // final renderScrollTop, not the requested one.
826
+ if (GEOMETRY_TRACE_ENABLED) {
827
+ noteScrollGeometry({
828
+ sticky,
829
+ shrunk,
830
+ grew,
831
+ atBottom,
832
+ scrollTopBeforeFollow,
833
+ cur,
834
+ scrollTop,
835
+ renderScrollTop: clamped,
836
+ scrollHeight,
837
+ prevScrollHeight,
838
+ innerHeight,
839
+ maxScroll,
840
+ prevMaxScroll,
841
+ clampMin: cMin ?? null,
842
+ clampMax: cMax ?? null,
843
+ });
844
+ }
845
+ // Wheel-drain selection translate (#438): the drain moved content
846
+ // by (scrollTop - scrollTopBeforeFollow) rows this frame, minus
847
+ // what at-bottom follow already reported above (followDelta is 0
848
+ // unless the follow branch fired — and when it did, it cleared
849
+ // pendingScrollDelta, so the drain contributed nothing). Record
850
+ // the remainder as a follow-scroll event with a SIGNED delta so
851
+ // ink.tsx re-anchors any active selection to the text. Without
852
+ // this, wheel scrolling leaves the highlight pinned to screen
853
+ // rows and copy-on-select grabs whatever scrolled under it.
854
+ // scrollTo/scrollToElement jumps never contribute: they write
855
+ // scrollTop before scrollTopBeforeFollow is captured. Multi-frame
856
+ // drains record per-frame portions; the selection's virtual-row
857
+ // tracking accumulates the clamp overshoot across frames. Multiple
858
+ // boxes may each record; ink.tsx attributes by viewport containment.
859
+ const wheelDelta = scrollTop - scrollTopBeforeFollow - followDelta;
860
+ if (wheelDelta !== 0) {
861
+ const wheelVpTop = node.scrollViewportTop ?? 0;
862
+ followScrolls.push({
863
+ delta: wheelDelta,
864
+ viewportTop: wheelVpTop,
865
+ viewportBottom: wheelVpTop + innerHeight - 1
866
+ });
867
+ }
752
868
  // A manual scroll that lands exactly on the bottom re-pins sticky
753
869
  // IMMEDIATELY on this frame — the follow-block restore above only
754
870
  // fires when a later frame happens, but an idle stream (turn done,
@@ -1066,11 +1182,12 @@ function renderNodeToOutput(node, output, options) {
1066
1182
  // backgroundColor and opaque both disable child blit: the fill
1067
1183
  // overwrites the entire interior each render, so any child whose
1068
1184
  // layout position shifted would blit stale cells from prevScreen
1069
- // on top of the fresh fill. Previously opaque kept blit enabled
1070
- // on the assumption that plain-space fill + unchanged children =
1071
- // valid composite, but children CAN reposition (ScrollBox remeasure
1072
- // on re-render → /permissions body blanked on Down arrow, #25436).
1073
- ownBackgroundColor || node.style.opaque ? undefined : prevScreen, boxBackgroundColor);
1185
+ // on top of the fresh fill. bgChanged (effective background moved,
1186
+ // e.g. a hover highlight removed) must disable child blit for the
1187
+ // same reason: the old fill lives in prevScreen and the children's
1188
+ // blits would resurrect it — the frame then equals prevScreen and
1189
+ // the diff never clears the stale highlight.
1190
+ ownBackgroundColor || node.style.opaque || bgChanged ? undefined : prevScreen, boxBackgroundColor);
1074
1191
  }
1075
1192
  if (needsClip) {
1076
1193
  output.unclip();
@@ -1084,10 +1201,11 @@ function renderNodeToOutput(node, output, options) {
1084
1201
  renderChildren(node, output, x, y, hasRemovedChild, prevScreen, inheritedBackgroundColor);
1085
1202
  }
1086
1203
  // Cache layout bounds for dirty tracking
1087
- const rect = { x, y, width, height, top: yogaTop };
1204
+ const rect = { x, y, width, height, top: yogaTop, bg: effectiveBg };
1088
1205
  nodeCache.set(node, rect);
1089
1206
  if (node.style.position === 'absolute') {
1090
1207
  absoluteRectsCur.push(rect);
1208
+ absoluteHitList.push({ node, rect });
1091
1209
  }
1092
1210
  node.dirty = false;
1093
1211
  }
@@ -1201,6 +1319,7 @@ function blitEscapingAbsoluteDescendants(node, output, prevScreen, px, py, pw, p
1201
1319
  const cached = nodeCache.get(elem);
1202
1320
  if (cached) {
1203
1321
  absoluteRectsCur.push(cached);
1322
+ absoluteHitList.push({ node: elem, rect: cached });
1204
1323
  const cx = Math.floor(cached.x);
1205
1324
  const cy = Math.floor(cached.y);
1206
1325
  const cw = Math.floor(cached.width);
@@ -213,10 +213,12 @@ export declare function shiftAnchor(s: SelectionState, dRow: number, minRow: num
213
213
  * the selection is text-anchored at both ends — both must move. The
214
214
  * isDragging check in ink.tsx picks which shift to apply.
215
215
  *
216
- * If both ends would shift strictly BELOW minRow (unclamped), the selected
217
- * text has scrolled entirely off the top. Clear it — otherwise a single
218
- * inverted cell lingers at the viewport top as a ghost (native terminals
219
- * drop the selection when it leaves scrollback). Landing AT minRow is
216
+ * If both ends would shift strictly BELOW minRow or strictly ABOVE maxRow
217
+ * (unclamped), the selected text has scrolled entirely off the viewport —
218
+ * off the top via streaming follow / wheel-down, off the bottom via
219
+ * wheel-up. Clear it — otherwise a single inverted cell lingers at the
220
+ * edge as a ghost (native terminals drop the selection when it leaves
221
+ * scrollback). Landing AT the edge row is
220
222
  * still valid: that cell holds the correct text. Returns true if the
221
223
  * selection was cleared so the caller can notify React-land subscribers
222
224
  * (useHasSelection) — the caller is inside onRender so it can't use
@@ -229,6 +231,29 @@ export declare function shiftAnchor(s: SelectionState, dRow: number, minRow: num
229
231
  * entirely off the top, false otherwise.
230
232
  */
231
233
  export declare function shiftSelectionForFollow(s: SelectionState, dRow: number, minRow: number, maxRow: number): boolean;
234
+ /** A scroll event reported by a ScrollBox this frame (follow or wheel
235
+ * drain): signed delta plus the box's viewport bounds. Structurally
236
+ * identical to FollowScroll in render-node-to-output. */
237
+ export type ScrollEvent = {
238
+ delta: number;
239
+ viewportTop: number;
240
+ viewportBottom: number;
241
+ };
242
+ /**
243
+ * Pick which scroll event a selection belongs to when several ScrollBoxes
244
+ * scrolled in the same frame (e.g., the transcript still draining a wheel
245
+ * burst while an overlay panel's box scrolls). The selection follows the
246
+ * INNERMOST viewport containing the anchor: overlay panels render on top
247
+ * of the transcript, so a selection inside the overlap rows belongs to
248
+ * the panel, not the covered transcript beneath it. A selection outside
249
+ * every viewport (footer/prompt, static text) follows nothing — scrolling
250
+ * doesn't move the content under it.
251
+ * @param events - this frame's scroll events (signed deltas).
252
+ * @param anchorRow - the selection anchor's screen row, or null when no
253
+ * selection is active.
254
+ * @returns the event the selection should be translated by, or null.
255
+ */
256
+ export declare function pickFollowForSelection(events: ScrollEvent[], anchorRow: number | null): ScrollEvent | null;
232
257
  /**
233
258
  * True when a selection is active, meaning both anchor and focus are set.
234
259
  * @param s - the selection state to inspect.
@@ -1 +1 @@
1
- {"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../../src/ink/selection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../../src/ink/selection.ts"],"names":[],"mappings":"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"}
@@ -579,10 +579,12 @@ export function shiftAnchor(s, dRow, minRow, maxRow) {
579
579
  * the selection is text-anchored at both ends — both must move. The
580
580
  * isDragging check in ink.tsx picks which shift to apply.
581
581
  *
582
- * If both ends would shift strictly BELOW minRow (unclamped), the selected
583
- * text has scrolled entirely off the top. Clear it — otherwise a single
584
- * inverted cell lingers at the viewport top as a ghost (native terminals
585
- * drop the selection when it leaves scrollback). Landing AT minRow is
582
+ * If both ends would shift strictly BELOW minRow or strictly ABOVE maxRow
583
+ * (unclamped), the selected text has scrolled entirely off the viewport —
584
+ * off the top via streaming follow / wheel-down, off the bottom via
585
+ * wheel-up. Clear it — otherwise a single inverted cell lingers at the
586
+ * edge as a ghost (native terminals drop the selection when it leaves
587
+ * scrollback). Landing AT the edge row is
586
588
  * still valid: that cell holds the correct text. Returns true if the
587
589
  * selection was cleared so the caller can notify React-land subscribers
588
590
  * (useHasSelection) — the caller is inside onRender so it can't use
@@ -609,7 +611,11 @@ export function shiftSelectionForFollow(s, dRow, minRow, maxRow) {
609
611
  const rawFocus = s.focus
610
612
  ? (s.virtualFocusRow ?? s.focus.row) + dRow
611
613
  : undefined;
612
- if (rawAnchor < minRow && rawFocus !== undefined && rawFocus < minRow) {
614
+ // Both ends strictly past the same edge = selection fully scrolled off
615
+ // (top: follow/wheel-down; bottom: wheel-up). Symmetric clear — a
616
+ // clamped-to-edge pair would render as a 1-row ghost highlight.
617
+ if ((rawAnchor < minRow && rawFocus !== undefined && rawFocus < minRow) ||
618
+ (rawAnchor > maxRow && rawFocus !== undefined && rawFocus > maxRow)) {
613
619
  clearSelection(s);
614
620
  return true;
615
621
  }
@@ -640,6 +646,36 @@ export function shiftSelectionForFollow(s, dRow, minRow, maxRow) {
640
646
  }
641
647
  return false;
642
648
  }
649
+ /**
650
+ * Pick which scroll event a selection belongs to when several ScrollBoxes
651
+ * scrolled in the same frame (e.g., the transcript still draining a wheel
652
+ * burst while an overlay panel's box scrolls). The selection follows the
653
+ * INNERMOST viewport containing the anchor: overlay panels render on top
654
+ * of the transcript, so a selection inside the overlap rows belongs to
655
+ * the panel, not the covered transcript beneath it. A selection outside
656
+ * every viewport (footer/prompt, static text) follows nothing — scrolling
657
+ * doesn't move the content under it.
658
+ * @param events - this frame's scroll events (signed deltas).
659
+ * @param anchorRow - the selection anchor's screen row, or null when no
660
+ * selection is active.
661
+ * @returns the event the selection should be translated by, or null.
662
+ */
663
+ export function pickFollowForSelection(events, anchorRow) {
664
+ if (anchorRow === null)
665
+ return null;
666
+ let best = null;
667
+ let bestHeight = Infinity;
668
+ for (const e of events) {
669
+ if (anchorRow < e.viewportTop || anchorRow > e.viewportBottom)
670
+ continue;
671
+ const height = e.viewportBottom - e.viewportTop;
672
+ if (best === null || height < bestHeight) {
673
+ best = e;
674
+ bestHeight = height;
675
+ }
676
+ }
677
+ return best;
678
+ }
643
679
  /**
644
680
  * True when a selection is active, meaning both anchor and focus are set.
645
681
  * @param s - the selection state to inspect.