@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,1008 @@
1
+ /**
2
+ * Agent Adapter for AgentLoop ↔ TUI Context Integration
3
+ *
4
+ * Provides event mapping from AgentLoop to MessagesContext and ToolsContext,
5
+ * enabling seamless integration between the agent execution engine and the TUI.
6
+ *
7
+ * @module tui/adapters/agent-adapter
8
+ */
9
+
10
+ import type { AgentLoop, ExecutionResult } from "@vellum/core";
11
+ import { useCallback, useEffect, useMemo, useRef } from "react";
12
+ import { ICONS } from "../../utils/icons.js";
13
+ import type { ToolCallInfo } from "../context/MessagesContext.js";
14
+ import { useMessages } from "../context/MessagesContext.js";
15
+ import { useTools } from "../context/ToolsContext.js";
16
+ import { findLastSafeSplitPoint } from "../utils/findLastSafeSplitPoint.js";
17
+
18
+ // =============================================================================
19
+ // Types
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Interface for the Agent Adapter
24
+ *
25
+ * Provides methods to connect and disconnect from an AgentLoop,
26
+ * mapping its events to the TUI context providers.
27
+ */
28
+ export interface AgentAdapter {
29
+ /**
30
+ * Connect to an AgentLoop and start listening to its events
31
+ *
32
+ * @param agentLoop - The AgentLoop instance to connect to
33
+ */
34
+ connect: (agentLoop: AgentLoop) => void;
35
+
36
+ /**
37
+ * Disconnect from the current AgentLoop and stop listening to events
38
+ */
39
+ disconnect: () => void;
40
+ }
41
+
42
+ /**
43
+ * Options for the useAgentAdapter hook
44
+ */
45
+ export interface UseAgentAdapterOptions {
46
+ /**
47
+ * Whether to automatically clear contexts on disconnect
48
+ * @default false
49
+ */
50
+ clearOnDisconnect?: boolean;
51
+
52
+ /**
53
+ * Whether to enable message splitting for long streaming responses.
54
+ *
55
+ * When enabled, long messages are split at safe points (paragraph breaks)
56
+ * and completed portions are moved to historyMessages for <Static> rendering.
57
+ *
58
+ * **WARNING**: This can cause messages to disappear in VirtualizedList mode
59
+ * because historyMessages may be outside the visible render window.
60
+ * Only enable if using standard (non-virtualized) message rendering.
61
+ *
62
+ * @default false
63
+ */
64
+ enableMessageSplitting?: boolean;
65
+ }
66
+
67
+ /**
68
+ * Return value of the useAgentAdapter hook
69
+ */
70
+ export interface UseAgentAdapterReturn extends AgentAdapter {
71
+ /**
72
+ * Whether currently connected to an AgentLoop
73
+ */
74
+ isConnected: boolean;
75
+ }
76
+
77
+ // =============================================================================
78
+ // Message ID Tracking
79
+ // =============================================================================
80
+
81
+ /**
82
+ * Tracks the current streaming message for event correlation
83
+ */
84
+ interface StreamingMessage {
85
+ /** Message ID in the context */
86
+ id: string;
87
+ /** Accumulated content */
88
+ content: string;
89
+ /** Accumulated thinking content */
90
+ thinking: string;
91
+ /** Whether this message has started streaming */
92
+ hasStarted: boolean;
93
+ }
94
+
95
+ // =============================================================================
96
+ // Hook Implementation
97
+ // =============================================================================
98
+
99
+ /**
100
+ * Hook that creates an agent adapter for connecting AgentLoop events
101
+ * to MessagesContext and ToolsContext.
102
+ *
103
+ * Event mappings:
104
+ * - `stateChange` (to streaming) → addMessage to MessagesContext (streaming start)
105
+ * - `text` → appendToMessage in MessagesContext
106
+ * - `complete` → updateMessage (isStreaming: false) in MessagesContext
107
+ * - `toolStart` → addExecution to ToolsContext
108
+ * - `toolEnd` → updateExecution in ToolsContext
109
+ *
110
+ * @param options - Configuration options for the adapter
111
+ * @returns The agent adapter interface with connect/disconnect methods
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * function AgentContainer() {
116
+ * const adapter = useAgentAdapter();
117
+ * const loopRef = useRef<AgentLoop | null>(null);
118
+ *
119
+ * useEffect(() => {
120
+ * const loop = new AgentLoop(config);
121
+ * loopRef.current = loop;
122
+ * adapter.connect(loop);
123
+ *
124
+ * return () => {
125
+ * adapter.disconnect();
126
+ * };
127
+ * }, []);
128
+ *
129
+ * return <ChatUI />;
130
+ * }
131
+ * ```
132
+ */
133
+ export function useAgentAdapter(options: UseAgentAdapterOptions = {}): UseAgentAdapterReturn {
134
+ const { clearOnDisconnect = false, enableMessageSplitting = false } = options;
135
+
136
+ // Context hooks
137
+ const {
138
+ addMessage,
139
+ appendToMessage,
140
+ appendToThinking,
141
+ updateMessage,
142
+ clearMessages,
143
+ commitPendingMessage,
144
+ splitMessageAtSafePoint,
145
+ addToolGroup,
146
+ updateToolGroup,
147
+ historyMessages,
148
+ } = useMessages();
149
+ const { addExecution, updateExecution, clearExecutions, registerCallId } = useTools();
150
+
151
+ // =============================================================================
152
+ // Stable Refs for Context Methods
153
+ // =============================================================================
154
+ // Store context methods in refs to avoid callback recreation on every render.
155
+ // This prevents the connect/disconnect cycle that resets streamingMessageRef
156
+ // during active streaming, which was causing message splitting and flickering.
157
+
158
+ const addMessageRef = useRef(addMessage);
159
+ const appendToMessageRef = useRef(appendToMessage);
160
+ const appendToThinkingRef = useRef(appendToThinking);
161
+ const updateMessageRef = useRef(updateMessage);
162
+ const clearMessagesRef = useRef(clearMessages);
163
+ const commitPendingMessageRef = useRef(commitPendingMessage);
164
+ const splitMessageAtSafePointRef = useRef(splitMessageAtSafePoint);
165
+ const addToolGroupRef = useRef(addToolGroup);
166
+ const updateToolGroupRef = useRef(updateToolGroup);
167
+ const addExecutionRef = useRef(addExecution);
168
+ const updateExecutionRef = useRef(updateExecution);
169
+ const clearExecutionsRef = useRef(clearExecutions);
170
+ const registerCallIdRef = useRef(registerCallId);
171
+ const historyMessagesRef = useRef(historyMessages);
172
+
173
+ // Keep refs up-to-date without triggering callback recreation
174
+ useEffect(() => {
175
+ addMessageRef.current = addMessage;
176
+ appendToMessageRef.current = appendToMessage;
177
+ appendToThinkingRef.current = appendToThinking;
178
+ updateMessageRef.current = updateMessage;
179
+ clearMessagesRef.current = clearMessages;
180
+ commitPendingMessageRef.current = commitPendingMessage;
181
+ splitMessageAtSafePointRef.current = splitMessageAtSafePoint;
182
+ addToolGroupRef.current = addToolGroup;
183
+ updateToolGroupRef.current = updateToolGroup;
184
+ addExecutionRef.current = addExecution;
185
+ updateExecutionRef.current = updateExecution;
186
+ clearExecutionsRef.current = clearExecutions;
187
+ registerCallIdRef.current = registerCallId;
188
+ historyMessagesRef.current = historyMessages;
189
+ }, [
190
+ addMessage,
191
+ appendToMessage,
192
+ appendToThinking,
193
+ updateMessage,
194
+ clearMessages,
195
+ commitPendingMessage,
196
+ splitMessageAtSafePoint,
197
+ addToolGroup,
198
+ updateToolGroup,
199
+ addExecution,
200
+ updateExecution,
201
+ clearExecutions,
202
+ registerCallId,
203
+ historyMessages,
204
+ ]);
205
+
206
+ // =============================================================================
207
+ // Connection State Refs
208
+ // =============================================================================
209
+
210
+ // Track connection state
211
+ const connectedLoopRef = useRef<AgentLoop | null>(null);
212
+ const isConnectedRef = useRef(false);
213
+
214
+ // Track current streaming message
215
+ const streamingMessageRef = useRef<StreamingMessage | null>(null);
216
+
217
+ // Pending tool calls awaiting an assistant message to attach to (for persistence)
218
+ const pendingToolCallsRef = useRef<Map<string, ToolCallInfo>>(new Map());
219
+
220
+ // Map tool call IDs to execution IDs (for context correlation)
221
+ const toolIdMapRef = useRef<Map<string, string>>(new Map());
222
+
223
+ // Map tool call IDs to tool_group message IDs (for inline UI rendering)
224
+ const toolGroupMapRef = useRef<Map<string, string>>(new Map());
225
+
226
+ // =============================================================================
227
+ // Thinking Event Handling
228
+ // =============================================================================
229
+
230
+ /**
231
+ * Detect if the new assistant message should be marked as a continuation.
232
+ * A continuation occurs when:
233
+ * - The last message in history is a tool_group
234
+ * - The message before that was an assistant message
235
+ *
236
+ * This allows the UI to render a minimal `↳` indicator instead of the full header.
237
+ */
238
+ const isContinuationAfterToolGroup = useCallback((): boolean => {
239
+ const history = historyMessagesRef.current;
240
+ if (history.length < 2) {
241
+ return false;
242
+ }
243
+ const lastMessage = history[history.length - 1];
244
+ const secondLastMessage = history[history.length - 2];
245
+
246
+ // Check if last is tool_group and second-last is assistant
247
+ return lastMessage?.role === "tool_group" && secondLastMessage?.role === "assistant";
248
+ }, []);
249
+
250
+ /**
251
+ * Store or update a pending tool call until a streaming message exists.
252
+ * Ensures tool call data is persisted even when tools fire before text.
253
+ */
254
+ const upsertPendingToolCall = useCallback((toolCallInfo: ToolCallInfo) => {
255
+ const existing = pendingToolCallsRef.current.get(toolCallInfo.id);
256
+ if (existing) {
257
+ pendingToolCallsRef.current.set(toolCallInfo.id, {
258
+ ...existing,
259
+ ...toolCallInfo,
260
+ arguments:
261
+ Object.keys(toolCallInfo.arguments).length > 0
262
+ ? toolCallInfo.arguments
263
+ : existing.arguments,
264
+ });
265
+ } else {
266
+ pendingToolCallsRef.current.set(toolCallInfo.id, toolCallInfo);
267
+ }
268
+ }, []);
269
+
270
+ /**
271
+ * Flush any pending tool calls to attach to the next assistant message.
272
+ */
273
+ const flushPendingToolCalls = useCallback((): ToolCallInfo[] | undefined => {
274
+ if (pendingToolCallsRef.current.size === 0) {
275
+ return undefined;
276
+ }
277
+ const calls = Array.from(pendingToolCallsRef.current.values());
278
+ pendingToolCallsRef.current.clear();
279
+ return calls;
280
+ }, []);
281
+
282
+ /**
283
+ * Handle thinking streaming from AgentLoop.
284
+ * Appends thinking content directly to the streaming message.
285
+ */
286
+ const handleThinking = useCallback(
287
+ (text: string) => {
288
+ if (!streamingMessageRef.current) {
289
+ const pendingToolCalls = flushPendingToolCalls();
290
+ const isContinuation = isContinuationAfterToolGroup();
291
+ const id = addMessageRef.current({
292
+ role: "assistant",
293
+ content: "",
294
+ isStreaming: true,
295
+ isContinuation,
296
+ toolCalls: pendingToolCalls && pendingToolCalls.length > 0 ? pendingToolCalls : undefined,
297
+ });
298
+ streamingMessageRef.current = {
299
+ id,
300
+ content: "",
301
+ thinking: "",
302
+ hasStarted: true,
303
+ };
304
+ }
305
+ streamingMessageRef.current.thinking += text;
306
+ // Append thinking content to the message via context
307
+ appendToThinkingRef.current(streamingMessageRef.current.id, text);
308
+ },
309
+ [flushPendingToolCalls, isContinuationAfterToolGroup]
310
+ );
311
+
312
+ /**
313
+ * Handle text streaming from AgentLoop
314
+ * Maps to: appendToMessage in MessagesContext
315
+ *
316
+ * Uses refs for context methods to maintain callback stability and prevent
317
+ * message splitting during re-renders.
318
+ *
319
+ * When content exceeds the threshold, checks for safe split points
320
+ * (paragraph breaks, headers, list items - NOT inside code blocks)
321
+ * and splits to move completed content to Static for better performance.
322
+ */
323
+ const handleText = useCallback(
324
+ (text: string) => {
325
+ const streaming = streamingMessageRef.current;
326
+
327
+ if (!streaming) {
328
+ // Start a new streaming message if we receive text without a message
329
+ const pendingToolCalls = flushPendingToolCalls();
330
+ const isContinuation = isContinuationAfterToolGroup();
331
+ const id = addMessageRef.current({
332
+ role: "assistant",
333
+ content: text,
334
+ isStreaming: true,
335
+ isContinuation,
336
+ toolCalls: pendingToolCalls && pendingToolCalls.length > 0 ? pendingToolCalls : undefined,
337
+ });
338
+ streamingMessageRef.current = {
339
+ id,
340
+ content: text,
341
+ thinking: "",
342
+ hasStarted: true,
343
+ };
344
+ } else {
345
+ // Append to existing streaming message
346
+ streaming.content += text;
347
+ appendToMessageRef.current(streaming.id, text);
348
+
349
+ // Only split messages if explicitly enabled.
350
+ // Splitting can cause messages to disappear in VirtualizedList mode
351
+ // because split portions are moved to historyMessages which may not be rendered.
352
+ if (enableMessageSplitting) {
353
+ // Check if we should split at a safe point to improve performance
354
+ // This moves completed content to Static where it won't re-render
355
+ // Uses newline-gated strategy - only splits at paragraph breaks (\n\n)
356
+ const splitIndex = findLastSafeSplitPoint(streaming.content);
357
+ if (splitIndex > 0) {
358
+ splitMessageAtSafePointRef.current(splitIndex);
359
+ // Update local tracking to reflect the split
360
+ streaming.content = streaming.content.slice(splitIndex);
361
+ }
362
+ }
363
+ }
364
+ },
365
+ [enableMessageSplitting, flushPendingToolCalls, isContinuationAfterToolGroup]
366
+ ); // Depends on enableMessageSplitting flag
367
+
368
+ /**
369
+ * Handle message/complete events from AgentLoop
370
+ * Maps to: commitPendingMessage (move pending to history for Static rendering)
371
+ *
372
+ * When streaming completes, the pending message is committed to history.
373
+ * This moves it to Ink's <Static> component where it will never re-render.
374
+ */
375
+ const handleComplete = useCallback(() => {
376
+ const streaming = streamingMessageRef.current;
377
+
378
+ if (streaming) {
379
+ // NOTE: Do NOT copy thinking to content - they are separate concerns.
380
+ // Thinking content should stay in the thinking field and be rendered
381
+ // by the ThinkingBlock component, not mixed with regular content.
382
+ // Commit the pending message to history (moves to <Static>)
383
+ // This is more efficient than just marking isStreaming: false
384
+ // because the message will never re-render once in <Static>
385
+ commitPendingMessageRef.current();
386
+ streamingMessageRef.current = null;
387
+ }
388
+ pendingToolCallsRef.current.clear();
389
+ }, []); // Empty deps = stable callback
390
+
391
+ /**
392
+ * Handle error events from AgentLoop
393
+ * Ensures streaming state is properly reset when an error occurs.
394
+ */
395
+ const handleError = useCallback((error: Error) => {
396
+ const streaming = streamingMessageRef.current;
397
+
398
+ if (streaming) {
399
+ const content =
400
+ streaming.content.trim().length > 0
401
+ ? streaming.content
402
+ : `${ICONS.warning} ${error.message}`;
403
+ // Mark the message as no longer streaming and surface the error
404
+ updateMessageRef.current(streaming.id, { isStreaming: false, content });
405
+ commitPendingMessageRef.current();
406
+ streamingMessageRef.current = null;
407
+ pendingToolCallsRef.current.clear();
408
+ } else {
409
+ addMessageRef.current({
410
+ role: "assistant",
411
+ content: `${ICONS.warning} ${error.message}`,
412
+ isStreaming: false,
413
+ });
414
+ }
415
+ pendingToolCallsRef.current.clear();
416
+
417
+ // Log error for debugging (could also emit to a context/store if needed)
418
+ console.error("[AgentAdapter] AgentLoop error:", error.message);
419
+ }, []); // Empty deps = stable callback
420
+
421
+ /**
422
+ * Commit the current streaming message before inserting tool rows.
423
+ * Keeps tool_group messages inline between assistant segments.
424
+ */
425
+ const finalizeStreamingMessage = useCallback(() => {
426
+ const streaming = streamingMessageRef.current;
427
+ if (!streaming) {
428
+ return;
429
+ }
430
+
431
+ const hasContent = streaming.content.trim().length > 0;
432
+ const hasThinking = streaming.thinking.trim().length > 0;
433
+ if (hasContent || hasThinking) {
434
+ commitPendingMessageRef.current();
435
+ }
436
+ streamingMessageRef.current = null;
437
+ }, []);
438
+
439
+ /**
440
+ * Create or update a tool_group message for inline tool display.
441
+ */
442
+ const upsertToolGroup = useCallback(
443
+ (
444
+ callId: string,
445
+ name: string,
446
+ input: Record<string, unknown>,
447
+ status: "pending" | "running" | "completed" | "error",
448
+ result?: unknown,
449
+ error?: string
450
+ ) => {
451
+ const toolCallInfo: ToolCallInfo = {
452
+ id: callId,
453
+ name,
454
+ arguments: input,
455
+ status,
456
+ result,
457
+ error,
458
+ };
459
+
460
+ const existingGroupId = toolGroupMapRef.current.get(callId);
461
+ if (!existingGroupId) {
462
+ finalizeStreamingMessage();
463
+ const groupId = addToolGroupRef.current([toolCallInfo]);
464
+ toolGroupMapRef.current.set(callId, groupId);
465
+ return;
466
+ }
467
+
468
+ updateToolGroupRef.current(existingGroupId, toolCallInfo);
469
+ },
470
+ [finalizeStreamingMessage]
471
+ );
472
+
473
+ /**
474
+ * Sync a tool call to the streaming assistant message's toolCalls array.
475
+ * If no message is streaming yet, stash the call for the next assistant message.
476
+ *
477
+ * Tool calls are persisted on assistant messages; UI rendering uses tool_group rows.
478
+ */
479
+ const syncToolCallToMessage = useCallback(
480
+ (
481
+ callId: string,
482
+ name: string,
483
+ input: Record<string, unknown>,
484
+ status: "pending" | "running" | "completed" | "error",
485
+ result?: unknown,
486
+ error?: string
487
+ ) => {
488
+ const toolCallInfo: ToolCallInfo = {
489
+ id: callId,
490
+ name,
491
+ arguments: input,
492
+ status,
493
+ result,
494
+ error,
495
+ };
496
+
497
+ if (!streamingMessageRef.current) {
498
+ upsertPendingToolCall(toolCallInfo);
499
+ return;
500
+ }
501
+
502
+ // Update existing message's toolCalls (merging handled by context)
503
+ updateMessageRef.current(streamingMessageRef.current.id, {
504
+ toolCalls: [toolCallInfo],
505
+ });
506
+ },
507
+ [upsertPendingToolCall]
508
+ );
509
+
510
+ /**
511
+ * Handle tool start from AgentLoop
512
+ * Maps to: addExecution in ToolsContext + tool_group message
513
+ */
514
+ const handleToolStart = useCallback(
515
+ (callId: string, name: string, input: Record<string, unknown>) => {
516
+ // Persist tool call info on assistant message and render inline tool row.
517
+ syncToolCallToMessage(callId, name, input, "running");
518
+ upsertToolGroup(callId, name, input, "running");
519
+
520
+ const existingExecutionId = toolIdMapRef.current.get(callId);
521
+
522
+ // If we already created a pending execution due to permissionRequired,
523
+ // treat toolStart as an update (not a new execution) to avoid duplicates.
524
+ if (existingExecutionId) {
525
+ updateExecutionRef.current(existingExecutionId, {
526
+ status: "running",
527
+ startedAt: new Date(),
528
+ });
529
+ return;
530
+ }
531
+
532
+ // Add execution to tools context
533
+ const executionId = addExecutionRef.current({
534
+ toolName: name,
535
+ params: input,
536
+ status: "running",
537
+ startedAt: new Date(),
538
+ });
539
+
540
+ // Map the AgentLoop callId to our execution ID
541
+ toolIdMapRef.current.set(callId, executionId);
542
+ registerCallIdRef.current(callId, executionId);
543
+ },
544
+ [syncToolCallToMessage, upsertToolGroup]
545
+ );
546
+
547
+ /**
548
+ * Handle tool end from AgentLoop
549
+ * Maps to: updateExecution in ToolsContext + tool_group message update
550
+ */
551
+ const handleToolEnd = useCallback(
552
+ (callId: string, name: string, result: ExecutionResult) => {
553
+ // Look up the execution ID from our map
554
+ const executionId = toolIdMapRef.current.get(callId);
555
+
556
+ if (executionId) {
557
+ // Update the execution with result
558
+ updateExecutionRef.current(executionId, {
559
+ status: result.result.success ? "complete" : "error",
560
+ result: result.result.success ? result.result.output : undefined,
561
+ error: !result.result.success ? new Error(String(result.result.error)) : undefined,
562
+ completedAt: new Date(),
563
+ });
564
+
565
+ // Clean up the map entry
566
+ toolIdMapRef.current.delete(callId);
567
+ }
568
+
569
+ const isSuccess = result.result.success;
570
+ upsertToolGroup(
571
+ callId,
572
+ name,
573
+ {}, // Args not available here; merge keeps previous arguments if present.
574
+ isSuccess ? "completed" : "error",
575
+ isSuccess ? result.result.output : undefined,
576
+ !isSuccess ? String(result.result.error) : undefined
577
+ );
578
+ toolGroupMapRef.current.delete(callId);
579
+ },
580
+ [upsertToolGroup]
581
+ );
582
+
583
+ /**
584
+ * Handle permission required events
585
+ * Maps to: addExecution with 'pending' status in ToolsContext
586
+ */
587
+ const handlePermissionRequired = useCallback(
588
+ (callId: string, name: string, input: Record<string, unknown>) => {
589
+ // Persist tool call info and show inline pending tool row.
590
+ syncToolCallToMessage(callId, name, input, "pending");
591
+ upsertToolGroup(callId, name, input, "pending");
592
+
593
+ // Add execution in pending state (awaiting approval)
594
+ const executionId = addExecutionRef.current({
595
+ toolName: name,
596
+ params: input,
597
+ status: "pending",
598
+ });
599
+
600
+ // Map the callId to execution ID
601
+ toolIdMapRef.current.set(callId, executionId);
602
+ registerCallIdRef.current(callId, executionId);
603
+ },
604
+ [syncToolCallToMessage, upsertToolGroup]
605
+ );
606
+
607
+ /**
608
+ * Handle permission granted events
609
+ * Maps to: updateExecution with 'running' status in ToolsContext
610
+ */
611
+ const handlePermissionGranted = useCallback(
612
+ (callId: string, _name: string) => {
613
+ const executionId = toolIdMapRef.current.get(callId);
614
+
615
+ if (executionId) {
616
+ updateExecutionRef.current(executionId, {
617
+ status: "running",
618
+ startedAt: new Date(),
619
+ });
620
+ }
621
+
622
+ upsertToolGroup(callId, _name, {}, "running");
623
+ },
624
+ [upsertToolGroup]
625
+ );
626
+
627
+ /**
628
+ * Handle permission denied events
629
+ * Maps to: updateExecution with 'rejected' status in ToolsContext
630
+ */
631
+ const handlePermissionDenied = useCallback(
632
+ (callId: string, _name: string, reason: string) => {
633
+ const executionId = toolIdMapRef.current.get(callId);
634
+
635
+ if (executionId) {
636
+ updateExecutionRef.current(executionId, {
637
+ status: "rejected",
638
+ error: new Error(reason),
639
+ });
640
+
641
+ // Clean up the map entry
642
+ toolIdMapRef.current.delete(callId);
643
+ }
644
+
645
+ upsertToolGroup(callId, _name, {}, "error", undefined, reason);
646
+ toolGroupMapRef.current.delete(callId);
647
+ },
648
+ [upsertToolGroup]
649
+ );
650
+
651
+ /**
652
+ * Connect to an AgentLoop instance
653
+ *
654
+ * Now has empty dependencies since all handlers are stable (using refs).
655
+ * This prevents unnecessary disconnect/reconnect cycles during re-renders.
656
+ */
657
+ const connect = useCallback(
658
+ (agentLoop: AgentLoop) => {
659
+ // Skip reconnection if already connected to the same loop
660
+ // This prevents resetting streaming state during re-renders
661
+ if (connectedLoopRef.current === agentLoop) {
662
+ return;
663
+ }
664
+
665
+ // Disconnect from any existing loop first
666
+ if (connectedLoopRef.current) {
667
+ // Remove existing event listeners
668
+ connectedLoopRef.current.off("text", handleText);
669
+ connectedLoopRef.current.off("thinking", handleThinking);
670
+ connectedLoopRef.current.off("complete", handleComplete);
671
+ connectedLoopRef.current.off("error", handleError);
672
+ connectedLoopRef.current.off("toolStart", handleToolStart);
673
+ connectedLoopRef.current.off("toolEnd", handleToolEnd);
674
+ connectedLoopRef.current.off("permissionRequired", handlePermissionRequired);
675
+ connectedLoopRef.current.off("permissionGranted", handlePermissionGranted);
676
+ connectedLoopRef.current.off("permissionDenied", handlePermissionDenied);
677
+ }
678
+
679
+ // Reset state only when connecting to a NEW loop
680
+ streamingMessageRef.current = null;
681
+ pendingToolCallsRef.current.clear();
682
+ toolIdMapRef.current.clear();
683
+ toolGroupMapRef.current.clear();
684
+
685
+ // Subscribe to AgentLoop events
686
+ agentLoop.on("text", handleText);
687
+ agentLoop.on("thinking", handleThinking);
688
+ agentLoop.on("complete", handleComplete);
689
+ agentLoop.on("error", handleError);
690
+ agentLoop.on("toolStart", handleToolStart);
691
+ agentLoop.on("toolEnd", handleToolEnd);
692
+ agentLoop.on("permissionRequired", handlePermissionRequired);
693
+ agentLoop.on("permissionGranted", handlePermissionGranted);
694
+ agentLoop.on("permissionDenied", handlePermissionDenied);
695
+
696
+ // Store reference
697
+ connectedLoopRef.current = agentLoop;
698
+ isConnectedRef.current = true;
699
+ },
700
+ [
701
+ handleText,
702
+ handleThinking,
703
+ handleComplete,
704
+ handleError,
705
+ handleToolStart,
706
+ handleToolEnd,
707
+ handlePermissionRequired,
708
+ handlePermissionGranted,
709
+ handlePermissionDenied,
710
+ ]
711
+ );
712
+
713
+ /**
714
+ * Disconnect from the current AgentLoop
715
+ *
716
+ * Uses refs for clear functions to maintain callback stability.
717
+ */
718
+ const disconnect = useCallback(() => {
719
+ if (connectedLoopRef.current) {
720
+ // Remove all event listeners
721
+ connectedLoopRef.current.off("text", handleText);
722
+ connectedLoopRef.current.off("thinking", handleThinking);
723
+ connectedLoopRef.current.off("complete", handleComplete);
724
+ connectedLoopRef.current.off("error", handleError);
725
+ connectedLoopRef.current.off("toolStart", handleToolStart);
726
+ connectedLoopRef.current.off("toolEnd", handleToolEnd);
727
+ connectedLoopRef.current.off("permissionRequired", handlePermissionRequired);
728
+ connectedLoopRef.current.off("permissionGranted", handlePermissionGranted);
729
+ connectedLoopRef.current.off("permissionDenied", handlePermissionDenied);
730
+
731
+ // Clear reference
732
+ connectedLoopRef.current = null;
733
+ isConnectedRef.current = false;
734
+
735
+ // Reset state
736
+ streamingMessageRef.current = null;
737
+ pendingToolCallsRef.current.clear();
738
+ toolIdMapRef.current.clear();
739
+ toolGroupMapRef.current.clear();
740
+
741
+ // Optionally clear contexts (using refs for stability)
742
+ if (clearOnDisconnect) {
743
+ clearMessagesRef.current();
744
+ clearExecutionsRef.current();
745
+ }
746
+ }
747
+ }, [
748
+ handleText,
749
+ handleThinking,
750
+ handleComplete,
751
+ handleError,
752
+ handleToolStart,
753
+ handleToolEnd,
754
+ handlePermissionRequired,
755
+ handlePermissionGranted,
756
+ handlePermissionDenied,
757
+ clearOnDisconnect,
758
+ ]);
759
+
760
+ // Cleanup on unmount
761
+ useEffect(() => {
762
+ return () => {
763
+ if (connectedLoopRef.current) {
764
+ disconnect();
765
+ }
766
+ };
767
+ }, [disconnect]);
768
+
769
+ // Memoize return value to ensure stable object reference across renders.
770
+ // This prevents useEffect re-runs in consumers that depend on the adapter object,
771
+ // which was causing disconnect/reconnect cycles during streaming.
772
+ return useMemo(
773
+ () => ({
774
+ connect,
775
+ disconnect,
776
+ isConnected: isConnectedRef.current,
777
+ }),
778
+ [connect, disconnect]
779
+ );
780
+ }
781
+
782
+ // =============================================================================
783
+ // Factory Function (Non-Hook Alternative)
784
+ // =============================================================================
785
+
786
+ /**
787
+ * Context dispatch functions required by the adapter factory
788
+ */
789
+ export interface AdapterDispatchers {
790
+ /** Add a message to the messages context */
791
+ addMessage: (message: { role: "assistant"; content: string; isStreaming?: boolean }) => string;
792
+ /** Append content to an existing message */
793
+ appendToMessage: (id: string, content: string) => void;
794
+ /** Update a message's properties */
795
+ updateMessage: (id: string, updates: Partial<{ content: string; isStreaming: boolean }>) => void;
796
+ /** Add a tool execution to the tools context */
797
+ addExecution: (execution: {
798
+ toolName: string;
799
+ params: Record<string, unknown>;
800
+ status?: "pending" | "approved" | "rejected" | "running" | "complete" | "error";
801
+ startedAt?: Date;
802
+ }) => string;
803
+ /** Update a tool execution */
804
+ updateExecution: (
805
+ id: string,
806
+ updates: {
807
+ status?: "pending" | "approved" | "rejected" | "running" | "complete" | "error";
808
+ result?: unknown;
809
+ error?: Error;
810
+ startedAt?: Date;
811
+ completedAt?: Date;
812
+ }
813
+ ) => void;
814
+ }
815
+
816
+ /**
817
+ * Creates an agent adapter without using React hooks.
818
+ *
819
+ * Useful for testing or non-React environments.
820
+ *
821
+ * @param dispatchers - Context dispatch functions
822
+ * @returns An AgentAdapter interface
823
+ *
824
+ * @example
825
+ * ```typescript
826
+ * const dispatchers = {
827
+ * addMessage: (msg) => { ... },
828
+ * appendToMessage: (id, content) => { ... },
829
+ * updateMessage: (id, updates) => { ... },
830
+ * addExecution: (exec) => { ... },
831
+ * updateExecution: (id, updates) => { ... },
832
+ * };
833
+ *
834
+ * const adapter = createAgentAdapter(dispatchers);
835
+ * adapter.connect(agentLoop);
836
+ *
837
+ * // Later...
838
+ * adapter.disconnect();
839
+ * ```
840
+ */
841
+ export function createAgentAdapter(dispatchers: AdapterDispatchers): AgentAdapter {
842
+ let connectedLoop: AgentLoop | null = null;
843
+ let streamingMessage: StreamingMessage | null = null;
844
+ const toolIdMap = new Map<string, string>();
845
+
846
+ const handleText = (text: string) => {
847
+ if (!streamingMessage) {
848
+ const id = dispatchers.addMessage({
849
+ role: "assistant",
850
+ content: text,
851
+ isStreaming: true,
852
+ });
853
+ streamingMessage = { id, content: text, thinking: "", hasStarted: true };
854
+ } else {
855
+ streamingMessage.content += text;
856
+ dispatchers.appendToMessage(streamingMessage.id, text);
857
+ }
858
+ };
859
+
860
+ const handleThinking = (text: string) => {
861
+ if (!streamingMessage) {
862
+ const id = dispatchers.addMessage({
863
+ role: "assistant",
864
+ content: "",
865
+ isStreaming: true,
866
+ });
867
+ streamingMessage = { id, content: "", thinking: "", hasStarted: true };
868
+ }
869
+ streamingMessage.thinking += text;
870
+ };
871
+
872
+ const handleComplete = () => {
873
+ if (streamingMessage) {
874
+ if (streamingMessage.content.trim().length === 0 && streamingMessage.thinking.trim()) {
875
+ dispatchers.updateMessage(streamingMessage.id, { content: streamingMessage.thinking });
876
+ }
877
+ dispatchers.updateMessage(streamingMessage.id, { isStreaming: false });
878
+ streamingMessage = null;
879
+ }
880
+ };
881
+
882
+ const handleError = (error: Error) => {
883
+ if (streamingMessage) {
884
+ dispatchers.updateMessage(streamingMessage.id, { isStreaming: false });
885
+ streamingMessage = null;
886
+ }
887
+ console.error("[AgentAdapter] AgentLoop error:", error.message);
888
+ };
889
+
890
+ const handleToolStart = (callId: string, name: string, input: Record<string, unknown>) => {
891
+ const executionId = dispatchers.addExecution({
892
+ toolName: name,
893
+ params: input,
894
+ status: "running",
895
+ startedAt: new Date(),
896
+ });
897
+ toolIdMap.set(callId, executionId);
898
+ };
899
+
900
+ const handleToolEnd = (callId: string, _name: string, result: ExecutionResult) => {
901
+ const executionId = toolIdMap.get(callId);
902
+ if (executionId) {
903
+ dispatchers.updateExecution(executionId, {
904
+ status: result.result.success ? "complete" : "error",
905
+ result: result.result.success ? result.result.output : undefined,
906
+ error: !result.result.success ? new Error(String(result.result.error)) : undefined,
907
+ completedAt: new Date(),
908
+ });
909
+ toolIdMap.delete(callId);
910
+ }
911
+ };
912
+
913
+ const handlePermissionRequired = (
914
+ callId: string,
915
+ name: string,
916
+ input: Record<string, unknown>
917
+ ) => {
918
+ const executionId = dispatchers.addExecution({
919
+ toolName: name,
920
+ params: input,
921
+ status: "pending",
922
+ });
923
+ toolIdMap.set(callId, executionId);
924
+ };
925
+
926
+ const handlePermissionGranted = (callId: string, _name: string) => {
927
+ const executionId = toolIdMap.get(callId);
928
+ if (executionId) {
929
+ dispatchers.updateExecution(executionId, {
930
+ status: "running",
931
+ startedAt: new Date(),
932
+ });
933
+ }
934
+ };
935
+
936
+ const handlePermissionDenied = (callId: string, _name: string, reason: string) => {
937
+ const executionId = toolIdMap.get(callId);
938
+ if (executionId) {
939
+ dispatchers.updateExecution(executionId, {
940
+ status: "rejected",
941
+ error: new Error(reason),
942
+ });
943
+ toolIdMap.delete(callId);
944
+ }
945
+ };
946
+
947
+ return {
948
+ connect(agentLoop: AgentLoop) {
949
+ // Skip reconnection if already connected to the same loop
950
+ if (connectedLoop === agentLoop) {
951
+ return;
952
+ }
953
+
954
+ // Disconnect existing
955
+ if (connectedLoop) {
956
+ connectedLoop.off("text", handleText);
957
+ connectedLoop.off("thinking", handleThinking);
958
+ connectedLoop.off("complete", handleComplete);
959
+ connectedLoop.off("error", handleError);
960
+ connectedLoop.off("toolStart", handleToolStart);
961
+ connectedLoop.off("toolEnd", handleToolEnd);
962
+ connectedLoop.off("permissionRequired", handlePermissionRequired);
963
+ connectedLoop.off("permissionGranted", handlePermissionGranted);
964
+ connectedLoop.off("permissionDenied", handlePermissionDenied);
965
+ }
966
+
967
+ // Reset state only when connecting to a NEW loop
968
+ streamingMessage = null;
969
+ toolIdMap.clear();
970
+
971
+ // Subscribe
972
+ agentLoop.on("text", handleText);
973
+ agentLoop.on("thinking", handleThinking);
974
+ agentLoop.on("complete", handleComplete);
975
+ agentLoop.on("error", handleError);
976
+ agentLoop.on("toolStart", handleToolStart);
977
+ agentLoop.on("toolEnd", handleToolEnd);
978
+ agentLoop.on("permissionRequired", handlePermissionRequired);
979
+ agentLoop.on("permissionGranted", handlePermissionGranted);
980
+ agentLoop.on("permissionDenied", handlePermissionDenied);
981
+
982
+ connectedLoop = agentLoop;
983
+ },
984
+
985
+ disconnect() {
986
+ if (connectedLoop) {
987
+ connectedLoop.off("text", handleText);
988
+ connectedLoop.off("thinking", handleThinking);
989
+ connectedLoop.off("complete", handleComplete);
990
+ connectedLoop.off("error", handleError);
991
+ connectedLoop.off("toolStart", handleToolStart);
992
+ connectedLoop.off("toolEnd", handleToolEnd);
993
+ connectedLoop.off("permissionRequired", handlePermissionRequired);
994
+ connectedLoop.off("permissionGranted", handlePermissionGranted);
995
+ connectedLoop.off("permissionDenied", handlePermissionDenied);
996
+ connectedLoop = null;
997
+ }
998
+ streamingMessage = null;
999
+ toolIdMap.clear();
1000
+ },
1001
+ };
1002
+ }
1003
+
1004
+ // =============================================================================
1005
+ // Exports
1006
+ // =============================================================================
1007
+
1008
+ export default useAgentAdapter;