@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,193 @@
1
+ /**
2
+ * HotkeyHelpModal Component (Chain 24)
3
+ *
4
+ * Modal overlay displaying available keyboard shortcuts.
5
+ * Helps users discover and learn hotkey bindings.
6
+ *
7
+ * @module tui/components/common/HotkeyHelpModal
8
+ */
9
+
10
+ import { Box, Text, useInput } from "ink";
11
+ import type React from "react";
12
+ import { useMemo } from "react";
13
+ import { useTheme } from "../../theme/index.js";
14
+
15
+ // =============================================================================
16
+ // Types
17
+ // =============================================================================
18
+
19
+ /**
20
+ * A single hotkey binding definition.
21
+ */
22
+ export interface HotkeyBinding {
23
+ /** Key combination (e.g., "Ctrl+C", "?", "Esc") */
24
+ readonly key: string;
25
+ /** Description of what the hotkey does */
26
+ readonly description: string;
27
+ /** Optional scope/category (e.g., "Global", "Editor", "Navigation") */
28
+ readonly scope?: string;
29
+ }
30
+
31
+ /**
32
+ * Props for the HotkeyHelpModal component.
33
+ */
34
+ export interface HotkeyHelpModalProps {
35
+ /** Whether the modal is visible */
36
+ readonly isVisible: boolean;
37
+ /** Callback when the modal should close */
38
+ readonly onClose: () => void;
39
+ /** List of hotkey bindings to display */
40
+ readonly hotkeys: readonly HotkeyBinding[];
41
+ /** Title for the modal (default: "Keyboard Shortcuts") */
42
+ readonly title?: string;
43
+ }
44
+
45
+ // =============================================================================
46
+ // Constants
47
+ // =============================================================================
48
+
49
+ /** Default hotkeys commonly available in the TUI */
50
+ export const DEFAULT_HOTKEYS: HotkeyBinding[] = [
51
+ { key: "F1", description: "Show this help", scope: "Global" },
52
+ { key: "Ctrl+Shift+A", description: "Open approval queue", scope: "Global" },
53
+ { key: "Ctrl+C", description: "Exit application", scope: "Global" },
54
+ { key: "Ctrl+L", description: "Clear screen", scope: "Global" },
55
+ { key: "Esc", description: "Cancel / Close modal", scope: "Global" },
56
+ { key: "↑/↓", description: "Navigate history / options", scope: "Navigation" },
57
+ { key: "Tab", description: "Autocomplete / Next field", scope: "Input" },
58
+ { key: "Enter", description: "Submit / Confirm", scope: "Input" },
59
+ { key: "Ctrl+U", description: "Clear input line", scope: "Input" },
60
+ ];
61
+
62
+ // =============================================================================
63
+ // Helper Functions
64
+ // =============================================================================
65
+
66
+ /**
67
+ * Group hotkeys by their scope.
68
+ */
69
+ function groupHotkeysByScope(hotkeys: readonly HotkeyBinding[]): Map<string, HotkeyBinding[]> {
70
+ const grouped = new Map<string, HotkeyBinding[]>();
71
+
72
+ for (const hotkey of hotkeys) {
73
+ const scope = hotkey.scope ?? "General";
74
+ const existing = grouped.get(scope) ?? [];
75
+ grouped.set(scope, [...existing, hotkey]);
76
+ }
77
+
78
+ return grouped;
79
+ }
80
+
81
+ /**
82
+ * Calculate the maximum key width for alignment.
83
+ */
84
+ function getMaxKeyWidth(hotkeys: readonly HotkeyBinding[]): number {
85
+ return Math.max(...hotkeys.map((h) => h.key.length), 8);
86
+ }
87
+
88
+ // =============================================================================
89
+ // HotkeyHelpModal Component
90
+ // =============================================================================
91
+
92
+ /**
93
+ * HotkeyHelpModal - Modal overlay for keyboard shortcut help.
94
+ *
95
+ * Features:
96
+ * - Groups hotkeys by scope/category
97
+ * - Aligned key-description columns
98
+ * - Dismissible with Esc or ?
99
+ * - Themeable styling
100
+ *
101
+ * @example
102
+ * ```tsx
103
+ * const [showHelp, setShowHelp] = useState(false);
104
+ *
105
+ * <HotkeyHelpModal
106
+ * isVisible={showHelp}
107
+ * onClose={() => setShowHelp(false)}
108
+ * hotkeys={[
109
+ * { key: "Ctrl+S", description: "Save file", scope: "Editor" },
110
+ * { key: "Ctrl+Z", description: "Undo", scope: "Editor" },
111
+ * ]}
112
+ * />
113
+ * ```
114
+ */
115
+ export function HotkeyHelpModal({
116
+ isVisible,
117
+ onClose,
118
+ hotkeys,
119
+ title = "Keyboard Shortcuts",
120
+ }: HotkeyHelpModalProps): React.JSX.Element | null {
121
+ const { theme } = useTheme();
122
+
123
+ // Handle keyboard input for closing
124
+ useInput(
125
+ (input, key) => {
126
+ if (key.escape || input === "q") {
127
+ onClose();
128
+ }
129
+ },
130
+ { isActive: isVisible }
131
+ );
132
+
133
+ // Group hotkeys by scope
134
+ const groupedHotkeys = useMemo(() => groupHotkeysByScope(hotkeys), [hotkeys]);
135
+ const maxKeyWidth = useMemo(() => getMaxKeyWidth(hotkeys), [hotkeys]);
136
+
137
+ // Don't render if not visible
138
+ if (!isVisible) {
139
+ return null;
140
+ }
141
+
142
+ return (
143
+ <Box
144
+ flexDirection="column"
145
+ borderStyle="round"
146
+ borderColor={theme.colors.primary}
147
+ paddingX={2}
148
+ paddingY={1}
149
+ >
150
+ {/* Title */}
151
+ <Box marginBottom={1}>
152
+ <Text color={theme.colors.primary} bold>
153
+ {title}
154
+ </Text>
155
+ </Box>
156
+
157
+ {/* Hotkey groups */}
158
+ {Array.from(groupedHotkeys.entries()).map(([scope, scopeHotkeys]) => (
159
+ <Box key={scope} flexDirection="column" marginBottom={1}>
160
+ {/* Scope header */}
161
+ <Box marginBottom={0}>
162
+ <Text color={theme.semantic.text.muted} underline>
163
+ {scope}
164
+ </Text>
165
+ </Box>
166
+
167
+ {/* Hotkeys in this scope */}
168
+ {scopeHotkeys.map((hotkey) => (
169
+ <Box key={hotkey.key}>
170
+ <Box width={maxKeyWidth + 2}>
171
+ <Text color={theme.colors.info} bold>
172
+ {hotkey.key.padEnd(maxKeyWidth)}
173
+ </Text>
174
+ </Box>
175
+ <Text color={theme.semantic.text.secondary}>{hotkey.description}</Text>
176
+ </Box>
177
+ ))}
178
+ </Box>
179
+ ))}
180
+
181
+ {/* Close hint */}
182
+ <Box marginTop={1}>
183
+ <Text dimColor>Press Esc or q to close</Text>
184
+ </Box>
185
+ </Box>
186
+ );
187
+ }
188
+
189
+ // =============================================================================
190
+ // Exports
191
+ // =============================================================================
192
+
193
+ export default HotkeyHelpModal;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * HotkeyHints Component
3
+ *
4
+ * Small, reusable hint bar for displaying discoverable keybindings.
5
+ * Designed to be safe in narrow Ink layouts: single-line, dim, truncates.
6
+ *
7
+ * Memoized to prevent re-renders when parent context updates (e.g., message streaming).
8
+ */
9
+
10
+ import { Box, Text } from "ink";
11
+ import type React from "react";
12
+ import { memo } from "react";
13
+
14
+ export type HotkeyHint = {
15
+ readonly keys: string;
16
+ readonly label: string;
17
+ };
18
+
19
+ export interface HotkeyHintsProps {
20
+ readonly hints: ReadonlyArray<HotkeyHint>;
21
+ /** Separator between items (default: " │ ") */
22
+ readonly separator?: string;
23
+ }
24
+
25
+ /**
26
+ * Compare two HotkeyHint arrays for equality.
27
+ */
28
+ function hintsAreEqual(
29
+ prevHints: ReadonlyArray<HotkeyHint>,
30
+ nextHints: ReadonlyArray<HotkeyHint>
31
+ ): boolean {
32
+ if (prevHints.length !== nextHints.length) return false;
33
+ for (let i = 0; i < prevHints.length; i++) {
34
+ const prevHint = prevHints[i];
35
+ const nextHint = nextHints[i];
36
+ if (prevHint?.keys !== nextHint?.keys || prevHint?.label !== nextHint?.label) {
37
+ return false;
38
+ }
39
+ }
40
+ return true;
41
+ }
42
+
43
+ /**
44
+ * Custom comparison for React.memo - only re-render when hints or separator actually change.
45
+ */
46
+ function arePropsEqual(prev: HotkeyHintsProps, next: HotkeyHintsProps): boolean {
47
+ return prev.separator === next.separator && hintsAreEqual(prev.hints, next.hints);
48
+ }
49
+
50
+ function HotkeyHintsImpl({ hints, separator = " │ " }: HotkeyHintsProps): React.JSX.Element {
51
+ if (hints.length === 0) {
52
+ return <Box />;
53
+ }
54
+
55
+ return (
56
+ <Box>
57
+ <Text dimColor wrap="truncate">
58
+ {hints.map((hint, index) => (
59
+ <Text key={`${hint.keys}-${hint.label}`}>
60
+ {index > 0 ? separator : ""}
61
+ <Text bold>{hint.keys}</Text>
62
+ <Text> {hint.label}</Text>
63
+ </Text>
64
+ ))}
65
+ </Text>
66
+ </Box>
67
+ );
68
+ }
69
+
70
+ export const HotkeyHints = memo(HotkeyHintsImpl, arePropsEqual);
@@ -0,0 +1,354 @@
1
+ /**
2
+ * MaxSizedBox Component
3
+ *
4
+ * A container component that limits content to a maximum size
5
+ * and shows a "Show More" indicator when content is truncated.
6
+ *
7
+ * This component is essential for preventing overflow in terminal UIs
8
+ * while maintaining a good user experience by indicating when content
9
+ * has been truncated.
10
+ *
11
+ * Ported from Gemini CLI for Vellum TUI.
12
+ *
13
+ * @module tui/components/common/MaxSizedBox
14
+ */
15
+
16
+ import { Box, type DOMElement, Text } from "ink";
17
+ import type React from "react";
18
+ import { type ReactNode, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
19
+ import { useOverflowOptional } from "../../context/OverflowContext.js";
20
+ import { useFlickerDetector } from "../../hooks/useFlickerDetector.js";
21
+
22
+ // =============================================================================
23
+ // Types
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Props for the MaxSizedBox component
28
+ */
29
+ export interface MaxSizedBoxProps {
30
+ /** Content to render (will be truncated if exceeds maxHeight) */
31
+ readonly children: ReactNode;
32
+ /** Maximum height in rows/lines */
33
+ readonly maxHeight: number;
34
+ /** Minimum height in rows/lines (default: 1) */
35
+ readonly minHeight?: number;
36
+ /**
37
+ * Text to show when content is truncated.
38
+ * Set to null to hide the indicator.
39
+ * @default "... (more)"
40
+ */
41
+ readonly truncationIndicator?: string | null;
42
+ /**
43
+ * Color for the truncation indicator
44
+ * @default "dim"
45
+ */
46
+ readonly indicatorColor?: string;
47
+ /**
48
+ * Callback when expanded state changes
49
+ * @param expanded - Whether the content is now expanded
50
+ */
51
+ readonly onExpandChange?: (expanded: boolean) => void;
52
+ /**
53
+ * Callback when overflow state changes
54
+ * @param isOverflowing - Whether content is currently overflowing
55
+ */
56
+ readonly onOverflowChange?: (isOverflowing: boolean) => void;
57
+ /**
58
+ * Initial expanded state (default: false)
59
+ */
60
+ readonly initialExpanded?: boolean;
61
+ /**
62
+ * Allow expanding the content (default: false)
63
+ * When true, the truncation indicator becomes "Show More" / "Show Less"
64
+ */
65
+ readonly expandable?: boolean;
66
+ /**
67
+ * Unique ID for overflow tracking (auto-generated if not provided)
68
+ */
69
+ readonly id?: string;
70
+ /** Width of the box (default: 100%) */
71
+ readonly width?: number | string;
72
+ /** Padding within the box */
73
+ readonly padding?: number;
74
+ /** Horizontal padding */
75
+ readonly paddingX?: number;
76
+ /** Vertical padding */
77
+ readonly paddingY?: number;
78
+ }
79
+
80
+ /**
81
+ * State returned by the MaxSizedBox for external control
82
+ */
83
+ export interface MaxSizedBoxState {
84
+ /** Whether content is currently overflowing */
85
+ readonly isOverflowing: boolean;
86
+ /** Whether content is currently expanded */
87
+ readonly isExpanded: boolean;
88
+ /** Measured content height */
89
+ readonly contentHeight: number;
90
+ /** Amount of overflow (0 if content fits) */
91
+ readonly overflowAmount: number;
92
+ }
93
+
94
+ // =============================================================================
95
+ // Constants
96
+ // =============================================================================
97
+
98
+ /** Default truncation indicator text */
99
+ const DEFAULT_TRUNCATION_INDICATOR = "... (more)";
100
+
101
+ /** Expandable show more text */
102
+ const SHOW_MORE_TEXT = "▼ Show More";
103
+
104
+ /** Expandable show less text */
105
+ const SHOW_LESS_TEXT = "▲ Show Less";
106
+
107
+ // =============================================================================
108
+ // Component
109
+ // =============================================================================
110
+
111
+ /**
112
+ * MaxSizedBox limits content to a maximum height with optional truncation indicator.
113
+ *
114
+ * Features:
115
+ * - **Height limiting**: Prevents content from exceeding specified bounds
116
+ * - **Truncation indicator**: Shows when content is cut off
117
+ * - **Expandable mode**: Optional expand/collapse functionality
118
+ * - **Overflow tracking**: Integrates with OverflowContext
119
+ * - **Flicker prevention**: Uses debounced overflow detection
120
+ *
121
+ * @example
122
+ * ```tsx
123
+ * // Basic usage - truncate at 10 rows
124
+ * <MaxSizedBox maxHeight={10}>
125
+ * <Text>{longContent}</Text>
126
+ * </MaxSizedBox>
127
+ *
128
+ * // With expand functionality
129
+ * <MaxSizedBox
130
+ * maxHeight={5}
131
+ * expandable
132
+ * onExpandChange={(expanded) => console.log('Expanded:', expanded)}
133
+ * >
134
+ * <CodeBlock code={sourceCode} />
135
+ * </MaxSizedBox>
136
+ *
137
+ * // Custom truncation indicator
138
+ * <MaxSizedBox
139
+ * maxHeight={20}
140
+ * truncationIndicator="[Content truncated...]"
141
+ * indicatorColor="yellow"
142
+ * >
143
+ * <LogOutput logs={logs} />
144
+ * </MaxSizedBox>
145
+ *
146
+ * // Hide truncation indicator
147
+ * <MaxSizedBox maxHeight={10} truncationIndicator={null}>
148
+ * <Content />
149
+ * </MaxSizedBox>
150
+ * ```
151
+ */
152
+ export function MaxSizedBox({
153
+ children,
154
+ maxHeight,
155
+ minHeight = 1,
156
+ truncationIndicator = DEFAULT_TRUNCATION_INDICATOR,
157
+ indicatorColor = "dim",
158
+ onExpandChange,
159
+ onOverflowChange,
160
+ initialExpanded = false,
161
+ expandable = false,
162
+ id: providedId,
163
+ width = "100%",
164
+ padding,
165
+ paddingX,
166
+ paddingY,
167
+ }: MaxSizedBoxProps): React.JSX.Element {
168
+ // Generate unique ID if not provided
169
+ const generatedId = useId();
170
+ const componentId = providedId ?? generatedId;
171
+
172
+ // Expansion state
173
+ const [isExpanded, setIsExpanded] = useState(initialExpanded);
174
+
175
+ // Content measurement
176
+ const contentRef = useRef<DOMElement | null>(null);
177
+ const [measuredHeight, setMeasuredHeight] = useState(0);
178
+
179
+ // Optional overflow context integration
180
+ const overflowContext = useOverflowOptional();
181
+
182
+ // Flicker detection for stable overflow state
183
+ const { isOverflowing, overflow } = useFlickerDetector({
184
+ contentHeight: measuredHeight,
185
+ containerHeight: maxHeight,
186
+ threshold: 0,
187
+ debounce: true,
188
+ });
189
+
190
+ /**
191
+ * Measure content height on mount and updates
192
+ */
193
+ const measureContent = useCallback(() => {
194
+ if (contentRef.current) {
195
+ // In Ink, we estimate height based on the rendered output
196
+ // This is a simplified measurement - actual implementation may
197
+ // need to parse ANSI output or use Ink's measurement APIs
198
+ const element = contentRef.current;
199
+ const yogaNode = element.yogaNode;
200
+ if (yogaNode) {
201
+ const computedHeight = yogaNode.getComputedHeight();
202
+ setMeasuredHeight(Math.ceil(computedHeight));
203
+ }
204
+ }
205
+ }, []);
206
+
207
+ /**
208
+ * Effect to measure content on mount.
209
+ * Measurement happens on every render since content could change.
210
+ * We call measureContent directly in the effect body which captures
211
+ * the current contentRef value.
212
+ */
213
+ useEffect(() => {
214
+ measureContent();
215
+ });
216
+
217
+ /**
218
+ * Effect to notify parent of overflow changes
219
+ */
220
+ useEffect(() => {
221
+ onOverflowChange?.(isOverflowing);
222
+ }, [isOverflowing, onOverflowChange]);
223
+
224
+ /**
225
+ * Effect to register/unregister with overflow context
226
+ */
227
+ useEffect(() => {
228
+ if (!overflowContext) return;
229
+
230
+ if (isOverflowing && !isExpanded) {
231
+ overflowContext.registerOverflow(componentId);
232
+ } else {
233
+ overflowContext.unregisterOverflow(componentId);
234
+ }
235
+
236
+ return () => {
237
+ overflowContext.unregisterOverflow(componentId);
238
+ };
239
+ }, [isOverflowing, isExpanded, componentId, overflowContext]);
240
+
241
+ /**
242
+ * Handle expand/collapse toggle.
243
+ * Reserved for future interactive functionality (keyboard/click handlers).
244
+ */
245
+ const _handleToggleExpand = useCallback(() => {
246
+ const newExpanded = !isExpanded;
247
+ setIsExpanded(newExpanded);
248
+ onExpandChange?.(newExpanded);
249
+ }, [isExpanded, onExpandChange]);
250
+
251
+ // Preserve reference for future use
252
+ void _handleToggleExpand;
253
+
254
+ /**
255
+ * Calculate effective height
256
+ */
257
+ const effectiveHeight = useMemo(() => {
258
+ if (isExpanded) {
259
+ // When expanded, use measured height (no limit)
260
+ return Math.max(minHeight, measuredHeight);
261
+ }
262
+ // When collapsed, limit to maxHeight
263
+ return Math.max(minHeight, Math.min(maxHeight, measuredHeight));
264
+ }, [isExpanded, maxHeight, minHeight, measuredHeight]);
265
+
266
+ /**
267
+ * Determine if truncation indicator should show
268
+ */
269
+ const showTruncationIndicator = useMemo(() => {
270
+ // Don't show if no indicator text
271
+ if (truncationIndicator === null) return false;
272
+ // Show if overflowing and not expanded
273
+ return isOverflowing && !isExpanded;
274
+ }, [truncationIndicator, isOverflowing, isExpanded]);
275
+
276
+ /**
277
+ * Get indicator text based on mode
278
+ */
279
+ const indicatorText = useMemo(() => {
280
+ if (expandable) {
281
+ return isExpanded ? SHOW_LESS_TEXT : SHOW_MORE_TEXT;
282
+ }
283
+ return truncationIndicator;
284
+ }, [expandable, isExpanded, truncationIndicator]);
285
+
286
+ /**
287
+ * Render the content with optional truncation
288
+ */
289
+ const resolvedHeight = measuredHeight > 0 ? effectiveHeight : undefined;
290
+
291
+ return (
292
+ <Box
293
+ flexDirection="column"
294
+ width={width}
295
+ height={resolvedHeight}
296
+ overflow="hidden"
297
+ padding={padding}
298
+ paddingX={paddingX}
299
+ paddingY={paddingY}
300
+ >
301
+ {/* Content container */}
302
+ <Box ref={contentRef} flexDirection="column" flexGrow={1} overflow="hidden">
303
+ {children}
304
+ </Box>
305
+
306
+ {/* Truncation indicator */}
307
+ {showTruncationIndicator && indicatorText && (
308
+ <Box>
309
+ <Text color={indicatorColor} dimColor={!expandable}>
310
+ {indicatorText}
311
+ {overflow > 0 && !expandable && ` (+${overflow} lines)`}
312
+ </Text>
313
+ </Box>
314
+ )}
315
+
316
+ {/* Show "Show Less" when expanded */}
317
+ {expandable && isExpanded && (
318
+ <Box>
319
+ <Text color={indicatorColor}>{SHOW_LESS_TEXT}</Text>
320
+ </Box>
321
+ )}
322
+ </Box>
323
+ );
324
+ }
325
+
326
+ // =============================================================================
327
+ // Convenience Hook
328
+ // =============================================================================
329
+
330
+ /**
331
+ * Hook to get MaxSizedBox-like overflow calculations without the component.
332
+ * Useful for custom implementations that need the same logic.
333
+ *
334
+ * @param contentHeight - Current content height
335
+ * @param maxHeight - Maximum allowed height
336
+ * @returns Overflow state and metrics
337
+ */
338
+ export function useMaxSizedBox(contentHeight: number, maxHeight: number): MaxSizedBoxState {
339
+ const { isOverflowing, overflow } = useFlickerDetector({
340
+ contentHeight,
341
+ containerHeight: maxHeight,
342
+ threshold: 0,
343
+ });
344
+
345
+ return useMemo(
346
+ () => ({
347
+ isOverflowing,
348
+ isExpanded: false,
349
+ contentHeight,
350
+ overflowAmount: Math.max(0, overflow),
351
+ }),
352
+ [isOverflowing, contentHeight, overflow]
353
+ );
354
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * NewMessagesBadge Component
3
+ *
4
+ * Badge showing unread message count with scroll-to-bottom action.
5
+ * Displays "↓ N new messages" when user is in manual scroll mode.
6
+ *
7
+ * @module tui/components/common/NewMessagesBadge
8
+ */
9
+
10
+ import { Box, Text } from "ink";
11
+ import type React from "react";
12
+ import { useTheme } from "../../theme/index.js";
13
+
14
+ // =============================================================================
15
+ // Types
16
+ // =============================================================================
17
+
18
+ /**
19
+ * Props for NewMessagesBadge component.
20
+ */
21
+ export interface NewMessagesBadgeProps {
22
+ /** Number of new messages */
23
+ readonly count: number;
24
+ /** Callback when clicked/activated */
25
+ readonly onScrollToBottom?: () => void;
26
+ }
27
+
28
+ // =============================================================================
29
+ // Component
30
+ // =============================================================================
31
+
32
+ /**
33
+ * Badge showing unread message count with scroll-to-bottom hint.
34
+ *
35
+ * Displays "↓ N new messages (press End)" when count > 0.
36
+ * Hidden when count = 0 (no visual clutter).
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * <NewMessagesBadge
41
+ * count={unreadCount}
42
+ * onScrollToBottom={() => scrollController.resumeFollow()}
43
+ * />
44
+ * ```
45
+ */
46
+ export function NewMessagesBadge({
47
+ count,
48
+ onScrollToBottom: _onScrollToBottom,
49
+ }: NewMessagesBadgeProps): React.ReactElement | null {
50
+ const { theme } = useTheme();
51
+
52
+ // Don't render if no new messages
53
+ if (count <= 0) {
54
+ return null;
55
+ }
56
+
57
+ // Format message text
58
+ const messageText = count === 1 ? "1 new message" : `${count} new messages`;
59
+
60
+ return (
61
+ <Box justifyContent="center" paddingX={1}>
62
+ <Text color={theme.colors.info}>↓ {messageText} (press End)</Text>
63
+ </Box>
64
+ );
65
+ }