@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,870 @@
1
+ /**
2
+ * Messages Context and State Management
3
+ *
4
+ * Provides message state management for the Vellum TUI including
5
+ * message storage, streaming support, and tool call tracking.
6
+ *
7
+ * @module tui/context/MessagesContext
8
+ */
9
+
10
+ import React, {
11
+ createContext,
12
+ type Dispatch,
13
+ type ReactNode,
14
+ useCallback,
15
+ useContext,
16
+ useMemo,
17
+ useReducer,
18
+ } from "react";
19
+
20
+ // =============================================================================
21
+ // Types
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Role of a message sender
26
+ */
27
+ export type MessageRole = "user" | "assistant" | "system" | "tool" | "tool_group";
28
+
29
+ /**
30
+ * Status of a tool call within a message.
31
+ * - pending: Tool call created but not started
32
+ * - running: Tool is currently executing (show spinner)
33
+ * - completed: Tool finished successfully (show checkmark)
34
+ * - error: Tool failed (show error icon)
35
+ */
36
+ export type ToolCallStatus = "pending" | "running" | "completed" | "error";
37
+
38
+ /**
39
+ * Information about a tool call within a message
40
+ */
41
+ export interface ToolCallInfo {
42
+ /** Unique identifier for the tool call */
43
+ readonly id: string;
44
+ /** Name of the tool being called */
45
+ readonly name: string;
46
+ /** Arguments passed to the tool */
47
+ readonly arguments: Record<string, unknown>;
48
+ /** Result of the tool call, if completed successfully */
49
+ readonly result?: unknown;
50
+ /** Error message if the tool call failed */
51
+ readonly error?: string;
52
+ /** Status of the tool call */
53
+ readonly status: ToolCallStatus;
54
+ }
55
+
56
+ /**
57
+ * Token usage information for a message turn.
58
+ */
59
+ export interface MessageTokenUsage {
60
+ /** Number of input tokens */
61
+ readonly inputTokens: number;
62
+ /** Number of output tokens */
63
+ readonly outputTokens: number;
64
+ /** Number of tokens used for thinking/reasoning (if applicable) */
65
+ readonly thinkingTokens?: number;
66
+ /** Number of tokens read from cache (if applicable) */
67
+ readonly cacheReadTokens?: number;
68
+ }
69
+
70
+ /**
71
+ * A single message in the conversation
72
+ */
73
+ export interface Message {
74
+ /** Unique identifier for the message */
75
+ readonly id: string;
76
+ /** Role of the message sender */
77
+ readonly role: MessageRole;
78
+ /** Content of the message */
79
+ readonly content: string;
80
+ /** Timestamp when the message was created */
81
+ readonly timestamp: Date;
82
+ /** Whether the message is currently being streamed */
83
+ readonly isStreaming?: boolean;
84
+ /** Tool calls associated with this message */
85
+ readonly toolCalls?: readonly ToolCallInfo[];
86
+ /** Token usage for this message turn (assistant messages only) */
87
+ readonly tokenUsage?: MessageTokenUsage;
88
+ /** Whether this message is a continuation of a previous split message */
89
+ readonly isContinuation?: boolean;
90
+ /** Thinking/reasoning content (for models with extended thinking) */
91
+ readonly thinking?: string;
92
+ /** Duration of thinking in milliseconds (for extended thinking models) */
93
+ readonly thinkingDuration?: number;
94
+ /** Whether thinking phase is complete (false during streaming) */
95
+ readonly isThinkingComplete?: boolean;
96
+ }
97
+
98
+ /**
99
+ * Messages state interface
100
+ *
101
+ * Uses a split architecture for optimal rendering:
102
+ * - `historyMessages`: Completed messages rendered in Ink's <Static> (never re-render)
103
+ * - `pendingMessage`: Currently streaming message (only this causes re-renders)
104
+ */
105
+ export interface MessagesState {
106
+ /** Completed messages - rendered in <Static>, never re-render */
107
+ readonly historyMessages: readonly Message[];
108
+ /** Currently streaming message - the only thing that re-renders */
109
+ readonly pendingMessage: Message | null;
110
+ /** List of all messages in the conversation (computed: history + pending) */
111
+ readonly messages: readonly Message[];
112
+ /** Whether any message is currently streaming */
113
+ readonly isStreaming: boolean;
114
+ }
115
+
116
+ /**
117
+ * Initial messages state
118
+ */
119
+ const initialState: MessagesState = {
120
+ historyMessages: [],
121
+ pendingMessage: null,
122
+ messages: [],
123
+ isStreaming: false,
124
+ };
125
+
126
+ // =============================================================================
127
+ // Actions (Discriminated Union)
128
+ // =============================================================================
129
+
130
+ /**
131
+ * Add a new message
132
+ */
133
+ export interface AddMessageAction {
134
+ readonly type: "ADD_MESSAGE";
135
+ readonly message: Message;
136
+ }
137
+
138
+ /**
139
+ * Update an existing message
140
+ */
141
+ export interface UpdateMessageAction {
142
+ readonly type: "UPDATE_MESSAGE";
143
+ readonly id: string;
144
+ readonly updates: Partial<Omit<Message, "id">>;
145
+ }
146
+
147
+ /**
148
+ * Append content to an existing message (for streaming)
149
+ */
150
+ export interface AppendToMessageAction {
151
+ readonly type: "APPEND_TO_MESSAGE";
152
+ readonly id: string;
153
+ readonly content: string;
154
+ }
155
+
156
+ /**
157
+ * Replace the entire message list
158
+ */
159
+ export interface SetMessagesAction {
160
+ readonly type: "SET_MESSAGES";
161
+ readonly messages: readonly Message[];
162
+ }
163
+
164
+ /**
165
+ * Clear all messages
166
+ */
167
+ export interface ClearMessagesAction {
168
+ readonly type: "CLEAR_MESSAGES";
169
+ }
170
+
171
+ /**
172
+ * Set streaming state
173
+ */
174
+ export interface SetStreamingAction {
175
+ readonly type: "SET_STREAMING";
176
+ readonly isStreaming: boolean;
177
+ }
178
+
179
+ /**
180
+ * Commit pending message to history (for Static rendering)
181
+ */
182
+ export interface CommitPendingMessageAction {
183
+ readonly type: "COMMIT_PENDING_MESSAGE";
184
+ }
185
+
186
+ /**
187
+ * Split a long streaming message at a safe point
188
+ * Moves completed content to history and keeps remainder as pending
189
+ */
190
+ export interface SplitMessageAction {
191
+ readonly type: "SPLIT_MESSAGE";
192
+ /** Index to split at (content before this becomes history) */
193
+ readonly splitIndex: number;
194
+ }
195
+
196
+ /**
197
+ * Append thinking/reasoning content to an existing message (for streaming)
198
+ */
199
+ export interface AppendToThinkingAction {
200
+ readonly type: "APPEND_TO_THINKING";
201
+ readonly id: string;
202
+ readonly thinking: string;
203
+ }
204
+
205
+ /**
206
+ * Add a new tool_group message (Gemini-style independent tool execution)
207
+ */
208
+ export interface AddToolGroupAction {
209
+ readonly type: "ADD_TOOL_GROUP";
210
+ readonly toolCalls: readonly ToolCallInfo[];
211
+ }
212
+
213
+ /**
214
+ * Update an existing tool_group message with tool call status
215
+ */
216
+ export interface UpdateToolGroupAction {
217
+ readonly type: "UPDATE_TOOL_GROUP";
218
+ readonly groupId: string;
219
+ readonly toolCall: ToolCallInfo;
220
+ }
221
+
222
+ /**
223
+ * Discriminated union of all message actions
224
+ */
225
+ export type MessagesAction =
226
+ | AddMessageAction
227
+ | UpdateMessageAction
228
+ | AppendToMessageAction
229
+ | SetMessagesAction
230
+ | ClearMessagesAction
231
+ | SetStreamingAction
232
+ | CommitPendingMessageAction
233
+ | SplitMessageAction
234
+ | AppendToThinkingAction
235
+ | AddToolGroupAction
236
+ | UpdateToolGroupAction;
237
+
238
+ // =============================================================================
239
+ // Helper Functions
240
+ // =============================================================================
241
+
242
+ /**
243
+ * Compute the combined messages array from history + pending
244
+ */
245
+ function computeMessages(
246
+ historyMessages: readonly Message[],
247
+ pendingMessage: Message | null
248
+ ): readonly Message[] {
249
+ if (pendingMessage) {
250
+ return [...historyMessages, pendingMessage];
251
+ }
252
+ return historyMessages;
253
+ }
254
+
255
+ /**
256
+ * Merge tool calls: Update existing calls by ID or add new ones.
257
+ * This allows updating tool status (running → completed/error) without losing other calls.
258
+ */
259
+ function mergeToolCalls(
260
+ existing: readonly ToolCallInfo[] | undefined,
261
+ incoming: readonly ToolCallInfo[] | undefined
262
+ ): readonly ToolCallInfo[] | undefined {
263
+ if (!incoming || incoming.length === 0) {
264
+ return existing;
265
+ }
266
+ if (!existing || existing.length === 0) {
267
+ return incoming;
268
+ }
269
+
270
+ // Create a map of existing calls for efficient lookup
271
+ const callMap = new Map<string, ToolCallInfo>();
272
+ for (const call of existing) {
273
+ callMap.set(call.id, call);
274
+ }
275
+
276
+ // Merge incoming calls
277
+ for (const call of incoming) {
278
+ const existingCall = callMap.get(call.id);
279
+ if (existingCall) {
280
+ // Merge: keep existing args if incoming doesn't have them
281
+ callMap.set(call.id, {
282
+ ...existingCall,
283
+ ...call,
284
+ // Preserve arguments if not provided in incoming
285
+ arguments: Object.keys(call.arguments).length > 0 ? call.arguments : existingCall.arguments,
286
+ });
287
+ } else {
288
+ // Add new call
289
+ callMap.set(call.id, call);
290
+ }
291
+ }
292
+
293
+ return Array.from(callMap.values());
294
+ }
295
+
296
+ // =============================================================================
297
+ // Reducer
298
+ // =============================================================================
299
+
300
+ /**
301
+ * Messages state reducer
302
+ *
303
+ * Uses a split architecture for optimal rendering:
304
+ * - historyMessages: Completed messages (for <Static>)
305
+ * - pendingMessage: Currently streaming message (causes re-renders)
306
+ *
307
+ * @param state - Current messages state
308
+ * @param action - Action to apply
309
+ * @returns New messages state
310
+ */
311
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Reducer with many action types for message state management
312
+ function messagesReducer(state: MessagesState, action: MessagesAction): MessagesState {
313
+ switch (action.type) {
314
+ case "ADD_MESSAGE": {
315
+ const isStreaming = action.message.isStreaming ?? false;
316
+
317
+ if (isStreaming) {
318
+ // Streaming message becomes pendingMessage
319
+ return {
320
+ ...state,
321
+ pendingMessage: action.message,
322
+ messages: computeMessages(state.historyMessages, action.message),
323
+ isStreaming: true,
324
+ };
325
+ }
326
+ // Non-streaming message goes directly to history
327
+ const newHistory = [...state.historyMessages, action.message];
328
+ return {
329
+ ...state,
330
+ historyMessages: newHistory,
331
+ messages: computeMessages(newHistory, state.pendingMessage),
332
+ isStreaming: state.pendingMessage?.isStreaming ?? false,
333
+ };
334
+ }
335
+
336
+ case "UPDATE_MESSAGE": {
337
+ // Check if updating pending message
338
+ if (state.pendingMessage?.id === action.id) {
339
+ // Merge toolCalls instead of replacing
340
+ const mergedToolCalls = mergeToolCalls(
341
+ state.pendingMessage.toolCalls,
342
+ action.updates.toolCalls
343
+ );
344
+ const updatedPending = {
345
+ ...state.pendingMessage,
346
+ ...action.updates,
347
+ toolCalls: mergedToolCalls,
348
+ };
349
+ return {
350
+ ...state,
351
+ pendingMessage: updatedPending,
352
+ messages: computeMessages(state.historyMessages, updatedPending),
353
+ isStreaming: updatedPending.isStreaming ?? false,
354
+ };
355
+ }
356
+
357
+ // Update in history
358
+ const messageIndex = state.historyMessages.findIndex((m) => m.id === action.id);
359
+ if (messageIndex === -1) {
360
+ return state;
361
+ }
362
+
363
+ const updatedHistory = [...state.historyMessages];
364
+ const existingMessage = updatedHistory[messageIndex];
365
+ if (existingMessage) {
366
+ // Merge toolCalls for history messages too
367
+ const mergedToolCalls = mergeToolCalls(existingMessage.toolCalls, action.updates.toolCalls);
368
+ updatedHistory[messageIndex] = {
369
+ ...existingMessage,
370
+ ...action.updates,
371
+ toolCalls: mergedToolCalls,
372
+ };
373
+ }
374
+
375
+ return {
376
+ ...state,
377
+ historyMessages: updatedHistory,
378
+ messages: computeMessages(updatedHistory, state.pendingMessage),
379
+ isStreaming: state.pendingMessage?.isStreaming ?? false,
380
+ };
381
+ }
382
+
383
+ case "APPEND_TO_MESSAGE": {
384
+ // Appending only makes sense for pending (streaming) message
385
+ if (state.pendingMessage?.id === action.id) {
386
+ const updatedPending = {
387
+ ...state.pendingMessage,
388
+ content: state.pendingMessage.content + action.content,
389
+ };
390
+ return {
391
+ ...state,
392
+ pendingMessage: updatedPending,
393
+ messages: computeMessages(state.historyMessages, updatedPending),
394
+ };
395
+ }
396
+
397
+ // Fallback: update in history (for backward compat)
398
+ const messageIndex = state.historyMessages.findIndex((m) => m.id === action.id);
399
+ if (messageIndex === -1) {
400
+ return state;
401
+ }
402
+
403
+ const updatedHistory = [...state.historyMessages];
404
+ const existingMessage = updatedHistory[messageIndex];
405
+ if (existingMessage) {
406
+ updatedHistory[messageIndex] = {
407
+ ...existingMessage,
408
+ content: existingMessage.content + action.content,
409
+ };
410
+ }
411
+
412
+ return {
413
+ ...state,
414
+ historyMessages: updatedHistory,
415
+ messages: computeMessages(updatedHistory, state.pendingMessage),
416
+ };
417
+ }
418
+
419
+ case "SET_MESSAGES": {
420
+ // Separate streaming and non-streaming messages
421
+ const streaming = action.messages.find((m) => m.isStreaming === true) ?? null;
422
+ const history = action.messages.filter((m) => m.isStreaming !== true);
423
+ return {
424
+ historyMessages: history,
425
+ pendingMessage: streaming,
426
+ messages: action.messages,
427
+ isStreaming: streaming !== null,
428
+ };
429
+ }
430
+
431
+ case "CLEAR_MESSAGES":
432
+ return {
433
+ ...initialState,
434
+ };
435
+
436
+ case "SET_STREAMING":
437
+ return {
438
+ ...state,
439
+ isStreaming: action.isStreaming,
440
+ };
441
+
442
+ case "COMMIT_PENDING_MESSAGE": {
443
+ if (!state.pendingMessage) {
444
+ return state;
445
+ }
446
+ // Move pending to history with isStreaming: false
447
+ const completedMessage = {
448
+ ...state.pendingMessage,
449
+ isStreaming: false,
450
+ };
451
+ const newHistory = [...state.historyMessages, completedMessage];
452
+ return {
453
+ historyMessages: newHistory,
454
+ pendingMessage: null,
455
+ messages: newHistory,
456
+ isStreaming: false,
457
+ };
458
+ }
459
+
460
+ case "SPLIT_MESSAGE": {
461
+ if (!state.pendingMessage) {
462
+ return state;
463
+ }
464
+
465
+ const content = state.pendingMessage.content;
466
+ if (action.splitIndex <= 0 || action.splitIndex >= content.length) {
467
+ return state;
468
+ }
469
+
470
+ // Create completed portion for history
471
+ const completedMessage: Message = {
472
+ ...state.pendingMessage,
473
+ id: generateMessageId(), // New ID for the split-off portion
474
+ content: content.slice(0, action.splitIndex),
475
+ isStreaming: false,
476
+ };
477
+
478
+ // Keep remainder as pending - mark as continuation of split message
479
+ const remainingMessage: Message = {
480
+ ...state.pendingMessage,
481
+ content: content.slice(action.splitIndex),
482
+ isContinuation: true,
483
+ };
484
+
485
+ const newHistory = [...state.historyMessages, completedMessage];
486
+ return {
487
+ historyMessages: newHistory,
488
+ pendingMessage: remainingMessage,
489
+ messages: computeMessages(newHistory, remainingMessage),
490
+ isStreaming: true,
491
+ };
492
+ }
493
+
494
+ case "APPEND_TO_THINKING": {
495
+ // Appending thinking only makes sense for pending (streaming) message
496
+ if (state.pendingMessage?.id === action.id) {
497
+ const updatedPending = {
498
+ ...state.pendingMessage,
499
+ thinking: (state.pendingMessage.thinking ?? "") + action.thinking,
500
+ };
501
+ return {
502
+ ...state,
503
+ pendingMessage: updatedPending,
504
+ messages: computeMessages(state.historyMessages, updatedPending),
505
+ };
506
+ }
507
+ return state;
508
+ }
509
+
510
+ case "ADD_TOOL_GROUP": {
511
+ // Create a new tool_group message (Gemini-style independent tool execution)
512
+ const toolGroupMessage: Message = {
513
+ id: generateMessageId(),
514
+ role: "tool_group",
515
+ content: "", // tool_group doesn't need content
516
+ timestamp: new Date(),
517
+ toolCalls: action.toolCalls,
518
+ isStreaming: false,
519
+ };
520
+ const newHistory = [...state.historyMessages, toolGroupMessage];
521
+ return {
522
+ ...state,
523
+ historyMessages: newHistory,
524
+ messages: computeMessages(newHistory, state.pendingMessage),
525
+ };
526
+ }
527
+
528
+ case "UPDATE_TOOL_GROUP": {
529
+ // Find and update the tool_group message with the specified groupId
530
+ const groupIndex = state.historyMessages.findIndex(
531
+ (m) => m.id === action.groupId && m.role === "tool_group"
532
+ );
533
+ if (groupIndex === -1) {
534
+ return state;
535
+ }
536
+
537
+ const updatedHistory = [...state.historyMessages];
538
+ const existingGroup = updatedHistory[groupIndex];
539
+ if (existingGroup) {
540
+ // Merge the tool call update into existing toolCalls
541
+ const mergedToolCalls = mergeToolCalls(existingGroup.toolCalls, [action.toolCall]);
542
+ updatedHistory[groupIndex] = {
543
+ ...existingGroup,
544
+ toolCalls: mergedToolCalls,
545
+ };
546
+ }
547
+
548
+ return {
549
+ ...state,
550
+ historyMessages: updatedHistory,
551
+ messages: computeMessages(updatedHistory, state.pendingMessage),
552
+ };
553
+ }
554
+
555
+ default:
556
+ // Exhaustive check - TypeScript will error if a case is missing
557
+ return state;
558
+ }
559
+ }
560
+
561
+ // =============================================================================
562
+ // ID Generation
563
+ // =============================================================================
564
+
565
+ /**
566
+ * Generate a unique message ID
567
+ *
568
+ * Uses crypto.randomUUID() when available, falls back to timestamp-based ID
569
+ */
570
+ function generateMessageId(): string {
571
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
572
+ return crypto.randomUUID();
573
+ }
574
+ // Fallback for environments without crypto.randomUUID
575
+ return `msg-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
576
+ }
577
+
578
+ // =============================================================================
579
+ // Context
580
+ // =============================================================================
581
+
582
+ /**
583
+ * Context value interface
584
+ */
585
+ export interface MessagesContextValue {
586
+ /** Current messages state */
587
+ readonly state: MessagesState;
588
+ /** Dispatch function for state updates */
589
+ readonly dispatch: Dispatch<MessagesAction>;
590
+ /** All messages in the conversation (history + pending) */
591
+ readonly messages: readonly Message[];
592
+ /** Completed messages for <Static> rendering (never re-render) */
593
+ readonly historyMessages: readonly Message[];
594
+ /** Currently streaming message (only this causes re-renders) */
595
+ readonly pendingMessage: Message | null;
596
+ /** Add a new message, returns the generated ID */
597
+ readonly addMessage: (message: Omit<Message, "id" | "timestamp">) => string;
598
+ /** Update an existing message */
599
+ readonly updateMessage: (id: string, updates: Partial<Omit<Message, "id">>) => void;
600
+ /** Append content to a message (for streaming) */
601
+ readonly appendToMessage: (id: string, content: string) => void;
602
+ /** Append thinking/reasoning content to a message (for streaming) */
603
+ readonly appendToThinking: (id: string, thinking: string) => void;
604
+ /** Replace the entire message list */
605
+ readonly setMessages: (messages: readonly Message[]) => void;
606
+ /** Clear all messages */
607
+ readonly clearMessages: () => void;
608
+ /** Commit pending message to history (for Static rendering) */
609
+ readonly commitPendingMessage: () => void;
610
+ /** Split a long streaming message at a safe point (e.g., paragraph boundary) */
611
+ readonly splitMessageAtSafePoint: (splitIndex: number) => void;
612
+ /** Add a new tool_group message (Gemini-style), returns the group ID */
613
+ readonly addToolGroup: (toolCalls: readonly ToolCallInfo[]) => string;
614
+ /** Update an existing tool_group message with a tool call update */
615
+ readonly updateToolGroup: (groupId: string, toolCall: ToolCallInfo) => void;
616
+ }
617
+
618
+ /**
619
+ * React context for messages state
620
+ *
621
+ * Initialized as undefined to detect usage outside provider
622
+ */
623
+ const MessagesContext = createContext<MessagesContextValue | undefined>(undefined);
624
+
625
+ // =============================================================================
626
+ // Hook
627
+ // =============================================================================
628
+
629
+ /**
630
+ * Hook to access the messages state and actions
631
+ *
632
+ * Must be used within a MessagesProvider component.
633
+ *
634
+ * @returns The current messages context value with state and actions
635
+ * @throws Error if used outside MessagesProvider
636
+ *
637
+ * @example
638
+ * ```tsx
639
+ * function ChatComponent() {
640
+ * const { messages, addMessage, appendToMessage, clearMessages } = useMessages();
641
+ *
642
+ * // Add a new message
643
+ * const handleSend = (content: string) => {
644
+ * const id = addMessage({ role: 'user', content });
645
+ * console.log('Created message:', id);
646
+ * };
647
+ *
648
+ * // Handle streaming content
649
+ * const handleStream = (id: string, chunk: string) => {
650
+ * appendToMessage(id, chunk);
651
+ * };
652
+ *
653
+ * // Clear conversation
654
+ * const handleClear = () => clearMessages();
655
+ *
656
+ * return <Box>...</Box>;
657
+ * }
658
+ * ```
659
+ */
660
+ export function useMessages(): MessagesContextValue {
661
+ const context = useContext(MessagesContext);
662
+
663
+ if (context === undefined) {
664
+ throw new Error(
665
+ "useMessages must be used within a MessagesProvider. " +
666
+ "Ensure your component is wrapped in <MessagesProvider>."
667
+ );
668
+ }
669
+
670
+ return context;
671
+ }
672
+
673
+ // =============================================================================
674
+ // Provider Props
675
+ // =============================================================================
676
+
677
+ /**
678
+ * Props for the MessagesProvider component
679
+ */
680
+ export interface MessagesProviderProps {
681
+ /**
682
+ * Initial messages to populate the conversation
683
+ */
684
+ readonly initialMessages?: readonly Message[];
685
+
686
+ /**
687
+ * Children to render within the messages context
688
+ */
689
+ readonly children: ReactNode;
690
+ }
691
+
692
+ // =============================================================================
693
+ // Provider Component
694
+ // =============================================================================
695
+
696
+ /**
697
+ * Messages state provider component
698
+ *
699
+ * Provides messages state context to all child components, enabling
700
+ * access to the message list and actions via the useMessages hook.
701
+ *
702
+ * @example
703
+ * ```tsx
704
+ * // Using default initial state
705
+ * <MessagesProvider>
706
+ * <ChatApp />
707
+ * </MessagesProvider>
708
+ *
709
+ * // Using initial messages
710
+ * <MessagesProvider initialMessages={[{ id: '1', role: 'system', content: 'Hello', timestamp: new Date() }]}>
711
+ * <ChatApp />
712
+ * </MessagesProvider>
713
+ * ```
714
+ */
715
+ export function MessagesProvider({
716
+ initialMessages,
717
+ children,
718
+ }: MessagesProviderProps): React.JSX.Element {
719
+ // State management with useReducer
720
+ const [state, dispatch] = useReducer(
721
+ messagesReducer,
722
+ initialMessages,
723
+ (messages): MessagesState => {
724
+ // Separate streaming and non-streaming from initial messages
725
+ const streaming = messages?.find((m) => m.isStreaming === true) ?? null;
726
+ const history = messages?.filter((m) => m.isStreaming !== true) ?? [];
727
+ return {
728
+ historyMessages: history,
729
+ pendingMessage: streaming,
730
+ messages: messages ?? [],
731
+ isStreaming: streaming !== null,
732
+ };
733
+ }
734
+ );
735
+
736
+ /**
737
+ * Add a new message to the conversation
738
+ * @returns The generated message ID
739
+ */
740
+ const addMessage = useCallback((message: Omit<Message, "id" | "timestamp">): string => {
741
+ const id = generateMessageId();
742
+ const fullMessage: Message = {
743
+ ...message,
744
+ id,
745
+ timestamp: new Date(),
746
+ };
747
+ dispatch({ type: "ADD_MESSAGE", message: fullMessage });
748
+ return id;
749
+ }, []);
750
+
751
+ /**
752
+ * Update an existing message
753
+ */
754
+ const updateMessage = useCallback((id: string, updates: Partial<Omit<Message, "id">>): void => {
755
+ dispatch({ type: "UPDATE_MESSAGE", id, updates });
756
+ }, []);
757
+
758
+ /**
759
+ * Append content to an existing message (for streaming)
760
+ */
761
+ const appendToMessage = useCallback((id: string, content: string): void => {
762
+ dispatch({ type: "APPEND_TO_MESSAGE", id, content });
763
+ }, []);
764
+
765
+ /**
766
+ * Append thinking/reasoning content to an existing message (for streaming)
767
+ */
768
+ const appendToThinking = useCallback((id: string, thinking: string): void => {
769
+ dispatch({ type: "APPEND_TO_THINKING", id, thinking });
770
+ }, []);
771
+
772
+ /**
773
+ * Clear all messages
774
+ */
775
+ const clearMessages = useCallback((): void => {
776
+ dispatch({ type: "CLEAR_MESSAGES" });
777
+ }, []);
778
+
779
+ /**
780
+ * Replace the entire message list
781
+ */
782
+ const setMessages = useCallback((messages: readonly Message[]): void => {
783
+ dispatch({ type: "SET_MESSAGES", messages });
784
+ }, []);
785
+
786
+ /**
787
+ * Commit pending message to history (for Static rendering)
788
+ */
789
+ const commitPendingMessage = useCallback((): void => {
790
+ dispatch({ type: "COMMIT_PENDING_MESSAGE" });
791
+ }, []);
792
+
793
+ /**
794
+ * Split a long streaming message at a safe point
795
+ */
796
+ const splitMessageAtSafePoint = useCallback((splitIndex: number): void => {
797
+ dispatch({ type: "SPLIT_MESSAGE", splitIndex });
798
+ }, []);
799
+
800
+ /**
801
+ * Add a new tool_group message (Gemini-style independent tool execution)
802
+ * @returns The generated group ID
803
+ */
804
+ const addToolGroup = useCallback((toolCalls: readonly ToolCallInfo[]): string => {
805
+ const id = generateMessageId();
806
+ // Dispatch with ID embedded in toolCalls (reducer will use generateMessageId for the message)
807
+ // Actually, we need the reducer to return the ID. For now, we generate it here and pass via action.
808
+ const toolGroupMessage: Message = {
809
+ id,
810
+ role: "tool_group",
811
+ content: "",
812
+ timestamp: new Date(),
813
+ toolCalls,
814
+ isStreaming: false,
815
+ };
816
+ dispatch({ type: "ADD_MESSAGE", message: toolGroupMessage });
817
+ return id;
818
+ }, []);
819
+
820
+ /**
821
+ * Update an existing tool_group message with a tool call update
822
+ */
823
+ const updateToolGroup = useCallback((groupId: string, toolCall: ToolCallInfo): void => {
824
+ dispatch({ type: "UPDATE_TOOL_GROUP", groupId, toolCall });
825
+ }, []);
826
+
827
+ /**
828
+ * Memoized context value
829
+ */
830
+ const contextValue = useMemo<MessagesContextValue>(
831
+ () => ({
832
+ state,
833
+ dispatch,
834
+ messages: state.messages,
835
+ historyMessages: state.historyMessages,
836
+ pendingMessage: state.pendingMessage,
837
+ addMessage,
838
+ updateMessage,
839
+ appendToMessage,
840
+ appendToThinking,
841
+ setMessages,
842
+ clearMessages,
843
+ commitPendingMessage,
844
+ splitMessageAtSafePoint,
845
+ addToolGroup,
846
+ updateToolGroup,
847
+ }),
848
+ [
849
+ state,
850
+ addMessage,
851
+ updateMessage,
852
+ appendToMessage,
853
+ appendToThinking,
854
+ setMessages,
855
+ clearMessages,
856
+ commitPendingMessage,
857
+ splitMessageAtSafePoint,
858
+ addToolGroup,
859
+ updateToolGroup,
860
+ ]
861
+ );
862
+
863
+ return <MessagesContext value={contextValue}>{children}</MessagesContext>;
864
+ }
865
+
866
+ // =============================================================================
867
+ // Exports
868
+ // =============================================================================
869
+
870
+ export { MessagesContext, initialState };