@butlerw/vellum 0.1.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 (446) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/__fixtures__/responses/code-generation.json +42 -0
  4. package/__fixtures__/responses/error-response.json +20 -0
  5. package/__fixtures__/responses/hello-world.json +32 -0
  6. package/dist/auth-6MCXESOH.js +26 -0
  7. package/dist/chunk-SECXJGWA.js +597 -0
  8. package/dist/index.js +34023 -0
  9. package/package.json +67 -0
  10. package/src/__tests__/commands.e2e.test.ts +728 -0
  11. package/src/__tests__/credentials.test.ts +713 -0
  12. package/src/__tests__/mode-e2e.test.ts +391 -0
  13. package/src/__tests__/tui-integration.test.tsx +1271 -0
  14. package/src/agents/__tests__/task-persistence.test.ts +235 -0
  15. package/src/agents/commands/delegate.ts +240 -0
  16. package/src/agents/commands/index.ts +10 -0
  17. package/src/agents/commands/resume.ts +335 -0
  18. package/src/agents/index.ts +29 -0
  19. package/src/agents/task-persistence.ts +272 -0
  20. package/src/agents/task-resumption.ts +242 -0
  21. package/src/app.tsx +4737 -0
  22. package/src/commands/__tests__/.gitkeep +1 -0
  23. package/src/commands/__tests__/agents.test.ts +606 -0
  24. package/src/commands/__tests__/auth.test.ts +626 -0
  25. package/src/commands/__tests__/autocomplete.test.ts +683 -0
  26. package/src/commands/__tests__/batch.test.ts +287 -0
  27. package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
  28. package/src/commands/__tests__/completion.test.ts +238 -0
  29. package/src/commands/__tests__/core.test.ts +363 -0
  30. package/src/commands/__tests__/executor.test.ts +496 -0
  31. package/src/commands/__tests__/exit-codes.test.ts +220 -0
  32. package/src/commands/__tests__/init.test.ts +243 -0
  33. package/src/commands/__tests__/language.test.ts +353 -0
  34. package/src/commands/__tests__/mode-cli.test.ts +667 -0
  35. package/src/commands/__tests__/model.test.ts +277 -0
  36. package/src/commands/__tests__/parser.test.ts +493 -0
  37. package/src/commands/__tests__/performance.bench.ts +380 -0
  38. package/src/commands/__tests__/registry.test.ts +534 -0
  39. package/src/commands/__tests__/resume.test.ts +449 -0
  40. package/src/commands/__tests__/security.test.ts +845 -0
  41. package/src/commands/__tests__/stream-json.test.ts +372 -0
  42. package/src/commands/__tests__/user-commands.test.ts +597 -0
  43. package/src/commands/adapters.ts +267 -0
  44. package/src/commands/agent.ts +395 -0
  45. package/src/commands/agents/generate.ts +506 -0
  46. package/src/commands/agents/index.ts +272 -0
  47. package/src/commands/agents/show.ts +271 -0
  48. package/src/commands/agents/validate.ts +387 -0
  49. package/src/commands/auth.ts +883 -0
  50. package/src/commands/autocomplete.ts +480 -0
  51. package/src/commands/batch/command.ts +388 -0
  52. package/src/commands/batch/executor.ts +361 -0
  53. package/src/commands/batch/index.ts +12 -0
  54. package/src/commands/commit.ts +235 -0
  55. package/src/commands/completion/index.ts +371 -0
  56. package/src/commands/condense.ts +191 -0
  57. package/src/commands/config.ts +344 -0
  58. package/src/commands/context-provider.ts +173 -0
  59. package/src/commands/copy.ts +329 -0
  60. package/src/commands/core/clear.ts +38 -0
  61. package/src/commands/core/exit.ts +43 -0
  62. package/src/commands/core/help.ts +354 -0
  63. package/src/commands/core/index.ts +15 -0
  64. package/src/commands/cost.ts +179 -0
  65. package/src/commands/credentials.tsx +618 -0
  66. package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
  67. package/src/commands/custom-agents/create.ts +377 -0
  68. package/src/commands/custom-agents/export.ts +135 -0
  69. package/src/commands/custom-agents/import.ts +199 -0
  70. package/src/commands/custom-agents/index.ts +372 -0
  71. package/src/commands/custom-agents/info.ts +318 -0
  72. package/src/commands/custom-agents/list.ts +267 -0
  73. package/src/commands/custom-agents/validate.ts +388 -0
  74. package/src/commands/diff-mode.ts +241 -0
  75. package/src/commands/env.ts +53 -0
  76. package/src/commands/executor.ts +579 -0
  77. package/src/commands/exit-codes.ts +202 -0
  78. package/src/commands/index.ts +701 -0
  79. package/src/commands/init/index.ts +15 -0
  80. package/src/commands/init/prompts.ts +366 -0
  81. package/src/commands/init/templates/commands-readme.md +80 -0
  82. package/src/commands/init/templates/example-command.md +79 -0
  83. package/src/commands/init/templates/example-skill.md +168 -0
  84. package/src/commands/init/templates/example-workflow.md +101 -0
  85. package/src/commands/init/templates/prompts-readme.md +52 -0
  86. package/src/commands/init/templates/rules-readme.md +63 -0
  87. package/src/commands/init/templates/skills-readme.md +83 -0
  88. package/src/commands/init/templates/workflows-readme.md +94 -0
  89. package/src/commands/init.ts +391 -0
  90. package/src/commands/install.ts +90 -0
  91. package/src/commands/language.ts +191 -0
  92. package/src/commands/loaders/.gitkeep +1 -0
  93. package/src/commands/lsp.ts +199 -0
  94. package/src/commands/markdown-commands.ts +253 -0
  95. package/src/commands/mcp.ts +588 -0
  96. package/src/commands/memory/export.ts +341 -0
  97. package/src/commands/memory/index.ts +148 -0
  98. package/src/commands/memory/list.ts +261 -0
  99. package/src/commands/memory/search.ts +346 -0
  100. package/src/commands/memory/utils.ts +15 -0
  101. package/src/commands/metrics.ts +75 -0
  102. package/src/commands/migrate/index.ts +16 -0
  103. package/src/commands/migrate/prompts.ts +477 -0
  104. package/src/commands/mode.ts +331 -0
  105. package/src/commands/model.ts +298 -0
  106. package/src/commands/onboard.ts +205 -0
  107. package/src/commands/open.ts +169 -0
  108. package/src/commands/output/stream-json.ts +373 -0
  109. package/src/commands/parser/chain-parser.ts +370 -0
  110. package/src/commands/parser/index.ts +29 -0
  111. package/src/commands/parser/pipe-parser.ts +480 -0
  112. package/src/commands/parser.ts +588 -0
  113. package/src/commands/persistence.ts +355 -0
  114. package/src/commands/progress.ts +18 -0
  115. package/src/commands/prompt/index.ts +17 -0
  116. package/src/commands/prompt/validate.ts +621 -0
  117. package/src/commands/prompt-priority.ts +401 -0
  118. package/src/commands/registry.ts +374 -0
  119. package/src/commands/sandbox/index.ts +131 -0
  120. package/src/commands/security/index.ts +21 -0
  121. package/src/commands/security/input-sanitizer.ts +168 -0
  122. package/src/commands/security/permission-checker.ts +456 -0
  123. package/src/commands/security/sensitive-data.ts +350 -0
  124. package/src/commands/session/delete.ts +38 -0
  125. package/src/commands/session/export.ts +39 -0
  126. package/src/commands/session/index.ts +26 -0
  127. package/src/commands/session/list.ts +26 -0
  128. package/src/commands/session/resume.ts +562 -0
  129. package/src/commands/session/search.ts +434 -0
  130. package/src/commands/session/show.ts +26 -0
  131. package/src/commands/settings.ts +368 -0
  132. package/src/commands/setup.ts +23 -0
  133. package/src/commands/shell/index.ts +16 -0
  134. package/src/commands/shell/setup.ts +422 -0
  135. package/src/commands/shell-init.ts +50 -0
  136. package/src/commands/shell-integration/index.ts +194 -0
  137. package/src/commands/skill.ts +1220 -0
  138. package/src/commands/spec.ts +558 -0
  139. package/src/commands/status.ts +246 -0
  140. package/src/commands/theme.ts +211 -0
  141. package/src/commands/think.ts +551 -0
  142. package/src/commands/trust.ts +211 -0
  143. package/src/commands/tutorial.ts +522 -0
  144. package/src/commands/types.ts +512 -0
  145. package/src/commands/update.ts +274 -0
  146. package/src/commands/usage.ts +213 -0
  147. package/src/commands/user-commands.ts +630 -0
  148. package/src/commands/utils.ts +142 -0
  149. package/src/commands/vim.ts +152 -0
  150. package/src/commands/workflow.ts +257 -0
  151. package/src/components/header.tsx +25 -0
  152. package/src/components/input.tsx +25 -0
  153. package/src/components/message-list.tsx +32 -0
  154. package/src/components/status-bar.tsx +23 -0
  155. package/src/index.tsx +614 -0
  156. package/src/onboarding/__tests__/tutorial.test.ts +740 -0
  157. package/src/onboarding/index.ts +69 -0
  158. package/src/onboarding/tips/index.ts +9 -0
  159. package/src/onboarding/tips/tip-engine.ts +459 -0
  160. package/src/onboarding/tutorial/index.ts +88 -0
  161. package/src/onboarding/tutorial/lessons/basics.ts +151 -0
  162. package/src/onboarding/tutorial/lessons/index.ts +151 -0
  163. package/src/onboarding/tutorial/lessons/modes.ts +230 -0
  164. package/src/onboarding/tutorial/lessons/tools.ts +172 -0
  165. package/src/onboarding/tutorial/progress-tracker.ts +350 -0
  166. package/src/onboarding/tutorial/storage.ts +249 -0
  167. package/src/onboarding/tutorial/tutorial-system.ts +462 -0
  168. package/src/onboarding/tutorial/types.ts +310 -0
  169. package/src/orchestrator-singleton.ts +129 -0
  170. package/src/shutdown.ts +33 -0
  171. package/src/test/e2e/assertions.ts +267 -0
  172. package/src/test/e2e/fixtures.ts +204 -0
  173. package/src/test/e2e/harness.ts +575 -0
  174. package/src/test/e2e/index.ts +57 -0
  175. package/src/test/e2e/types.ts +228 -0
  176. package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
  177. package/src/test/fixtures/fake-response-loader.ts +314 -0
  178. package/src/test/fixtures/index.ts +20 -0
  179. package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
  180. package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
  181. package/src/tui/__tests__/mode-components.test.tsx +395 -0
  182. package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
  183. package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
  184. package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
  185. package/src/tui/adapters/agent-adapter.ts +1008 -0
  186. package/src/tui/adapters/index.ts +48 -0
  187. package/src/tui/adapters/message-adapter.ts +315 -0
  188. package/src/tui/adapters/persistence-bridge.ts +331 -0
  189. package/src/tui/adapters/session-adapter.ts +419 -0
  190. package/src/tui/buffered-stdout.ts +223 -0
  191. package/src/tui/components/AgentProgress.tsx +424 -0
  192. package/src/tui/components/Banner/AsciiArt.ts +160 -0
  193. package/src/tui/components/Banner/Banner.tsx +355 -0
  194. package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
  195. package/src/tui/components/Banner/ShimmerText.tsx +193 -0
  196. package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
  197. package/src/tui/components/Banner/index.ts +61 -0
  198. package/src/tui/components/Banner/useShimmer.ts +241 -0
  199. package/src/tui/components/ChatView.tsx +11 -0
  200. package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
  201. package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
  202. package/src/tui/components/Checkpoint/index.ts +19 -0
  203. package/src/tui/components/CostDisplay.tsx +226 -0
  204. package/src/tui/components/InitErrorBanner.tsx +122 -0
  205. package/src/tui/components/Input/Autocomplete.tsx +603 -0
  206. package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
  207. package/src/tui/components/Input/HighlightedText.tsx +236 -0
  208. package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
  209. package/src/tui/components/Input/TextInput.tsx +1002 -0
  210. package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
  211. package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
  212. package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
  213. package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
  214. package/src/tui/components/Input/highlight.ts +362 -0
  215. package/src/tui/components/Input/index.ts +36 -0
  216. package/src/tui/components/Input/slash-command-utils.ts +135 -0
  217. package/src/tui/components/Layout.tsx +432 -0
  218. package/src/tui/components/McpPanel.tsx +137 -0
  219. package/src/tui/components/MemoryPanel.tsx +448 -0
  220. package/src/tui/components/Messages/CodeBlock.tsx +527 -0
  221. package/src/tui/components/Messages/DiffView.tsx +679 -0
  222. package/src/tui/components/Messages/ImageReference.tsx +89 -0
  223. package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
  224. package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
  225. package/src/tui/components/Messages/MessageBubble.tsx +270 -0
  226. package/src/tui/components/Messages/MessageList.tsx +1719 -0
  227. package/src/tui/components/Messages/StreamingText.tsx +216 -0
  228. package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
  229. package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
  230. package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
  231. package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
  232. package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
  233. package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
  234. package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
  235. package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
  236. package/src/tui/components/Messages/index.ts +25 -0
  237. package/src/tui/components/ModeIndicator.tsx +177 -0
  238. package/src/tui/components/ModeSelector.tsx +216 -0
  239. package/src/tui/components/ModelSelector.tsx +339 -0
  240. package/src/tui/components/OnboardingWizard.tsx +670 -0
  241. package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
  242. package/src/tui/components/RateLimitIndicator.tsx +82 -0
  243. package/src/tui/components/ScreenReaderLayout.tsx +295 -0
  244. package/src/tui/components/SettingsPanel.tsx +643 -0
  245. package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
  246. package/src/tui/components/Sidebar/index.ts +9 -0
  247. package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
  248. package/src/tui/components/Status/index.ts +12 -0
  249. package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
  250. package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
  251. package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
  252. package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
  253. package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
  254. package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
  255. package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
  256. package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
  257. package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
  258. package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
  259. package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
  260. package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
  261. package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
  262. package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
  263. package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
  264. package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
  265. package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
  266. package/src/tui/components/StatusBar/index.ts +60 -0
  267. package/src/tui/components/TipBanner.tsx +115 -0
  268. package/src/tui/components/TodoItem.tsx +208 -0
  269. package/src/tui/components/TodoPanel.tsx +455 -0
  270. package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
  271. package/src/tui/components/Tools/OptionSelector.tsx +160 -0
  272. package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
  273. package/src/tui/components/Tools/ToolParams.tsx +483 -0
  274. package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
  275. package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
  276. package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
  277. package/src/tui/components/Tools/index.ts +21 -0
  278. package/src/tui/components/TrustPrompt.tsx +279 -0
  279. package/src/tui/components/UpdateBanner.tsx +166 -0
  280. package/src/tui/components/VimModeIndicator.tsx +112 -0
  281. package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
  282. package/src/tui/components/backtrack/index.ts +13 -0
  283. package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
  284. package/src/tui/components/common/CostWarning.tsx +294 -0
  285. package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
  286. package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
  287. package/src/tui/components/common/ErrorBoundary.tsx +140 -0
  288. package/src/tui/components/common/GradientText.tsx +224 -0
  289. package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
  290. package/src/tui/components/common/HotkeyHints.tsx +70 -0
  291. package/src/tui/components/common/MaxSizedBox.tsx +354 -0
  292. package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
  293. package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
  294. package/src/tui/components/common/ScrollIndicator.tsx +160 -0
  295. package/src/tui/components/common/Spinner.tsx +342 -0
  296. package/src/tui/components/common/StreamingIndicator.tsx +316 -0
  297. package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
  298. package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
  299. package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
  300. package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
  301. package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
  302. package/src/tui/components/common/VirtualizedList/index.ts +30 -0
  303. package/src/tui/components/common/VirtualizedList/types.ts +107 -0
  304. package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
  305. package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
  306. package/src/tui/components/common/index.ts +110 -0
  307. package/src/tui/components/index.ts +79 -0
  308. package/src/tui/components/session/CheckpointPanel.tsx +323 -0
  309. package/src/tui/components/session/RollbackDialog.tsx +169 -0
  310. package/src/tui/components/session/SessionItem.tsx +136 -0
  311. package/src/tui/components/session/SessionListPanel.tsx +252 -0
  312. package/src/tui/components/session/SessionPicker.tsx +449 -0
  313. package/src/tui/components/session/SessionPreview.tsx +240 -0
  314. package/src/tui/components/session/__tests__/session.test.tsx +408 -0
  315. package/src/tui/components/session/index.ts +28 -0
  316. package/src/tui/components/session/types.ts +116 -0
  317. package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
  318. package/src/tui/components/theme/index.ts +227 -0
  319. package/src/tui/components/theme/tokens.ts +484 -0
  320. package/src/tui/config/defaults.ts +134 -0
  321. package/src/tui/config/index.ts +17 -0
  322. package/src/tui/context/AnimationContext.tsx +284 -0
  323. package/src/tui/context/AppContext.tsx +349 -0
  324. package/src/tui/context/BracketedPasteContext.tsx +372 -0
  325. package/src/tui/context/LspContext.tsx +192 -0
  326. package/src/tui/context/McpContext.tsx +325 -0
  327. package/src/tui/context/MessagesContext.tsx +870 -0
  328. package/src/tui/context/OverflowContext.tsx +213 -0
  329. package/src/tui/context/RateLimitContext.tsx +108 -0
  330. package/src/tui/context/ResilienceContext.tsx +275 -0
  331. package/src/tui/context/RootProvider.tsx +136 -0
  332. package/src/tui/context/ScrollContext.tsx +331 -0
  333. package/src/tui/context/ToolsContext.tsx +702 -0
  334. package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
  335. package/src/tui/context/index.ts +140 -0
  336. package/src/tui/enterprise-integration.ts +282 -0
  337. package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
  338. package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
  339. package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
  340. package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
  341. package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
  342. package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
  343. package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
  344. package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
  345. package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
  346. package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
  347. package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
  348. package/src/tui/hooks/index.ts +252 -0
  349. package/src/tui/hooks/useAgentLoop.ts +712 -0
  350. package/src/tui/hooks/useAlternateBuffer.ts +398 -0
  351. package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
  352. package/src/tui/hooks/useBacktrack.ts +443 -0
  353. package/src/tui/hooks/useBracketedPaste.ts +104 -0
  354. package/src/tui/hooks/useCollapsible.ts +240 -0
  355. package/src/tui/hooks/useCopyMode.ts +382 -0
  356. package/src/tui/hooks/useCostSummary.ts +75 -0
  357. package/src/tui/hooks/useDesktopNotification.ts +414 -0
  358. package/src/tui/hooks/useDiffMode.ts +44 -0
  359. package/src/tui/hooks/useFileChangeStats.ts +110 -0
  360. package/src/tui/hooks/useFileSuggestions.ts +284 -0
  361. package/src/tui/hooks/useFlickerDetector.ts +250 -0
  362. package/src/tui/hooks/useGitStatus.ts +200 -0
  363. package/src/tui/hooks/useHotkeys.ts +579 -0
  364. package/src/tui/hooks/useImagePaste.ts +114 -0
  365. package/src/tui/hooks/useInputHighlight.ts +145 -0
  366. package/src/tui/hooks/useInputHistory.ts +246 -0
  367. package/src/tui/hooks/useKeyboardScroll.ts +209 -0
  368. package/src/tui/hooks/useLineBuffer.ts +356 -0
  369. package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
  370. package/src/tui/hooks/useModeController.ts +167 -0
  371. package/src/tui/hooks/useModeShortcuts.ts +196 -0
  372. package/src/tui/hooks/usePermissionHandler.ts +146 -0
  373. package/src/tui/hooks/usePersistence.ts +480 -0
  374. package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
  375. package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
  376. package/src/tui/hooks/useProviderStatus.ts +270 -0
  377. package/src/tui/hooks/useRateLimitStatus.ts +90 -0
  378. package/src/tui/hooks/useScreenReader.ts +315 -0
  379. package/src/tui/hooks/useScrollController.ts +450 -0
  380. package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
  381. package/src/tui/hooks/useSidebarPanelData.ts +115 -0
  382. package/src/tui/hooks/useSmoothScroll.ts +202 -0
  383. package/src/tui/hooks/useSnapshots.ts +300 -0
  384. package/src/tui/hooks/useStateAndRef.ts +50 -0
  385. package/src/tui/hooks/useTerminalSize.ts +206 -0
  386. package/src/tui/hooks/useToolApprovalController.ts +91 -0
  387. package/src/tui/hooks/useVim.ts +334 -0
  388. package/src/tui/hooks/useWorkspace.ts +56 -0
  389. package/src/tui/i18n/__tests__/init.test.ts +278 -0
  390. package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
  391. package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
  392. package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
  393. package/src/tui/i18n/index.ts +72 -0
  394. package/src/tui/i18n/init.ts +131 -0
  395. package/src/tui/i18n/language-config.ts +106 -0
  396. package/src/tui/i18n/locale-detection.ts +173 -0
  397. package/src/tui/i18n/settings-integration.ts +557 -0
  398. package/src/tui/i18n/tui-namespace.ts +538 -0
  399. package/src/tui/i18n/types.ts +312 -0
  400. package/src/tui/index.ts +43 -0
  401. package/src/tui/lsp-integration.ts +409 -0
  402. package/src/tui/metrics-integration.ts +366 -0
  403. package/src/tui/plugins.ts +383 -0
  404. package/src/tui/resilience.ts +342 -0
  405. package/src/tui/sandbox-integration.ts +317 -0
  406. package/src/tui/services/clipboard.ts +348 -0
  407. package/src/tui/services/fuzzy-search.ts +441 -0
  408. package/src/tui/services/index.ts +72 -0
  409. package/src/tui/services/markdown-renderer.ts +565 -0
  410. package/src/tui/services/open-external.ts +247 -0
  411. package/src/tui/services/syntax-highlighter.ts +483 -0
  412. package/src/tui/slash-commands.ts +12 -0
  413. package/src/tui/theme/index.ts +15 -0
  414. package/src/tui/theme/provider.tsx +206 -0
  415. package/src/tui/tip-integration.ts +300 -0
  416. package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
  417. package/src/tui/types/ink-extended.ts +87 -0
  418. package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
  419. package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
  420. package/src/tui/utils/__tests__/text-width.test.ts +158 -0
  421. package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
  422. package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
  423. package/src/tui/utils/bracketedPaste.ts +107 -0
  424. package/src/tui/utils/cursor-manager.ts +131 -0
  425. package/src/tui/utils/detectTerminal.ts +596 -0
  426. package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
  427. package/src/tui/utils/heightEstimator.ts +198 -0
  428. package/src/tui/utils/index.ts +91 -0
  429. package/src/tui/utils/isNarrowWidth.ts +52 -0
  430. package/src/tui/utils/stdoutGuard.ts +90 -0
  431. package/src/tui/utils/synchronized-update.ts +70 -0
  432. package/src/tui/utils/text-width.ts +225 -0
  433. package/src/tui/utils/textSanitizer.ts +225 -0
  434. package/src/tui/utils/textUtils.ts +114 -0
  435. package/src/tui/utils/ui-sizing.ts +192 -0
  436. package/src/tui-blessed/app.ts +160 -0
  437. package/src/tui-blessed/index.ts +2 -0
  438. package/src/tui-blessed/neo-blessed.d.ts +6 -0
  439. package/src/tui-blessed/test.ts +21 -0
  440. package/src/tui-blessed/types.ts +14 -0
  441. package/src/utils/icons.ts +130 -0
  442. package/src/utils/index.ts +33 -0
  443. package/src/utils/resume-hint.ts +86 -0
  444. package/src/version.ts +1 -0
  445. package/tsconfig.json +8 -0
  446. package/vitest.config.ts +35 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * useScrollAnchor Hook
