@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,216 @@
1
+ /**
2
+ * StreamingText Component (T019)
3
+ *
4
+ * Displays text content with an animated blinking cursor while streaming.
5
+ * Supports optional typewriter effect for smoother character-by-character display.
6
+ * The cursor is removed when streaming completes, and an optional callback
7
+ * is invoked.
8
+ *
9
+ * @module tui/components/Messages/StreamingText
10
+ */
11
+
12
+ import { Text } from "ink";
13
+ import { useEffect, useMemo, useRef, useState } from "react";
14
+ import { useAnimation } from "../../context/AnimationContext.js";
15
+ import { sanitize } from "../../utils/textSanitizer.js";
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Props for the StreamingText component.
23
+ */
24
+ export interface StreamingTextProps {
25
+ /** The text content to display */
26
+ readonly content: string;
27
+ /** Whether the text is currently streaming */
28
+ readonly isStreaming: boolean;
29
+ /** Character to use for the cursor (default: '▊') */
30
+ readonly cursorChar?: string;
31
+ /** Whether the cursor should blink (default: true) */
32
+ readonly cursorBlink?: boolean;
33
+ /** Callback invoked when streaming completes */
34
+ readonly onComplete?: () => void;
35
+ /** Enable typewriter effect for smoother display (default: true) */
36
+ readonly typewriterEffect?: boolean;
37
+ /** Delay between characters in ms when typewriter is enabled (default: 8) */
38
+ readonly typewriterSpeed?: number;
39
+ }
40
+
41
+ // =============================================================================
42
+ // Constants
43
+ // =============================================================================
44
+
45
+ /** Default cursor character */
46
+ const DEFAULT_CURSOR_CHAR = "▊";
47
+
48
+ /** Default typewriter speed (characters per interval) */
49
+ const DEFAULT_TYPEWRITER_SPEED_MS = 8;
50
+
51
+ /** Characters to release per tick for faster catch-up */
52
+ const CHARS_PER_TICK = 3;
53
+
54
+ // =============================================================================
55
+ // Main Component
56
+ // =============================================================================
57
+
58
+ /**
59
+ * StreamingText displays text with an animated cursor while streaming.
60
+ *
61
+ * Features:
62
+ * - Displays text content
63
+ * - Shows blinking cursor at end while streaming
64
+ * - Removes cursor when streaming completes
65
+ * - Supports cursor customization (character and blink behavior)
66
+ * - Optional typewriter effect for smoother display
67
+ * - Calls onComplete callback when isStreaming changes to false
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * // Basic usage
72
+ * <StreamingText
73
+ * content={text}
74
+ * isStreaming={isTyping}
75
+ * />
76
+ *
77
+ * // With completion callback
78
+ * <StreamingText
79
+ * content={text}
80
+ * isStreaming={isTyping}
81
+ * onComplete={() => enableInput()}
82
+ * />
83
+ *
84
+ * // Custom cursor
85
+ * <StreamingText
86
+ * content={text}
87
+ * isStreaming={isTyping}
88
+ * cursorChar="_"
89
+ * cursorBlink={false}
90
+ * />
91
+ *
92
+ * // With typewriter effect
93
+ * <StreamingText
94
+ * content={text}
95
+ * isStreaming={isTyping}
96
+ * typewriterEffect={true}
97
+ * typewriterSpeed={10}
98
+ * />
99
+ * ```
100
+ */
101
+ export function StreamingText({
102
+ content,
103
+ isStreaming,
104
+ cursorChar = DEFAULT_CURSOR_CHAR,
105
+ cursorBlink = true,
106
+ onComplete,
107
+ typewriterEffect = true,
108
+ typewriterSpeed = DEFAULT_TYPEWRITER_SPEED_MS,
109
+ }: StreamingTextProps): React.JSX.Element {
110
+ // Track cursor visibility for blinking effect
111
+ const [cursorVisible, setCursorVisible] = useState(true);
112
+
113
+ // Track previous streaming state to detect completion
114
+ const prevIsStreamingRef = useRef<boolean | null>(null);
115
+
116
+ // Track if this is the initial mount (for immediate display when not streaming)
117
+ const isInitialMountRef = useRef(true);
118
+
119
+ // Typewriter effect state - initialize to full length if not streaming on mount
120
+ const [displayedLength, setDisplayedLength] = useState(() => (isStreaming ? 0 : content.length));
121
+ const prevContentLengthRef = useRef(content.length);
122
+
123
+ // Handle typewriter effect - gradually reveal characters
124
+ useEffect(() => {
125
+ // Skip typewriter if disabled
126
+ if (!typewriterEffect) {
127
+ setDisplayedLength(content.length);
128
+ return;
129
+ }
130
+
131
+ // When streaming stops, immediately show all content
132
+ if (!isStreaming) {
133
+ setDisplayedLength(content.length);
134
+ return;
135
+ }
136
+
137
+ // Mark initial mount as complete
138
+ isInitialMountRef.current = false;
139
+
140
+ // If we're caught up, nothing to do
141
+ if (displayedLength >= content.length) {
142
+ return;
143
+ }
144
+
145
+ // Release characters progressively
146
+ const timer = setTimeout(() => {
147
+ setDisplayedLength((prev) => {
148
+ // Release multiple chars per tick for faster catch-up when behind
149
+ const behind = content.length - prev;
150
+ const charsToRelease = behind > 20 ? Math.min(behind, CHARS_PER_TICK * 3) : CHARS_PER_TICK;
151
+ return Math.min(prev + charsToRelease, content.length);
152
+ });
153
+ }, typewriterSpeed);
154
+
155
+ return () => clearTimeout(timer);
156
+ }, [content.length, displayedLength, isStreaming, typewriterEffect, typewriterSpeed]);
157
+
158
+ // Reset displayed length when content is cleared (new message)
159
+ useEffect(() => {
160
+ if (content.length < prevContentLengthRef.current) {
161
+ // Content was reset (new message started)
162
+ setDisplayedLength(0);
163
+ }
164
+ prevContentLengthRef.current = content.length;
165
+ }, [content.length]);
166
+
167
+ // Use global animation context for cursor blink to prevent flickering
168
+ const { frame, isPaused } = useAnimation();
169
+
170
+ // Derive cursor visibility from animation frame instead of independent timer
171
+ // This prevents competing setIntervals from causing flicker
172
+ const derivedCursorVisible = useMemo(() => {
173
+ // Always show cursor when not streaming or blink disabled
174
+ if (!isStreaming || !cursorBlink) return true;
175
+ // Show cursor when animation is paused (e.g., input focused)
176
+ if (isPaused) return true;
177
+ // Toggle every ~4 frames (~500ms blink cycle at 120ms/200ms tick rate)
178
+ return Math.floor(frame / 4) % 2 === 0;
179
+ }, [frame, isPaused, isStreaming, cursorBlink]);
180
+
181
+ // Sync local state with derived value for compatibility with existing logic
182
+ useEffect(() => {
183
+ setCursorVisible(derivedCursorVisible);
184
+ }, [derivedCursorVisible]);
185
+
186
+ // Handle streaming completion callback
187
+ useEffect(() => {
188
+ // Only trigger callback if we were previously streaming and now we're not
189
+ // Skip on initial mount (prevIsStreamingRef.current is null)
190
+ if (prevIsStreamingRef.current === true && !isStreaming) {
191
+ onComplete?.();
192
+ }
193
+
194
+ // Update ref after callback logic
195
+ prevIsStreamingRef.current = isStreaming;
196
+ }, [isStreaming, onComplete]);
197
+
198
+ // Sanitize content (normalize line endings, strip dangerous ANSI)
199
+ const sanitizedContent = useMemo(() => sanitize(content), [content]);
200
+
201
+ // Determine what text to display
202
+ const displayText = typewriterEffect
203
+ ? sanitizedContent.slice(0, displayedLength)
204
+ : sanitizedContent;
205
+
206
+ // Determine cursor to display (show cursor while typewriter is still catching up)
207
+ const showCursor = isStreaming || (typewriterEffect && displayedLength < sanitizedContent.length);
208
+ const cursor = showCursor && cursorVisible ? cursorChar : "";
209
+
210
+ return (
211
+ <Text wrap="wrap">
212
+ {displayText}
213
+ {cursor}
214
+ </Text>
215
+ );
216
+ }
@@ -0,0 +1,408 @@
1
+ /**
2
+ * ThinkingBlock Component
3
+ *
4
+ * Displays thinking/reasoning content from extended thinking models
5
+ * with collapsible UI, duration display, and streaming indicator.
6
+ *
7
+ * Features:
8
+ * - Collapsible: Toggle between collapsed (1-line summary) and expanded view
9
+ * - Duration display: Shows how long thinking took (e.g., "Thought for 3.2s")
10
+ * - Streaming indicator: Shows spinner while thinking is in progress
11
+ * - Character count: Shows length in collapsed mode (e.g., "[...] (1,234 chars)")
12
+ * - Keyboard toggle: 't' key to toggle expand/collapse
13
+ * - Visual distinction: Box border to separate from main content
14
+ * - Tool calls: Optional inline display of tool calls within thinking block
15
+ *
16
+ * @module tui/components/Messages/ThinkingBlock
17
+ */
18
+
19
+ import { Box, Text } from "ink";
20
+ import type React from "react";
21
+ import { useMemo } from "react";
22
+ import { useAnimationFrame } from "../../context/AnimationContext.js";
23
+ import type { ToolCallInfo } from "../../context/MessagesContext.js";
24
+ import { useCollapsible } from "../../hooks/useCollapsible.js";
25
+ import type { ThinkingDisplayMode } from "../../i18n/index.js";
26
+ import { useTheme } from "../../theme/index.js";
27
+ import { SPINNER_STYLES, Spinner } from "../common/Spinner.js";
28
+
29
+ // Spinner frames for tool call status
30
+ const TOOL_SPINNER_FRAMES = ["-", "\\", "|", "/"];
31
+
32
+ // =============================================================================
33
+ // Types
34
+ // =============================================================================
35
+
36
+ /**
37
+ * Props for the ThinkingBlock component.
38
+ */
39
+ export interface ThinkingBlockProps {
40
+ /** Thinking/reasoning content to display */
41
+ readonly content: string;
42
+ /** Duration of thinking in milliseconds (optional) */
43
+ readonly durationMs?: number;
44
+ /** Whether thinking is still in progress (shows spinner) */
45
+ readonly isStreaming?: boolean;
46
+ /** Whether initially collapsed (default: true) */
47
+ readonly initialCollapsed?: boolean;
48
+ /** Unique ID for state persistence (optional) */
49
+ readonly persistenceId?: string;
50
+ /** Enable keyboard toggle with 't' key (default: false to avoid conflicts) */
51
+ readonly enableKeyboardToggle?: boolean;
52
+ /** Maximum lines to show in collapsed preview (default: 1) */
53
+ readonly collapsedPreviewLines?: number;
54
+ /** Maximum characters to show in collapsed preview (default: 80) */
55
+ readonly collapsedPreviewChars?: number;
56
+ /** Show character count in header (default: true) */
57
+ readonly showCharCount?: boolean;
58
+ /** Callback when toggle state changes */
59
+ readonly onToggle?: (collapsed: boolean) => void;
60
+ /** Tool calls to display inline within the thinking block */
61
+ readonly toolCalls?: readonly ToolCallInfo[];
62
+ /**
63
+ * Display mode for thinking content.
64
+ * - "full": Show content (default, can expand/collapse)
65
+ * - "compact": Only show header, no content preview, cannot expand
66
+ */
67
+ readonly displayMode?: ThinkingDisplayMode;
68
+ }
69
+
70
+ // =============================================================================
71
+ // Helper Functions
72
+ // =============================================================================
73
+
74
+ /**
75
+ * Format character count for display.
76
+ */
77
+ function formatCharCount(count: number): string {
78
+ if (count < 1000) {
79
+ return `${count} chars`;
80
+ }
81
+ const k = count / 1000;
82
+ return k >= 10 ? `${Math.round(k)}K chars` : `${k.toFixed(1)}K chars`;
83
+ }
84
+
85
+ /**
86
+ * Format duration in milliseconds to human-readable string.
87
+ */
88
+ function formatThinkingDuration(ms: number): string {
89
+ if (ms < 1000) {
90
+ return `${ms}ms`;
91
+ }
92
+ const seconds = ms / 1000;
93
+ if (seconds < 60) {
94
+ return `${seconds.toFixed(1)}s`;
95
+ }
96
+ const minutes = Math.floor(seconds / 60);
97
+ const remainingSeconds = (seconds % 60).toFixed(0);
98
+ return `${minutes}m ${remainingSeconds}s`;
99
+ }
100
+
101
+ /**
102
+ * Get a preview of the content for collapsed mode.
103
+ */
104
+ function getPreview(content: string, maxLines: number, maxChars: number): string {
105
+ if (!content) return "";
106
+
107
+ // Split by newlines and take first N lines
108
+ const lines = content.split("\n").slice(0, maxLines);
109
+ let preview = lines.join(" ").trim();
110
+
111
+ // Truncate to max chars
112
+ if (preview.length > maxChars) {
113
+ preview = `${preview.slice(0, maxChars - 3)}...`;
114
+ }
115
+
116
+ return preview;
117
+ }
118
+
119
+ // =============================================================================
120
+ // Component
121
+ // =============================================================================
122
+
123
+ /**
124
+ * ThinkingBlock - Collapsible display for model thinking/reasoning content.
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * // Basic usage
129
+ * <ThinkingBlock content="Let me think about this..." />
130
+ *
131
+ * // With streaming indicator
132
+ * <ThinkingBlock
133
+ * content={thinkingContent}
134
+ * isStreaming={true}
135
+ * />
136
+ *
137
+ * // With duration and keyboard toggle
138
+ * <ThinkingBlock
139
+ * content={thinkingContent}
140
+ * durationMs={3200}
141
+ * enableKeyboardToggle
142
+ * />
143
+ *
144
+ * // Initially expanded
145
+ * <ThinkingBlock
146
+ * content={thinkingContent}
147
+ * initialCollapsed={false}
148
+ * />
149
+ * ```
150
+ */
151
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: ThinkingBlock UI inherently complex due to multiple render modes
152
+ export function ThinkingBlock({
153
+ content,
154
+ durationMs,
155
+ isStreaming = false,
156
+ initialCollapsed = true,
157
+ persistenceId,
158
+ enableKeyboardToggle = false,
159
+ collapsedPreviewLines = 1,
160
+ collapsedPreviewChars = 80,
161
+ showCharCount = true,
162
+ onToggle,
163
+ toolCalls,
164
+ displayMode = "full",
165
+ }: ThinkingBlockProps): React.JSX.Element | null {
166
+ const { theme } = useTheme();
167
+
168
+ // Animation frame for tool call spinners
169
+ const frameIndex = useAnimationFrame(TOOL_SPINNER_FRAMES);
170
+
171
+ // In compact mode, force collapsed and disable keyboard toggle
172
+ const isCompactMode = displayMode === "compact";
173
+ const effectiveInitialCollapsed = isCompactMode ? true : initialCollapsed;
174
+ const effectiveKeyboardToggle = isCompactMode ? false : enableKeyboardToggle;
175
+
176
+ const { isCollapsed, toggle: _toggle } = useCollapsible({
177
+ initialCollapsed: effectiveInitialCollapsed,
178
+ toggleKey: effectiveKeyboardToggle ? "t" : undefined,
179
+ keyboardEnabled: effectiveKeyboardToggle,
180
+ persistenceId,
181
+ onToggle,
182
+ });
183
+
184
+ // In compact mode, always show as collapsed (never expandable)
185
+ const effectiveIsCollapsed = isCompactMode ? true : isCollapsed;
186
+
187
+ // Theme colors
188
+ const thinkingColor = theme.colors.warning ?? "yellow";
189
+ const mutedColor = theme.semantic.text.muted;
190
+ const borderColor = theme.colors.warning ?? "yellow";
191
+ const accentColor = theme.colors.accent ?? "cyan";
192
+ const successColor = theme.colors.success ?? "green";
193
+ const errorColor = theme.colors.error ?? "red";
194
+
195
+ // Memoized values
196
+ const charCount = useMemo(() => content.length, [content]);
197
+ const preview = useMemo(
198
+ () => getPreview(content, collapsedPreviewLines, collapsedPreviewChars),
199
+ [content, collapsedPreviewLines, collapsedPreviewChars]
200
+ );
201
+
202
+ // Don't render if no content
203
+ if (!content && !isStreaming) {
204
+ return null;
205
+ }
206
+
207
+ // Build header text
208
+ const headerParts: string[] = [];
209
+
210
+ // Status icon and text
211
+ if (isStreaming) {
212
+ headerParts.push("Thinking");
213
+ } else if (durationMs !== undefined && durationMs > 0) {
214
+ headerParts.push(`Thought for ${formatThinkingDuration(durationMs)}`);
215
+ } else {
216
+ headerParts.push("Thought");
217
+ }
218
+
219
+ // Character count (when collapsed or streaming)
220
+ if (showCharCount && charCount > 0 && (effectiveIsCollapsed || isStreaming)) {
221
+ headerParts.push(`(${formatCharCount(charCount)})`);
222
+ }
223
+
224
+ // Toggle hint (not shown in compact mode since it's not toggleable)
225
+ if (!isStreaming && !isCompactMode) {
226
+ headerParts.push(effectiveIsCollapsed ? "[expand ▼]" : "[collapse ▲]");
227
+ }
228
+
229
+ return (
230
+ <Box
231
+ flexDirection="column"
232
+ marginLeft={2}
233
+ marginTop={0}
234
+ marginBottom={0}
235
+ borderStyle="round"
236
+ borderColor={borderColor}
237
+ paddingX={1}
238
+ borderLeft
239
+ borderRight={false}
240
+ borderTop={false}
241
+ borderBottom={false}
242
+ >
243
+ {/* Header row with toggle */}
244
+ <Box flexDirection="row" alignItems="center">
245
+ {/* Streaming spinner */}
246
+ {isStreaming && (
247
+ <>
248
+ <Spinner color={thinkingColor} frames={SPINNER_STYLES.dots} />
249
+ <Text> </Text>
250
+ </>
251
+ )}
252
+
253
+ {/* Icon */}
254
+ <Text color={thinkingColor}>[...] </Text>
255
+
256
+ {/* Header text - clickable area concept (visual only in TUI) */}
257
+ <Text
258
+ color={thinkingColor}
259
+ dimColor={!isStreaming}
260
+ italic
261
+ // In terminal, we can't have onClick, but visual cue
262
+ >
263
+ {headerParts.join(" ")}
264
+ </Text>
265
+
266
+ {/* Keyboard hint */}
267
+ {effectiveKeyboardToggle && !isStreaming && (
268
+ <Text color={mutedColor} dimColor>
269
+ {" "}
270
+ (press 't')
271
+ </Text>
272
+ )}
273
+ </Box>
274
+
275
+ {/* Content area - In compact mode, don't show any content (no preview) */}
276
+ {isCompactMode ? null : effectiveIsCollapsed ? (
277
+ // Collapsed: show preview only
278
+ content && (
279
+ <Box marginLeft={2} marginTop={0}>
280
+ <Text color={mutedColor} dimColor wrap="truncate">
281
+ {preview}
282
+ </Text>
283
+ </Box>
284
+ )
285
+ ) : (
286
+ // Expanded: show full content
287
+ <Box marginLeft={2} marginTop={0} flexDirection="column">
288
+ <Text color={thinkingColor} dimColor wrap="wrap">
289
+ {content}
290
+ </Text>
291
+ </Box>
292
+ )}
293
+
294
+ {/* Tool calls (when present) */}
295
+ {toolCalls && toolCalls.length > 0 && (
296
+ <Box marginLeft={2} marginTop={0} flexDirection="column">
297
+ {toolCalls.map((toolCall) => {
298
+ // Determine status indicator and color
299
+ let statusIcon: string;
300
+ let statusColor: string;
301
+
302
+ switch (toolCall.status) {
303
+ case "running":
304
+ case "pending":
305
+ statusIcon = TOOL_SPINNER_FRAMES[frameIndex] ?? "-";
306
+ statusColor = accentColor;
307
+ break;
308
+ case "completed":
309
+ statusIcon = "+";
310
+ statusColor = successColor;
311
+ break;
312
+ case "error":
313
+ statusIcon = "x";
314
+ statusColor = errorColor;
315
+ break;
316
+ default:
317
+ statusIcon = "o";
318
+ statusColor = mutedColor;
319
+ }
320
+
321
+ return (
322
+ <Box key={toolCall.id} flexDirection="row">
323
+ <Text color={statusColor}>{statusIcon}</Text>
324
+ <Text> </Text>
325
+ <Text color={accentColor} bold>
326
+ {toolCall.name}
327
+ </Text>
328
+ {toolCall.status === "error" && toolCall.error && (
329
+ <Text color={errorColor} dimColor>
330
+ {" "}
331
+ — {toolCall.error}
332
+ </Text>
333
+ )}
334
+ </Box>
335
+ );
336
+ })}
337
+ </Box>
338
+ )}
339
+ </Box>
340
+ );
341
+ }
342
+
343
+ // =============================================================================
344
+ // Compact Variant
345
+ // =============================================================================
346
+
347
+ /**
348
+ * Props for the CompactThinkingIndicator component.
349
+ */
350
+ export interface CompactThinkingIndicatorProps {
351
+ /** Duration of thinking in milliseconds */
352
+ readonly durationMs?: number;
353
+ /** Whether thinking is still in progress */
354
+ readonly isStreaming?: boolean;
355
+ /** Character count to display */
356
+ readonly charCount?: number;
357
+ }
358
+
359
+ /**
360
+ * CompactThinkingIndicator - Minimal inline thinking status.
361
+ *
362
+ * Use this when you just want to show that thinking occurred
363
+ * without the full collapsible content.
364
+ *
365
+ * @example
366
+ * ```tsx
367
+ * <CompactThinkingIndicator durationMs={3200} charCount={1500} />
368
+ * // Renders: 💭 Thought for 3.2s (1.5K chars)
369
+ * ```
370
+ */
371
+ export function CompactThinkingIndicator({
372
+ durationMs,
373
+ isStreaming = false,
374
+ charCount,
375
+ }: CompactThinkingIndicatorProps): React.JSX.Element {
376
+ const { theme } = useTheme();
377
+ const thinkingColor = theme.colors.warning ?? "yellow";
378
+
379
+ const parts: string[] = ["[...]"];
380
+
381
+ if (isStreaming) {
382
+ parts.push("Thinking...");
383
+ } else if (durationMs !== undefined && durationMs > 0) {
384
+ parts.push(`Thought for ${formatThinkingDuration(durationMs)}`);
385
+ } else {
386
+ parts.push("Thought");
387
+ }
388
+
389
+ if (charCount !== undefined && charCount > 0) {
390
+ parts.push(`(${formatCharCount(charCount)})`);
391
+ }
392
+
393
+ return (
394
+ <Box flexDirection="row" alignItems="center">
395
+ {isStreaming && (
396
+ <>
397
+ <Spinner color={thinkingColor} frames={SPINNER_STYLES.dots} />
398
+ <Text> </Text>
399
+ </>
400
+ )}
401
+ <Text color={thinkingColor} dimColor italic>
402
+ {parts.join(" ")}
403
+ </Text>
404
+ </Box>
405
+ );
406
+ }
407
+
408
+ export default ThinkingBlock;