@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,579 @@
1
+ /**
2
+ * useHotkeys Hook (T042)
3
+ *
4
+ * React hook for managing keyboard shortcuts in the TUI.
5
+ * Provides a unified system for hotkey registration with support for
6
+ * modifier keys (Ctrl, Shift, Alt) and scoped handlers.
7
+ *
8
+ * @module @vellum/cli
9
+ */
10
+
11
+ import type { Key } from "ink";
12
+ import { useInput } from "ink";
13
+ import { useCallback, useMemo } from "react";
14
+ import { useApp } from "../context/AppContext.js";
15
+ import type { ExtendedKey } from "../types/ink-extended.js";
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Scope for hotkey activation.
23
+ * - global: Active regardless of focused area
24
+ * - input: Active only when input area is focused
25
+ * - messages: Active only when message area is focused
26
+ */
27
+ export type HotkeyScope = "global" | "input" | "messages" | "tools";
28
+
29
+ /**
30
+ * Definition for a single hotkey binding.
31
+ */
32
+ export interface HotkeyDefinition {
33
+ /** The key to match (e.g., 'c', 'l', 'v', 'f1') */
34
+ readonly key: string;
35
+ /** Whether Ctrl modifier is required */
36
+ readonly ctrl?: boolean;
37
+ /** Whether Shift modifier is required */
38
+ readonly shift?: boolean;
39
+ /** Whether Alt/Option modifier is required */
40
+ readonly alt?: boolean;
41
+ /** Handler function to execute when hotkey is triggered */
42
+ readonly handler: () => void;
43
+ /** Human-readable description for help display */
44
+ readonly description?: string;
45
+ /** Scope where the hotkey is active (default: 'global') */
46
+ readonly scope?: HotkeyScope;
47
+ }
48
+
49
+ /**
50
+ * Options for the useHotkeys hook.
51
+ */
52
+ export interface UseHotkeysOptions {
53
+ /** Whether hotkey handling is enabled (default: true) */
54
+ readonly enabled?: boolean;
55
+ /** Override scope for all hotkeys in this hook */
56
+ readonly scope?: HotkeyScope;
57
+ }
58
+
59
+ /**
60
+ * Return value of useHotkeys hook.
61
+ */
62
+ export interface UseHotkeysReturn {
63
+ /** Get all registered hotkey definitions */
64
+ readonly hotkeys: ReadonlyArray<HotkeyDefinition>;
65
+ /** Check if a key combination matches any hotkey */
66
+ readonly matchHotkey: (
67
+ key: string,
68
+ modifiers: { ctrl?: boolean; shift?: boolean; alt?: boolean }
69
+ ) => HotkeyDefinition | null;
70
+ }
71
+
72
+ // =============================================================================
73
+ // Key Normalization
74
+ // =============================================================================
75
+
76
+ /**
77
+ * Normalize a key string to a consistent format.
78
+ * Handles special keys and case normalization.
79
+ */
80
+ function normalizeKey(key: string): string {
81
+ // Handle special keys
82
+ const specialKeys: Record<string, string> = {
83
+ escape: "escape",
84
+ esc: "escape",
85
+ return: "return",
86
+ enter: "return",
87
+ tab: "tab",
88
+ backspace: "backspace",
89
+ delete: "delete",
90
+ up: "up",
91
+ down: "down",
92
+ left: "left",
93
+ right: "right",
94
+ pageup: "pageup",
95
+ pagedown: "pagedown",
96
+ home: "home",
97
+ end: "end",
98
+ f1: "f1",
99
+ f2: "f2",
100
+ f3: "f3",
101
+ f4: "f4",
102
+ f5: "f5",
103
+ f6: "f6",
104
+ f7: "f7",
105
+ f8: "f8",
106
+ f9: "f9",
107
+ f10: "f10",
108
+ f11: "f11",
109
+ f12: "f12",
110
+ };
111
+
112
+ const normalized = key.toLowerCase();
113
+ return specialKeys[normalized] ?? normalized;
114
+ }
115
+
116
+ /**
117
+ * Get the ink key flag for a special key.
118
+ */
119
+ function getInkKeyFlag(inkKey: Key, normalizedHotkey: string): boolean | undefined {
120
+ const inkKeyMap: Record<string, boolean | undefined> = {
121
+ return: inkKey.return,
122
+ escape: inkKey.escape,
123
+ tab: inkKey.tab,
124
+ backspace: inkKey.backspace,
125
+ delete: inkKey.delete,
126
+ up: inkKey.upArrow,
127
+ down: inkKey.downArrow,
128
+ left: inkKey.leftArrow,
129
+ right: inkKey.rightArrow,
130
+ pageup: inkKey.pageUp,
131
+ pagedown: inkKey.pageDown,
132
+ };
133
+ return inkKeyMap[normalizedHotkey];
134
+ }
135
+
136
+ /**
137
+ * Check if the key component matches (ignoring modifiers).
138
+ */
139
+ function keyMatches(normalizedInput: string, normalizedHotkey: string, inkKey: Key): boolean {
140
+ // Windows/Ink may deliver Ctrl+\\ as the raw control character 0x1C (File Separator)
141
+ // instead of a literal "\\" input key. Treat that control byte as "\\" for matching.
142
+ if (normalizedHotkey === "\\" && normalizedInput === "\x1c") {
143
+ return true;
144
+ }
145
+
146
+ // Direct string match
147
+ if (normalizedInput === normalizedHotkey) {
148
+ return true;
149
+ }
150
+
151
+ // Check for special key flags from Ink
152
+ const inkFlag = getInkKeyFlag(inkKey, normalizedHotkey);
153
+ return inkFlag === true;
154
+ }
155
+
156
+ /**
157
+ * Check if modifiers match exactly.
158
+ */
159
+ function modifiersMatch(inkKey: Key, hotkey: HotkeyDefinition): boolean {
160
+ const ctrlRequired = hotkey.ctrl ?? false;
161
+ const shiftRequired = hotkey.shift ?? false;
162
+ const altRequired = hotkey.alt ?? false;
163
+
164
+ const ctrlPressed = inkKey.ctrl ?? false;
165
+ const shiftPressed = inkKey.shift ?? false;
166
+ const altPressed = inkKey.meta ?? false; // Ink uses 'meta' for Alt
167
+
168
+ return (
169
+ ctrlRequired === ctrlPressed && shiftRequired === shiftPressed && altRequired === altPressed
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Check if a key event matches a hotkey definition.
175
+ */
176
+ function matchesHotkey(inputKey: string, inkKey: Key, hotkey: HotkeyDefinition): boolean {
177
+ const normalizedInput = normalizeKey(inputKey);
178
+ const normalizedHotkey = normalizeKey(hotkey.key);
179
+
180
+ // Check key matches first
181
+ if (!keyMatches(normalizedInput, normalizedHotkey, inkKey)) {
182
+ return false;
183
+ }
184
+
185
+ // Then check modifiers
186
+ // If Ink delivers Ctrl+\\ as \x1c without setting the ctrl modifier, interpret \x1c as Ctrl+\\.
187
+ const inkKeyWithCtrlBackslashFix: Key =
188
+ normalizedHotkey === "\\" && normalizedInput === "\x1c" ? { ...inkKey, ctrl: true } : inkKey;
189
+
190
+ return modifiersMatch(inkKeyWithCtrlBackslashFix, hotkey);
191
+ }
192
+
193
+ // =============================================================================
194
+ // Hook Implementation
195
+ // =============================================================================
196
+
197
+ /**
198
+ * useHotkeys hook for managing keyboard shortcuts.
199
+ *
200
+ * Provides a declarative way to register hotkeys with support for:
201
+ * - Modifier keys (Ctrl, Shift, Alt)
202
+ * - Scoped handlers (global, input, messages)
203
+ * - Help text generation
204
+ *
205
+ * @example
206
+ * ```tsx
207
+ * function MyComponent() {
208
+ * const { dispatch } = useApp();
209
+ *
210
+ * useHotkeys([
211
+ * {
212
+ * key: 'c',
213
+ * ctrl: true,
214
+ * handler: () => process.exit(0),
215
+ * description: 'Exit application',
216
+ * scope: 'global',
217
+ * },
218
+ * {
219
+ * key: 'v',
220
+ * ctrl: true,
221
+ * handler: () => dispatch({ type: 'TOGGLE_VIM_MODE' }),
222
+ * description: 'Toggle Vim mode',
223
+ * scope: 'input',
224
+ * },
225
+ * ]);
226
+ *
227
+ * return <Box>...</Box>;
228
+ * }
229
+ * ```
230
+ *
231
+ * @param hotkeys - Array of hotkey definitions to register
232
+ * @param options - Configuration options
233
+ * @returns Object with hotkey utilities
234
+ */
235
+ export function useHotkeys(
236
+ hotkeys: ReadonlyArray<HotkeyDefinition>,
237
+ options: UseHotkeysOptions = {}
238
+ ): UseHotkeysReturn {
239
+ const { enabled = true, scope: optionsScope } = options;
240
+ const { state } = useApp();
241
+
242
+ // Memoize the hotkey list with scope overrides applied
243
+ const resolvedHotkeys = useMemo(
244
+ () =>
245
+ hotkeys.map((hotkey) => ({
246
+ ...hotkey,
247
+ scope: optionsScope ?? hotkey.scope ?? "global",
248
+ })),
249
+ [hotkeys, optionsScope]
250
+ );
251
+
252
+ // Match a key combination against registered hotkeys
253
+ const matchHotkey = useCallback(
254
+ (
255
+ key: string,
256
+ modifiers: { ctrl?: boolean; shift?: boolean; alt?: boolean }
257
+ ): HotkeyDefinition | null => {
258
+ const inkKey = {
259
+ ctrl: modifiers.ctrl ?? false,
260
+ shift: modifiers.shift ?? false,
261
+ meta: modifiers.alt ?? false,
262
+ escape: key.toLowerCase() === "escape",
263
+ return: key === "\r" || key.toLowerCase() === "return",
264
+ tab: key === "\t" || key.toLowerCase() === "tab",
265
+ backspace: key === "\x7f" || key.toLowerCase() === "backspace",
266
+ delete: key.toLowerCase() === "delete",
267
+ upArrow: key.toLowerCase() === "up",
268
+ downArrow: key.toLowerCase() === "down",
269
+ leftArrow: key.toLowerCase() === "left",
270
+ rightArrow: key.toLowerCase() === "right",
271
+ pageUp: key.toLowerCase() === "pageup",
272
+ pageDown: key.toLowerCase() === "pagedown",
273
+ home: key.toLowerCase() === "home",
274
+ end: key.toLowerCase() === "end",
275
+ } as ExtendedKey;
276
+
277
+ for (const hotkey of resolvedHotkeys) {
278
+ if (matchesHotkey(key, inkKey, hotkey)) {
279
+ return hotkey;
280
+ }
281
+ }
282
+
283
+ return null;
284
+ },
285
+ [resolvedHotkeys]
286
+ );
287
+
288
+ // Check if a hotkey should be active based on current scope
289
+ const isScopeActive = useCallback(
290
+ (hotkeyScope: HotkeyScope): boolean => {
291
+ if (hotkeyScope === "global") {
292
+ return true;
293
+ }
294
+ // Map focusedArea to hotkey scope
295
+ const focusedArea = state.focusedArea;
296
+ if (hotkeyScope === "input" && focusedArea === "input") {
297
+ return true;
298
+ }
299
+ if (hotkeyScope === "messages" && focusedArea === "messages") {
300
+ return true;
301
+ }
302
+ if (hotkeyScope === "tools" && focusedArea === "tools") {
303
+ return true;
304
+ }
305
+ return false;
306
+ },
307
+ [state.focusedArea]
308
+ );
309
+
310
+ // Handle keyboard input via Ink's useInput
311
+ useInput(
312
+ (input, key) => {
313
+ if (!enabled) return;
314
+
315
+ // Find matching hotkey
316
+ for (const hotkey of resolvedHotkeys) {
317
+ if (matchesHotkey(input, key, hotkey)) {
318
+ // Check scope
319
+ if (isScopeActive(hotkey.scope as HotkeyScope)) {
320
+ hotkey.handler();
321
+ return;
322
+ }
323
+ }
324
+ }
325
+ },
326
+ { isActive: enabled }
327
+ );
328
+
329
+ return {
330
+ hotkeys: resolvedHotkeys,
331
+ matchHotkey,
332
+ };
333
+ }
334
+
335
+ // =============================================================================
336
+ // Standard Hotkey Presets
337
+ // =============================================================================
338
+
339
+ /**
340
+ * Standard hotkey definitions for common TUI operations.
341
+ * Use these as a starting point and customize as needed.
342
+ *
343
+ * @example
344
+ * ```tsx
345
+ * import { useHotkeys, createStandardHotkeys } from './useHotkeys.js';
346
+ *
347
+ * function App() {
348
+ * const { dispatch } = useApp();
349
+ *
350
+ * useHotkeys(createStandardHotkeys({
351
+ * onInterrupt: () => process.exit(0),
352
+ * onClearScreen: () => console.clear(),
353
+ * onToggleVim: () => dispatch({ type: 'TOGGLE_VIM_MODE' }),
354
+ * onShowHelp: () => setShowHelp(true),
355
+ * }));
356
+ *
357
+ * return <Box>...</Box>;
358
+ * }
359
+ * ```
360
+ */
361
+ export interface StandardHotkeyHandlers {
362
+ /** Ctrl+C: Interrupt/cancel current operation */
363
+ readonly onInterrupt?: () => void;
364
+ /** Ctrl+L: Clear the screen */
365
+ readonly onClearScreen?: () => void;
366
+ /** Ctrl+V: Toggle Vim editing mode */
367
+ readonly onToggleVim?: () => void;
368
+ /** Ctrl+Y: Accept suggestion */
369
+ readonly onAcceptSuggestion?: () => void;
370
+ /** Ctrl+T: Toggle thinking display */
371
+ readonly onToggleThinking?: () => void;
372
+ /** F1: Show help */
373
+ readonly onShowHelp?: () => void;
374
+ /** Ctrl+Shift+1: Switch to trust mode 1 (paranoid) */
375
+ readonly onTrustMode1?: () => void;
376
+ /** Ctrl+Shift+2: Switch to trust mode 2 (cautious) */
377
+ readonly onTrustMode2?: () => void;
378
+ /** Ctrl+Shift+3: Switch to trust mode 3 (balanced) */
379
+ readonly onTrustMode3?: () => void;
380
+ /** Ctrl+Shift+4: Switch to trust mode 4 (trusting) */
381
+ readonly onTrustMode4?: () => void;
382
+ /** Ctrl+Shift+5: Switch to trust mode 5 (yolo) */
383
+ readonly onTrustMode5?: () => void;
384
+ }
385
+
386
+ /**
387
+ * Create standard hotkey definitions with provided handlers.
388
+ *
389
+ * @param handlers - Object with handler functions for standard hotkeys
390
+ * @returns Array of hotkey definitions
391
+ */
392
+ export function createStandardHotkeys(
393
+ handlers: StandardHotkeyHandlers
394
+ ): ReadonlyArray<HotkeyDefinition> {
395
+ const hotkeys: HotkeyDefinition[] = [];
396
+
397
+ if (handlers.onInterrupt) {
398
+ hotkeys.push({
399
+ key: "c",
400
+ ctrl: true,
401
+ handler: handlers.onInterrupt,
402
+ description: "Interrupt/cancel",
403
+ scope: "global",
404
+ });
405
+ }
406
+
407
+ if (handlers.onClearScreen) {
408
+ hotkeys.push({
409
+ key: "l",
410
+ ctrl: true,
411
+ handler: handlers.onClearScreen,
412
+ description: "Clear screen",
413
+ scope: "global",
414
+ });
415
+ }
416
+
417
+ if (handlers.onToggleVim) {
418
+ hotkeys.push({
419
+ key: "v",
420
+ ctrl: true,
421
+ handler: handlers.onToggleVim,
422
+ description: "Toggle Vim mode",
423
+ scope: "input",
424
+ });
425
+ }
426
+
427
+ if (handlers.onAcceptSuggestion) {
428
+ hotkeys.push({
429
+ key: "y",
430
+ ctrl: true,
431
+ handler: handlers.onAcceptSuggestion,
432
+ description: "Accept suggestion",
433
+ scope: "input",
434
+ });
435
+ }
436
+
437
+ if (handlers.onToggleThinking) {
438
+ hotkeys.push({
439
+ key: "t",
440
+ ctrl: true,
441
+ handler: handlers.onToggleThinking,
442
+ description: "Toggle thinking display",
443
+ scope: "global",
444
+ });
445
+ }
446
+
447
+ if (handlers.onShowHelp) {
448
+ hotkeys.push({
449
+ key: "f1",
450
+ handler: handlers.onShowHelp,
451
+ description: "Show help",
452
+ scope: "global",
453
+ });
454
+ }
455
+
456
+ // Trust mode hotkeys (Ctrl+Shift+1-5)
457
+ if (handlers.onTrustMode1) {
458
+ hotkeys.push({
459
+ key: "!",
460
+ ctrl: true,
461
+ shift: true,
462
+ handler: handlers.onTrustMode1,
463
+ description: "Paranoid mode",
464
+ scope: "global",
465
+ });
466
+ }
467
+
468
+ if (handlers.onTrustMode2) {
469
+ hotkeys.push({
470
+ key: "@",
471
+ ctrl: true,
472
+ shift: true,
473
+ handler: handlers.onTrustMode2,
474
+ description: "Cautious mode",
475
+ scope: "global",
476
+ });
477
+ }
478
+
479
+ if (handlers.onTrustMode3) {
480
+ hotkeys.push({
481
+ key: "#",
482
+ ctrl: true,
483
+ shift: true,
484
+ handler: handlers.onTrustMode3,
485
+ description: "Balanced mode",
486
+ scope: "global",
487
+ });
488
+ }
489
+
490
+ if (handlers.onTrustMode4) {
491
+ hotkeys.push({
492
+ key: "$",
493
+ ctrl: true,
494
+ shift: true,
495
+ handler: handlers.onTrustMode4,
496
+ description: "Trusting mode",
497
+ scope: "global",
498
+ });
499
+ }
500
+
501
+ if (handlers.onTrustMode5) {
502
+ hotkeys.push({
503
+ key: "%",
504
+ ctrl: true,
505
+ shift: true,
506
+ handler: handlers.onTrustMode5,
507
+ description: "YOLO mode",
508
+ scope: "global",
509
+ });
510
+ }
511
+
512
+ return hotkeys;
513
+ }
514
+
515
+ /**
516
+ * Format hotkey for display in help text.
517
+ *
518
+ * @param hotkey - Hotkey definition to format
519
+ * @returns Formatted string like "Ctrl+Shift+V"
520
+ */
521
+ export function formatHotkey(hotkey: HotkeyDefinition): string {
522
+ const parts: string[] = [];
523
+
524
+ if (hotkey.ctrl) parts.push("Ctrl");
525
+ if (hotkey.shift) parts.push("Shift");
526
+ if (hotkey.alt) parts.push("Alt");
527
+
528
+ // Format the key
529
+ const key = hotkey.key.length === 1 ? hotkey.key.toUpperCase() : hotkey.key;
530
+ parts.push(key);
531
+
532
+ return parts.join("+");
533
+ }
534
+
535
+ /**
536
+ * Generate help text from hotkey definitions.
537
+ *
538
+ * @param hotkeys - Array of hotkey definitions
539
+ * @returns Formatted help text string
540
+ */
541
+ export function generateHotkeyHelp(hotkeys: ReadonlyArray<HotkeyDefinition>): string {
542
+ const lines: string[] = ["Keyboard Shortcuts:", ""];
543
+
544
+ // Group by scope
545
+ const byScope = new Map<HotkeyScope, HotkeyDefinition[]>();
546
+
547
+ for (const hotkey of hotkeys) {
548
+ const scope = (hotkey.scope ?? "global") as HotkeyScope;
549
+ if (!byScope.has(scope)) {
550
+ byScope.set(scope, []);
551
+ }
552
+ const scopeArray = byScope.get(scope);
553
+ if (scopeArray) {
554
+ scopeArray.push(hotkey);
555
+ }
556
+ }
557
+
558
+ // Format each scope
559
+ const scopeLabels: Record<HotkeyScope, string> = {
560
+ global: "Global",
561
+ input: "Input Area",
562
+ messages: "Messages Area",
563
+ tools: "Tools Area",
564
+ };
565
+
566
+ for (const [scope, scopeHotkeys] of byScope) {
567
+ lines.push(` ${scopeLabels[scope]}:`);
568
+
569
+ for (const hotkey of scopeHotkeys) {
570
+ const formatted = formatHotkey(hotkey);
571
+ const description = hotkey.description ?? "No description";
572
+ lines.push(` ${formatted.padEnd(20)} ${description}`);
573
+ }
574
+
575
+ lines.push("");
576
+ }
577
+
578
+ return lines.join("\n");
579
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Image Paste Hook
3
+ *
4
+ * Handles pasting images from clipboard into the TUI.
5
+ * Placeholder implementation - to be expanded.
6
+ *
7
+ * @module tui/hooks/useImagePaste
8
+ */
9
+
10
+ import { useCallback, useState } from "react";
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ export interface PastedImage {
17
+ /** Image data as base64 */
18
+ data: string;
19
+ /** MIME type of the image */
20
+ mimeType: string;
21
+ /** Image dimensions */
22
+ width?: number;
23
+ height?: number;
24
+ /** File name if available */
25
+ filename?: string;
26
+ }
27
+
28
+ export interface UseImagePasteOptions {
29
+ /** Whether image paste is enabled */
30
+ enabled?: boolean;
31
+ /** Callback when image is pasted */
32
+ onImagePaste?: (image: PastedImage) => void;
33
+ /** Maximum image size in bytes */
34
+ maxSize?: number;
35
+ /** Allowed MIME types */
36
+ allowedTypes?: string[];
37
+ }
38
+
39
+ export interface UseImagePasteResult {
40
+ /** Last pasted image */
41
+ pastedImage: PastedImage | null;
42
+ /** Whether paste is in progress */
43
+ isPasting: boolean;
44
+ /** Any error that occurred */
45
+ error: Error | null;
46
+ /** Clear the pasted image */
47
+ clear: () => void;
48
+ /** Handle paste event manually */
49
+ handlePaste: (data: string, mimeType: string) => void;
50
+ }
51
+
52
+ // =============================================================================
53
+ // Hook
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Hook to handle image pasting from clipboard
58
+ */
59
+ export function useImagePaste(options: UseImagePasteOptions = {}): UseImagePasteResult {
60
+ const { enabled = true, onImagePaste, maxSize, allowedTypes } = options;
61
+ const [pastedImage, setPastedImage] = useState<PastedImage | null>(null);
62
+ const [isPasting, setIsPasting] = useState(false);
63
+ const [error, setError] = useState<Error | null>(null);
64
+
65
+ const clear = useCallback(() => {
66
+ setPastedImage(null);
67
+ setError(null);
68
+ }, []);
69
+
70
+ const handlePaste = useCallback(
71
+ (data: string, mimeType: string) => {
72
+ if (!enabled) return;
73
+
74
+ setIsPasting(true);
75
+ setError(null);
76
+
77
+ try {
78
+ // Validate MIME type
79
+ if (allowedTypes && !allowedTypes.includes(mimeType)) {
80
+ throw new Error(`Unsupported image type: ${mimeType}`);
81
+ }
82
+
83
+ // Validate size
84
+ const sizeInBytes = (data.length * 3) / 4; // Approximate base64 decode size
85
+ if (maxSize && sizeInBytes > maxSize) {
86
+ throw new Error(`Image too large: ${sizeInBytes} bytes (max: ${maxSize})`);
87
+ }
88
+
89
+ const image: PastedImage = {
90
+ data,
91
+ mimeType,
92
+ };
93
+
94
+ setPastedImage(image);
95
+ onImagePaste?.(image);
96
+ } catch (err) {
97
+ setError(err instanceof Error ? err : new Error(String(err)));
98
+ } finally {
99
+ setIsPasting(false);
100
+ }
101
+ },
102
+ [enabled, onImagePaste, maxSize, allowedTypes]
103
+ );
104
+
105
+ return {
106
+ pastedImage,
107
+ isPasting,
108
+ error,
109
+ clear,
110
+ handlePaste,
111
+ };
112
+ }
113
+
114
+ export default useImagePaste;