3
+ *
4
+ * Manages scroll position as an anchor (index + offset) for stability
5
+ * during content changes. This approach is more robust than pure pixel-based
6
+ * scrolling when items resize or new items are added.
7
+ *
8
+ * @module tui/components/common/VirtualizedList/hooks/useScrollAnchor
9
+ */
10
+
11
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
12
+ import { SCROLL_TO_ITEM_END, type ScrollAnchor } from "../types.js";
13
+
14
+ /**
15
+ * Props for the useScrollAnchor hook.
16
+ */
17
+ export interface UseScrollAnchorProps {
18
+ /** Number of data items */
19
+ readonly dataLength: number;
20
+ /** Array of cumulative offsets for each item */
21
+ readonly offsets: readonly number[];
22
+ /** Array of measured or estimated heights */
23
+ readonly heights: readonly number[];
24
+ /** Total height of all content */
25
+ readonly totalHeight: number;
26
+ /** Height of the visible container */
27
+ readonly containerHeight: number;
28
+ /** Initial scroll index */
29
+ readonly initialScrollIndex?: number;
30
+ /** Initial offset within the scroll index */
31
+ readonly initialScrollOffsetInIndex?: number;
32
+ }
33
+
34
+ /**
35
+ * Return type for the useScrollAnchor hook.
36
+ */
37
+ export interface UseScrollAnchorReturn {
38
+ /** Current scroll anchor */
39
+ readonly scrollAnchor: ScrollAnchor;
40
+ /** Set the scroll anchor directly */
41
+ readonly setScrollAnchor: (anchor: ScrollAnchor) => void;
42
+ /** Whether currently sticking to bottom (auto-scroll enabled) */
43
+ readonly isStickingToBottom: boolean;
44
+ /** Set sticking to bottom state */
45
+ readonly setIsStickingToBottom: (value: boolean) => void;
46
+ /** Computed pixel scroll position from anchor */
47
+ readonly scrollTop: number;
48
+ /** Get anchor for a given scroll position */
49
+ readonly getAnchorForScrollTop: (scrollTop: number) => ScrollAnchor;
50
+ }
51
+
52
+ /**
53
+ * Find the last index where predicate returns true.
54
+ */
55
+ function findLastIndex<T>(
56
+ array: readonly T[],
57
+ predicate: (value: T, index: number) => boolean
58
+ ): number {
59
+ for (let i = array.length - 1; i >= 0; i--) {
60
+ const item = array[i];
61
+ if (item !== undefined && predicate(item, i)) {
62
+ return i;
63
+ }
64
+ }
65
+ return -1;
66
+ }
67
+
68
+ /**
69
+ * Hook for managing scroll position as an anchor.
70
+ *
71
+ * @param props - Configuration for scroll anchor behavior
72
+ * @returns Scroll anchor state and utilities
73
+ */
74
+ export function useScrollAnchor(props: UseScrollAnchorProps): UseScrollAnchorReturn {
75
+ const {
76
+ dataLength,
77
+ offsets,
78
+ heights,
79
+ totalHeight,
80
+ containerHeight,
81
+ initialScrollIndex,
82
+ initialScrollOffsetInIndex,
83
+ } = props;
84
+
85
+ // Initialize scroll anchor based on initial props
86
+ const [scrollAnchor, setScrollAnchor] = useState<ScrollAnchor>(() => {
87
+ const scrollToEnd =
88
+ initialScrollIndex === SCROLL_TO_ITEM_END ||
89
+ (typeof initialScrollIndex === "number" &&
90
+ initialScrollIndex >= dataLength - 1 &&
91
+ initialScrollOffsetInIndex === SCROLL_TO_ITEM_END);
92
+
93
+ if (scrollToEnd) {
94
+ return {
95
+ index: dataLength > 0 ? dataLength - 1 : 0,
96
+ offset: SCROLL_TO_ITEM_END,
97
+ };
98
+ }
99
+
100
+ if (typeof initialScrollIndex === "number") {
101
+ return {
102
+ index: Math.max(0, Math.min(dataLength - 1, initialScrollIndex)),
103
+ offset: initialScrollOffsetInIndex ?? 0,
104
+ };
105
+ }
106
+
107
+ return { index: 0, offset: 0 };
108
+ });
109
+
110
+ // Track whether we're sticking to bottom (auto-scroll)
111
+ const [isStickingToBottom, setIsStickingToBottom] = useState(() => {
112
+ const scrollToEnd =
113
+ initialScrollIndex === SCROLL_TO_ITEM_END ||
114
+ (typeof initialScrollIndex === "number" &&
115
+ initialScrollIndex >= dataLength - 1 &&
116
+ initialScrollOffsetInIndex === SCROLL_TO_ITEM_END);
117
+ return scrollToEnd;
118
+ });
119
+
120
+ // Track if initial scroll has been set
121
+ const isInitialScrollSet = useRef(false);
122
+
123
+ // Convert scroll position to anchor
124
+ // FIX: Added bounds validation to prevent invalid anchor indices
125
+ const getAnchorForScrollTop = useCallback(
126
+ (scrollTop: number): ScrollAnchor => {
127
+ // Handle edge cases
128
+ if (dataLength === 0) {
129
+ return { index: 0, offset: 0 };
130
+ }
131
+ if (scrollTop <= 0) {
132
+ return { index: 0, offset: 0 };
133
+ }
134
+
135
+ const index = findLastIndex(offsets, (offset) => offset <= scrollTop);
136
+ if (index === -1) {
137
+ return { index: 0, offset: 0 };
138
+ }
139
+
140
+ // FIX: Ensure index is within valid bounds
141
+ const safeIndex = Math.max(0, Math.min(dataLength - 1, index));
142
+ const offsetValue = offsets[safeIndex] ?? 0;
143
+ const itemHeight = heights[safeIndex] ?? 0;
144
+
145
+ // FIX: Clamp offset to be within the item's height
146
+ const rawOffset = scrollTop - offsetValue;
147
+ const safeOffset = Math.max(0, Math.min(itemHeight, rawOffset));
148
+
149
+ return { index: safeIndex, offset: safeOffset };
150
+ },
151
+ [offsets, dataLength, heights]
152
+ );
153
+
154
+ // Compute pixel scroll position from anchor
155
+ const scrollTop = (() => {
156
+ const offset = offsets[scrollAnchor.index];
157
+ if (typeof offset !== "number") {
158
+ return 0;
159
+ }
160
+
161
+ if (scrollAnchor.offset === SCROLL_TO_ITEM_END) {
162
+ const itemHeight = heights[scrollAnchor.index] ?? 0;
163
+ return Math.max(0, offset + itemHeight - containerHeight);
164
+ }
165
+
166
+ return Math.max(0, offset + scrollAnchor.offset);
167
+ })();
168
+
169
+ // Track previous values for auto-scroll logic
170
+ const prevDataLength = useRef(dataLength);
171
+ const prevTotalHeight = useRef(totalHeight);
172
+ const prevScrollTop = useRef(scrollTop);
173
+ const prevContainerHeight = useRef(containerHeight);
174
+
175
+ // Handle auto-scroll and anchor adjustments
176
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Scroll anchor logic with multiple edge cases
177
+ useLayoutEffect(() => {
178
+ const contentPreviouslyFit = prevTotalHeight.current <= prevContainerHeight.current;
179
+ const wasScrolledToBottomPixels =
180
+ prevScrollTop.current >= prevTotalHeight.current - prevContainerHeight.current - 1;
181
+ const wasAtBottom = contentPreviouslyFit || wasScrolledToBottomPixels;
182
+
183
+ // If the user was at the bottom, they are now sticking
184
+ if (wasAtBottom && scrollTop >= prevScrollTop.current) {
185
+ setIsStickingToBottom(true);
186
+ }
187
+
188
+ const listGrew = dataLength > prevDataLength.current;
189
+ const containerChanged = prevContainerHeight.current !== containerHeight;
190
+ // Detect content height growth (triggers during streaming output)
191
+ const contentHeightGrew = totalHeight > prevTotalHeight.current;
192
+
193
+ // Scroll to end conditions:
194
+ // 1. List grew AND we were already at the bottom (or sticking)
195
+ // 2. We are sticking to bottom AND container size changed
196
+ // 3. We are sticking to bottom AND content height grew (streaming content)
197
+ if (
198
+ (listGrew && (isStickingToBottom || wasAtBottom)) ||
199
+ (isStickingToBottom && containerChanged) ||
200
+ (isStickingToBottom && contentHeightGrew)
201
+ ) {
202
+ setScrollAnchor({
203
+ index: dataLength > 0 ? dataLength - 1 : 0,
204
+ offset: SCROLL_TO_ITEM_END,
205
+ });
206
+ if (!isStickingToBottom) {
207
+ setIsStickingToBottom(true);
208
+ }
209
+ }
210
+ // List shrunk or scroll position is invalid
211
+ else if (
212
+ (scrollAnchor.index >= dataLength || scrollTop > totalHeight - containerHeight) &&
213
+ dataLength > 0
214
+ ) {
215
+ const newScrollTop = Math.max(0, totalHeight - containerHeight);
216
+ setScrollAnchor(getAnchorForScrollTop(newScrollTop));
217
+ } else if (dataLength === 0) {
218
+ // List is empty, reset to top
219
+ setScrollAnchor({ index: 0, offset: 0 });
220
+ }
221
+
222
+ // Update refs for next render
223
+ prevDataLength.current = dataLength;
224
+ prevTotalHeight.current = totalHeight;
225
+ prevScrollTop.current = scrollTop;
226
+ prevContainerHeight.current = containerHeight;
227
+ }, [
228
+ dataLength,
229
+ totalHeight,
230
+ scrollTop,
231
+ containerHeight,
232
+ scrollAnchor.index,
233
+ getAnchorForScrollTop,
234
+ isStickingToBottom,
235
+ ]);
236
+
237
+ // Handle initial scroll position
238
+ useLayoutEffect(() => {
239
+ if (
240
+ isInitialScrollSet.current ||
241
+ offsets.length <= 1 ||
242
+ totalHeight <= 0 ||
243
+ containerHeight <= 0
244
+ ) {
245
+ return;
246
+ }
247
+
248
+ if (typeof initialScrollIndex === "number") {
249
+ const scrollToEnd =
250
+ initialScrollIndex === SCROLL_TO_ITEM_END ||
251
+ (initialScrollIndex >= dataLength - 1 && initialScrollOffsetInIndex === SCROLL_TO_ITEM_END);
252
+
253
+ if (scrollToEnd) {
254
+ setScrollAnchor({
255
+ index: dataLength - 1,
256
+ offset: SCROLL_TO_ITEM_END,
257
+ });
258
+ setIsStickingToBottom(true);
259
+ isInitialScrollSet.current = true;
260
+ return;
261
+ }
262
+
263
+ const index = Math.max(0, Math.min(dataLength - 1, initialScrollIndex));
264
+ const offset = initialScrollOffsetInIndex ?? 0;
265
+ const newScrollTop = (offsets[index] ?? 0) + offset;
266
+
267
+ const clampedScrollTop = Math.max(0, Math.min(totalHeight - containerHeight, newScrollTop));
268
+
269
+ setScrollAnchor(getAnchorForScrollTop(clampedScrollTop));
270
+ isInitialScrollSet.current = true;
271
+ }
272
+ }, [
273
+ initialScrollIndex,
274
+ initialScrollOffsetInIndex,
275
+ offsets,
276
+ totalHeight,
277
+ containerHeight,
278
+ getAnchorForScrollTop,
279
+ dataLength,
280
+ ]);
281
+
282
+ return {
283
+ scrollAnchor,
284
+ setScrollAnchor,
285
+ isStickingToBottom,
286
+ setIsStickingToBottom,
287
+ scrollTop,
288
+ getAnchorForScrollTop,
289
+ };
290
+ }
@@ -0,0 +1,340 @@
1
+ /**
2
+ * useVirtualization Hook
3
+ *
4
+ * Manages height calculations, measurement, and visible range computation
5
+ * for virtualized list rendering.
6
+ *
7
+ * @module tui/components/common/VirtualizedList/hooks/useVirtualization
8
+ */
9
+
10
+ import type { DOMElement } from "ink";
11
+ import { measureElement } from "ink";
12
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
13
+
14
+ /**
15
+ * Props for the useVirtualization hook.
16
+ */
17
+ export interface UseVirtualizationProps {
18
+ /** Number of data items */
19
+ readonly dataLength: number;
20
+ /** Function or fixed value for estimated item height */
21
+ readonly estimatedItemHeight: number | ((index: number) => number);
22
+ /** Current scroll position in pixels */
23
+ readonly scrollTop: number;
24
+ /** Height of the visible container */
25
+ readonly containerHeight: number;
26
+ }
27
+
28
+ /**
29
+ * Minimum viewport dimensions to prevent degenerate cases.
30
+ */
31
+ export const MIN_VIEWPORT_HEIGHT = 8;
32
+ export const MIN_VIEWPORT_WIDTH = 20;
33
+
34
+ /**
35
+ * Return type for the useVirtualization hook.
36
+ */
37
+ export interface UseVirtualizationReturn {
38
+ /** Array of measured or estimated heights */
39
+ readonly heights: readonly number[];
40
+ /** Array of cumulative offsets */
41
+ readonly offsets: readonly number[];
42
+ /** Total height of all content */
43
+ readonly totalHeight: number;
44
+ /** First visible item index */
45
+ readonly startIndex: number;
46
+ /** Last visible item index */
47
+ readonly endIndex: number;
48
+ /** Height of spacer above visible items */
49
+ readonly topSpacerHeight: number;
50
+ /** Height of spacer below visible items */
51
+ readonly bottomSpacerHeight: number;
52
+ /** Ref callback for item elements */
53
+ readonly itemRefCallback: (index: number, el: DOMElement | null) => void;
54
+ /** Ref for the container element */
55
+ readonly containerRef: React.RefObject<DOMElement | null>;
56
+ /** Measured container height */
57
+ readonly measuredContainerHeight: number;
58
+ /** True if the last item exceeds viewport height (needs clipping) */
59
+ readonly isOversize: boolean;
60
+ }
61
+
62
+ /**
63
+ * Find the last index where predicate returns true.
64
+ */
65
+ function findLastIndex<T>(
66
+ array: readonly T[],
67
+ predicate: (value: T, index: number) => boolean
68
+ ): number {
69
+ for (let i = array.length - 1; i >= 0; i--) {
70
+ const item = array[i];
71
+ if (item !== undefined && predicate(item, i)) {
72
+ return i;
73
+ }
74
+ }
75
+ return -1;
76
+ }
77
+
78
+ /**
79
+ * Minimum valid height for any item to prevent invisible/zero-height items
80
+ */
81
+ const MIN_ITEM_HEIGHT = 1;
82
+
83
+ /**
84
+ * Get estimated height for an index using the estimator.
85
+ * FIX: Always returns at least MIN_ITEM_HEIGHT to prevent zero/negative heights
86
+ * that can cause infinite scroll loops or invisible items.
87
+ */
88
+ function getEstimatedHeight(
89
+ estimator: number | ((index: number) => number),
90
+ index: number
91
+ ): number {
92
+ const estimated = typeof estimator === "function" ? estimator(index) : estimator;
93
+ // FIX: Ensure height is always valid (positive integer)
94
+ return Math.max(MIN_ITEM_HEIGHT, Math.round(estimated));
95
+ }
96
+
97
+ /**
98
+ * Hook for managing virtualization state.
99
+ *
100
+ * @param props - Configuration for virtualization
101
+ * @returns Virtualization state and utilities
102
+ */
103
+ export function useVirtualization(props: UseVirtualizationProps): UseVirtualizationReturn {
104
+ const { dataLength, estimatedItemHeight, scrollTop, containerHeight } = props;
105
+
106
+ // Apply minimum viewport clamping to prevent degenerate cases
107
+ const safeContainerHeight = Math.max(MIN_VIEWPORT_HEIGHT, containerHeight);
108
+
109
+ // Container ref for measuring viewport
110
+ const containerRef = useRef<DOMElement | null>(null);
111
+ const [measuredContainerHeight, setMeasuredContainerHeight] = useState(safeContainerHeight);
112
+
113
+ // Item refs for measurement
114
+ const itemRefs = useRef<Array<DOMElement | null>>([]);
115
+
116
+ // Heights cache - measured or estimated
117
+ const [heights, setHeights] = useState<number[]>(() => {
118
+ const initial: number[] = [];
119
+ for (let i = 0; i < dataLength; i++) {
120
+ initial[i] = getEstimatedHeight(estimatedItemHeight, i);
121
+ }
122
+ return initial;
123
+ });
124
+
125
+ // Calculate offsets and total height
126
+ const { totalHeight, offsets } = useMemo(() => {
127
+ const offsets: number[] = [0];
128
+ let totalHeight = 0;
129
+ for (let i = 0; i < dataLength; i++) {
130
+ const height = heights[i] ?? getEstimatedHeight(estimatedItemHeight, i);
131
+ totalHeight += height;
132
+ offsets.push(totalHeight);
133
+ }
134
+ return { totalHeight, offsets };
135
+ }, [heights, dataLength, estimatedItemHeight]);
136
+
137
+ // Sync heights array with data length changes
138
+ useEffect(() => {
139
+ setHeights((prevHeights) => {
140
+ if (dataLength === prevHeights.length) {
141
+ return prevHeights;
142
+ }
143
+
144
+ const newHeights = [...prevHeights];
145
+ if (dataLength < prevHeights.length) {
146
+ // Shrink
147
+ newHeights.length = dataLength;
148
+ } else {
149
+ // Grow - add estimated heights for new items
150
+ for (let i = prevHeights.length; i < dataLength; i++) {
151
+ newHeights[i] = getEstimatedHeight(estimatedItemHeight, i);
152
+ }
153
+ }
154
+ return newHeights;
155
+ });
156
+ }, [dataLength, estimatedItemHeight]);
157
+
158
+ // Calculate visible range with OVERFLOW GUARD
159
+ // This ensures we NEVER render more items than fit in the viewport
160
+ // FIX: Improved off-by-one handling - findLastIndex returns the index where offset <= scrollTop
161
+ // We don't need to subtract 1; that was causing negative indices and blank areas
162
+ const foundStartIndex = findLastIndex(offsets, (offset) => offset <= scrollTop);
163
+ // If no offset found (scrollTop < 0 or empty), start at 0
164
+ // Otherwise use the found index directly (no -1 subtraction)
165
+ const rawStartIndex = Math.max(0, foundStartIndex === -1 ? 0 : foundStartIndex);
166
+
167
+ const endIndexOffset = offsets.findIndex((offset) => offset > scrollTop + safeContainerHeight);
168
+ // FIX: Handle edge case where no offset exceeds viewport - show all remaining items
169
+ const rawEndIndex =
170
+ endIndexOffset === -1
171
+ ? dataLength - 1
172
+ : Math.min(dataLength - 1, Math.max(0, endIndexOffset - 1));
173
+
174
+ // FRAME HEIGHT GUARD: Calculate safe render range to prevent overflow
175
+ // Simplified version: no isOversize (ClippedMessage doesn't exist yet)
176
+ // Always show at least MIN_ITEMS_TO_SHOW to prevent content disappearing
177
+ const { startIndex, endIndex } = useMemo(() => {
178
+ // CRITICAL FIX: Always render at least one item if data exists
179
+ // Previous logic returned empty range causing blank screen
180
+ if (dataLength === 0) {
181
+ return { startIndex: 0, endIndex: -1 };
182
+ }
183
+
184
+ // Ensure we have valid indices even if raw calculation failed
185
+ const safeRawEndIndex = rawEndIndex < 0 ? dataLength - 1 : rawEndIndex;
186
+ const safeRawStartIndex = Math.min(rawStartIndex, safeRawEndIndex);
187
+
188
+ // Safety: ensure we have valid container height
189
+ const effectiveContainerHeight = Math.max(MIN_VIEWPORT_HEIGHT, safeContainerHeight);
190
+
191
+ // If raw range is small enough, just use it directly (don't over-optimize)
192
+ const rawCount = safeRawEndIndex - safeRawStartIndex + 1;
193
+ if (rawCount <= 3) {
194
+ return { startIndex: safeRawStartIndex, endIndex: safeRawEndIndex };
195
+ }
196
+
197
+ // For larger ranges, do soft trimming but always keep at least 2 items
198
+ let totalRenderedHeight = 0;
199
+ let safeStartIndex = safeRawEndIndex;
200
+ const MIN_ITEMS_TO_SHOW = 2;
201
+
202
+ for (let i = safeRawEndIndex; i >= safeRawStartIndex; i--) {
203
+ const itemHeight = heights[i] ?? getEstimatedHeight(estimatedItemHeight, i);
204
+ // Use actual height, not capped - let Ink handle overflow
205
+
206
+ const itemCount = safeRawEndIndex - safeStartIndex + 1;
207
+ if (
208
+ totalRenderedHeight + itemHeight > effectiveContainerHeight &&
209
+ itemCount >= MIN_ITEMS_TO_SHOW
210
+ ) {
211
+ // Would overflow AND we have minimum items - stop
212
+ break;
213
+ }
214
+
215
+ totalRenderedHeight += itemHeight;
216
+ safeStartIndex = i;
217
+ }
218
+
219
+ return { startIndex: safeStartIndex, endIndex: safeRawEndIndex };
220
+ }, [rawStartIndex, rawEndIndex, heights, safeContainerHeight, estimatedItemHeight, dataLength]);
221
+
222
+ // isOversize is kept for API compatibility but always false
223
+ // (ClippedMessage doesn't exist yet, so we can't handle oversize items)
224
+ const isOversize = false;
225
+
226
+ // Calculate spacer heights
227
+ const topSpacerHeight = offsets[startIndex] ?? 0;
228
+ const bottomSpacerHeight = totalHeight - (offsets[endIndex + 1] ?? totalHeight);
229
+
230
+ // Item ref callback for measurement
231
+ const itemRefCallback = (index: number, el: DOMElement | null) => {
232
+ itemRefs.current[index] = el;
233
+ };
234
+
235
+ // Track previous values to avoid unnecessary measurements
236
+ const prevStartIndexRef = useRef(startIndex);
237
+ const prevEndIndexRef = useRef(endIndex);
238
+ const prevDataLengthRef = useRef(dataLength);
239
+
240
+ // Periodic measurement tick to catch dynamic content height changes
241
+ // This ensures collapsible content like ThinkingBlock gets re-measured when expanded
242
+ const [measureTick, setMeasureTick] = useState(0);
243
+ const REMEASURE_INTERVAL_MS = 250; // Check every 250ms for height changes
244
+
245
+ // Set up periodic measurement timer
246
+ useEffect(() => {
247
+ const timer = setInterval(() => {
248
+ setMeasureTick((t) => t + 1);
249
+ }, REMEASURE_INTERVAL_MS);
250
+ return () => clearInterval(timer);
251
+ }, []);
252
+
253
+ // Measure container and visible items when visible range changes or on periodic tick
254
+ // FIX: Added proper dependency array to prevent measuring on every single render
255
+ // which was causing extreme CPU usage and frame drops
256
+ // FIX2: Added periodic re-measurement to catch dynamic content changes (ThinkingBlock expand/collapse)
257
+ // FIX3: Enhanced detection for significant height changes (>5px threshold) to catch collapsible content
258
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Item measurement logic with height tracking
259
+ // biome-ignore lint/correctness/useExhaustiveDependencies: measureTick intentionally triggers periodic re-measurement
260
+ useLayoutEffect(() => {
261
+ // Check if we actually need to re-measure
262
+ const rangeChanged =
263
+ prevStartIndexRef.current !== startIndex ||
264
+ prevEndIndexRef.current !== endIndex ||
265
+ prevDataLengthRef.current !== dataLength;
266
+
267
+ prevStartIndexRef.current = startIndex;
268
+ prevEndIndexRef.current = endIndex;
269
+ prevDataLengthRef.current = dataLength;
270
+
271
+ // Measure container
272
+ if (containerRef.current) {
273
+ const height = Math.round(measureElement(containerRef.current).height);
274
+ if (measuredContainerHeight !== height && height > 0) {
275
+ setMeasuredContainerHeight(height);
276
+ }
277
+ }
278
+
279
+ // FIX3: Check for significant height changes in visible items
280
+ // This catches ThinkingBlock expand/collapse which can cause large height deltas
281
+ const HEIGHT_CHANGE_THRESHOLD = 5; // pixels
282
+ let forceRemeasure = false;
283
+
284
+ for (let i = startIndex; i <= endIndex; i++) {
285
+ const itemRef = itemRefs.current[i];
286
+ if (itemRef) {
287
+ const currentHeight = Math.round(measureElement(itemRef).height);
288
+ const cachedHeight = heights[i] ?? 0;
289
+ if (Math.abs(currentHeight - cachedHeight) > HEIGHT_CHANGE_THRESHOLD) {
290
+ forceRemeasure = true;
291
+ break;
292
+ }
293
+ }
294
+ }
295
+
296
+ // Measure visible items when:
297
+ // 1. Range changed (scroll/data update)
298
+ // 2. Initial mount (heights.length === 0)
299
+ // 3. measureTick changed (periodic check for dynamic content)
300
+ // 4. FIX3: Significant height change detected (forceRemeasure)
301
+ // Note: measureTick is in deps, so this runs periodically
302
+ if (!rangeChanged && heights.length > 0 && !forceRemeasure) {
303
+ // No changes needed - skip expensive remeasurement
304
+ return;
305
+ }
306
+
307
+ // Measure visible items
308
+ let newHeights: number[] | null = null;
309
+ for (let i = startIndex; i <= endIndex; i++) {
310
+ const itemRef = itemRefs.current[i];
311
+ if (itemRef) {
312
+ const height = Math.round(measureElement(itemRef).height);
313
+ // Only update if height actually changed and is valid
314
+ if (height > 0 && height !== heights[i]) {
315
+ if (!newHeights) {
316
+ newHeights = [...heights];
317
+ }
318
+ newHeights[i] = height;
319
+ }
320
+ }
321
+ }
322
+ if (newHeights) {
323
+ setHeights(newHeights);
324
+ }
325
+ }, [startIndex, endIndex, dataLength, heights, measuredContainerHeight, measureTick]);
326
+
327
+ return {
328
+ heights,
329
+ offsets,
330
+ totalHeight,
331
+ startIndex,
332
+ endIndex,
333
+ topSpacerHeight,
334
+ bottomSpacerHeight,
335
+ itemRefCallback,
336
+ containerRef,
337
+ measuredContainerHeight,
338
+ isOversize,
339
+ };
340
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * VirtualizedList Component
3
+ *
4
+ * A high-performance virtualized list for terminal UIs that only
5
+ * renders visible items. Ported from Gemini CLI.
6
+ *
7
+ * @module tui/components/common/VirtualizedList
8
+ */
9
+
10
+ export {
11
+ type UseBatchedScrollReturn,
12
+ type UseScrollAnchorProps,
13
+ type UseScrollAnchorReturn,
14
+ type UseVirtualizationProps,
15
+ type UseVirtualizationReturn,
16
+ useBatchedScroll,
17
+ useScrollAnchor,
18
+ useVirtualization,
19
+ } from "./hooks/index.js";
20
+
21
+ export {
22
+ type HeightCache,
23
+ SCROLL_TO_ITEM_END,
24
+ type ScrollAnchor,
25
+ } from "./types.js";
26
+ export {
27
+ VirtualizedList,
28
+ type VirtualizedListProps,
29
+ type VirtualizedListRef,
30
+ } from "./VirtualizedList.js";