@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,206 @@
1
+ /**
2
+ * Terminal Size Hook
3
+ *
4
+ * React hook for subscribing to terminal resize events.
5
+ * Provides reactive terminal dimensions with debouncing.
6
+ *
7
+ * @module tui/hooks/useTerminalSize
8
+ */
9
+
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
+ import { isNarrowWidth } from "../utils/isNarrowWidth.js";
12
+ import { getMaxContentWidth, getTerminalHeight, getTerminalWidth } from "../utils/ui-sizing.js";
13
+
14
+ /**
15
+ * Terminal size state returned by useTerminalSize hook.
16
+ */
17
+ export interface TerminalSize {
18
+ /** Terminal width in columns */
19
+ width: number;
20
+ /** Terminal height in rows */
21
+ height: number;
22
+ /** Whether terminal is narrow (<= 80 columns) */
23
+ isNarrow: boolean;
24
+ /** Maximum content width for responsive layouts */
25
+ maxContentWidth: number;
26
+ }
27
+
28
+ /**
29
+ * Options for useTerminalSize hook.
30
+ */
31
+ export interface UseTerminalSizeOptions {
32
+ /**
33
+ * Debounce delay in milliseconds for resize events.
34
+ * Set to 0 to disable debouncing.
35
+ * @default 100
36
+ */
37
+ debounceMs?: number;
38
+
39
+ /**
40
+ * Initial width to use before first measurement.
41
+ * @default 80
42
+ */
43
+ initialWidth?: number;
44
+
45
+ /**
46
+ * Initial height to use before first measurement.
47
+ * @default 24
48
+ */
49
+ initialHeight?: number;
50
+ }
51
+
52
+ /**
53
+ * Hook for subscribing to terminal resize events.
54
+ *
55
+ * Provides reactive terminal dimensions that update when the
56
+ * terminal is resized. Includes computed values for narrow
57
+ * width detection and responsive content width.
58
+ *
59
+ * @param options - Configuration options
60
+ * @returns Terminal size state
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * function MyComponent() {
65
+ * const { width, height, isNarrow, maxContentWidth } = useTerminalDimensions();
66
+ *
67
+ * return (
68
+ * <Box width={maxContentWidth}>
69
+ * {isNarrow ? <CompactView /> : <FullView />}
70
+ * <Text>Terminal: {width}x{height}</Text>
71
+ * </Box>
72
+ * );
73
+ * }
74
+ * ```
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * // With custom debounce
79
+ * const size = useTerminalDimensions({ debounceMs: 200 });
80
+ * ```
81
+ */
82
+ export function useTerminalDimensions(options: UseTerminalSizeOptions = {}): TerminalSize {
83
+ const { debounceMs = 100, initialWidth = 80, initialHeight = 24 } = options;
84
+
85
+ // Get initial dimensions
86
+ const [dimensions, setDimensions] = useState(() => ({
87
+ width: getTerminalWidth(initialWidth),
88
+ height: getTerminalHeight(initialHeight),
89
+ }));
90
+
91
+ // Debounce timer ref
92
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
93
+
94
+ // Handler for resize events
95
+ const handleResize = useCallback(() => {
96
+ const newWidth = getTerminalWidth(initialWidth);
97
+ const newHeight = getTerminalHeight(initialHeight);
98
+
99
+ setDimensions((prev) => {
100
+ // Only update if dimensions actually changed
101
+ if (prev.width === newWidth && prev.height === newHeight) {
102
+ return prev;
103
+ }
104
+ return { width: newWidth, height: newHeight };
105
+ });
106
+ }, [initialWidth, initialHeight]);
107
+
108
+ // Debounced resize handler
109
+ const debouncedHandleResize = useCallback(() => {
110
+ if (timerRef.current) {
111
+ clearTimeout(timerRef.current);
112
+ }
113
+
114
+ if (debounceMs === 0) {
115
+ handleResize();
116
+ } else {
117
+ timerRef.current = setTimeout(handleResize, debounceMs);
118
+ }
119
+ }, [handleResize, debounceMs]);
120
+
121
+ // Subscribe to resize events
122
+ useEffect(() => {
123
+ // Initial measurement
124
+ handleResize();
125
+
126
+ // Subscribe to SIGWINCH (terminal resize signal)
127
+ process.stdout.on("resize", debouncedHandleResize);
128
+
129
+ return () => {
130
+ // Cleanup
131
+ process.stdout.off("resize", debouncedHandleResize);
132
+ if (timerRef.current) {
133
+ clearTimeout(timerRef.current);
134
+ }
135
+ };
136
+ }, [handleResize, debouncedHandleResize]);
137
+
138
+ // Compute derived values
139
+ const result = useMemo<TerminalSize>(
140
+ () => ({
141
+ width: dimensions.width,
142
+ height: dimensions.height,
143
+ isNarrow: isNarrowWidth(dimensions.width),
144
+ maxContentWidth: getMaxContentWidth(dimensions.width),
145
+ }),
146
+ [dimensions.width, dimensions.height]
147
+ );
148
+
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Lightweight hook that only tracks narrow width state.
154
+ *
155
+ * Use this when you only need to know if the terminal is narrow,
156
+ * without the overhead of tracking full dimensions.
157
+ *
158
+ * @param debounceMs - Debounce delay for resize events
159
+ * @returns Whether terminal is narrow
160
+ *
161
+ * @example
162
+ * ```tsx
163
+ * function MyComponent() {
164
+ * const isNarrow = useIsNarrowWidth();
165
+ * return isNarrow ? <CompactView /> : <FullView />;
166
+ * }
167
+ * ```
168
+ */
169
+ export function useIsNarrowWidth(debounceMs = 100): boolean {
170
+ const [narrow, setNarrow] = useState(() => isNarrowWidth(getTerminalWidth(80)));
171
+
172
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
173
+
174
+ useEffect(() => {
175
+ const handleResize = () => {
176
+ const newNarrow = isNarrowWidth(getTerminalWidth(80));
177
+ setNarrow((prev) => (prev === newNarrow ? prev : newNarrow));
178
+ };
179
+
180
+ const debouncedHandleResize = () => {
181
+ if (timerRef.current) {
182
+ clearTimeout(timerRef.current);
183
+ }
184
+
185
+ if (debounceMs === 0) {
186
+ handleResize();
187
+ } else {
188
+ timerRef.current = setTimeout(handleResize, debounceMs);
189
+ }
190
+ };
191
+
192
+ // Initial check
193
+ handleResize();
194
+
195
+ process.stdout.on("resize", debouncedHandleResize);
196
+
197
+ return () => {
198
+ process.stdout.off("resize", debouncedHandleResize);
199
+ if (timerRef.current) {
200
+ clearTimeout(timerRef.current);
201
+ }
202
+ };
203
+ }, [debounceMs]);
204
+
205
+ return narrow;
206
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * useToolApprovalController
3
+ *
4
+ * Bridges ToolsContext pending approvals to an optional AgentLoop instance.
5
+ *
6
+ * - Source of truth for UI is ToolsContext.
7
+ * - When a user approves/rejects, we update ToolsContext and (if provided)
8
+ * signal the AgentLoop to resume via grantPermission/denyPermission.
9
+ */
10
+
11
+ import { useCallback, useMemo } from "react";
12
+ import type { RiskLevel } from "../components/Tools/PermissionDialog.js";
13
+ import { useTools } from "../context/ToolsContext.js";
14
+
15
+ export interface PermissionGate {
16
+ grantPermission: () => void;
17
+ denyPermission: () => void;
18
+ }
19
+
20
+ export interface UseToolApprovalControllerOptions {
21
+ readonly agentLoop?: PermissionGate;
22
+ }
23
+
24
+ export interface ToolApprovalViewModel {
25
+ readonly activeApproval: ReturnType<typeof useTools>["pendingApproval"][number] | null;
26
+ readonly activeRiskLevel: RiskLevel;
27
+ readonly approveActive: (mode?: "once" | "always") => void;
28
+ readonly rejectActive: () => void;
29
+ }
30
+
31
+ function inferRiskLevel(toolName: string): RiskLevel {
32
+ const normalized = toolName.toLowerCase();
33
+
34
+ // Conservative defaults: most tool calls are medium risk.
35
+ // Elevate for tools that can mutate the system or run commands.
36
+ if (
37
+ normalized.includes("bash") ||
38
+ normalized.includes("shell") ||
39
+ normalized.includes("exec") ||
40
+ normalized.includes("write") ||
41
+ normalized.includes("edit") ||
42
+ normalized.includes("delete")
43
+ ) {
44
+ return "high";
45
+ }
46
+
47
+ return "medium";
48
+ }
49
+
50
+ export function useToolApprovalController(
51
+ options: UseToolApprovalControllerOptions = {}
52
+ ): ToolApprovalViewModel {
53
+ const { agentLoop } = options;
54
+ const { pendingApproval, respondToPermissionRequest } = useTools();
55
+
56
+ const activeApproval = pendingApproval[0] ?? null;
57
+
58
+ const activeRiskLevel = useMemo<RiskLevel>(() => {
59
+ if (!activeApproval) return "medium";
60
+ return inferRiskLevel(activeApproval.toolName);
61
+ }, [activeApproval]);
62
+
63
+ const approveActive = useCallback(
64
+ (mode: "once" | "always" = "once") => {
65
+ if (!activeApproval) return;
66
+
67
+ // Resolve any core permission prompt associated with this execution.
68
+ // (No-op if none is pending.)
69
+ respondToPermissionRequest(activeApproval.id, mode);
70
+
71
+ // If an AgentLoop is driving tool execution, resume it.
72
+ // AgentLoop only supports a single pending permission at a time.
73
+ agentLoop?.grantPermission();
74
+ },
75
+ [activeApproval, respondToPermissionRequest, agentLoop]
76
+ );
77
+
78
+ const rejectActive = useCallback(() => {
79
+ if (!activeApproval) return;
80
+
81
+ respondToPermissionRequest(activeApproval.id, "reject");
82
+ agentLoop?.denyPermission();
83
+ }, [activeApproval, respondToPermissionRequest, agentLoop]);
84
+
85
+ return {
86
+ activeApproval,
87
+ activeRiskLevel,
88
+ approveActive,
89
+ rejectActive,
90
+ };
91
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * useVim Hook (T041)
3
+ *
4
+ * React hook for Vim editing mode in the TUI.
5
+ * Provides modal editing with NORMAL, INSERT, VISUAL, and COMMAND modes.
6
+ *
7
+ * @module @vellum/cli
8
+ */
9
+
10
+ import { useCallback, useState } from "react";
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Vim editing modes.
18
+ */
19
+ export type VimMode = "NORMAL" | "INSERT" | "VISUAL" | "COMMAND";
20
+
21
+ /**
22
+ * Motion actions for cursor movement.
23
+ */
24
+ export interface VimMotionAction {
25
+ type: "motion";
26
+ direction: "h" | "j" | "k" | "l" | "w" | "b" | "e" | "0" | "$";
27
+ }
28
+
29
+ /**
30
+ * Mode change actions.
31
+ */
32
+ export interface VimModeAction {
33
+ type: "mode";
34
+ target: VimMode;
35
+ }
36
+
37
+ /**
38
+ * Edit actions (delete, yank, paste).
39
+ */
40
+ export interface VimEditAction {
41
+ type: "delete" | "yank" | "paste";
42
+ }
43
+
44
+ /**
45
+ * Union of all Vim actions.
46
+ */
47
+ export type VimAction = VimMotionAction | VimModeAction | VimEditAction;
48
+
49
+ /**
50
+ * Key modifiers for Vim key handling.
51
+ */
52
+ export interface KeyModifiers {
53
+ ctrl?: boolean;
54
+ shift?: boolean;
55
+ }
56
+
57
+ /**
58
+ * Return value of useVim hook.
59
+ */
60
+ export interface UseVimReturn {
61
+ /** Whether Vim mode is enabled */
62
+ enabled: boolean;
63
+ /** Current Vim mode */
64
+ mode: VimMode;
65
+ /** Toggle Vim mode on/off */
66
+ toggle: () => void;
67
+ /** Set the current Vim mode */
68
+ setMode: (mode: VimMode) => void;
69
+ /** Handle a key press and return the resulting action */
70
+ handleKey: (key: string, modifiers?: KeyModifiers) => VimAction | null;
71
+ }
72
+
73
+ // =============================================================================
74
+ // Key Mappings
75
+ // =============================================================================
76
+
77
+ /**
78
+ * Motion keys in NORMAL and VISUAL modes.
79
+ */
80
+ const MOTION_KEYS: Record<string, VimMotionAction["direction"]> = {
81
+ h: "h", // left
82
+ j: "j", // down
83
+ k: "k", // up
84
+ l: "l", // right
85
+ w: "w", // word forward
86
+ b: "b", // word backward
87
+ e: "e", // word end
88
+ "0": "0", // line start
89
+ $: "$", // line end
90
+ };
91
+
92
+ /**
93
+ * Keys that trigger mode transitions from NORMAL mode.
94
+ */
95
+ const MODE_TRANSITION_KEYS: Record<string, VimMode> = {
96
+ i: "INSERT", // insert before cursor
97
+ a: "INSERT", // insert after cursor (append)
98
+ I: "INSERT", // insert at line start
99
+ A: "INSERT", // insert at line end
100
+ o: "INSERT", // open line below
101
+ O: "INSERT", // open line above
102
+ v: "VISUAL", // visual mode
103
+ V: "VISUAL", // visual line mode
104
+ ":": "COMMAND", // command mode
105
+ };
106
+
107
+ /**
108
+ * Edit action keys in NORMAL mode.
109
+ */
110
+ const EDIT_KEYS: Record<string, VimEditAction["type"]> = {
111
+ x: "delete", // delete character
112
+ d: "delete", // delete (with motion)
113
+ y: "yank", // yank (copy)
114
+ p: "paste", // paste after
115
+ P: "paste", // paste before
116
+ };
117
+
118
+ // =============================================================================
119
+ // Hook Implementation
120
+ // =============================================================================
121
+
122
+ /**
123
+ * React hook for Vim editing mode.
124
+ *
125
+ * Provides modal editing with NORMAL, INSERT, VISUAL, and COMMAND modes.
126
+ * Handles key presses and returns appropriate actions for the calling component
127
+ * to execute.
128
+ *
129
+ * @returns UseVimReturn object with Vim state and control functions
130
+ *
131
+ * @example
132
+ * ```tsx
133
+ * function Editor() {
134
+ * const vim = useVim();
135
+ *
136
+ * const handleKeyPress = (key: string) => {
137
+ * const action = vim.handleKey(key);
138
+ * if (action?.type === 'motion') {
139
+ * moveCursor(action.direction);
140
+ * }
141
+ * };
142
+ *
143
+ * return (
144
+ * <Box>
145
+ * <Text>Mode: {vim.mode}</Text>
146
+ * <TextInput onKeyPress={handleKeyPress} />
147
+ * </Box>
148
+ * );
149
+ * }
150
+ * ```
151
+ */
152
+ export function useVim(): UseVimReturn {
153
+ const [enabled, setEnabled] = useState(false);
154
+ const [mode, setModeState] = useState<VimMode>("NORMAL");
155
+
156
+ /**
157
+ * Toggle Vim mode on/off.
158
+ * When disabled, switches to INSERT mode.
159
+ * When enabled, switches to NORMAL mode.
160
+ */
161
+ const toggle = useCallback(() => {
162
+ setEnabled((prev) => {
163
+ const next = !prev;
164
+ // When enabling, start in NORMAL mode
165
+ // When disabling, mode doesn't matter but reset to NORMAL
166
+ setModeState("NORMAL");
167
+ return next;
168
+ });
169
+ }, []);
170
+
171
+ /**
172
+ * Set the current Vim mode.
173
+ */
174
+ const setMode = useCallback((newMode: VimMode) => {
175
+ setModeState(newMode);
176
+ }, []);
177
+
178
+ /**
179
+ * Handle a key press in NORMAL mode.
180
+ */
181
+ const handleNormalKey = useCallback((key: string, modifiers?: KeyModifiers): VimAction | null => {
182
+ // Check for Ctrl+c to exit to NORMAL (redundant but explicit)
183
+ if (modifiers?.ctrl && key === "c") {
184
+ return { type: "mode", target: "NORMAL" };
185
+ }
186
+
187
+ // In Vim mode we generally ignore Ctrl+<key> combos so global hotkeys can run.
188
+ // (Explicit Ctrl combos like Ctrl+C / Ctrl+[ are handled above or in other modes.)
189
+ if (modifiers?.ctrl) {
190
+ return null;
191
+ }
192
+
193
+ // Motion keys
194
+ if (key in MOTION_KEYS) {
195
+ const direction = MOTION_KEYS[key] as VimMotionAction["direction"];
196
+ return { type: "motion", direction };
197
+ }
198
+
199
+ // Mode transition keys
200
+ if (key in MODE_TRANSITION_KEYS) {
201
+ const target = MODE_TRANSITION_KEYS[key] as VimMode;
202
+ setModeState(target);
203
+ return { type: "mode", target };
204
+ }
205
+
206
+ // Edit keys
207
+ if (key in EDIT_KEYS) {
208
+ const editType = EDIT_KEYS[key] as VimEditAction["type"];
209
+ return { type: editType };
210
+ }
211
+
212
+ return null;
213
+ }, []);
214
+
215
+ /**
216
+ * Handle a key press in INSERT mode.
217
+ */
218
+ const handleInsertKey = useCallback((key: string, modifiers?: KeyModifiers): VimAction | null => {
219
+ // Escape or Ctrl+c returns to NORMAL mode
220
+ if (key === "escape" || (modifiers?.ctrl && key === "c")) {
221
+ setModeState("NORMAL");
222
+ return { type: "mode", target: "NORMAL" };
223
+ }
224
+
225
+ // Ctrl+[ is equivalent to Escape
226
+ if (modifiers?.ctrl && key === "[") {
227
+ setModeState("NORMAL");
228
+ return { type: "mode", target: "NORMAL" };
229
+ }
230
+
231
+ // All other keys pass through in INSERT mode
232
+ return null;
233
+ }, []);
234
+
235
+ /**
236
+ * Handle a key press in VISUAL mode.
237
+ */
238
+ const handleVisualKey = useCallback((key: string, modifiers?: KeyModifiers): VimAction | null => {
239
+ // Escape or Ctrl+c returns to NORMAL mode
240
+ if (key === "escape" || (modifiers?.ctrl && key === "c")) {
241
+ setModeState("NORMAL");
242
+ return { type: "mode", target: "NORMAL" };
243
+ }
244
+
245
+ // Ctrl+[ is equivalent to Escape
246
+ if (modifiers?.ctrl && key === "[") {
247
+ setModeState("NORMAL");
248
+ return { type: "mode", target: "NORMAL" };
249
+ }
250
+
251
+ // Ignore other Ctrl+<key> combos so global hotkeys can run.
252
+ if (modifiers?.ctrl) {
253
+ return null;
254
+ }
255
+
256
+ // Motion keys work in VISUAL mode
257
+ if (key in MOTION_KEYS) {
258
+ const direction = MOTION_KEYS[key] as VimMotionAction["direction"];
259
+ return { type: "motion", direction };
260
+ }
261
+
262
+ // v toggles back to NORMAL
263
+ if (key === "v") {
264
+ setModeState("NORMAL");
265
+ return { type: "mode", target: "NORMAL" };
266
+ }
267
+
268
+ // y yanks selection and returns to NORMAL
269
+ if (key === "y") {
270
+ setModeState("NORMAL");
271
+ return { type: "yank" };
272
+ }
273
+
274
+ // d deletes selection and returns to NORMAL
275
+ if (key === "d" || key === "x") {
276
+ setModeState("NORMAL");
277
+ return { type: "delete" };
278
+ }
279
+
280
+ return null;
281
+ }, []);
282
+
283
+ /**
284
+ * Handle a key press in COMMAND mode.
285
+ */
286
+ const handleCommandKey = useCallback(
287
+ (key: string, modifiers?: KeyModifiers): VimAction | null => {
288
+ // Escape or Ctrl+c returns to NORMAL mode
289
+ if (key === "escape" || (modifiers?.ctrl && key === "c")) {
290
+ setModeState("NORMAL");
291
+ return { type: "mode", target: "NORMAL" };
292
+ }
293
+
294
+ // Command mode passes through keys for command input
295
+ return null;
296
+ },
297
+ []
298
+ );
299
+
300
+ /**
301
+ * Handle a key press and return the resulting action.
302
+ * Returns null if the key should be passed through to the input.
303
+ */
304
+ const handleKey = useCallback(
305
+ (key: string, modifiers?: KeyModifiers): VimAction | null => {
306
+ // If Vim mode is disabled, pass through all keys
307
+ if (!enabled) {
308
+ return null;
309
+ }
310
+
311
+ switch (mode) {
312
+ case "NORMAL":
313
+ return handleNormalKey(key, modifiers);
314
+ case "INSERT":
315
+ return handleInsertKey(key, modifiers);
316
+ case "VISUAL":
317
+ return handleVisualKey(key, modifiers);
318
+ case "COMMAND":
319
+ return handleCommandKey(key, modifiers);
320
+ default:
321
+ return null;
322
+ }
323
+ },
324
+ [enabled, mode, handleNormalKey, handleInsertKey, handleVisualKey, handleCommandKey]
325
+ );
326
+
327
+ return {
328
+ enabled,
329
+ mode,
330
+ toggle,
331
+ setMode,
332
+ handleKey,
333
+ };
334
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * useWorkspace Hook
3
+ *
4
+ * Provides current workspace (working directory) information.
5
+ * Returns the directory name and full path for display in the header bar.
6
+ *
7
+ * @module tui/hooks/useWorkspace
8
+ */
9
+
10
+ import * as path from "node:path";
11
+ import { useMemo } from "react";
12
+
13
+ // =============================================================================
14
+ // Types
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Workspace information returned by the hook.
19
+ */
20
+ export interface WorkspaceInfo {
21
+ /** Short name of the workspace directory */
22
+ readonly name: string;
23
+ /** Full absolute path to the workspace */
24
+ readonly path: string;
25
+ }
26
+
27
+ // =============================================================================
28
+ // Hook Implementation
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Hook to get current workspace information.
33
+ *
34
+ * Uses process.cwd() to determine the current working directory.
35
+ * Memoized to avoid recalculation on every render.
36
+ *
37
+ * @returns WorkspaceInfo with name and path
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * function Header() {
42
+ * const { name, path } = useWorkspace();
43
+ * return <Text>{ name}</Text>;
44
+ * }
45
+ * ```
46
+ */
47
+ export function useWorkspace(): WorkspaceInfo {
48
+ return useMemo(() => {
49
+ const cwd = process.cwd();
50
+ const name = path.basename(cwd);
51
+ return {
52
+ name,
53
+ path: cwd,
54
+ };
55
+ }, []);
56
+ }