@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,225 @@
1
+ /**
2
+ * Text Width Utilities
3
+ *
4
+ * Visual width calculation and manipulation utilities for terminal text.
5
+ * Handles CJK characters, emojis, and ANSI escape sequences correctly.
6
+ *
7
+ * @module tui/utils/text-width
8
+ */
9
+
10
+ import stringWidth from "string-width";
11
+ import wrapAnsi from "wrap-ansi";
12
+
13
+ /**
14
+ * Text alignment options for padding.
15
+ */
16
+ export type TextAlign = "left" | "center" | "right";
17
+
18
+ /**
19
+ * Get the visual width of text in terminal columns.
20
+ *
21
+ * This accounts for:
22
+ * - CJK characters (width 2)
23
+ * - Emojis (width 2)
24
+ * - ANSI escape sequences (width 0)
25
+ * - Zero-width characters
26
+ *
27
+ * @param text - Text to measure
28
+ * @returns Visual width in terminal columns
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * getVisualWidth("hello") // 5
33
+ * getVisualWidth("你好") // 4 (2 chars × 2 width)
34
+ * getVisualWidth("👋") // 2
35
+ * getVisualWidth("\x1b[31mred\x1b[0m") // 3 (ANSI codes have 0 width)
36
+ * ```
37
+ */
38
+ export function getVisualWidth(text: string): number {
39
+ return stringWidth(text);
40
+ }
41
+
42
+ /**
43
+ * Truncate text to fit within a maximum visual width.
44
+ *
45
+ * Adds ellipsis if truncation is needed. Handles ANSI codes correctly,
46
+ * ensuring escape sequences are properly closed.
47
+ *
48
+ * @param text - Text to truncate
49
+ * @param maxWidth - Maximum visual width
50
+ * @param ellipsis - Ellipsis string (default: "…")
51
+ * @returns Truncated text with ellipsis if needed
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * truncateToWidth("Hello World", 8) // "Hello W…"
56
+ * truncateToWidth("Short", 10) // "Short"
57
+ * truncateToWidth("你好世界", 5) // "你好…"
58
+ * ```
59
+ */
60
+ export function truncateToWidth(text: string, maxWidth: number, ellipsis = "…"): string {
61
+ const textWidth = getVisualWidth(text);
62
+
63
+ if (textWidth <= maxWidth) {
64
+ return text;
65
+ }
66
+
67
+ const ellipsisWidth = getVisualWidth(ellipsis);
68
+ const targetWidth = maxWidth - ellipsisWidth;
69
+
70
+ if (targetWidth <= 0) {
71
+ return ellipsis.slice(0, maxWidth);
72
+ }
73
+
74
+ // Build truncated string character by character
75
+ let result = "";
76
+ let currentWidth = 0;
77
+
78
+ for (const char of text) {
79
+ const charWidth = getVisualWidth(char);
80
+ if (currentWidth + charWidth > targetWidth) {
81
+ break;
82
+ }
83
+ result += char;
84
+ currentWidth += charWidth;
85
+ }
86
+
87
+ return result + ellipsis;
88
+ }
89
+
90
+ /**
91
+ * Pad text to a specific visual width with alignment.
92
+ *
93
+ * @param text - Text to pad
94
+ * @param width - Target visual width
95
+ * @param align - Alignment: "left", "center", or "right"
96
+ * @param padChar - Character to use for padding (default: space)
97
+ * @returns Padded text
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * padToWidth("Hi", 10, "left") // "Hi "
102
+ * padToWidth("Hi", 10, "right") // " Hi"
103
+ * padToWidth("Hi", 10, "center") // " Hi "
104
+ * padToWidth("你好", 8, "center") // " 你好 "
105
+ * ```
106
+ */
107
+ export function padToWidth(
108
+ text: string,
109
+ width: number,
110
+ align: TextAlign = "left",
111
+ padChar = " "
112
+ ): string {
113
+ const textWidth = getVisualWidth(text);
114
+ const padCharWidth = getVisualWidth(padChar);
115
+
116
+ if (textWidth >= width) {
117
+ return text;
118
+ }
119
+
120
+ const totalPadding = width - textWidth;
121
+ const padCount = Math.floor(totalPadding / padCharWidth);
122
+
123
+ switch (align) {
124
+ case "right": {
125
+ return padChar.repeat(padCount) + text;
126
+ }
127
+ case "center": {
128
+ const leftPad = Math.floor(padCount / 2);
129
+ const rightPad = padCount - leftPad;
130
+ return padChar.repeat(leftPad) + text + padChar.repeat(rightPad);
131
+ }
132
+ default: {
133
+ // "left" alignment (default)
134
+ return text + padChar.repeat(padCount);
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Wrap text at visual width boundaries.
141
+ *
142
+ * Uses wrap-ansi for proper handling of ANSI codes and
143
+ * word boundaries. Preserves ANSI styling across lines.
144
+ *
145
+ * @param text - Text to wrap
146
+ * @param width - Maximum line width
147
+ * @param options - Wrapping options
148
+ * @returns Wrapped text with newlines
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * wrapToWidth("This is a long line that needs wrapping", 20)
153
+ * // "This is a long line\nthat needs wrapping"
154
+ *
155
+ * wrapToWidth("LongWordThatCantBreak", 10, { hard: true })
156
+ * // "LongWordTh\natCantBrea\nk"
157
+ * ```
158
+ */
159
+ export function wrapToWidth(
160
+ text: string,
161
+ width: number,
162
+ options: {
163
+ /** Hard wrap long words (default: false) */
164
+ hard?: boolean;
165
+ /** Trim whitespace at line ends (default: true) */
166
+ trim?: boolean;
167
+ /** Preserve leading whitespace (default: false) */
168
+ wordWrap?: boolean;
169
+ } = {}
170
+ ): string {
171
+ const { hard = false, trim = true, wordWrap = true } = options;
172
+
173
+ return wrapAnsi(text, width, {
174
+ hard,
175
+ trim,
176
+ wordWrap,
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Split text into lines respecting visual width.
182
+ *
183
+ * Similar to wrapToWidth but returns an array of lines
184
+ * instead of a single string with newlines.
185
+ *
186
+ * @param text - Text to split
187
+ * @param width - Maximum line width
188
+ * @param options - Wrapping options
189
+ * @returns Array of lines
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * splitLines("Hello World", 6)
194
+ * // ["Hello", "World"]
195
+ * ```
196
+ */
197
+ export function splitLines(
198
+ text: string,
199
+ width: number,
200
+ options: Parameters<typeof wrapToWidth>[2] = {}
201
+ ): string[] {
202
+ return wrapToWidth(text, width, options).split("\n");
203
+ }
204
+
205
+ /**
206
+ * Calculate the number of lines text will occupy at a given width.
207
+ *
208
+ * @param text - Text to measure
209
+ * @param width - Available width
210
+ * @param options - Wrapping options
211
+ * @returns Number of lines
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * countLines("Hello World", 6) // 2
216
+ * countLines("Hi", 10) // 1
217
+ * ```
218
+ */
219
+ export function countLines(
220
+ text: string,
221
+ width: number,
222
+ options: Parameters<typeof wrapToWidth>[2] = {}
223
+ ): number {
224
+ return splitLines(text, width, options).length;
225
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Text Sanitizer for TUI
3
+ *
4
+ * Provides consistent text normalization across all TUI components.
5
+ * Centralizes sanitization logic previously scattered in TextInput.
6
+ *
7
+ * @module tui/utils/textSanitizer
8
+ */
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ /**
15
+ * Options for text sanitization.
16
+ */
17
+ export interface SanitizeOptions {
18
+ /** Convert tabs to spaces (default: 2) */
19
+ readonly tabWidth?: number;
20
+ /** Maximum line length before wrap hint (default: 0 = no limit) */
21
+ readonly maxLineLength?: number;
22
+ /** Strip ALL ANSI codes (default: false, just sanitize dangerous ones) */
23
+ readonly stripAllAnsi?: boolean;
24
+ }
25
+
26
+ // =============================================================================
27
+ // Constants
28
+ // =============================================================================
29
+
30
+ /** Default tab width in spaces */
31
+ const DEFAULT_TAB_WIDTH = 2;
32
+
33
+ /**
34
+ * SGR (Select Graphic Rendition) pattern - safe color/style codes
35
+ * Format: ESC [ <params> m
36
+ * Params: 0-109 for colors/styles
37
+ */
38
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - matching ANSI SGR sequences
39
+ const SGR_PATTERN = /\x1b\[[\d;]*m/g;
40
+
41
+ /**
42
+ * All CSI (Control Sequence Introducer) sequences
43
+ * Format: ESC [ <params> <final byte>
44
+ * Final bytes: 0x40-0x7E (@-~)
45
+ */
46
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - matching all ANSI CSI sequences
47
+ const CSI_PATTERN = /\x1b\[[\d;?]*[ -/]*[@-~]/g;
48
+
49
+ /**
50
+ * OSC (Operating System Command) sequences
51
+ * Format: ESC ] ... BEL or ESC ] ... ST
52
+ */
53
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - matching ANSI OSC sequences
54
+ const OSC_PATTERN = /\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
55
+
56
+ /**
57
+ * Control characters to strip (except \t=0x09, \n=0x0A, \x1B=ESC for ANSI)
58
+ * Includes: NUL-HT(0x00-0x08), VT-FF(0x0B-0x0C), SO-SUB(0x0E-0x1A), FS-US(0x1C-0x1F), DEL(0x7F), C1(0x80-0x9F)
59
+ * Note: ESC(0x1B) is preserved for ANSI sequences
60
+ */
61
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - stripping dangerous control characters
62
+ const CONTROL_CHARS_PATTERN = /[\x00-\x08\x0b\x0c\x0e-\x1a\x1c-\x1f\x7f\x80-\x9f]/g;
63
+
64
+ /**
65
+ * Standalone ESC characters (not part of a valid sequence)
66
+ * Matches ESC not followed by [ or ]
67
+ */
68
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - matching orphan escape characters
69
+ const ORPHAN_ESC_PATTERN = /\x1b(?![[\]])/g;
70
+
71
+ // =============================================================================
72
+ // Core Functions
73
+ // =============================================================================
74
+
75
+ /**
76
+ * Sanitize text for safe TUI rendering (without ANSI awareness).
77
+ *
78
+ * Performs the following transformations:
79
+ * 1. CRLF → LF (Windows line endings)
80
+ * 2. Lone CR → LF (old Mac line endings)
81
+ * 3. Unicode line/paragraph separators → LF
82
+ * 4. Remove dangerous control characters (keeps \n, \t, \x1B for ANSI)
83
+ * 5. Tab → spaces (configurable width)
84
+ *
85
+ * @param text - Raw text to sanitize
86
+ * @param options - Sanitization options
87
+ * @returns Sanitized text safe for rendering
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * // Basic usage
92
+ * const clean = sanitizeText("Hello\r\nWorld\t!");
93
+ * // => "Hello\nWorld !"
94
+ *
95
+ * // Custom tab width
96
+ * const clean = sanitizeText("A\tB", { tabWidth: 4 });
97
+ * // => "A B"
98
+ * ```
99
+ */
100
+ export function sanitizeText(text: string, options?: SanitizeOptions): string {
101
+ const tabWidth = options?.tabWidth ?? DEFAULT_TAB_WIDTH;
102
+
103
+ // Step 1-3: Normalize line endings
104
+ let result = text
105
+ .replace(/\r\n/g, "\n") // CRLF → LF
106
+ .replace(/\r/g, "\n") // Lone CR → LF
107
+ .replace(/\u2028|\u2029/g, "\n"); // Unicode line separators
108
+
109
+ // Step 4: Remove dangerous control characters (keep \n=0x0A, \t=0x09, \x1B=ESC)
110
+ result = result.replace(CONTROL_CHARS_PATTERN, "");
111
+
112
+ // Step 5: Convert tabs to spaces
113
+ if (tabWidth > 0) {
114
+ const spaces = " ".repeat(tabWidth);
115
+ result = result.replace(/\t/g, spaces);
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ /**
122
+ * Sanitize ANSI codes using an allow-list approach.
123
+ *
124
+ * Keeps safe codes:
125
+ * - SGR (Select Graphic Rendition): Colors and text styles `\x1b[...m`
126
+ *
127
+ * Removes dangerous codes:
128
+ * - Cursor movement: `\x1b[...H`, `\x1b[...A/B/C/D/E/F/G`
129
+ * - Screen clearing: `\x1b[...J`, `\x1b[...K`
130
+ * - Scrolling: `\x1b[...S`, `\x1b[...T`
131
+ * - Other CSI sequences
132
+ * - OSC sequences (terminal titles, hyperlinks in raw form)
133
+ *
134
+ * @param text - Text containing ANSI codes
135
+ * @param options - Sanitization options
136
+ * @returns Text with only safe ANSI codes preserved
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * // Colors preserved
141
+ * const text = "\x1b[31mRed\x1b[0m";
142
+ * sanitizeAnsi(text) // => "\x1b[31mRed\x1b[0m"
143
+ *
144
+ * // Cursor movement removed
145
+ * const text = "\x1b[2JCleared\x1b[H";
146
+ * sanitizeAnsi(text) // => "Cleared"
147
+ *
148
+ * // Strip all ANSI
149
+ * sanitizeAnsi(text, { stripAllAnsi: true }) // => "Cleared"
150
+ * ```
151
+ */
152
+ export function sanitizeAnsi(text: string, options?: SanitizeOptions): string {
153
+ if (options?.stripAllAnsi) {
154
+ // Remove ALL ANSI sequences
155
+ return text.replace(CSI_PATTERN, "").replace(OSC_PATTERN, "");
156
+ }
157
+
158
+ // Collect safe SGR sequences and their positions
159
+ const sgrRegex = new RegExp(SGR_PATTERN.source, "g");
160
+
161
+ // Build set of exact positions of safe SGR sequences
162
+ const safePositions = new Map<number, number>(); // start -> end
163
+ for (let match = sgrRegex.exec(text); match !== null; match = sgrRegex.exec(text)) {
164
+ safePositions.set(match.index, match.index + match[0].length);
165
+ }
166
+
167
+ // Remove dangerous CSI sequences (not SGR)
168
+ let result = "";
169
+ let lastIndex = 0;
170
+ const csiRegex = new RegExp(CSI_PATTERN.source, "g");
171
+
172
+ for (let match = csiRegex.exec(text); match !== null; match = csiRegex.exec(text)) {
173
+ const start = match.index;
174
+ const end = start + match[0].length;
175
+
176
+ // Add text before this match
177
+ result += text.slice(lastIndex, start);
178
+
179
+ // Check if this is a safe SGR sequence
180
+ const isSafe = safePositions.has(start) && safePositions.get(start) === end;
181
+ if (isSafe) {
182
+ result += match[0];
183
+ }
184
+ // Otherwise, skip (remove) the dangerous sequence
185
+
186
+ lastIndex = end;
187
+ }
188
+
189
+ // Add remaining text
190
+ result += text.slice(lastIndex);
191
+
192
+ // Remove OSC sequences (titles, hyperlinks, etc.)
193
+ result = result.replace(OSC_PATTERN, "");
194
+
195
+ return result;
196
+ }
197
+
198
+ /**
199
+ * Combined sanitization: text normalization + ANSI filtering.
200
+ *
201
+ * This is the recommended function for most use cases.
202
+ * Applies both `sanitizeAnsi` and `sanitizeText` in sequence.
203
+ *
204
+ * @param text - Raw text to sanitize
205
+ * @param options - Sanitization options
206
+ * @returns Fully sanitized text safe for TUI rendering
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * // Combined sanitization
211
+ * const raw = "Hello\r\nWorld\x1b[2J\x1b[31mRed\x1b[0m\t!";
212
+ * const clean = sanitize(raw);
213
+ * // => "Hello\nWorld\x1b[31mRed\x1b[0m !"
214
+ * ```
215
+ */
216
+ export function sanitize(text: string, options?: SanitizeOptions): string {
217
+ // Step 1: Sanitize ANSI (remove dangerous sequences, keep colors)
218
+ const ansiClean = sanitizeAnsi(text, options);
219
+
220
+ // Step 2: Sanitize text (normalize line endings, remove control chars, convert tabs)
221
+ const textClean = sanitizeText(ansiClean, options);
222
+
223
+ // Step 3: Remove any orphan ESC characters left behind (incomplete sequences)
224
+ return textClean.replace(ORPHAN_ESC_PATTERN, "");
225
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Text Utilities (T002 Hardening)
3
+ *
4
+ * Utility functions for text manipulation in the TUI.
5
+ * Provides hard-wrapping to prevent unpredictable terminal soft-wrapping.
6
+ *
7
+ * @module tui/utils/textUtils
8
+ */
9
+
10
+ import stringWidth from "string-width";
11
+
12
+ /**
13
+ * Hard-wrap text to prevent terminal soft-wrapping.
14
+ * Forces newlines at column boundaries so terminal doesn't wrap unpredictably.
15
+ *
16
+ * Uses string-width for accurate CJK/Emoji/ANSI handling.
17
+ * This is a simpler implementation that doesn't require wrap-ansi as a direct dependency.
18
+ *
19
+ * @param text - The text to wrap
20
+ * @param columns - Maximum column width
21
+ * @returns Text with hard line breaks inserted
22
+ */
23
+ export function hardWrap(text: string, columns: number): string {
24
+ if (!text || columns <= 0) {
25
+ return text;
26
+ }
27
+
28
+ const lines = text.split("\n");
29
+ const wrappedLines: string[] = [];
30
+
31
+ for (const line of lines) {
32
+ if (stringWidth(line) <= columns) {
33
+ wrappedLines.push(line);
34
+ continue;
35
+ }
36
+
37
+ // Need to hard-wrap this line
38
+ let remaining = line;
39
+ while (remaining.length > 0) {
40
+ const segment = truncateToWidth(remaining, columns);
41
+ wrappedLines.push(segment);
42
+ remaining = remaining.slice(segment.length);
43
+
44
+ // Safety: prevent infinite loop if no progress
45
+ if (segment.length === 0 && remaining.length > 0) {
46
+ // Force at least one character if we're stuck
47
+ const firstChar = remaining[0] ?? "";
48
+ wrappedLines.push(firstChar);
49
+ remaining = remaining.slice(1);
50
+ }
51
+ }
52
+ }
53
+
54
+ return wrappedLines.join("\n");
55
+ }
56
+
57
+ /**
58
+ * Truncate a string to fit within a specified display width.
59
+ * Uses string-width for accurate width calculation.
60
+ *
61
+ * @param str - The string to truncate
62
+ * @param maxWidth - Maximum display width
63
+ * @returns Truncated string that fits within maxWidth
64
+ */
65
+ function truncateToWidth(str: string, maxWidth: number): string {
66
+ if (stringWidth(str) <= maxWidth) {
67
+ return str;
68
+ }
69
+
70
+ // Binary search for the right length
71
+ let low = 0;
72
+ let high = str.length;
73
+
74
+ while (low < high) {
75
+ const mid = Math.ceil((low + high) / 2);
76
+ const slice = str.slice(0, mid);
77
+ if (stringWidth(slice) <= maxWidth) {
78
+ low = mid;
79
+ } else {
80
+ high = mid - 1;
81
+ }
82
+ }
83
+
84
+ return str.slice(0, low);
85
+ }
86
+
87
+ /**
88
+ * Truncate text to fit within a maximum display width with ellipsis.
89
+ * Uses string-width for accurate CJK/Emoji/ANSI handling.
90
+ *
91
+ * @param text - The text to truncate
92
+ * @param maxWidth - Maximum display width in terminal cells
93
+ * @param ellipsis - Ellipsis character (default: "…")
94
+ * @returns Truncated text that fits within maxWidth cells
95
+ */
96
+ export function truncateToDisplayWidth(
97
+ text: string,
98
+ maxWidth: number,
99
+ ellipsis: string = "…"
100
+ ): string {
101
+ if (!text || maxWidth <= 0) return "";
102
+
103
+ const textWidth = stringWidth(text);
104
+ if (textWidth <= maxWidth) return text;
105
+
106
+ const ellipsisWidth = stringWidth(ellipsis);
107
+ const targetWidth = maxWidth - ellipsisWidth;
108
+
109
+ if (targetWidth <= 0) return ellipsis.slice(0, maxWidth);
110
+
111
+ // Use existing truncateToWidth for the main content
112
+ const truncated = truncateToWidth(text, targetWidth);
113
+ return truncated + ellipsis;
114
+ }