@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,145 @@
1
+ /**
2
+ * useInputHighlight Hook (T009-HL)
3
+ *
4
+ * React hook that memoizes input text highlighting transformations.
5
+ * Provides efficient highlight parsing with caching.
6
+ *
7
+ * @module tui/hooks/useInputHighlight
8
+ */
9
+
10
+ import { useMemo, useRef } from "react";
11
+ import {
12
+ findSegmentAtCursor,
13
+ type HighlightResult,
14
+ type HighlightSegment,
15
+ parseHighlights,
16
+ } from "../components/Input/highlight.js";
17
+
18
+ // =============================================================================
19
+ // Types
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Options for the useInputHighlight hook.
24
+ */
25
+ export interface UseInputHighlightOptions {
26
+ /** The input text to highlight */
27
+ readonly text: string;
28
+ /** Whether highlighting is enabled */
29
+ readonly enabled?: boolean;
30
+ /** Current cursor position (optional) */
31
+ readonly cursorPosition?: number;
32
+ }
33
+
34
+ /**
35
+ * Return value from useInputHighlight hook.
36
+ */
37
+ export interface UseInputHighlightReturn {
38
+ /** Parsed highlight result */
39
+ readonly result: HighlightResult;
40
+ /** Whether any highlights were found */
41
+ readonly hasHighlights: boolean;
42
+ /** The segment containing the cursor (if any) */
43
+ readonly cursorSegment: HighlightSegment | undefined;
44
+ /** Whether highlighting is active */
45
+ readonly isHighlighting: boolean;
46
+ }
47
+
48
+ // =============================================================================
49
+ // Hook Implementation
50
+ // =============================================================================
51
+
52
+ /**
53
+ * Hook for memoized input text highlighting.
54
+ *
55
+ * Caches the highlight parsing result and only recomputes when text changes.
56
+ * Also provides cursor-aware segment information.
57
+ *
58
+ * @param options - Hook configuration
59
+ * @returns Highlight result and metadata
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * function MyInput({ value, cursorPosition }) {
64
+ * const { result, hasHighlights, cursorSegment } = useInputHighlight({
65
+ * text: value,
66
+ * enabled: true,
67
+ * cursorPosition,
68
+ * });
69
+ *
70
+ * return (
71
+ * <HighlightedText
72
+ * text={value}
73
+ * highlightResult={result}
74
+ * cursorPosition={cursorPosition}
75
+ * showCursor
76
+ * />
77
+ * );
78
+ * }
79
+ * ```
80
+ */
81
+ export function useInputHighlight({
82
+ text,
83
+ enabled = true,
84
+ cursorPosition,
85
+ }: UseInputHighlightOptions): UseInputHighlightReturn {
86
+ // Cache for avoiding reparse on cursor-only changes
87
+ const cacheRef = useRef<{ text: string; result: HighlightResult } | null>(null);
88
+
89
+ // Parse highlights (memoized by text)
90
+ const result = useMemo(() => {
91
+ // Return empty result if disabled
92
+ if (!enabled) {
93
+ return { segments: [], hasHighlights: false };
94
+ }
95
+
96
+ // Check cache first
97
+ if (cacheRef.current?.text === text) {
98
+ return cacheRef.current.result;
99
+ }
100
+
101
+ // Parse and cache
102
+ const parsed = parseHighlights(text);
103
+ cacheRef.current = { text, result: parsed };
104
+ return parsed;
105
+ }, [text, enabled]);
106
+
107
+ // Find cursor segment (memoized by cursor position and segments)
108
+ const cursorSegment = useMemo(() => {
109
+ if (cursorPosition === undefined || !result.hasHighlights) {
110
+ return undefined;
111
+ }
112
+ return findSegmentAtCursor(result.segments, cursorPosition);
113
+ }, [result.segments, cursorPosition, result.hasHighlights]);
114
+
115
+ return {
116
+ result,
117
+ hasHighlights: result.hasHighlights,
118
+ cursorSegment,
119
+ isHighlighting: enabled && result.hasHighlights,
120
+ };
121
+ }
122
+
123
+ // =============================================================================
124
+ // Utility Hooks
125
+ // =============================================================================
126
+
127
+ /**
128
+ * Hook for multiline input highlighting.
129
+ * Parses each line separately for better performance with large inputs.
130
+ *
131
+ * @param lines - Array of line strings
132
+ * @param enabled - Whether highlighting is enabled
133
+ * @returns Array of highlight results, one per line
134
+ */
135
+ export function useMultilineHighlight(
136
+ lines: readonly string[],
137
+ enabled = true
138
+ ): readonly HighlightResult[] {
139
+ return useMemo(() => {
140
+ if (!enabled) {
141
+ return lines.map(() => ({ segments: [], hasHighlights: false }));
142
+ }
143
+ return lines.map((line) => parseHighlights(line));
144
+ }, [lines, enabled]);
145
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * useInputHistory Hook (T012)
3
+ *
4
+ * React hook for managing input history with navigation.
5
+ * Supports navigating through previous entries and optional persistence.
6
+ *
7
+ * @module @vellum/cli
8
+ */
9
+
10
+ import { useCallback, useRef, useState } from "react";
11
+
12
+ /**
13
+ * Options for the useInputHistory hook.
14
+ */
15
+ export interface UseInputHistoryOptions {
16
+ /** Maximum number of history entries to keep (default: 100) */
17
+ maxItems?: number;
18
+ /** localStorage key for persistence (if provided, history will be persisted) */
19
+ persistKey?: string;
20
+ }
21
+
22
+ /**
23
+ * Return value of useInputHistory hook.
24
+ */
25
+ export interface UseInputHistoryReturn {
26
+ /** Read-only array of history entries (newest last) */
27
+ history: readonly string[];
28
+ /** Current navigation index (-1 when not navigating) */
29
+ currentIndex: number;
30
+ /** Add a new entry to history */
31
+ addToHistory: (entry: string) => void;
32
+ /** Navigate up (older) or down (newer) through history */
33
+ navigateHistory: (direction: "up" | "down") => string | null;
34
+ /** Clear all history entries */
35
+ clearHistory: () => void;
36
+ /** Get the current entry at navigation index */
37
+ getCurrentEntry: () => string | null;
38
+ }
39
+
40
+ /**
41
+ * Load history from storage.
42
+ */
43
+ function loadFromStorage(key: string): string[] {
44
+ try {
45
+ if (typeof globalThis.localStorage !== "undefined") {
46
+ const stored = globalThis.localStorage.getItem(key);
47
+ if (stored) {
48
+ const parsed = JSON.parse(stored);
49
+ if (Array.isArray(parsed)) {
50
+ return parsed.filter((item): item is string => typeof item === "string");
51
+ }
52
+ }
53
+ }
54
+ } catch {
55
+ // Ignore storage errors
56
+ }
57
+ return [];
58
+ }
59
+
60
+ /**
61
+ * Save history to storage.
62
+ */
63
+ function saveToStorage(key: string, history: string[]): void {
64
+ try {
65
+ if (typeof globalThis.localStorage !== "undefined") {
66
+ globalThis.localStorage.setItem(key, JSON.stringify(history));
67
+ }
68
+ } catch {
69
+ // Ignore storage errors
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Hook for managing input history with navigation.
75
+ *
76
+ * Provides functionality for storing command history, navigating through
77
+ * previous entries with up/down arrows, and optional persistence.
78
+ *
79
+ * @param options - Configuration options
80
+ * @returns History state and manipulation functions
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * function InputComponent() {
85
+ * const [input, setInput] = useState('');
86
+ * const { navigateHistory, addToHistory } = useInputHistory({ maxItems: 50 });
87
+ *
88
+ * const handleKeyDown = (key: string) => {
89
+ * if (key === 'up') {
90
+ * const prev = navigateHistory('up');
91
+ * if (prev !== null) setInput(prev);
92
+ * } else if (key === 'down') {
93
+ * const next = navigateHistory('down');
94
+ * if (next !== null) setInput(next);
95
+ * }
96
+ * };
97
+ *
98
+ * const handleSubmit = () => {
99
+ * if (input.trim()) {
100
+ * addToHistory(input);
101
+ * // process input...
102
+ * setInput('');
103
+ * }
104
+ * };
105
+ *
106
+ * return <TextInput value={input} onSubmit={handleSubmit} />;
107
+ * }
108
+ * ```
109
+ */
110
+ export function useInputHistory(options: UseInputHistoryOptions = {}): UseInputHistoryReturn {
111
+ const { maxItems = 100, persistKey } = options;
112
+
113
+ // Initialize history from storage if persistKey provided
114
+ const [history, setHistory] = useState<string[]>(() => {
115
+ if (persistKey) {
116
+ return loadFromStorage(persistKey);
117
+ }
118
+ return [];
119
+ });
120
+
121
+ // Current navigation index: -1 means not navigating (at the "new entry" position)
122
+ const [currentIndex, setCurrentIndex] = useState(-1);
123
+
124
+ // Ref to track the temp entry when user starts navigating
125
+ const tempEntryRef = useRef<string>("");
126
+
127
+ /**
128
+ * Add a new entry to history.
129
+ * Skips if entry is empty or same as the last entry (no consecutive duplicates).
130
+ * Resets navigation index.
131
+ */
132
+ const addToHistory = useCallback(
133
+ (entry: string) => {
134
+ const trimmed = entry.trim();
135
+ if (!trimmed) {
136
+ return;
137
+ }
138
+
139
+ setHistory((prev) => {
140
+ // Skip if same as last entry (no consecutive duplicates)
141
+ if (prev.length > 0 && prev[prev.length - 1] === trimmed) {
142
+ return prev;
143
+ }
144
+
145
+ const newHistory = [...prev, trimmed];
146
+
147
+ // Trim to maxItems if exceeded
148
+ const trimmedHistory =
149
+ newHistory.length > maxItems ? newHistory.slice(-maxItems) : newHistory;
150
+
151
+ // Persist if key provided
152
+ if (persistKey) {
153
+ saveToStorage(persistKey, trimmedHistory);
154
+ }
155
+
156
+ return trimmedHistory;
157
+ });
158
+
159
+ // Reset navigation index
160
+ setCurrentIndex(-1);
161
+ tempEntryRef.current = "";
162
+ },
163
+ [maxItems, persistKey]
164
+ );
165
+
166
+ /**
167
+ * Navigate through history.
168
+ * 'up' moves to older entries, 'down' moves to newer entries.
169
+ * Returns the entry at the new position, or null if at boundary.
170
+ */
171
+ const navigateHistory = useCallback(
172
+ (direction: "up" | "down"): string | null => {
173
+ if (history.length === 0) {
174
+ return null;
175
+ }
176
+
177
+ let newIndex: number;
178
+
179
+ if (direction === "up") {
180
+ // Moving to older entries
181
+ if (currentIndex === -1) {
182
+ // Start navigating from the most recent entry
183
+ newIndex = history.length - 1;
184
+ } else if (currentIndex > 0) {
185
+ // Move to older entry
186
+ newIndex = currentIndex - 1;
187
+ } else {
188
+ // Already at the oldest entry
189
+ return null;
190
+ }
191
+ } else {
192
+ // direction === 'down'
193
+ // Moving to newer entries
194
+ if (currentIndex === -1) {
195
+ // Not navigating, nothing to do
196
+ return null;
197
+ }
198
+ if (currentIndex < history.length - 1) {
199
+ // Move to newer entry
200
+ newIndex = currentIndex + 1;
201
+ } else {
202
+ // At the newest entry, return to "new entry" position
203
+ setCurrentIndex(-1);
204
+ return tempEntryRef.current || null;
205
+ }
206
+ }
207
+
208
+ setCurrentIndex(newIndex);
209
+ return history[newIndex] ?? null;
210
+ },
211
+ [history, currentIndex]
212
+ );
213
+
214
+ /**
215
+ * Clear all history entries.
216
+ */
217
+ const clearHistory = useCallback(() => {
218
+ setHistory([]);
219
+ setCurrentIndex(-1);
220
+ tempEntryRef.current = "";
221
+
222
+ if (persistKey) {
223
+ saveToStorage(persistKey, []);
224
+ }
225
+ }, [persistKey]);
226
+
227
+ /**
228
+ * Get the current entry at navigation index.
229
+ * Returns null if not navigating or history is empty.
230
+ */
231
+ const getCurrentEntry = useCallback((): string | null => {
232
+ if (currentIndex === -1 || history.length === 0) {
233
+ return null;
234
+ }
235
+ return history[currentIndex] ?? null;
236
+ }, [history, currentIndex]);
237
+
238
+ return {
239
+ history,
240
+ currentIndex,
241
+ addToHistory,
242
+ navigateHistory,
243
+ clearHistory,
244
+ getCurrentEntry,
245
+ };
246
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Keyboard Scroll Hook
3
+ *
4
+ * Handles keyboard events for scrolling in the TUI.
5
+ * Integrates with useScrollController to provide intuitive keyboard navigation.
6
+ *
7
+ * Supported keys:
8
+ * - PageUp / Ctrl+U: Scroll up half viewport
9
+ * - PageDown / Ctrl+D: Scroll down half viewport
10
+ * - Home / Ctrl+Home: Jump to top
11
+ * - End / Ctrl+End: Jump to bottom (follow mode)
12
+ * - ↑ / k: Scroll up one line
13
+ * - ↓ / j: Scroll down one line
14
+ *
15
+ * @module tui/hooks/useKeyboardScroll
16
+ */
17
+
18
+ import type { Key } from "ink";
19
+ import { useInput } from "ink";
20
+ import { useMemo } from "react";
21
+ import { isEndKey, isHomeKey } from "../types/ink-extended.js";
22
+ import type { ViewportScrollActions, ViewportScrollState } from "./useScrollController.js";
23
+
24
+ // =============================================================================
25
+ // Types
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Options for useKeyboardScroll hook
30
+ */
31
+ export interface UseKeyboardScrollOptions {
32
+ /** Current scroll state from useScrollController */
33
+ readonly state: ViewportScrollState;
34
+ /** Scroll actions from useScrollController */
35
+ readonly actions: ViewportScrollActions;
36
+ /** Whether keyboard handling is enabled (default: true) */
37
+ readonly enabled?: boolean;
38
+ /** Enable vim-style keys (j/k for up/down) (default: true) */
39
+ readonly vimKeys?: boolean;
40
+ /** Custom lines per single step (default: 1) */
41
+ readonly stepLines?: number;
42
+ /** Custom lines per half-page (default: viewportHeight / 2) */
43
+ readonly halfPageLines?: number;
44
+ }
45
+
46
+ /**
47
+ * Return value of useKeyboardScroll hook
48
+ */
49
+ export interface UseKeyboardScrollReturn {
50
+ /** Manual key handler for custom input handling */
51
+ readonly handleKey: (input: string, key: Key) => boolean;
52
+ /** Shortcut definitions for help display */
53
+ readonly shortcuts: ReadonlyArray<KeyboardScrollShortcut>;
54
+ }
55
+
56
+ /**
57
+ * Shortcut definition for display
58
+ */
59
+ export interface KeyboardScrollShortcut {
60
+ /** Key combination (e.g., "PageUp", "Ctrl+U") */
61
+ readonly key: string;
62
+ /** Description of the action */
63
+ readonly description: string;
64
+ }
65
+
66
+ // =============================================================================
67
+ // Constants
68
+ // =============================================================================
69
+
70
+ /**
71
+ * Standard shortcuts for help display
72
+ */
73
+ const SCROLL_SHORTCUTS: ReadonlyArray<KeyboardScrollShortcut> = [
74
+ { key: "↑/k", description: "Scroll up one line" },
75
+ { key: "↓/j", description: "Scroll down one line" },
76
+ { key: "PageUp/Ctrl+U", description: "Scroll up half page" },
77
+ { key: "PageDown/Ctrl+D", description: "Scroll down half page" },
78
+ { key: "Home", description: "Jump to top" },
79
+ { key: "End", description: "Jump to bottom (follow mode)" },
80
+ ];
81
+
82
+ // =============================================================================
83
+ // Hook
84
+ // =============================================================================
85
+
86
+ /**
87
+ * useKeyboardScroll - Handles keyboard events for scrolling
88
+ *
89
+ * Automatically registers keyboard handlers with useInput when enabled.
90
+ * Also provides a manual handleKey function for custom input handling.
91
+ *
92
+ * @example
93
+ * ```tsx
94
+ * const [scrollState, scrollActions] = useScrollController({ viewportHeight: 20 });
95
+ *
96
+ * // Automatic keyboard handling
97
+ * useKeyboardScroll({
98
+ * state: scrollState,
99
+ * actions: scrollActions,
100
+ * enabled: isFocused,
101
+ * });
102
+ *
103
+ * // Or manual handling in existing useInput
104
+ * const { handleKey } = useKeyboardScroll({
105
+ * state: scrollState,
106
+ * actions: scrollActions,
107
+ * enabled: false, // Don't auto-register
108
+ * });
109
+ *
110
+ * useInput((input, key) => {
111
+ * if (handleKey(input, key)) return; // Handled by scroll
112
+ * // ... other handlers
113
+ * });
114
+ * ```
115
+ *
116
+ * @param options - Configuration options
117
+ * @returns Scroll handling utilities
118
+ */
119
+ export function useKeyboardScroll(options: UseKeyboardScrollOptions): UseKeyboardScrollReturn {
120
+ const { state, actions, enabled = true, vimKeys = true, stepLines = 1, halfPageLines } = options;
121
+
122
+ // Calculate half-page lines (default to half viewport)
123
+ const computedHalfPage = halfPageLines ?? Math.max(1, Math.floor(state.viewportHeight / 2));
124
+
125
+ // Maximum offset for jump to top
126
+ const maxOffset = Math.max(0, state.totalHeight - state.viewportHeight);
127
+
128
+ /**
129
+ * Handle a keyboard input, returning true if handled
130
+ */
131
+ const handleKey = useMemo(() => {
132
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Keyboard handler needs many key combinations
133
+ return (input: string, key: Key): boolean => {
134
+ // Arrow up or vim 'k'
135
+ if (key.upArrow || (vimKeys && input === "k")) {
136
+ actions.scrollUp(stepLines);
137
+ return true;
138
+ }
139
+
140
+ // Arrow down or vim 'j'
141
+ if (key.downArrow || (vimKeys && input === "j")) {
142
+ actions.scrollDown(stepLines);
143
+ return true;
144
+ }
145
+
146
+ // PageUp (Ink provides this as key.pageUp in some versions)
147
+ // Also handle Ctrl+U
148
+ if (key.pageUp || (key.ctrl && input === "u")) {
149
+ actions.scrollUp(computedHalfPage);
150
+ return true;
151
+ }
152
+
153
+ // PageDown
154
+ // Also handle Ctrl+D
155
+ if (key.pageDown || (key.ctrl && input === "d")) {
156
+ actions.scrollDown(computedHalfPage);
157
+ return true;
158
+ }
159
+
160
+ // Home - jump to top
161
+ if (isHomeKey(input)) {
162
+ actions.jumpTo(maxOffset);
163
+ return true;
164
+ }
165
+
166
+ // End - jump to bottom (follow mode)
167
+ if (isEndKey(input)) {
168
+ actions.scrollToBottom();
169
+ return true;
170
+ }
171
+
172
+ return false;
173
+ };
174
+ }, [actions, vimKeys, stepLines, computedHalfPage, maxOffset]);
175
+
176
+ // Auto-register with useInput when enabled
177
+ useInput(
178
+ (input, key) => {
179
+ handleKey(input, key);
180
+ },
181
+ { isActive: enabled }
182
+ );
183
+
184
+ return {
185
+ handleKey,
186
+ shortcuts: SCROLL_SHORTCUTS,
187
+ };
188
+ }
189
+
190
+ // =============================================================================
191
+ // Utility Functions
192
+ // =============================================================================
193
+
194
+ /**
195
+ * Format shortcuts for display in help
196
+ */
197
+ export function formatScrollShortcuts(): string {
198
+ return SCROLL_SHORTCUTS.map((s) => `${s.key}: ${s.description}`).join("\n");
199
+ }
200
+
201
+ /**
202
+ * Get shortcuts without vim keys
203
+ */
204
+ export function getScrollShortcutsNoVim(): ReadonlyArray<KeyboardScrollShortcut> {
205
+ return SCROLL_SHORTCUTS.map((shortcut) => ({
206
+ ...shortcut,
207
+ key: shortcut.key.replace("/k", "").replace("/j", ""),
208
+ }));
209
+ }