@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,1271 @@
1
+ /**
2
+ * TUI Integration Tests
3
+ *
4
+ * End-to-end integration tests for TUI flows covering:
5
+ * 1. Send message → see in MessageList
6
+ * 2. Receive response → streaming updates
7
+ * 3. Tool request → approval dialog
8
+ * 4. Tool result → status update
9
+ *
10
+ * Uses ink-testing-library for rendering tests.
11
+ *
12
+ * @module __tests__/tui-integration.test
13
+ */
14
+
15
+ import { getIcons, type IconSet } from "@vellum/shared";
16
+ import { Box, Text } from "ink";
17
+ import { render } from "ink-testing-library";
18
+ import type React from "react";
19
+ import { act, useEffect } from "react";
20
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
21
+
22
+ // Mock StreamingText to disable typewriter effect in integration tests
23
+ // This ensures content appears immediately for reliable assertions
24
+ vi.mock("../tui/components/Messages/StreamingText.js", async (importOriginal) => {
25
+ const original =
26
+ await importOriginal<typeof import("../tui/components/Messages/StreamingText.js")>();
27
+ return {
28
+ ...original,
29
+ StreamingText: (props: React.ComponentProps<typeof original.StreamingText>) => (
30
+ <original.StreamingText {...props} typewriterEffect={false} />
31
+ ),
32
+ };
33
+ });
34
+
35
+ // Icons are fetched in beforeEach to ensure setup has run first
36
+ let icons: IconSet;
37
+
38
+ beforeEach(() => {
39
+ // Get icons after setup has configured Unicode mode
40
+ icons = getIcons();
41
+ });
42
+
43
+ import {
44
+ type Message,
45
+ MessageList,
46
+ PermissionDialog,
47
+ RootProvider,
48
+ StreamingText,
49
+ ToolsPanel,
50
+ useMessages,
51
+ useToolApprovalController,
52
+ useTools,
53
+ } from "../tui/index.js";
54
+
55
+ // =============================================================================
56
+ // Test Utilities
57
+ // =============================================================================
58
+
59
+ /**
60
+ * Wrapper component to provide all contexts for integration tests
61
+ */
62
+ function IntegrationWrapper({
63
+ children,
64
+ initialMessages = [],
65
+ }: {
66
+ children: React.ReactNode;
67
+ initialMessages?: readonly Message[];
68
+ }) {
69
+ return (
70
+ <RootProvider theme="dark" initialMessages={initialMessages}>
71
+ {children}
72
+ </RootProvider>
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Create a test message with defaults
78
+ */
79
+ function createTestMessage(overrides: Partial<Message> = {}): Message {
80
+ return {
81
+ id: `msg-${Math.random().toString(36).slice(2)}`,
82
+ role: "user",
83
+ content: "Test message",
84
+ timestamp: new Date(),
85
+ ...overrides,
86
+ };
87
+ }
88
+
89
+ // =============================================================================
90
+ // Flow 1: Send Message → See in MessageList
91
+ // =============================================================================
92
+
93
+ describe("Integration: Send Message → MessageList", () => {
94
+ it("adds user message and displays it in MessageList", async () => {
95
+ // Component that sends a message and displays list
96
+ function TestComponent() {
97
+ const { messages, addMessage } = useMessages();
98
+
99
+ useEffect(() => {
100
+ // Simulate sending a message after mount
101
+ addMessage({ role: "user", content: "Hello, AI assistant!" });
102
+ }, [addMessage]);
103
+
104
+ return <MessageList messages={messages} />;
105
+ }
106
+
107
+ const { lastFrame } = render(
108
+ <IntegrationWrapper>
109
+ <TestComponent />
110
+ </IntegrationWrapper>
111
+ );
112
+
113
+ await act(async () => {
114
+ await new Promise((resolve) => setTimeout(resolve, 10));
115
+ });
116
+
117
+ const frame = lastFrame() ?? "";
118
+ expect(frame).toContain("Hello, AI assistant!");
119
+ expect(frame).toContain("You"); // User role label
120
+ expect(frame).toContain(icons.user); // User role icon
121
+ });
122
+
123
+ it("adds multiple messages in sequence and displays them all", async () => {
124
+ function TestComponent() {
125
+ const { messages, addMessage } = useMessages();
126
+
127
+ useEffect(() => {
128
+ addMessage({ role: "user", content: "First question" });
129
+ addMessage({ role: "assistant", content: "First answer" });
130
+ addMessage({ role: "user", content: "Second question" });
131
+ }, [addMessage]);
132
+
133
+ return <MessageList messages={messages} />;
134
+ }
135
+
136
+ const { lastFrame } = render(
137
+ <IntegrationWrapper>
138
+ <TestComponent />
139
+ </IntegrationWrapper>
140
+ );
141
+
142
+ await act(async () => {
143
+ await new Promise((resolve) => setTimeout(resolve, 10));
144
+ });
145
+
146
+ const frame = lastFrame() ?? "";
147
+ expect(frame).toContain("First question");
148
+ expect(frame).toContain("First answer");
149
+ expect(frame).toContain("Second question");
150
+ expect(frame).toContain("You"); // User messages
151
+ expect(frame).toContain("Vellum"); // Assistant message
152
+ });
153
+
154
+ it("displays messages with correct role icons", async () => {
155
+ function TestComponent() {
156
+ const { messages, addMessage } = useMessages();
157
+
158
+ useEffect(() => {
159
+ addMessage({ role: "user", content: "User message" });
160
+ addMessage({ role: "assistant", content: "Assistant message" });
161
+ addMessage({ role: "system", content: "System message" });
162
+ addMessage({ role: "tool", content: "Tool result" });
163
+ }, [addMessage]);
164
+
165
+ return <MessageList messages={messages} />;
166
+ }
167
+
168
+ const { lastFrame } = render(
169
+ <IntegrationWrapper>
170
+ <TestComponent />
171
+ </IntegrationWrapper>
172
+ );
173
+
174
+ await act(async () => {
175
+ await new Promise((resolve) => setTimeout(resolve, 10));
176
+ });
177
+
178
+ const frame = lastFrame() ?? "";
179
+ expect(frame).toContain(icons.user); // user
180
+ expect(frame).toContain(icons.assistant); // assistant
181
+ expect(frame).toContain(icons.system); // system
182
+ expect(frame).toContain(icons.tool); // tool
183
+ });
184
+
185
+ it("handles initial messages in provider", () => {
186
+ const initialMessages: Message[] = [
187
+ createTestMessage({ id: "init-1", role: "system", content: "System prompt" }),
188
+ createTestMessage({ id: "init-2", role: "user", content: "Initial user query" }),
189
+ ];
190
+
191
+ function TestComponent() {
192
+ const { messages } = useMessages();
193
+ return <MessageList messages={messages} />;
194
+ }
195
+
196
+ const { lastFrame } = render(
197
+ <IntegrationWrapper initialMessages={initialMessages}>
198
+ <TestComponent />
199
+ </IntegrationWrapper>
200
+ );
201
+
202
+ const frame = lastFrame() ?? "";
203
+ expect(frame).toContain("System prompt");
204
+ expect(frame).toContain("Initial user query");
205
+ });
206
+
207
+ it("clears all messages when clearMessages is called", async () => {
208
+ function TestComponent() {
209
+ const { messages, addMessage, clearMessages } = useMessages();
210
+
211
+ useEffect(() => {
212
+ addMessage({ role: "user", content: "Message to be cleared" });
213
+ // Clear after adding
214
+ setTimeout(() => clearMessages(), 5);
215
+ }, [addMessage, clearMessages]);
216
+
217
+ return <MessageList messages={messages} />;
218
+ }
219
+
220
+ const { lastFrame } = render(
221
+ <IntegrationWrapper>
222
+ <TestComponent />
223
+ </IntegrationWrapper>
224
+ );
225
+
226
+ // Wait for clear
227
+ await act(async () => {
228
+ await new Promise((resolve) => setTimeout(resolve, 20));
229
+ });
230
+
231
+ const frame = lastFrame() ?? "";
232
+ expect(frame).toContain("No messages yet");
233
+ });
234
+ });
235
+
236
+ // =============================================================================
237
+ // Flow 2: Receive Response → Streaming Updates
238
+ // =============================================================================
239
+
240
+ describe("Integration: Receive Response → Streaming Updates", () => {
241
+ beforeEach(() => {
242
+ vi.useFakeTimers();
243
+ });
244
+
245
+ afterEach(() => {
246
+ vi.useRealTimers();
247
+ });
248
+
249
+ it("shows streaming indicator during response", async () => {
250
+ function TestComponent() {
251
+ const { messages, addMessage, updateMessage } = useMessages();
252
+
253
+ useEffect(() => {
254
+ // Add a streaming message
255
+ const id = addMessage({
256
+ role: "assistant",
257
+ content: "Generating...",
258
+ isStreaming: true,
259
+ });
260
+
261
+ // Complete streaming after delay
262
+ setTimeout(() => {
263
+ updateMessage(id, { isStreaming: false, content: "Complete response" });
264
+ }, 500);
265
+ }, [addMessage, updateMessage]);
266
+
267
+ return <MessageList messages={messages} />;
268
+ }
269
+
270
+ const { lastFrame } = render(
271
+ <IntegrationWrapper>
272
+ <TestComponent />
273
+ </IntegrationWrapper>
274
+ );
275
+
276
+ // Wait for render
277
+ await act(async () => {
278
+ vi.advanceTimersByTime(10);
279
+ });
280
+
281
+ let frame = lastFrame() ?? "";
282
+ expect(frame).toContain("streaming");
283
+ expect(frame).toContain("Generating...");
284
+
285
+ // After completion
286
+ await act(async () => {
287
+ vi.advanceTimersByTime(600);
288
+ });
289
+
290
+ frame = lastFrame() ?? "";
291
+ expect(frame).toContain("Complete response");
292
+ });
293
+
294
+ it("appends content during streaming", async () => {
295
+ function TestComponent() {
296
+ const { messages, addMessage, appendToMessage } = useMessages();
297
+
298
+ useEffect(() => {
299
+ const id = addMessage({
300
+ role: "assistant",
301
+ content: "Hello",
302
+ isStreaming: true,
303
+ });
304
+
305
+ // Simulate streaming chunks
306
+ setTimeout(() => appendToMessage(id, ", world"), 100);
307
+ setTimeout(() => appendToMessage(id, "!"), 200);
308
+ }, [addMessage, appendToMessage]);
309
+
310
+ return <MessageList messages={messages} />;
311
+ }
312
+
313
+ const { lastFrame } = render(
314
+ <IntegrationWrapper>
315
+ <TestComponent />
316
+ </IntegrationWrapper>
317
+ );
318
+
319
+ // Wait for initial render
320
+ await act(async () => {
321
+ vi.advanceTimersByTime(10);
322
+ });
323
+ expect(lastFrame()).toContain("Hello");
324
+
325
+ // First append
326
+ await act(async () => {
327
+ vi.advanceTimersByTime(100);
328
+ });
329
+ expect(lastFrame()).toContain("Hello, world");
330
+
331
+ // Second append
332
+ await act(async () => {
333
+ vi.advanceTimersByTime(100);
334
+ });
335
+ expect(lastFrame()).toContain("Hello, world!");
336
+ });
337
+
338
+ it("renders StreamingText component with cursor during streaming", async () => {
339
+ // Disable typewriter for immediate content display test
340
+ const { lastFrame } = render(
341
+ <IntegrationWrapper>
342
+ <StreamingText content="Typing in progress" isStreaming={true} typewriterEffect={false} />
343
+ </IntegrationWrapper>
344
+ );
345
+
346
+ const frame = lastFrame() ?? "";
347
+ expect(frame).toContain("Typing in progress");
348
+ expect(frame).toContain("▊"); // Default cursor
349
+ });
350
+
351
+ it("hides cursor when streaming completes", () => {
352
+ const { lastFrame } = render(
353
+ <IntegrationWrapper>
354
+ <StreamingText content="Complete message" isStreaming={false} />
355
+ </IntegrationWrapper>
356
+ );
357
+
358
+ const frame = lastFrame() ?? "";
359
+ expect(frame).toContain("Complete message");
360
+ expect(frame).not.toContain("▊");
361
+ });
362
+
363
+ it("shows empty state with ellipsis during streaming", async () => {
364
+ function TestComponent() {
365
+ const { messages, addMessage } = useMessages();
366
+
367
+ useEffect(() => {
368
+ addMessage({
369
+ role: "assistant",
370
+ content: "",
371
+ isStreaming: true,
372
+ });
373
+ }, [addMessage]);
374
+
375
+ return <MessageList messages={messages} />;
376
+ }
377
+
378
+ const { lastFrame } = render(
379
+ <IntegrationWrapper>
380
+ <TestComponent />
381
+ </IntegrationWrapper>
382
+ );
383
+
384
+ // Wait for message to be added
385
+ await act(async () => {
386
+ vi.advanceTimersByTime(10);
387
+ });
388
+
389
+ const frame = lastFrame() ?? "";
390
+ // Empty streaming message shows ellipsis placeholder
391
+ expect(frame).toContain("...");
392
+ });
393
+ });
394
+
395
+ // =============================================================================
396
+ // Flow 3: Tool Request → Approval Dialog
397
+ // =============================================================================
398
+
399
+ describe("Integration: Tool Request → Approval Dialog", () => {
400
+ it("shows pending tool execution in dialog", async () => {
401
+ function TestComponent() {
402
+ const { pendingApproval, addExecution, approveExecution, rejectExecution } = useTools();
403
+
404
+ useEffect(() => {
405
+ addExecution({
406
+ toolName: "read_file",
407
+ params: { path: "/test.txt" },
408
+ });
409
+ }, [addExecution]);
410
+
411
+ const pending = pendingApproval[0];
412
+ if (!pending) return <Text>No pending tools</Text>;
413
+
414
+ return (
415
+ <PermissionDialog
416
+ execution={pending}
417
+ riskLevel="low"
418
+ onApprove={() => approveExecution(pending.id)}
419
+ onReject={() => rejectExecution(pending.id)}
420
+ />
421
+ );
422
+ }
423
+
424
+ const { lastFrame } = render(
425
+ <RootProvider>
426
+ <TestComponent />
427
+ </RootProvider>
428
+ );
429
+
430
+ // Wait for state update
431
+ await act(async () => {
432
+ await new Promise((resolve) => setTimeout(resolve, 20));
433
+ });
434
+
435
+ const frame = lastFrame() ?? "";
436
+ expect(frame).toContain("read_file");
437
+ expect(frame).toContain("Low Risk");
438
+ expect(frame).toContain("●"); // Low risk icon
439
+ });
440
+
441
+ it("displays different risk levels for tools", () => {
442
+ const riskLevels = [
443
+ { level: "low" as const, label: "Low Risk", icon: "●" },
444
+ { level: "medium" as const, label: "Medium Risk", icon: "▲" },
445
+ { level: "high" as const, label: "High Risk", icon: "◆" },
446
+ { level: "critical" as const, label: "Critical Risk", icon: "⬢" },
447
+ ];
448
+
449
+ for (const { level, label, icon } of riskLevels) {
450
+ const execution = {
451
+ id: "test-1",
452
+ toolName: "dangerous_operation",
453
+ params: {},
454
+ status: "pending" as const,
455
+ };
456
+
457
+ const { lastFrame } = render(
458
+ <RootProvider>
459
+ <PermissionDialog
460
+ execution={execution}
461
+ riskLevel={level}
462
+ onApprove={vi.fn()}
463
+ onReject={vi.fn()}
464
+ />
465
+ </RootProvider>
466
+ );
467
+
468
+ const frame = lastFrame() ?? "";
469
+ expect(frame).toContain(label);
470
+ expect(frame).toContain(icon);
471
+ }
472
+ });
473
+
474
+ it("shows tool parameters in dialog", () => {
475
+ const execution = {
476
+ id: "test-1",
477
+ toolName: "write_file",
478
+ params: {
479
+ path: "/output.txt",
480
+ content: "Hello",
481
+ },
482
+ status: "pending" as const,
483
+ };
484
+
485
+ const { lastFrame } = render(
486
+ <RootProvider>
487
+ <PermissionDialog
488
+ execution={execution}
489
+ riskLevel="medium"
490
+ onApprove={vi.fn()}
491
+ onReject={vi.fn()}
492
+ />
493
+ </RootProvider>
494
+ );
495
+
496
+ const frame = lastFrame() ?? "";
497
+ expect(frame).toContain("write_file");
498
+ expect(frame).toContain("path");
499
+ expect(frame).toContain("/output.txt");
500
+ });
501
+
502
+ it("processes multiple pending tool requests", async () => {
503
+ function TestComponent() {
504
+ const { pendingApproval, addExecution } = useTools();
505
+
506
+ useEffect(() => {
507
+ addExecution({ toolName: "read_file", params: { path: "/a.txt" } });
508
+ addExecution({ toolName: "write_file", params: { path: "/b.txt" } });
509
+ addExecution({ toolName: "execute_command", params: { cmd: "ls" } });
510
+ }, [addExecution]);
511
+
512
+ return (
513
+ <Box flexDirection="column">
514
+ <Text>Pending: {pendingApproval.length}</Text>
515
+ {pendingApproval.map((exec) => (
516
+ <Text key={exec.id}>{exec.toolName}</Text>
517
+ ))}
518
+ </Box>
519
+ );
520
+ }
521
+
522
+ const { lastFrame } = render(
523
+ <RootProvider>
524
+ <TestComponent />
525
+ </RootProvider>
526
+ );
527
+
528
+ // Wait for state updates
529
+ await act(async () => {
530
+ await new Promise((resolve) => setTimeout(resolve, 20));
531
+ });
532
+
533
+ const frame = lastFrame() ?? "";
534
+ expect(frame).toContain("Pending: 3");
535
+ expect(frame).toContain("read_file");
536
+ expect(frame).toContain("write_file");
537
+ expect(frame).toContain("execute_command");
538
+ });
539
+ });
540
+
541
+ // =============================================================================
542
+ // Flow 3b: Tool Approval Controller → Resume AgentLoop
543
+ // =============================================================================
544
+
545
+ describe("Integration: Tool Approval Controller → Resume", () => {
546
+ it("approves the active pending tool and calls grantPermission()", async () => {
547
+ const permissionGate = {
548
+ grantPermission: vi.fn(),
549
+ denyPermission: vi.fn(),
550
+ };
551
+
552
+ function TestComponent() {
553
+ const { addExecution, executions } = useTools();
554
+ const { activeApproval, approveActive } = useToolApprovalController({
555
+ agentLoop: permissionGate,
556
+ });
557
+
558
+ useEffect(() => {
559
+ addExecution({ toolName: "read_file", params: { path: "/test.txt" } });
560
+ }, [addExecution]);
561
+
562
+ useEffect(() => {
563
+ if (activeApproval) {
564
+ approveActive("once");
565
+ }
566
+ }, [activeApproval, approveActive]);
567
+
568
+ return <Text>{executions[0]?.status ?? "none"}</Text>;
569
+ }
570
+
571
+ const { lastFrame } = render(
572
+ <RootProvider>
573
+ <TestComponent />
574
+ </RootProvider>
575
+ );
576
+
577
+ await act(async () => {
578
+ await new Promise((resolve) => setTimeout(resolve, 20));
579
+ });
580
+
581
+ expect(permissionGate.grantPermission).toHaveBeenCalledTimes(1);
582
+ expect(permissionGate.denyPermission).toHaveBeenCalledTimes(0);
583
+
584
+ const frame = lastFrame() ?? "";
585
+ expect(frame).toContain("approved");
586
+ });
587
+
588
+ it("rejects the active pending tool and calls denyPermission()", async () => {
589
+ const permissionGate = {
590
+ grantPermission: vi.fn(),
591
+ denyPermission: vi.fn(),
592
+ };
593
+
594
+ function TestComponent() {
595
+ const { addExecution, executions } = useTools();
596
+ const { activeApproval, rejectActive } = useToolApprovalController({
597
+ agentLoop: permissionGate,
598
+ });
599
+
600
+ useEffect(() => {
601
+ addExecution({ toolName: "write_file", params: { path: "/out.txt" } });
602
+ }, [addExecution]);
603
+
604
+ useEffect(() => {
605
+ if (activeApproval) {
606
+ rejectActive();
607
+ }
608
+ }, [activeApproval, rejectActive]);
609
+
610
+ return <Text>{executions[0]?.status ?? "none"}</Text>;
611
+ }
612
+
613
+ const { lastFrame } = render(
614
+ <RootProvider>
615
+ <TestComponent />
616
+ </RootProvider>
617
+ );
618
+
619
+ await act(async () => {
620
+ await new Promise((resolve) => setTimeout(resolve, 20));
621
+ });
622
+
623
+ expect(permissionGate.denyPermission).toHaveBeenCalledTimes(1);
624
+ expect(permissionGate.grantPermission).toHaveBeenCalledTimes(0);
625
+
626
+ const frame = lastFrame() ?? "";
627
+ expect(frame).toContain("rejected");
628
+ });
629
+ });
630
+
631
+ // =============================================================================
632
+ // Flow 3c: Tools Panel → Execution Rendering
633
+ // =============================================================================
634
+
635
+ describe("Integration: ToolsPanel", () => {
636
+ it("renders recent tool executions and pending count", async () => {
637
+ function TestComponent() {
638
+ const { addExecution, updateExecution } = useTools();
639
+
640
+ useEffect(() => {
641
+ const first = addExecution({ toolName: "read_file", params: { path: "/a.txt" } });
642
+ const second = addExecution({ toolName: "write_file", params: { path: "/b.txt" } });
643
+ updateExecution(first, { status: "complete", completedAt: new Date() });
644
+ updateExecution(second, { status: "pending" });
645
+ }, [addExecution, updateExecution]);
646
+
647
+ return <ToolsPanel maxItems={10} />;
648
+ }
649
+
650
+ const { lastFrame } = render(
651
+ <RootProvider>
652
+ <TestComponent />
653
+ </RootProvider>
654
+ );
655
+
656
+ await act(async () => {
657
+ await new Promise((resolve) => setTimeout(resolve, 20));
658
+ });
659
+
660
+ const frame = lastFrame() ?? "";
661
+ expect(frame).toContain("Tools");
662
+ expect(frame).toContain("Pending: 1");
663
+ expect(frame).toContain("Total: 2");
664
+ expect(frame).toContain("read_file");
665
+ expect(frame).toContain("write_file");
666
+ });
667
+ });
668
+
669
+ // =============================================================================
670
+ // Flow 4: Tool Result → Status Update
671
+ // =============================================================================
672
+
673
+ describe("Integration: Tool Result → Status Update", () => {
674
+ it("shows running status after approval", async () => {
675
+ function TestComponent() {
676
+ const { executions, addExecution, approveExecution, updateExecution } = useTools();
677
+
678
+ useEffect(() => {
679
+ const id = addExecution({ toolName: "read_file", params: {} });
680
+ // Approve and start running
681
+ setTimeout(() => {
682
+ approveExecution(id);
683
+ updateExecution(id, { status: "running", startedAt: new Date() });
684
+ }, 10);
685
+ }, [addExecution, approveExecution, updateExecution]);
686
+
687
+ const exec = executions[0];
688
+ if (!exec) return <Text>No executions</Text>;
689
+
690
+ return (
691
+ <Text>
692
+ {exec.status} - {exec.toolName}
693
+ </Text>
694
+ );
695
+ }
696
+
697
+ const { lastFrame } = render(
698
+ <RootProvider>
699
+ <TestComponent />
700
+ </RootProvider>
701
+ );
702
+
703
+ // Wait for status update
704
+ await act(async () => {
705
+ await new Promise((resolve) => setTimeout(resolve, 20));
706
+ });
707
+
708
+ const frame = lastFrame() ?? "";
709
+ // Running status check
710
+ expect(frame).toContain("running");
711
+ expect(frame).toContain("read_file");
712
+ });
713
+
714
+ it("shows complete status with result", async () => {
715
+ function TestComponent() {
716
+ const { executions, addExecution, updateExecution } = useTools();
717
+
718
+ useEffect(() => {
719
+ const id = addExecution({ toolName: "read_file", params: {} });
720
+ // Complete execution
721
+ setTimeout(() => {
722
+ updateExecution(id, {
723
+ status: "complete",
724
+ result: "File contents here",
725
+ completedAt: new Date(),
726
+ });
727
+ }, 10);
728
+ }, [addExecution, updateExecution]);
729
+
730
+ const exec = executions[0];
731
+ if (!exec) return <Text>No executions</Text>;
732
+
733
+ return (
734
+ <Text>
735
+ {exec.status} - {exec.toolName}
736
+ </Text>
737
+ );
738
+ }
739
+
740
+ const { lastFrame } = render(
741
+ <RootProvider>
742
+ <TestComponent />
743
+ </RootProvider>
744
+ );
745
+
746
+ // Wait for completion
747
+ await act(async () => {
748
+ await new Promise((resolve) => setTimeout(resolve, 20));
749
+ });
750
+
751
+ const frame = lastFrame() ?? "";
752
+ expect(frame).toContain("complete");
753
+ expect(frame).toContain("read_file");
754
+ });
755
+
756
+ it("shows error status with error info", async () => {
757
+ function TestComponent() {
758
+ const { executions, addExecution, updateExecution } = useTools();
759
+
760
+ useEffect(() => {
761
+ const id = addExecution({ toolName: "execute_command", params: {} });
762
+ // Fail execution
763
+ setTimeout(() => {
764
+ updateExecution(id, {
765
+ status: "error",
766
+ error: new Error("Permission denied"),
767
+ completedAt: new Date(),
768
+ });
769
+ }, 10);
770
+ }, [addExecution, updateExecution]);
771
+
772
+ const exec = executions[0];
773
+ if (!exec) return <Text>No executions</Text>;
774
+
775
+ return (
776
+ <Text>
777
+ {exec.status} - {exec.toolName}
778
+ </Text>
779
+ );
780
+ }
781
+
782
+ const { lastFrame } = render(
783
+ <RootProvider>
784
+ <TestComponent />
785
+ </RootProvider>
786
+ );
787
+
788
+ // Wait for error
789
+ await act(async () => {
790
+ await new Promise((resolve) => setTimeout(resolve, 20));
791
+ });
792
+
793
+ const frame = lastFrame() ?? "";
794
+ expect(frame).toContain("error");
795
+ expect(frame).toContain("execute_command");
796
+ });
797
+
798
+ it("shows rejected status when tool is rejected", async () => {
799
+ function TestComponent() {
800
+ const { executions, addExecution, rejectExecution } = useTools();
801
+
802
+ useEffect(() => {
803
+ const id = addExecution({ toolName: "dangerous_tool", params: {} });
804
+ // Reject execution
805
+ setTimeout(() => rejectExecution(id), 10);
806
+ }, [addExecution, rejectExecution]);
807
+
808
+ const exec = executions[0];
809
+ if (!exec) return <Text>No executions</Text>;
810
+
811
+ return (
812
+ <Text>
813
+ {exec.status} - {exec.toolName}
814
+ </Text>
815
+ );
816
+ }
817
+
818
+ const { lastFrame } = render(
819
+ <RootProvider>
820
+ <TestComponent />
821
+ </RootProvider>
822
+ );
823
+
824
+ // Wait for rejection
825
+ await act(async () => {
826
+ await new Promise((resolve) => setTimeout(resolve, 20));
827
+ });
828
+
829
+ const frame = lastFrame() ?? "";
830
+ expect(frame).toContain("rejected");
831
+ expect(frame).toContain("dangerous_tool");
832
+ });
833
+
834
+ it("tracks full tool lifecycle: pending → approved → running → complete", async () => {
835
+ // Use fake timers for deterministic control over async state transitions
836
+ vi.useFakeTimers();
837
+
838
+ const statusHistory: string[] = [];
839
+
840
+ function TestComponent() {
841
+ const { executions, addExecution, approveExecution, updateExecution } = useTools();
842
+
843
+ useEffect(() => {
844
+ const id = addExecution({ toolName: "test_tool", params: {} });
845
+
846
+ // Lifecycle simulation with delays
847
+ setTimeout(() => {
848
+ approveExecution(id);
849
+ }, 100);
850
+
851
+ setTimeout(() => {
852
+ updateExecution(id, { status: "running", startedAt: new Date() });
853
+ }, 200);
854
+
855
+ setTimeout(() => {
856
+ updateExecution(id, {
857
+ status: "complete",
858
+ result: "success",
859
+ completedAt: new Date(),
860
+ });
861
+ }, 300);
862
+ }, [addExecution, approveExecution, updateExecution]);
863
+
864
+ const exec = executions[0];
865
+ const status = exec?.status;
866
+
867
+ // Track status changes via useEffect to capture every state transition
868
+ useEffect(() => {
869
+ if (status && !statusHistory.includes(status)) {
870
+ statusHistory.push(status);
871
+ }
872
+ }, [status]);
873
+
874
+ return <Text>{exec?.status ?? "none"}</Text>;
875
+ }
876
+
877
+ render(
878
+ <RootProvider>
879
+ <TestComponent />
880
+ </RootProvider>
881
+ );
882
+
883
+ // Initial render captures pending state
884
+ await act(async () => {
885
+ await vi.advanceTimersByTimeAsync(50);
886
+ });
887
+
888
+ // Advance to approved state (100ms)
889
+ await act(async () => {
890
+ await vi.advanceTimersByTimeAsync(100);
891
+ });
892
+
893
+ // Advance to running state (200ms)
894
+ await act(async () => {
895
+ await vi.advanceTimersByTimeAsync(100);
896
+ });
897
+
898
+ // Advance to complete state (300ms)
899
+ await act(async () => {
900
+ await vi.advanceTimersByTimeAsync(100);
901
+ });
902
+
903
+ // Restore real timers
904
+ vi.useRealTimers();
905
+
906
+ // Verify we went through the states
907
+ // Note: Due to React's batching, 'running' may be skipped in fast transitions.
908
+ // We assert the essential states and allow running to be optional.
909
+ expect(statusHistory).toContain("pending");
910
+ expect(statusHistory).toContain("approved");
911
+ expect(statusHistory).toContain("complete");
912
+ // Running is expected but may be batched away in some environments
913
+ // The key invariant is: pending → approved → complete happened in order
914
+ expect(statusHistory.indexOf("pending")).toBeLessThan(statusHistory.indexOf("approved"));
915
+ expect(statusHistory.indexOf("approved")).toBeLessThan(statusHistory.indexOf("complete"));
916
+ });
917
+
918
+ it("approves all pending tools at once", async () => {
919
+ function TestComponent() {
920
+ const { executions, pendingApproval, addExecution, approveAll } = useTools();
921
+
922
+ useEffect(() => {
923
+ addExecution({ toolName: "tool_1", params: {} });
924
+ addExecution({ toolName: "tool_2", params: {} });
925
+ addExecution({ toolName: "tool_3", params: {} });
926
+
927
+ // Approve all after adding
928
+ setTimeout(() => approveAll(), 10);
929
+ }, [addExecution, approveAll]);
930
+
931
+ return (
932
+ <Box flexDirection="column">
933
+ <Text>Pending: {pendingApproval.length}</Text>
934
+ <Text>Approved: {executions.filter((e) => e.status === "approved").length}</Text>
935
+ </Box>
936
+ );
937
+ }
938
+
939
+ const { lastFrame } = render(
940
+ <RootProvider>
941
+ <TestComponent />
942
+ </RootProvider>
943
+ );
944
+
945
+ // Wait for approve all
946
+ await act(async () => {
947
+ await new Promise((resolve) => setTimeout(resolve, 20));
948
+ });
949
+
950
+ const frame = lastFrame() ?? "";
951
+ expect(frame).toContain("Pending: 0");
952
+ expect(frame).toContain("Approved: 3");
953
+ });
954
+
955
+ it("clears all tool executions", async () => {
956
+ function TestComponent() {
957
+ const { executions, addExecution, clearExecutions } = useTools();
958
+
959
+ useEffect(() => {
960
+ addExecution({ toolName: "tool_1", params: {} });
961
+ addExecution({ toolName: "tool_2", params: {} });
962
+
963
+ // Clear after adding
964
+ setTimeout(() => clearExecutions(), 10);
965
+ }, [addExecution, clearExecutions]);
966
+
967
+ return <Text>Count: {executions.length}</Text>;
968
+ }
969
+
970
+ const { lastFrame } = render(
971
+ <RootProvider>
972
+ <TestComponent />
973
+ </RootProvider>
974
+ );
975
+
976
+ // Wait for clear
977
+ await act(async () => {
978
+ await new Promise((resolve) => setTimeout(resolve, 20));
979
+ });
980
+
981
+ expect(lastFrame()).toContain("Count: 0");
982
+ });
983
+ });
984
+
985
+ // =============================================================================
986
+ // Combined Integration Flows
987
+ // =============================================================================
988
+
989
+ describe("Integration: Combined Message and Tool Flows", () => {
990
+ it("shows tool_group message with tool calls", async () => {
991
+ function TestComponent() {
992
+ const { messages, addMessage } = useMessages();
993
+
994
+ useEffect(() => {
995
+ // Assistant message announcing tool usage
996
+ addMessage({
997
+ role: "assistant",
998
+ content: "Let me read that file for you",
999
+ });
1000
+ // Separate tool_group message for tool execution
1001
+ addMessage({
1002
+ role: "tool_group",
1003
+ content: "",
1004
+ toolCalls: [
1005
+ {
1006
+ id: "tc-1",
1007
+ name: "read_file",
1008
+ arguments: { path: "/test.txt" },
1009
+ status: "completed",
1010
+ result: "file contents",
1011
+ },
1012
+ ],
1013
+ });
1014
+ }, [addMessage]);
1015
+
1016
+ return <MessageList messages={messages} />;
1017
+ }
1018
+
1019
+ const { lastFrame } = render(
1020
+ <IntegrationWrapper>
1021
+ <TestComponent />
1022
+ </IntegrationWrapper>
1023
+ );
1024
+
1025
+ await act(async () => {
1026
+ await new Promise((resolve) => setTimeout(resolve, 10));
1027
+ });
1028
+
1029
+ const frame = lastFrame() ?? "";
1030
+ expect(frame).toContain("Let me read that file for you");
1031
+ expect(frame).toContain("read_file");
1032
+ });
1033
+
1034
+ it("handles conversation with interleaved messages and tool_group", async () => {
1035
+ function TestComponent() {
1036
+ const { messages, addMessage } = useMessages();
1037
+
1038
+ useEffect(() => {
1039
+ addMessage({ role: "user", content: "Read the config file" });
1040
+ addMessage({
1041
+ role: "assistant",
1042
+ content: "Reading config.json...",
1043
+ });
1044
+ // Tool execution as separate tool_group message
1045
+ addMessage({
1046
+ role: "tool_group",
1047
+ content: "",
1048
+ toolCalls: [
1049
+ {
1050
+ id: "tc-1",
1051
+ name: "read_file",
1052
+ arguments: { path: "config.json" },
1053
+ status: "completed",
1054
+ },
1055
+ ],
1056
+ });
1057
+ addMessage({ role: "tool", content: '{"debug": true}' });
1058
+ addMessage({
1059
+ role: "assistant",
1060
+ content: "The config has debug mode enabled",
1061
+ });
1062
+ }, [addMessage]);
1063
+
1064
+ return <MessageList messages={messages} />;
1065
+ }
1066
+
1067
+ const { lastFrame } = render(
1068
+ <IntegrationWrapper>
1069
+ <TestComponent />
1070
+ </IntegrationWrapper>
1071
+ );
1072
+
1073
+ await act(async () => {
1074
+ await new Promise((resolve) => setTimeout(resolve, 10));
1075
+ });
1076
+
1077
+ const frame = lastFrame() ?? "";
1078
+ expect(frame).toContain("Read the config file");
1079
+ expect(frame).toContain("Reading config.json...");
1080
+ expect(frame).toContain("read_file");
1081
+ expect(frame).toContain("debug");
1082
+ expect(frame).toContain("debug mode enabled");
1083
+ });
1084
+
1085
+ it("updates tool_group message after tool completion", async () => {
1086
+ function TestComponent() {
1087
+ const { messages, addMessage, updateMessage } = useMessages();
1088
+
1089
+ useEffect(() => {
1090
+ // Assistant announces processing
1091
+ addMessage({
1092
+ role: "assistant",
1093
+ content: "Processing...",
1094
+ });
1095
+
1096
+ // Tool_group with pending tool
1097
+ const toolGroupId = addMessage({
1098
+ role: "tool_group",
1099
+ content: "",
1100
+ toolCalls: [
1101
+ {
1102
+ id: "tc-1",
1103
+ name: "analyze",
1104
+ arguments: {},
1105
+ status: "pending",
1106
+ },
1107
+ ],
1108
+ });
1109
+
1110
+ // Update tool_group with completed tool
1111
+ setTimeout(() => {
1112
+ updateMessage(toolGroupId, {
1113
+ toolCalls: [
1114
+ {
1115
+ id: "tc-1",
1116
+ name: "analyze",
1117
+ arguments: {},
1118
+ status: "completed",
1119
+ result: { score: 95 },
1120
+ },
1121
+ ],
1122
+ });
1123
+ // Add completion message
1124
+ addMessage({
1125
+ role: "assistant",
1126
+ content: "Analysis complete",
1127
+ });
1128
+ }, 10);
1129
+ }, [addMessage, updateMessage]);
1130
+
1131
+ return <MessageList messages={messages} />;
1132
+ }
1133
+
1134
+ const { lastFrame } = render(
1135
+ <IntegrationWrapper>
1136
+ <TestComponent />
1137
+ </IntegrationWrapper>
1138
+ );
1139
+
1140
+ // After update
1141
+ await act(async () => {
1142
+ await new Promise((resolve) => setTimeout(resolve, 20));
1143
+ });
1144
+
1145
+ const frame = lastFrame() ?? "";
1146
+ expect(frame).toContain("analyze");
1147
+ expect(frame).toContain("Analysis complete");
1148
+ });
1149
+ });
1150
+
1151
+ // =============================================================================
1152
+ // Edge Cases and Error Handling
1153
+ // =============================================================================
1154
+
1155
+ describe("Integration: Edge Cases", () => {
1156
+ it("handles empty message list gracefully", () => {
1157
+ function TestComponent() {
1158
+ const { messages } = useMessages();
1159
+ return <MessageList messages={messages} />;
1160
+ }
1161
+
1162
+ const { lastFrame } = render(
1163
+ <IntegrationWrapper>
1164
+ <TestComponent />
1165
+ </IntegrationWrapper>
1166
+ );
1167
+
1168
+ expect(lastFrame()).toContain("No messages yet");
1169
+ });
1170
+
1171
+ it("handles messages with empty content", async () => {
1172
+ function TestComponent() {
1173
+ const { messages, addMessage } = useMessages();
1174
+
1175
+ useEffect(() => {
1176
+ addMessage({ role: "assistant", content: "", isStreaming: false });
1177
+ }, [addMessage]);
1178
+
1179
+ return <MessageList messages={messages} />;
1180
+ }
1181
+
1182
+ const { lastFrame } = render(
1183
+ <IntegrationWrapper>
1184
+ <TestComponent />
1185
+ </IntegrationWrapper>
1186
+ );
1187
+
1188
+ await act(async () => {
1189
+ await new Promise((resolve) => setTimeout(resolve, 10));
1190
+ });
1191
+
1192
+ const frame = lastFrame() ?? "";
1193
+ expect(frame).toContain("(empty)");
1194
+ });
1195
+
1196
+ it("handles tool execution with no pending approvals", () => {
1197
+ function TestComponent() {
1198
+ const { pendingApproval } = useTools();
1199
+ return <Text>Pending: {pendingApproval.length}</Text>;
1200
+ }
1201
+
1202
+ const { lastFrame } = render(
1203
+ <RootProvider>
1204
+ <TestComponent />
1205
+ </RootProvider>
1206
+ );
1207
+
1208
+ expect(lastFrame()).toContain("Pending: 0");
1209
+ });
1210
+
1211
+ it("handles update to non-existent message gracefully", async () => {
1212
+ function TestComponent() {
1213
+ const { messages, updateMessage, addMessage } = useMessages();
1214
+
1215
+ useEffect(() => {
1216
+ // Add one message
1217
+ addMessage({ role: "user", content: "Real message" });
1218
+ // Try to update non-existent
1219
+ updateMessage("non-existent-id", { content: "Updated" });
1220
+ }, [addMessage, updateMessage]);
1221
+
1222
+ return <MessageList messages={messages} />;
1223
+ }
1224
+
1225
+ const { lastFrame } = render(
1226
+ <IntegrationWrapper>
1227
+ <TestComponent />
1228
+ </IntegrationWrapper>
1229
+ );
1230
+
1231
+ await act(async () => {
1232
+ await new Promise((resolve) => setTimeout(resolve, 10));
1233
+ });
1234
+
1235
+ // Original message should still be there
1236
+ expect(lastFrame()).toContain("Real message");
1237
+ });
1238
+
1239
+ it("handles rapid message updates without losing data", async () => {
1240
+ function TestComponent() {
1241
+ const { messages, addMessage, appendToMessage } = useMessages();
1242
+
1243
+ useEffect(() => {
1244
+ const id = addMessage({ role: "assistant", content: "Start", isStreaming: true });
1245
+
1246
+ // Rapid updates
1247
+ for (let i = 0; i < 10; i++) {
1248
+ setTimeout(() => appendToMessage(id, `.${i}`), i * 2);
1249
+ }
1250
+ }, [addMessage, appendToMessage]);
1251
+
1252
+ return <MessageList messages={messages} />;
1253
+ }
1254
+
1255
+ const { lastFrame } = render(
1256
+ <IntegrationWrapper>
1257
+ <TestComponent />
1258
+ </IntegrationWrapper>
1259
+ );
1260
+
1261
+ // Wait for all updates with real timers
1262
+ await act(async () => {
1263
+ await new Promise((resolve) => setTimeout(resolve, 50));
1264
+ });
1265
+
1266
+ const frame = lastFrame() ?? "";
1267
+ expect(frame).toContain("Start");
1268
+ // Should have accumulated updates
1269
+ expect(frame).toContain(".9");
1270
+ });
1271
+ });