@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,483 @@
1
+ /**
2
+ * Syntax Highlighter Service
3
+ *
4
+ * Provides Shiki-based syntax highlighting with ANSI output for terminal rendering.
5
+ * Uses lazy initialization and singleton pattern to minimize startup overhead.
6
+ *
7
+ * @module tui/services/syntax-highlighter
8
+ */
9
+
10
+ import chalk, { type ChalkInstance } from "chalk";
11
+ import {
12
+ type BundledLanguage,
13
+ type BundledTheme,
14
+ createHighlighter,
15
+ type HighlighterGeneric,
16
+ type ThemedToken,
17
+ } from "shiki";
18
+
19
+ // =============================================================================
20
+ // Types
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Supported languages for syntax highlighting
25
+ */
26
+ export type SupportedLanguage =
27
+ | "typescript"
28
+ | "javascript"
29
+ | "python"
30
+ | "rust"
31
+ | "go"
32
+ | "json"
33
+ | "yaml"
34
+ | "bash"
35
+ | "markdown"
36
+ | "css"
37
+ | "html"
38
+ | "sql"
39
+ | "dockerfile"
40
+ | "toml"
41
+ | "tsx"
42
+ | "jsx";
43
+
44
+ /**
45
+ * Options for highlighting code
46
+ */
47
+ export interface HighlightOptions {
48
+ /** Programming language (auto-detected if not provided) */
49
+ readonly lang?: string;
50
+ /** Include line numbers in output */
51
+ readonly lineNumbers?: boolean;
52
+ /** Lines to highlight (1-indexed) */
53
+ readonly highlightLines?: number[];
54
+ }
55
+
56
+ /**
57
+ * Result of syntax highlighting
58
+ */
59
+ export interface SyntaxHighlightResult {
60
+ /** ANSI-colored output string */
61
+ readonly output: string;
62
+ /** Detected or specified language */
63
+ readonly language: string;
64
+ /** Whether highlighting was successful */
65
+ readonly success: boolean;
66
+ }
67
+
68
+ // =============================================================================
69
+ // Constants
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Default languages to preload
74
+ */
75
+ const DEFAULT_LANGUAGES: BundledLanguage[] = [
76
+ "typescript",
77
+ "javascript",
78
+ "tsx",
79
+ "jsx",
80
+ "python",
81
+ "rust",
82
+ "go",
83
+ "json",
84
+ "yaml",
85
+ "bash",
86
+ "markdown",
87
+ "css",
88
+ "html",
89
+ "sql",
90
+ "dockerfile",
91
+ "toml",
92
+ ];
93
+
94
+ /**
95
+ * Theme to use for highlighting
96
+ */
97
+ const THEME = "github-dark";
98
+
99
+ /**
100
+ * Language aliases for common variations
101
+ */
102
+ const LANGUAGE_ALIASES: Record<string, BundledLanguage> = {
103
+ js: "javascript",
104
+ ts: "typescript",
105
+ py: "python",
106
+ rb: "ruby",
107
+ sh: "bash",
108
+ shell: "bash",
109
+ zsh: "bash",
110
+ fish: "bash",
111
+ yml: "yaml",
112
+ md: "markdown",
113
+ rs: "rust",
114
+ golang: "go",
115
+ docker: "dockerfile",
116
+ };
117
+
118
+ /**
119
+ * File extension to language mapping for auto-detection
120
+ */
121
+ const EXTENSION_TO_LANGUAGE: Record<string, BundledLanguage> = {
122
+ ".ts": "typescript",
123
+ ".tsx": "tsx",
124
+ ".js": "javascript",
125
+ ".jsx": "jsx",
126
+ ".mjs": "javascript",
127
+ ".cjs": "javascript",
128
+ ".py": "python",
129
+ ".rs": "rust",
130
+ ".go": "go",
131
+ ".json": "json",
132
+ ".yaml": "yaml",
133
+ ".yml": "yaml",
134
+ ".md": "markdown",
135
+ ".css": "css",
136
+ ".html": "html",
137
+ ".htm": "html",
138
+ ".sql": "sql",
139
+ ".sh": "bash",
140
+ ".bash": "bash",
141
+ ".zsh": "bash",
142
+ ".toml": "toml",
143
+ };
144
+
145
+ /**
146
+ * Patterns for language auto-detection from code content
147
+ */
148
+ const LANGUAGE_PATTERNS: Array<{ pattern: RegExp; lang: BundledLanguage }> = [
149
+ // TypeScript/JavaScript
150
+ { pattern: /^import\s+.*\s+from\s+['"]|^export\s+(default\s+)?/m, lang: "typescript" },
151
+ { pattern: /^const\s+\w+\s*:\s*\w+|^interface\s+\w+|^type\s+\w+\s*=/m, lang: "typescript" },
152
+ { pattern: /^function\s+\w+|^const\s+\w+\s*=\s*\(|^let\s+|^var\s+/m, lang: "javascript" },
153
+ // Python
154
+ { pattern: /^def\s+\w+\s*\(|^class\s+\w+|^import\s+\w+|^from\s+\w+\s+import/m, lang: "python" },
155
+ // Rust
156
+ { pattern: /^fn\s+\w+|^struct\s+\w+|^impl\s+|^use\s+\w+::|^mod\s+\w+/m, lang: "rust" },
157
+ // Go
158
+ { pattern: /^package\s+\w+|^func\s+\w+|^import\s*\(|^type\s+\w+\s+struct/m, lang: "go" },
159
+ // JSON
160
+ { pattern: /^\s*\{[\s\S]*"[\w-]+":\s*[{"[\d]/m, lang: "json" },
161
+ // YAML
162
+ { pattern: /^[\w-]+:\s*[|>]?\s*$/m, lang: "yaml" },
163
+ // Bash
164
+ { pattern: /^#!\s*\/bin\/(ba)?sh|^#!/m, lang: "bash" },
165
+ { pattern: /^\s*(if|for|while)\s+\[|^\s*echo\s+|^\s*export\s+\w+=/m, lang: "bash" },
166
+ // SQL
167
+ { pattern: /^SELECT\s+|^INSERT\s+INTO|^CREATE\s+(TABLE|DATABASE)|^ALTER\s+TABLE/im, lang: "sql" },
168
+ // Dockerfile
169
+ { pattern: /^FROM\s+\w+|^RUN\s+|^CMD\s+|^ENTRYPOINT\s+/m, lang: "dockerfile" },
170
+ // HTML
171
+ { pattern: /^<!DOCTYPE\s+html|^<html|^<head|^<body/im, lang: "html" },
172
+ // CSS
173
+ { pattern: /^[.#]?\w+\s*\{[\s\S]*:[^}]+\}/m, lang: "css" },
174
+ // Markdown
175
+ { pattern: /^#\s+\w+|^\*\*\w+\*\*|^\[.+\]\(.+\)/m, lang: "markdown" },
176
+ ];
177
+
178
+ // =============================================================================
179
+ // Singleton State
180
+ // =============================================================================
181
+
182
+ let highlighterInstance: HighlighterGeneric<BundledLanguage, BundledTheme> | null = null;
183
+ let initPromise: Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> | null = null;
184
+
185
+ // =============================================================================
186
+ // Color Conversion
187
+ // =============================================================================
188
+
189
+ /**
190
+ * Convert hex color to chalk color function
191
+ */
192
+ function hexToChalk(hex: string | undefined): ChalkInstance {
193
+ if (!hex) return chalk;
194
+
195
+ // Remove # if present
196
+ const cleanHex = hex.replace(/^#/, "");
197
+
198
+ // Parse hex to RGB
199
+ const r = Number.parseInt(cleanHex.slice(0, 2), 16);
200
+ const g = Number.parseInt(cleanHex.slice(2, 4), 16);
201
+ const b = Number.parseInt(cleanHex.slice(4, 6), 16);
202
+
203
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
204
+ return chalk;
205
+ }
206
+
207
+ return chalk.rgb(r, g, b);
208
+ }
209
+
210
+ /**
211
+ * Convert Shiki tokens to ANSI-colored string
212
+ */
213
+ function tokensToAnsi(tokens: ThemedToken[][]): string {
214
+ const lines: string[] = [];
215
+
216
+ for (const lineTokens of tokens) {
217
+ let line = "";
218
+ for (const token of lineTokens) {
219
+ const colorFn = hexToChalk(token.color);
220
+ // Apply font style if present
221
+ let styled = colorFn;
222
+ if (token.fontStyle) {
223
+ if (token.fontStyle & 1) styled = styled.italic;
224
+ if (token.fontStyle & 2) styled = styled.bold;
225
+ if (token.fontStyle & 4) styled = styled.underline;
226
+ }
227
+ line += styled(token.content);
228
+ }
229
+ lines.push(line);
230
+ }
231
+
232
+ return lines.join("\n");
233
+ }
234
+
235
+ // =============================================================================
236
+ // Public API
237
+ // =============================================================================
238
+
239
+ /**
240
+ * Initialize the syntax highlighter
241
+ *
242
+ * Uses lazy loading - only initializes when first needed.
243
+ * Returns cached instance on subsequent calls.
244
+ */
245
+ export async function initializeHighlighter(): Promise<
246
+ HighlighterGeneric<BundledLanguage, BundledTheme>
247
+ > {
248
+ // Return cached instance
249
+ if (highlighterInstance) {
250
+ return highlighterInstance;
251
+ }
252
+
253
+ // Return pending initialization
254
+ if (initPromise) {
255
+ return initPromise;
256
+ }
257
+
258
+ // Start initialization
259
+ initPromise = createHighlighter({
260
+ themes: [THEME],
261
+ langs: DEFAULT_LANGUAGES,
262
+ });
263
+
264
+ try {
265
+ highlighterInstance = await initPromise;
266
+ return highlighterInstance;
267
+ } catch (error) {
268
+ initPromise = null;
269
+ throw error;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Detect language from code content
275
+ */
276
+ export function detectLanguage(code: string, filename?: string): BundledLanguage | undefined {
277
+ // Try filename extension first
278
+ if (filename) {
279
+ const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
280
+ const langFromExt = EXTENSION_TO_LANGUAGE[ext];
281
+ if (langFromExt) return langFromExt;
282
+ }
283
+
284
+ // Try content patterns
285
+ for (const { pattern, lang } of LANGUAGE_PATTERNS) {
286
+ if (pattern.test(code)) {
287
+ return lang;
288
+ }
289
+ }
290
+
291
+ return undefined;
292
+ }
293
+
294
+ /**
295
+ * Resolve language alias to canonical name
296
+ */
297
+ export function resolveLanguage(lang: string): BundledLanguage {
298
+ const normalized = lang.toLowerCase().trim();
299
+ return LANGUAGE_ALIASES[normalized] ?? (normalized as BundledLanguage);
300
+ }
301
+
302
+ /**
303
+ * Check if a language is supported
304
+ */
305
+ export function isLanguageSupported(lang: string): boolean {
306
+ const resolved = resolveLanguage(lang);
307
+ return DEFAULT_LANGUAGES.includes(resolved) || Object.values(LANGUAGE_ALIASES).includes(resolved);
308
+ }
309
+
310
+ /**
311
+ * Highlight code and return ANSI-colored string
312
+ *
313
+ * @param code - Source code to highlight
314
+ * @param options - Highlighting options
315
+ * @returns Highlighted code result
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * const result = await highlightCode('const x = 42;', { lang: 'typescript' });
320
+ * console.log(result.output);
321
+ * ```
322
+ */
323
+ export async function highlightCode(
324
+ code: string,
325
+ options: HighlightOptions = {}
326
+ ): Promise<SyntaxHighlightResult> {
327
+ const { lang, lineNumbers = false, highlightLines = [] } = options;
328
+
329
+ try {
330
+ const highlighter = await initializeHighlighter();
331
+
332
+ // Resolve language
333
+ let resolvedLang: BundledLanguage = "text" as BundledLanguage;
334
+ if (lang) {
335
+ resolvedLang = resolveLanguage(lang);
336
+ } else {
337
+ const detected = detectLanguage(code);
338
+ if (detected) resolvedLang = detected;
339
+ }
340
+
341
+ // Get tokens
342
+ const tokens = highlighter.codeToTokensBase(code, {
343
+ lang: resolvedLang,
344
+ theme: THEME,
345
+ });
346
+
347
+ // Convert to ANSI
348
+ let output = tokensToAnsi(tokens);
349
+
350
+ // Add line numbers if requested
351
+ if (lineNumbers) {
352
+ const lines = output.split("\n");
353
+ const width = String(lines.length).length;
354
+ const highlightSet = new Set(highlightLines);
355
+
356
+ output = lines
357
+ .map((line, i) => {
358
+ const num = i + 1;
359
+ const numStr = String(num).padStart(width, " ");
360
+ const prefix = highlightSet.has(num)
361
+ ? chalk.yellow.bold(`▶ ${numStr} │ `)
362
+ : chalk.dim(` ${numStr} │ `);
363
+ return prefix + line;
364
+ })
365
+ .join("\n");
366
+ }
367
+
368
+ return {
369
+ output,
370
+ language: resolvedLang,
371
+ success: true,
372
+ };
373
+ } catch (_error) {
374
+ // Fallback to plain text on error
375
+ let output = code;
376
+
377
+ if (lineNumbers) {
378
+ const lines = code.split("\n");
379
+ const width = String(lines.length).length;
380
+ output = lines
381
+ .map((line, i) => {
382
+ const numStr = String(i + 1).padStart(width, " ");
383
+ return chalk.dim(` ${numStr} │ `) + line;
384
+ })
385
+ .join("\n");
386
+ }
387
+
388
+ return {
389
+ output,
390
+ language: lang ?? "text",
391
+ success: false,
392
+ };
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Synchronous highlight using cached highlighter
398
+ *
399
+ * Returns null if highlighter is not yet initialized.
400
+ * Use this when you need synchronous rendering and can fall back to plain text.
401
+ */
402
+ export function highlightCodeSync(
403
+ code: string,
404
+ options: HighlightOptions = {}
405
+ ): SyntaxHighlightResult | null {
406
+ if (!highlighterInstance) {
407
+ return null;
408
+ }
409
+
410
+ const { lang, lineNumbers = false, highlightLines = [] } = options;
411
+
412
+ try {
413
+ // Resolve language
414
+ let resolvedLang: BundledLanguage = "text" as BundledLanguage;
415
+ if (lang) {
416
+ resolvedLang = resolveLanguage(lang);
417
+ } else {
418
+ const detected = detectLanguage(code);
419
+ if (detected) resolvedLang = detected;
420
+ }
421
+
422
+ // Get tokens
423
+ const tokens = highlighterInstance.codeToTokensBase(code, {
424
+ lang: resolvedLang,
425
+ theme: THEME,
426
+ });
427
+
428
+ // Convert to ANSI
429
+ let output = tokensToAnsi(tokens);
430
+
431
+ // Add line numbers if requested
432
+ if (lineNumbers) {
433
+ const lines = output.split("\n");
434
+ const width = String(lines.length).length;
435
+ const highlightSet = new Set(highlightLines);
436
+
437
+ output = lines
438
+ .map((line, i) => {
439
+ const num = i + 1;
440
+ const numStr = String(num).padStart(width, " ");
441
+ const prefix = highlightSet.has(num)
442
+ ? chalk.yellow.bold(`▶ ${numStr} │ `)
443
+ : chalk.dim(` ${numStr} │ `);
444
+ return prefix + line;
445
+ })
446
+ .join("\n");
447
+ }
448
+
449
+ return {
450
+ output,
451
+ language: resolvedLang,
452
+ success: true,
453
+ };
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Check if the highlighter is initialized
461
+ */
462
+ export function isHighlighterReady(): boolean {
463
+ return highlighterInstance !== null;
464
+ }
465
+
466
+ /**
467
+ * Preload the highlighter in the background
468
+ *
469
+ * Call this early in app startup to warm up the cache.
470
+ */
471
+ export function preloadHighlighter(): void {
472
+ // Fire and forget
473
+ initializeHighlighter().catch(() => {
474
+ // Silently ignore preload errors
475
+ });
476
+ }
477
+
478
+ /**
479
+ * Get list of supported languages
480
+ */
481
+ export function getSupportedLanguages(): readonly string[] {
482
+ return [...DEFAULT_LANGUAGES, ...Object.keys(LANGUAGE_ALIASES)];
483
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * TUI Slash Commands
3
+ *
4
+ * Provides TUI-specific slash command utilities.
5
+ * Re-exports commonly used command types and utilities.
6
+ *
7
+ * @module tui/slash-commands
8
+ */
9
+
10
+ // Re-export command types from the commands module
11
+ export type { CommandContext, CommandResult, SlashCommand } from "../commands/types.js";
12
+ export { error, interactive, pending, success } from "../commands/types.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * TUI Theme
3
+ *
4
+ * Theme configuration and styling utilities for the Vellum TUI.
5
+ */
6
+
7
+ // Re-export types from shared for convenience
8
+ export type { ThemeContextValue, ThemeName, VellumTheme } from "@vellum/shared";
9
+ // Theme provider and hook
10
+ export {
11
+ ThemeContext,
12
+ ThemeProvider,
13
+ type ThemeProviderProps,
14
+ useTheme,
15
+ } from "./provider.js";
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Theme Provider and Hook
3
+ *
4
+ * Provides theming context for the Vellum TUI with support for
5
+ * preset themes and runtime theme switching.
6
+ *
7
+ * @module tui/theme/provider
8
+ */
9
+
10
+ import type { ThemeContextValue, ThemePreset, VellumTheme } from "@vellum/shared";
11
+ import { defaultTheme, getThemeOrDefault, type ThemeName, themes } from "@vellum/shared";
12
+ import React, {
13
+ createContext,
14
+ type ReactNode,
15
+ useCallback,
16
+ useContext,
17
+ useMemo,
18
+ useState,
19
+ } from "react";
20
+
21
+ // =============================================================================
22
+ // Context
23
+ // =============================================================================
24
+
25
+ /**
26
+ * React context for theme state
27
+ *
28
+ * Initialized as undefined to detect usage outside provider
29
+ */
30
+ const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
31
+
32
+ // =============================================================================
33
+ // Hook
34
+ // =============================================================================
35
+
36
+ /**
37
+ * Hook to access the current theme
38
+ *
39
+ * Must be used within a ThemeProvider component.
40
+ *
41
+ * @returns The current theme context value
42
+ * @throws Error if used outside ThemeProvider
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * function MyComponent() {
47
+ * const { theme, setTheme } = useTheme();
48
+ * return <Box color={theme.colors.primary}>Hello</Box>;
49
+ * }
50
+ * ```
51
+ */
52
+ export function useTheme(): ThemeContextValue {
53
+ const context = useContext(ThemeContext);
54
+
55
+ if (context === undefined) {
56
+ throw new Error(
57
+ "useTheme must be used within a ThemeProvider. " +
58
+ "Ensure your component is wrapped in <ThemeProvider>."
59
+ );
60
+ }
61
+
62
+ return context;
63
+ }
64
+
65
+ // =============================================================================
66
+ // Provider Props
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Props for the ThemeProvider component
71
+ */
72
+ export interface ThemeProviderProps {
73
+ /**
74
+ * Initial theme - can be a theme name string or a VellumTheme object
75
+ *
76
+ * @default "dark"
77
+ */
78
+ readonly theme?: ThemeName | ThemePreset | VellumTheme;
79
+
80
+ /**
81
+ * Children to render within the theme context
82
+ */
83
+ readonly children: ReactNode;
84
+ }
85
+
86
+ // =============================================================================
87
+ // Helper Functions
88
+ // =============================================================================
89
+
90
+ /**
91
+ * Resolve a theme input to a VellumTheme object
92
+ *
93
+ * @param themeInput - Theme name, preset, or object
94
+ * @returns Resolved VellumTheme object
95
+ */
96
+ function resolveTheme(themeInput: ThemeName | ThemePreset | VellumTheme | undefined): VellumTheme {
97
+ // Handle undefined - use default
98
+ if (themeInput === undefined) {
99
+ return defaultTheme;
100
+ }
101
+
102
+ // Handle string theme names (both ThemeName and ThemePreset)
103
+ if (typeof themeInput === "string") {
104
+ return getThemeOrDefault(themeInput);
105
+ }
106
+
107
+ // Handle VellumTheme objects - return as-is
108
+ return themeInput;
109
+ }
110
+
111
+ /**
112
+ * Get the opposite mode theme name
113
+ *
114
+ * @param currentTheme - Current theme
115
+ * @returns Theme name with opposite mode
116
+ */
117
+ function getOppositeMode(currentTheme: VellumTheme): ThemeName {
118
+ const targetMode = currentTheme.mode === "dark" ? "light" : "dark";
119
+
120
+ // Try to find a theme with the same base name but different mode
121
+ // For example: dracula -> light, light -> dark
122
+ const themeNames = Object.keys(themes) as ThemeName[];
123
+
124
+ // First, look for a theme with opposite mode
125
+ const sameFamily = themeNames.find((name) => {
126
+ const theme = themes[name];
127
+ return theme.mode === targetMode;
128
+ });
129
+
130
+ return sameFamily ?? (targetMode === "dark" ? "dark" : "light");
131
+ }
132
+
133
+ // =============================================================================
134
+ // Provider Component
135
+ // =============================================================================
136
+
137
+ /**
138
+ * Theme provider component
139
+ *
140
+ * Provides theme context to all child components, enabling access to
141
+ * the current theme via the useTheme hook.
142
+ *
143
+ * @example
144
+ * ```tsx
145
+ * // Using a theme name
146
+ * <ThemeProvider theme="dark">
147
+ * <App />
148
+ * </ThemeProvider>
149
+ *
150
+ * // Using a theme preset
151
+ * <ThemeProvider theme="dracula">
152
+ * <App />
153
+ * </ThemeProvider>
154
+ *
155
+ * // Using a custom theme object
156
+ * <ThemeProvider theme={customTheme}>
157
+ * <App />
158
+ * </ThemeProvider>
159
+ * ```
160
+ */
161
+ export function ThemeProvider({
162
+ theme: initialTheme,
163
+ children,
164
+ }: ThemeProviderProps): React.JSX.Element {
165
+ // State for the current theme
166
+ const [currentTheme, setCurrentTheme] = useState<VellumTheme>(() => resolveTheme(initialTheme));
167
+
168
+ /**
169
+ * Set theme by name or object
170
+ */
171
+ const setTheme = useCallback((newTheme: ThemePreset | VellumTheme): void => {
172
+ const resolved = resolveTheme(newTheme);
173
+ setCurrentTheme(resolved);
174
+ }, []);
175
+
176
+ /**
177
+ * Toggle between dark and light modes
178
+ */
179
+ const toggleMode = useCallback((): void => {
180
+ const oppositeName = getOppositeMode(currentTheme);
181
+ const oppositeTheme = themes[oppositeName];
182
+ setCurrentTheme(oppositeTheme);
183
+ }, [currentTheme]);
184
+
185
+ /**
186
+ * Memoized context value
187
+ */
188
+ const contextValue = useMemo<ThemeContextValue>(
189
+ () => ({
190
+ theme: currentTheme,
191
+ themeName: currentTheme.name,
192
+ setTheme,
193
+ toggleMode,
194
+ }),
195
+ [currentTheme, setTheme, toggleMode]
196
+ );
197
+
198
+ return <ThemeContext value={contextValue}>{children}</ThemeContext>;
199
+ }
200
+
201
+ // =============================================================================
202
+ // Exports
203
+ // =============================================================================
204
+
205
+ export { ThemeContext };
206
+ export type { ThemeContextValue };