@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,679 @@
1
+ /**
2
+ * DiffView Component (T022)
3
+ *
4
+ * Renders unified diff format with proper styling for added, removed,
5
+ * and context lines. Supports line numbers and file headers.
6
+ *
7
+ * @module tui/components/Messages/DiffView
8
+ */
9
+
10
+ import { Box, Text } from "ink";
11
+ import type React from "react";
12
+ import { useMemo } from "react";
13
+ import type { DiffViewMode } from "../../i18n/index.js";
14
+ import { useTheme } from "../../theme/index.js";
15
+ import { getTerminalWidth } from "../../utils/ui-sizing.js";
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Minimum terminal width required for side-by-side mode.
23
+ * Below this, automatically degrades to unified mode.
24
+ */
25
+ const SIDE_BY_SIDE_MIN_WIDTH = 100;
26
+
27
+ /**
28
+ * Props for the DiffView component.
29
+ */
30
+ export interface DiffViewProps {
31
+ /** The unified diff content to display */
32
+ readonly diff: string;
33
+ /** Optional file name to show in header */
34
+ readonly fileName?: string;
35
+ /** Show line numbers (old/new) on the left (default: false) */
36
+ readonly showLineNumbers?: boolean;
37
+ /** Reduce spacing for compact display (default: false) */
38
+ readonly compact?: boolean;
39
+ /** Display mode: "unified" or "side-by-side" (default: "unified") */
40
+ readonly mode?: DiffViewMode;
41
+ }
42
+
43
+ /**
44
+ * Line type in a diff
45
+ */
46
+ type DiffLineType = "added" | "removed" | "context" | "hunk" | "header";
47
+
48
+ /**
49
+ * A parsed diff line
50
+ */
51
+ interface ParsedLine {
52
+ readonly type: DiffLineType;
53
+ readonly content: string;
54
+ readonly oldLineNumber?: number;
55
+ readonly newLineNumber?: number;
56
+ }
57
+
58
+ /**
59
+ * Parsed hunk information
60
+ */
61
+ interface HunkInfo {
62
+ readonly oldStart: number;
63
+ readonly oldCount: number;
64
+ readonly newStart: number;
65
+ readonly newCount: number;
66
+ }
67
+
68
+ // =============================================================================
69
+ // Diff Parser
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Parse hunk header to extract line numbers
74
+ * Format: @@ -oldStart,oldCount +newStart,newCount @@
75
+ */
76
+ function parseHunkHeader(line: string): HunkInfo | null {
77
+ const match = line.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
78
+ if (!match || !match[1] || !match[3]) {
79
+ return null;
80
+ }
81
+
82
+ return {
83
+ oldStart: Number.parseInt(match[1], 10),
84
+ oldCount: Number.parseInt(match[2] ?? "1", 10),
85
+ newStart: Number.parseInt(match[3], 10),
86
+ newCount: Number.parseInt(match[4] ?? "1", 10),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Parse a unified diff string into structured lines
92
+ */
93
+ function parseDiff(diff: string): ParsedLine[] {
94
+ const lines = diff.split("\n");
95
+ const result: ParsedLine[] = [];
96
+
97
+ let oldLine = 0;
98
+ let newLine = 0;
99
+
100
+ for (const line of lines) {
101
+ // Skip empty lines at end
102
+ if (line === "" && result.length > 0) {
103
+ continue;
104
+ }
105
+
106
+ // File headers (--- and +++ lines)
107
+ if (line.startsWith("---") || line.startsWith("+++")) {
108
+ result.push({ type: "header", content: line });
109
+ continue;
110
+ }
111
+
112
+ // Hunk header
113
+ if (line.startsWith("@@")) {
114
+ const hunkInfo = parseHunkHeader(line);
115
+ if (hunkInfo) {
116
+ oldLine = hunkInfo.oldStart;
117
+ newLine = hunkInfo.newStart;
118
+ }
119
+ result.push({ type: "hunk", content: line });
120
+ continue;
121
+ }
122
+
123
+ // Added line
124
+ if (line.startsWith("+")) {
125
+ result.push({
126
+ type: "added",
127
+ content: line,
128
+ newLineNumber: newLine,
129
+ });
130
+ newLine++;
131
+ continue;
132
+ }
133
+
134
+ // Removed line
135
+ if (line.startsWith("-")) {
136
+ result.push({
137
+ type: "removed",
138
+ content: line,
139
+ oldLineNumber: oldLine,
140
+ });
141
+ oldLine++;
142
+ continue;
143
+ }
144
+
145
+ // Context line (starts with space or is empty context)
146
+ if (line.startsWith(" ") || line === "") {
147
+ result.push({
148
+ type: "context",
149
+ content: line,
150
+ oldLineNumber: oldLine,
151
+ newLineNumber: newLine,
152
+ });
153
+ oldLine++;
154
+ newLine++;
155
+ continue;
156
+ }
157
+
158
+ // Other lines (like "")
159
+ result.push({ type: "context", content: line });
160
+ }
161
+
162
+ return result;
163
+ }
164
+
165
+ // =============================================================================
166
+ // Sub-components
167
+ // =============================================================================
168
+
169
+ /**
170
+ * File header component
171
+ */
172
+ interface FileHeaderProps {
173
+ readonly fileName: string;
174
+ }
175
+
176
+ function FileHeader({ fileName }: FileHeaderProps): React.ReactElement {
177
+ const { theme } = useTheme();
178
+
179
+ return (
180
+ <Box
181
+ borderStyle="single"
182
+ borderColor={theme.semantic.border.default}
183
+ borderBottom={false}
184
+ paddingX={1}
185
+ >
186
+ <Text color={theme.semantic.text.secondary} bold>
187
+ 📄 {fileName}
188
+ </Text>
189
+ </Box>
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Render a single diff line
195
+ */
196
+ interface DiffLineRendererProps {
197
+ readonly line: ParsedLine;
198
+ readonly showLineNumbers: boolean;
199
+ readonly lineNumberWidth: number;
200
+ }
201
+
202
+ /**
203
+ * Enhanced diff line styling configuration
204
+ */
205
+ interface DiffLineStyle {
206
+ readonly textColor: string;
207
+ readonly bgColor?: string;
208
+ readonly symbol: string;
209
+ readonly bold: boolean;
210
+ readonly dimContent: boolean;
211
+ }
212
+
213
+ function DiffLineRenderer({
214
+ line,
215
+ showLineNumbers,
216
+ lineNumberWidth,
217
+ }: DiffLineRendererProps): React.ReactElement {
218
+ const { theme } = useTheme();
219
+ const diffColors = theme.semantic.diff;
220
+
221
+ // Enhanced style configuration with better visual distinction
222
+ const getLineStyle = (): DiffLineStyle => {
223
+ switch (line.type) {
224
+ case "added":
225
+ return {
226
+ textColor: theme.colors.success,
227
+ bgColor: diffColors.added,
228
+ symbol: "▶",
229
+ bold: true,
230
+ dimContent: false,
231
+ };
232
+ case "removed":
233
+ return {
234
+ textColor: theme.colors.error,
235
+ bgColor: diffColors.removed,
236
+ symbol: "◀",
237
+ bold: true,
238
+ dimContent: false,
239
+ };
240
+ case "hunk":
241
+ return {
242
+ textColor: theme.colors.info,
243
+ symbol: "≡",
244
+ bold: false,
245
+ dimContent: false,
246
+ };
247
+ case "header":
248
+ return {
249
+ textColor: theme.semantic.text.muted,
250
+ symbol: "",
251
+ bold: false,
252
+ dimContent: true,
253
+ };
254
+ default:
255
+ return {
256
+ textColor: theme.semantic.text.secondary,
257
+ symbol: " ",
258
+ bold: false,
259
+ dimContent: false,
260
+ };
261
+ }
262
+ };
263
+
264
+ const style = getLineStyle();
265
+
266
+ // Format line numbers with proper alignment
267
+ const formatLineNumber = (num: number | undefined): string => {
268
+ if (num === undefined) {
269
+ return "".padStart(lineNumberWidth, " ");
270
+ }
271
+ return String(num).padStart(lineNumberWidth, " ");
272
+ };
273
+
274
+ // Get the content without the prefix character for display
275
+ const displayContent =
276
+ line.type === "added" || line.type === "removed" || line.type === "context"
277
+ ? line.content.slice(1)
278
+ : line.content;
279
+
280
+ // Render line numbers section
281
+ const renderLineNumbers = (): React.ReactElement | null => {
282
+ if (!showLineNumbers) return null;
283
+
284
+ if (line.type === "hunk" || line.type === "header") {
285
+ return (
286
+ <Box marginRight={1}>
287
+ <Text color={theme.semantic.text.muted}>{"".padStart(lineNumberWidth * 2 + 2, " ")}</Text>
288
+ <Text color={theme.semantic.border.muted}>│</Text>
289
+ </Box>
290
+ );
291
+ }
292
+
293
+ if (line.type === "added" || line.type === "removed" || line.type === "context") {
294
+ return (
295
+ <Box marginRight={1}>
296
+ {/* Old line number - dimmed for removed lines */}
297
+ <Text
298
+ color={line.type === "removed" ? theme.colors.error : theme.semantic.text.muted}
299
+ dimColor={line.type !== "removed"}
300
+ >
301
+ {formatLineNumber(line.oldLineNumber)}
302
+ </Text>
303
+ <Text color={theme.semantic.border.muted}> </Text>
304
+ {/* New line number - highlighted for added lines */}
305
+ <Text
306
+ color={line.type === "added" ? theme.colors.success : theme.semantic.text.muted}
307
+ dimColor={line.type !== "added"}
308
+ >
309
+ {formatLineNumber(line.newLineNumber)}
310
+ </Text>
311
+ <Text color={theme.semantic.border.muted}>│</Text>
312
+ </Box>
313
+ );
314
+ }
315
+
316
+ return null;
317
+ };
318
+
319
+ // Render the symbol prefix with enhanced visibility
320
+ const renderSymbol = (): React.ReactElement | null => {
321
+ if (line.type === "header") return null;
322
+
323
+ return (
324
+ <Text color={style.textColor} bold={style.bold}>
325
+ {style.symbol}
326
+ </Text>
327
+ );
328
+ };
329
+
330
+ return (
331
+ <Box>
332
+ {renderLineNumbers()}
333
+ <Box flexGrow={1}>
334
+ {renderSymbol()}
335
+ {style.symbol && <Text> </Text>}
336
+ <Text
337
+ color={style.textColor}
338
+ bold={style.bold && (line.type === "added" || line.type === "removed")}
339
+ dimColor={style.dimContent}
340
+ >
341
+ {displayContent}
342
+ </Text>
343
+ </Box>
344
+ </Box>
345
+ );
346
+ }
347
+
348
+ // =============================================================================
349
+ // Side-by-Side Types and Helpers
350
+ // =============================================================================
351
+
352
+ /**
353
+ * A paired line for side-by-side display.
354
+ * Left is the old version, right is the new version.
355
+ * Either side can be empty (for additions/deletions).
356
+ */
357
+ interface SideBySideLine {
358
+ readonly left: ParsedLine | null;
359
+ readonly right: ParsedLine | null;
360
+ }
361
+
362
+ /**
363
+ * Convert parsed diff lines to side-by-side pairs.
364
+ * Matches removed and added lines, and preserves context on both sides.
365
+ */
366
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Diff pairing logic requires multiple state transitions
367
+ function toSideBySidePairs(lines: ParsedLine[]): SideBySideLine[] {
368
+ const result: SideBySideLine[] = [];
369
+ let i = 0;
370
+
371
+ while (i < lines.length) {
372
+ const line = lines[i];
373
+ if (!line) break;
374
+
375
+ // Headers and hunks span both columns
376
+ if (line.type === "header" || line.type === "hunk") {
377
+ result.push({ left: line, right: line });
378
+ i++;
379
+ continue;
380
+ }
381
+
382
+ // Context lines appear on both sides
383
+ if (line.type === "context") {
384
+ result.push({ left: line, right: line });
385
+ i++;
386
+ continue;
387
+ }
388
+
389
+ // Collect consecutive removed lines
390
+ const removedLines: ParsedLine[] = [];
391
+ while (i < lines.length) {
392
+ const current = lines[i];
393
+ if (!current || current.type !== "removed") break;
394
+ removedLines.push(current);
395
+ i++;
396
+ }
397
+
398
+ // Collect consecutive added lines
399
+ const addedLines: ParsedLine[] = [];
400
+ while (i < lines.length) {
401
+ const current = lines[i];
402
+ if (!current || current.type !== "added") break;
403
+ addedLines.push(current);
404
+ i++;
405
+ }
406
+
407
+ // Pair removed and added lines
408
+ const maxLen = Math.max(removedLines.length, addedLines.length);
409
+ for (let j = 0; j < maxLen; j++) {
410
+ const leftLine = removedLines[j];
411
+ const rightLine = addedLines[j];
412
+ result.push({
413
+ left: leftLine ?? null,
414
+ right: rightLine ?? null,
415
+ });
416
+ }
417
+ }
418
+
419
+ return result;
420
+ }
421
+
422
+ // =============================================================================
423
+ // Side-by-Side Renderer
424
+ // =============================================================================
425
+
426
+ /**
427
+ * Render a single side-by-side row.
428
+ */
429
+ interface SideBySideRowProps {
430
+ readonly pair: SideBySideLine;
431
+ readonly showLineNumbers: boolean;
432
+ readonly lineNumberWidth: number;
433
+ readonly columnWidth: number;
434
+ }
435
+
436
+ function SideBySideRow({
437
+ pair,
438
+ showLineNumbers,
439
+ lineNumberWidth,
440
+ columnWidth,
441
+ }: SideBySideRowProps): React.ReactElement {
442
+ const { theme } = useTheme();
443
+
444
+ // Format line number
445
+ const formatLineNumber = (num: number | undefined): string => {
446
+ if (num === undefined) {
447
+ return "".padStart(lineNumberWidth, " ");
448
+ }
449
+ return String(num).padStart(lineNumberWidth, " ");
450
+ };
451
+
452
+ // Get content (remove prefix char for display)
453
+ const getContent = (line: ParsedLine | null): string => {
454
+ if (!line) return "";
455
+ if (line.type === "added" || line.type === "removed" || line.type === "context") {
456
+ return line.content.slice(1);
457
+ }
458
+ return line.content;
459
+ };
460
+
461
+ // Truncate content to fit column width
462
+ const truncateContent = (content: string, maxWidth: number): string => {
463
+ if (content.length <= maxWidth) {
464
+ return content;
465
+ }
466
+ return `${content.slice(0, maxWidth - 1)}…`;
467
+ };
468
+
469
+ // For headers/hunks, span both columns with enhanced styling
470
+ if (pair.left?.type === "header" || pair.left?.type === "hunk") {
471
+ const isHunk = pair.left.type === "hunk";
472
+ return (
473
+ <Box>
474
+ {isHunk && (
475
+ <Text color={theme.colors.info} bold>
476
+ ≡{" "}
477
+ </Text>
478
+ )}
479
+ <Text color={isHunk ? theme.colors.info : theme.semantic.text.muted} dimColor={!isHunk}>
480
+ {pair.left.content}
481
+ </Text>
482
+ </Box>
483
+ );
484
+ }
485
+
486
+ // Calculate content width per column
487
+ const lineNumSpace = showLineNumbers ? lineNumberWidth + 2 : 0;
488
+ const contentWidth = columnWidth - lineNumSpace - 3; // -3 for symbol, space, and padding
489
+
490
+ // Render left side (old/removed) with enhanced colors
491
+ const leftContent = getContent(pair.left);
492
+ const leftColor =
493
+ pair.left?.type === "removed" ? theme.colors.error : theme.semantic.text.secondary;
494
+ const leftSymbol = pair.left?.type === "removed" ? "◀" : pair.left ? " " : " ";
495
+ const leftIsBold = pair.left?.type === "removed";
496
+
497
+ // Render right side (new/added) with enhanced colors
498
+ const rightContent = getContent(pair.right);
499
+ const rightColor =
500
+ pair.right?.type === "added" ? theme.colors.success : theme.semantic.text.secondary;
501
+ const rightSymbol = pair.right?.type === "added" ? "▶" : pair.right ? " " : " ";
502
+ const rightIsBold = pair.right?.type === "added";
503
+
504
+ return (
505
+ <Box>
506
+ {/* Left column (old/removed) */}
507
+ <Box width={columnWidth}>
508
+ {showLineNumbers && (
509
+ <Text
510
+ color={pair.left?.type === "removed" ? theme.colors.error : theme.semantic.text.muted}
511
+ dimColor={pair.left?.type !== "removed"}
512
+ >
513
+ {formatLineNumber(pair.left?.oldLineNumber)}
514
+ </Text>
515
+ )}
516
+ {showLineNumbers && <Text color={theme.semantic.border.muted}>│</Text>}
517
+ <Text color={leftColor} bold={leftIsBold}>
518
+ {leftSymbol}{" "}
519
+ </Text>
520
+ <Text color={leftColor} bold={leftIsBold}>
521
+ {truncateContent(leftContent, contentWidth)}
522
+ </Text>
523
+ </Box>
524
+
525
+ {/* Center divider */}
526
+ <Text color={theme.semantic.border.default}>║</Text>
527
+
528
+ {/* Right column (new/added) */}
529
+ <Box width={columnWidth}>
530
+ {showLineNumbers && (
531
+ <Text
532
+ color={pair.right?.type === "added" ? theme.colors.success : theme.semantic.text.muted}
533
+ dimColor={pair.right?.type !== "added"}
534
+ >
535
+ {formatLineNumber(pair.right?.newLineNumber)}
536
+ </Text>
537
+ )}
538
+ {showLineNumbers && <Text color={theme.semantic.border.muted}>│</Text>}
539
+ <Text color={rightColor} bold={rightIsBold}>
540
+ {rightSymbol}{" "}
541
+ </Text>
542
+ <Text color={rightColor} bold={rightIsBold}>
543
+ {truncateContent(rightContent, contentWidth)}
544
+ </Text>
545
+ </Box>
546
+ </Box>
547
+ );
548
+ }
549
+
550
+ // =============================================================================
551
+ // Main Component
552
+ // =============================================================================
553
+
554
+ /**
555
+ * DiffView displays a diff with proper styling.
556
+ * Supports both unified and side-by-side display modes.
557
+ *
558
+ * @example
559
+ * ```tsx
560
+ * <DiffView
561
+ * diff={unifiedDiff}
562
+ * fileName="src/index.ts"
563
+ * showLineNumbers
564
+ * mode="side-by-side"
565
+ * />
566
+ * ```
567
+ */
568
+ export function DiffView({
569
+ diff,
570
+ fileName,
571
+ showLineNumbers = false,
572
+ compact = false,
573
+ mode = "unified",
574
+ }: DiffViewProps): React.ReactElement {
575
+ const { theme } = useTheme();
576
+
577
+ // Get terminal width for auto-degradation and column calculation
578
+ const terminalWidth = getTerminalWidth();
579
+
580
+ // Auto-degrade to unified mode if terminal is too narrow
581
+ const effectiveMode = terminalWidth < SIDE_BY_SIDE_MIN_WIDTH ? "unified" : mode;
582
+
583
+ // Parse the diff
584
+ const parsedLines = useMemo(() => parseDiff(diff), [diff]);
585
+
586
+ // Calculate line number width based on max line number
587
+ const lineNumberWidth = useMemo(() => {
588
+ let maxLineNumber = 0;
589
+ for (const line of parsedLines) {
590
+ if (line.oldLineNumber !== undefined && line.oldLineNumber > maxLineNumber) {
591
+ maxLineNumber = line.oldLineNumber;
592
+ }
593
+ if (line.newLineNumber !== undefined && line.newLineNumber > maxLineNumber) {
594
+ maxLineNumber = line.newLineNumber;
595
+ }
596
+ }
597
+ return Math.max(3, String(maxLineNumber).length);
598
+ }, [parsedLines]);
599
+
600
+ // Filter out file headers if fileName is provided (we'll show our own)
601
+ const displayLines = fileName
602
+ ? parsedLines.filter((line) => line.type !== "header")
603
+ : parsedLines;
604
+
605
+ // Generate stable keys for each line based on type and line numbers
606
+ const getLineKey = (line: ParsedLine, position: number): string => {
607
+ const oldNum = line.oldLineNumber ?? "x";
608
+ const newNum = line.newLineNumber ?? "x";
609
+ return `${line.type}-${oldNum}-${newNum}-${position}`;
610
+ };
611
+
612
+ // Calculate column width for side-by-side mode
613
+ // Account for: borders (4), divider (1), padding (2)
614
+ const columnWidth = Math.floor((terminalWidth - 7) / 2);
615
+
616
+ // Convert to side-by-side pairs if needed
617
+ const sideBySidePairs = useMemo(
618
+ () => (effectiveMode === "side-by-side" ? toSideBySidePairs(displayLines) : []),
619
+ [displayLines, effectiveMode]
620
+ );
621
+
622
+ // Render unified mode
623
+ if (effectiveMode === "unified") {
624
+ return (
625
+ <Box flexDirection="column">
626
+ {fileName && <FileHeader fileName={fileName} />}
627
+ <Box
628
+ flexDirection="column"
629
+ borderStyle="single"
630
+ borderColor={theme.semantic.border.default}
631
+ paddingX={compact ? 0 : 1}
632
+ paddingY={compact ? 0 : 0}
633
+ >
634
+ {displayLines.map((line, position) => (
635
+ <DiffLineRenderer
636
+ key={getLineKey(line, position)}
637
+ line={line}
638
+ showLineNumbers={showLineNumbers}
639
+ lineNumberWidth={lineNumberWidth}
640
+ />
641
+ ))}
642
+ </Box>
643
+ </Box>
644
+ );
645
+ }
646
+
647
+ // Generate stable key for side-by-side rows
648
+ const getSideBySideKey = (pair: SideBySideLine, position: number): string => {
649
+ const leftNum = pair.left?.oldLineNumber ?? pair.left?.newLineNumber ?? "x";
650
+ const rightNum = pair.right?.newLineNumber ?? pair.right?.oldLineNumber ?? "x";
651
+ const leftType = pair.left?.type ?? "empty";
652
+ const rightType = pair.right?.type ?? "empty";
653
+ return `sbs-${leftType}-${leftNum}-${rightType}-${rightNum}-${position}`;
654
+ };
655
+
656
+ // Render side-by-side mode
657
+ return (
658
+ <Box flexDirection="column">
659
+ {fileName && <FileHeader fileName={fileName} />}
660
+ <Box
661
+ flexDirection="column"
662
+ borderStyle="single"
663
+ borderColor={theme.semantic.border.default}
664
+ paddingX={compact ? 0 : 1}
665
+ paddingY={compact ? 0 : 0}
666
+ >
667
+ {sideBySidePairs.map((pair, index) => (
668
+ <SideBySideRow
669
+ key={getSideBySideKey(pair, index)}
670
+ pair={pair}
671
+ showLineNumbers={showLineNumbers}
672
+ lineNumberWidth={lineNumberWidth}
673
+ columnWidth={columnWidth}
674
+ />
675
+ ))}
676
+ </Box>
677
+ </Box>
678
+ );
679
+ }