@cueloop/client 0.1.0-alpha.0 → 0.1.0-alpha.100

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 (345) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +41 -27
  8. package/src/annotations/annotation-palette.ts +62 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +364 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
  14. package/src/annotations/discussions.ts +77 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +242 -0
  17. package/src/annotations/use-annotation-surface.tsx +1238 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/app/App.autoclose.test.tsx +144 -0
  20. package/src/app/App.deleted-scroll.test.tsx +187 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.identity.test.tsx +84 -0
  25. package/src/app/App.plan-render.test.tsx +209 -0
  26. package/src/app/App.plan-review.test.tsx +428 -0
  27. package/src/app/App.prototype-markdown.test.tsx +128 -0
  28. package/src/app/App.prototype.test.tsx +176 -0
  29. package/src/app/App.readonly.test.tsx +210 -0
  30. package/src/app/App.regression.test.tsx +99 -0
  31. package/src/app/App.rendering.test.tsx +134 -0
  32. package/src/app/App.share-dialog.test.tsx +177 -0
  33. package/src/app/App.submit-confirm.test.tsx +267 -0
  34. package/src/app/App.test.tsx +496 -0
  35. package/src/app/App.thread-agent.test.ts +12 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1650 -0
  38. package/src/app/App.visual.test.tsx +59 -0
  39. package/src/app/App.walk.test.tsx +299 -0
  40. package/src/app/App.workbench.test.tsx +1149 -0
  41. package/src/app/app-key-state.ts +61 -0
  42. package/src/app/app-screens.tsx +606 -0
  43. package/src/app/app-view-model.ts +168 -0
  44. package/src/app/components/AppHeader.stories.tsx +50 -0
  45. package/src/app/components/AppHeader.tsx +81 -0
  46. package/src/app/components/AppShell.brand.test.tsx +58 -0
  47. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  48. package/src/app/components/AppShell.stories.tsx +89 -0
  49. package/src/app/components/AppShell.tsx +358 -0
  50. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  51. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  52. package/src/app/components/Breadcrumb.tsx +45 -0
  53. package/src/app/components/MenuBar.stories.tsx +28 -0
  54. package/src/app/components/MenuBar.tsx +44 -0
  55. package/src/app/components/PaneGrid.stories.tsx +84 -0
  56. package/src/app/components/PaneGrid.tsx +128 -0
  57. package/src/app/components/PanelColumn.stories.tsx +25 -0
  58. package/src/app/components/PanelColumn.tsx +93 -0
  59. package/src/app/components/ShellHeader.stories.tsx +50 -0
  60. package/src/app/components/ShellHeader.tsx +137 -0
  61. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  62. package/src/app/components/WorkspacePanels.tsx +120 -0
  63. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  64. package/src/app/intent-dispatch.test.ts +728 -0
  65. package/src/app/intent-dispatch.ts +687 -0
  66. package/src/app/launch-layout.test.ts +78 -0
  67. package/src/app/launch-layout.ts +57 -0
  68. package/src/app/ready-signal.test.ts +85 -0
  69. package/src/app/ready-signal.ts +52 -0
  70. package/src/app/run.ts +109 -0
  71. package/src/{serve.test.ts → app/serve.test.ts} +24 -12
  72. package/src/app/serve.ts +111 -0
  73. package/src/app/version.ts +5 -0
  74. package/src/appearance/components/theme-context.stories.tsx +46 -0
  75. package/src/appearance/components/theme-context.tsx +37 -0
  76. package/src/appearance/theme-presets.test.ts +51 -0
  77. package/src/appearance/theme-presets.ts +219 -0
  78. package/src/appearance/theme.ts +112 -0
  79. package/src/appearance/visual/contrast.test.ts +35 -0
  80. package/src/appearance/visual/contrast.ts +33 -0
  81. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  82. package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
  83. package/src/diff/components/AnnotatableFileView.tsx +141 -0
  84. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  85. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  86. package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
  87. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  88. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  89. package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
  90. package/src/diff/components/DiffContentView.tsx +1055 -0
  91. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  92. package/src/diff/components/WalkWizard.tsx +146 -0
  93. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  94. package/src/diff/components/diff-content-view-layout.ts +84 -0
  95. package/src/diff/diff-fold.test.ts +78 -0
  96. package/src/diff/diff-fold.ts +126 -0
  97. package/src/diff/diff-hunk-curate.test.ts +315 -0
  98. package/src/diff/diff-hunk-curate.ts +152 -0
  99. package/src/diff/diff-intraline.test.ts +239 -0
  100. package/src/diff/diff-intraline.ts +238 -0
  101. package/src/diff/diff-syntax.test.ts +125 -0
  102. package/src/diff/diff-syntax.ts +186 -0
  103. package/src/diff/split-diff.test.ts +110 -0
  104. package/src/diff/split-diff.ts +144 -0
  105. package/src/diff/view-diff.test.ts +325 -0
  106. package/src/diff/view-diff.ts +522 -0
  107. package/src/diff/walk.test.ts +121 -0
  108. package/src/diff/walk.ts +73 -0
  109. package/src/index.ts +31 -2
  110. package/src/integrations/attribution.test.ts +31 -0
  111. package/src/integrations/attribution.ts +42 -0
  112. package/src/integrations/client-extension-registry.test.tsx +61 -0
  113. package/src/integrations/client-extension-registry.ts +159 -0
  114. package/src/integrations/clipboard.test.ts +23 -0
  115. package/src/integrations/clipboard.ts +33 -0
  116. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  117. package/src/integrations/components/ExtensionZone.tsx +145 -0
  118. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  119. package/src/integrations/components/ShareDialog.tsx +550 -0
  120. package/src/integrations/components/share-dialog-store.ts +55 -0
  121. package/src/integrations/editor.test.ts +149 -0
  122. package/src/integrations/editor.ts +160 -0
  123. package/src/integrations/github-identity.test.ts +58 -0
  124. package/src/integrations/github-identity.ts +41 -0
  125. package/src/integrations/herdr.test.ts +43 -0
  126. package/src/integrations/herdr.ts +38 -0
  127. package/src/integrations/integrations.test.ts +41 -0
  128. package/src/integrations/integrations.ts +32 -0
  129. package/src/integrations/pasted-image.test.ts +67 -0
  130. package/src/integrations/pasted-image.ts +60 -0
  131. package/src/integrations/share.ts +238 -0
  132. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  133. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  134. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  135. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  136. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  137. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  138. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  139. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  140. package/src/keyboard/components/NavModeHint.tsx +31 -0
  141. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  142. package/src/keyboard/components/SlashComposer.tsx +87 -0
  143. package/src/keyboard/key-bindings.test.ts +156 -0
  144. package/src/keyboard/key-bindings.ts +459 -0
  145. package/src/keyboard/keymap.test.ts +611 -0
  146. package/src/keyboard/keymap.ts +395 -0
  147. package/src/keyboard/skills.test.ts +74 -0
  148. package/src/keyboard/skills.ts +75 -0
  149. package/src/keyboard/slash-palette.test.ts +103 -0
  150. package/src/keyboard/slash-palette.ts +129 -0
  151. package/src/keyboard/thread-chords.test.ts +79 -0
  152. package/src/keyboard/thread-chords.ts +163 -0
  153. package/src/keyboard/use-shared-keyboard.ts +55 -0
  154. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  155. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  156. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  157. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  158. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  159. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  160. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  161. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  162. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  163. package/src/markdown/components/ThreadView.tsx +560 -0
  164. package/src/markdown/mark-runs.ts +121 -0
  165. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  166. package/src/markdown/markdown-grid.test.ts +101 -0
  167. package/src/markdown/markdown-grid.ts +203 -0
  168. package/src/markdown/markdown-highlight.test.ts +61 -0
  169. package/src/markdown/markdown-highlight.ts +124 -0
  170. package/src/markdown/thread-code-syntax.ts +45 -0
  171. package/src/markdown/use-thread-body-editing.tsx +54 -0
  172. package/src/markdown/view-plan.regression.test.ts +60 -0
  173. package/src/markdown/view-plan.test.ts +545 -0
  174. package/src/markdown/view-plan.ts +1009 -0
  175. package/src/performance/otlp-trace.test.ts +119 -0
  176. package/src/performance/otlp-trace.ts +134 -0
  177. package/src/performance/perf-report.ts +18 -0
  178. package/src/performance/perf-timings.test.ts +18 -0
  179. package/src/performance/perf-timings.ts +39 -0
  180. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  181. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  182. package/src/prototype/prototype-browser.test.ts +81 -0
  183. package/src/prototype/prototype-browser.ts +321 -0
  184. package/src/prototype/prototype-pixels.tsx +35 -0
  185. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  186. package/src/settings/components/SettingsDialog.tsx +183 -0
  187. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  188. package/src/settings/components/SettingsRows.tsx +112 -0
  189. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  190. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  191. package/src/settings/components/quick-actions-editor.tsx +222 -0
  192. package/src/settings/config-identity.test.ts +78 -0
  193. package/src/settings/config.test.ts +676 -0
  194. package/src/settings/config.ts +628 -0
  195. package/src/settings/use-settings-dialog.tsx +351 -0
  196. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  197. package/src/stories/stories-app.tsx +196 -0
  198. package/src/stories/stories.test.tsx +74 -0
  199. package/src/stories/story-fixtures.ts +102 -0
  200. package/src/stories/story-tree.test.ts +108 -0
  201. package/src/stories/story.ts +154 -0
  202. package/src/terminal/ansi-screen.test.ts +28 -0
  203. package/src/terminal/ansi-screen.ts +72 -0
  204. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  205. package/src/terminal/components/AnsiScreen.tsx +28 -0
  206. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  207. package/src/terminal/components/terminal-pane.ts +196 -0
  208. package/src/terminal/ghostty-terminal.test.ts +85 -0
  209. package/src/terminal/ghostty-terminal.ts +197 -0
  210. package/src/terminal/kitty-image.test.ts +66 -0
  211. package/src/terminal/kitty-image.ts +137 -0
  212. package/src/terminal/pty.test.ts +81 -0
  213. package/src/terminal/pty.ts +246 -0
  214. package/src/testing/test-support.test.ts +264 -0
  215. package/src/testing/test-support.ts +367 -0
  216. package/src/thread/agent-continuation.test.ts +32 -0
  217. package/src/thread/agent-continuation.ts +13 -0
  218. package/src/thread/agent-transcript.test.ts +161 -0
  219. package/src/thread/agent-transcript.ts +336 -0
  220. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  221. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  222. package/src/thread/components/AgentConfigControls.tsx +114 -0
  223. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  224. package/src/thread/components/AgentThreadPane.test.tsx +829 -0
  225. package/src/thread/components/AgentThreadPane.tsx +367 -0
  226. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  227. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  228. package/src/thread/components/ThreadFooter.tsx +77 -0
  229. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  230. package/src/thread/components/ThreadTree.test.tsx +41 -0
  231. package/src/thread/components/ThreadTree.tsx +267 -0
  232. package/src/thread/components/thread-tree.test.ts +137 -0
  233. package/src/thread/components/thread-tree.ts +112 -0
  234. package/src/thread/thread-controller.curate.test.ts +502 -0
  235. package/src/thread/thread-controller.derivation.test.ts +62 -0
  236. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  237. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  238. package/src/thread/thread-controller.share.test.ts +573 -0
  239. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  240. package/src/thread/thread-controller.tree.test.ts +263 -0
  241. package/src/thread/thread-controller.ts +2018 -0
  242. package/src/thread/tree-view.test.ts +128 -0
  243. package/src/thread/tree-view.ts +146 -0
  244. package/src/thread/use-thread-agent.test.tsx +208 -0
  245. package/src/thread/use-thread-agent.ts +111 -0
  246. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  247. package/src/ui/components/CodeBlock.tsx +80 -0
  248. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  249. package/src/ui/components/ConfirmCard.tsx +151 -0
  250. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  251. package/src/ui/components/ConfirmDialog.tsx +72 -0
  252. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  253. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  254. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  255. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  256. package/src/ui/components/PromptDialog.test.tsx +65 -0
  257. package/src/ui/components/PromptDialog.tsx +99 -0
  258. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  259. package/src/ui/components/RootOverlay.tsx +49 -0
  260. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  261. package/src/ui/components/ScrollArea.tsx +61 -0
  262. package/src/ui/components/Toast.stories.tsx +19 -0
  263. package/src/ui/components/Toast.tsx +54 -0
  264. package/src/ui/components/Tooltip.stories.tsx +26 -0
  265. package/src/ui/components/Tooltip.tsx +72 -0
  266. package/src/ui/components/menu-control.stories.tsx +35 -0
  267. package/src/ui/components/menu-control.tsx +43 -0
  268. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  269. package/src/ui/components/primitives/Button.tsx +57 -0
  270. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  271. package/src/ui/components/primitives/Card.tsx +62 -0
  272. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  273. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  274. package/src/ui/components/primitives/Dialog.tsx +69 -0
  275. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  276. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  277. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  278. package/src/ui/components/primitives/IconButton.tsx +58 -0
  279. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  280. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  281. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  282. package/src/ui/components/primitives/Switch.tsx +32 -0
  283. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  284. package/src/ui/components/primitives/Tabs.tsx +101 -0
  285. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  286. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  287. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  288. package/src/ui/components/primitives/Tree.tsx +133 -0
  289. package/src/ui/components/primitives/frame.test.ts +12 -0
  290. package/src/ui/components/primitives/frame.ts +17 -0
  291. package/src/ui/components/primitives/icons.ts +61 -0
  292. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  293. package/src/ui/components/primitives/tree-model.ts +137 -0
  294. package/src/ui/components/syntax-highlight.ts +133 -0
  295. package/src/ui/components/truncate-text.ts +7 -0
  296. package/src/ui/components/truncate-title.test.ts +31 -0
  297. package/src/ui/components/truncate-title.ts +15 -0
  298. package/src/ui/use-frame-measure.ts +74 -0
  299. package/src/ui/use-terminal-virtualizer.ts +259 -0
  300. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  301. package/src/workbench/components/ChangesColumn.tsx +203 -0
  302. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  303. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  304. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  305. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  306. package/src/workbench/components/EditorGrid.tsx +544 -0
  307. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  308. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  309. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  310. package/src/workbench/components/FileContentsView.tsx +100 -0
  311. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  312. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  313. package/src/workbench/components/GridTabContent.tsx +193 -0
  314. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  315. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  316. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  317. package/src/workbench/components/TreePane.stories.tsx +106 -0
  318. package/src/workbench/components/TreePane.tsx +155 -0
  319. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  320. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  321. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  322. package/src/workbench/components/editor-grid.test.ts +184 -0
  323. package/src/workbench/components/editor-grid.ts +281 -0
  324. package/src/workbench/components/file-tree.test.ts +60 -0
  325. package/src/workbench/components/file-tree.ts +112 -0
  326. package/src/workbench/use-changes-workbench.ts +205 -0
  327. package/src/workbench/use-remember-layout.ts +31 -0
  328. package/src/workbench/use-repo-changes.ts +91 -0
  329. package/src/workbench/workbench-snapshot.test.ts +99 -0
  330. package/src/workbench/workbench-snapshot.ts +59 -0
  331. package/src/App.diff.test.tsx +0 -99
  332. package/src/App.export.test.tsx +0 -80
  333. package/src/App.readonly.test.tsx +0 -148
  334. package/src/App.test.tsx +0 -217
  335. package/src/App.tsx +0 -0
  336. package/src/config.test.ts +0 -116
  337. package/src/config.ts +0 -113
  338. package/src/editor.ts +0 -34
  339. package/src/run.ts +0 -28
  340. package/src/serve.ts +0 -75
  341. package/src/theme.ts +0 -46
  342. package/src/view-diff.test.ts +0 -55
  343. package/src/view-diff.ts +0 -86
  344. package/src/view.test.ts +0 -157
  345. package/src/view.ts +0 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Syntax highlighting for diff rows: tree-sitter token colors mapped onto the
3
+ * diff sheet's hand-rolled rows, so annotation cards still interleave. A hunk is
4
+ * highlighted as a contiguous fragment (its context+addition lines as the new
5
+ * side, context+deletion lines as the old side) - never per single line, which
6
+ * would lose the multi-line grammar context - and the resulting token spans are
7
+ * mapped back to each row in line-relative columns. Async: the shared tree-sitter
8
+ * client resolves off the render path, so callers draw unstyled first.
9
+ */
10
+
11
+ import { getTreeSitterClient, type SimpleHighlight } from "@opentui/core";
12
+ import type { DiffRow } from "./view-diff";
13
+ import { filetypeForPath } from "../ui/components/syntax-highlight";
14
+
15
+ /** A syntax-colored run within one row, in line-relative columns [start, end). */
16
+ export interface SyntaxSpan {
17
+ start: number;
18
+ end: number;
19
+ group: string;
20
+ }
21
+
22
+ function stripTrailingNewline(text: string): string {
23
+ return text.replace(/\n$/, "");
24
+ }
25
+
26
+ interface HighlightJob {
27
+ filetype: string;
28
+ source: string;
29
+ rowIndexByLine: number[];
30
+ }
31
+
32
+ /**
33
+ * Split diff rows into per-hunk highlight jobs, one per side. Plain project
34
+ * files contain only context rows, so detect their language from the row path.
35
+ */
36
+ export function highlightJobs(rows: DiffRow[]): HighlightJob[] {
37
+ const jobs: HighlightJob[] = [];
38
+ let oldLines: string[] = [];
39
+ let newLines: string[] = [];
40
+ let oldRowIndexByLine: number[] = [];
41
+ let newRowIndexByLine: number[] = [];
42
+ let filetype: string | undefined;
43
+ let path: string | undefined;
44
+ let hasChange = false;
45
+
46
+ const flush = (): void => {
47
+ if (filetype) {
48
+ if (oldLines.length && (hasChange || newLines.length === 0))
49
+ jobs.push({ filetype, source: oldLines.join("\n"), rowIndexByLine: oldRowIndexByLine });
50
+ if (newLines.length)
51
+ jobs.push({ filetype, source: newLines.join("\n"), rowIndexByLine: newRowIndexByLine });
52
+ }
53
+ oldLines = [];
54
+ newLines = [];
55
+ oldRowIndexByLine = [];
56
+ newRowIndexByLine = [];
57
+ hasChange = false;
58
+ };
59
+
60
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
61
+ const row = rows[rowIndex]!;
62
+
63
+ if (row.file !== path) {
64
+ flush();
65
+ path = row.file;
66
+ filetype = filetypeForPath(path);
67
+ }
68
+
69
+ if (row.kind === "file" || row.kind === "hunk") {
70
+ flush();
71
+ continue;
72
+ }
73
+ const text = stripTrailingNewline(row.text);
74
+
75
+ if (row.kind === "ctx" || row.kind === "del") {
76
+ oldLines.push(text);
77
+ oldRowIndexByLine.push(rowIndex);
78
+ }
79
+ if (row.kind === "ctx" || row.kind === "add") {
80
+ newLines.push(text);
81
+ newRowIndexByLine.push(rowIndex);
82
+ }
83
+ if (row.kind === "add" || row.kind === "del") hasChange = true;
84
+ }
85
+ flush();
86
+
87
+ return jobs;
88
+ }
89
+
90
+ /**
91
+ * Resolve one source's flat, possibly-overlapping token highlights into
92
+ * per-line spans. Offsets are absolute into `source`; later highlights win on
93
+ * overlap, and a highlight is clamped to the line its start falls on.
94
+ */
95
+ export function spansByLine(source: string, highlights: SimpleHighlight[]): SyntaxSpan[][] {
96
+ const lineStarts: number[] = [0];
97
+
98
+ for (let index = 0; index < source.length; index++) {
99
+ if (source[index] === "\n") lineStarts.push(index + 1);
100
+ }
101
+ const lineOf = (offset: number): number => {
102
+ let low = 0;
103
+ let high = lineStarts.length - 1;
104
+
105
+ while (low < high) {
106
+ const mid = (low + high + 1) >> 1;
107
+
108
+ if (lineStarts[mid]! <= offset) low = mid;
109
+ else high = mid - 1;
110
+ }
111
+
112
+ return low;
113
+ };
114
+ // Per-line char->group, later highlights overriding, then coalesce into spans.
115
+ const groupsByLine: Array<Array<string | undefined>> = lineStarts.map((start, line) => {
116
+ const end = line + 1 < lineStarts.length ? lineStarts[line + 1]! - 1 : source.length;
117
+
118
+ return Array.from({ length: Math.max(0, end - start) }, () => undefined);
119
+ });
120
+
121
+ for (const [start, rawEnd, group] of highlights) {
122
+ // A capture can span newlines (block comments, template literals); color the
123
+ // covered columns on every line it crosses, not just the starting line. Offsets
124
+ // past the source (byte offsets from a multi-byte line) clamp to its end, or the
125
+ // walk below would sit on the last line forever.
126
+ const end = Math.min(rawEnd, source.length);
127
+ let cursor = Math.max(0, start);
128
+
129
+ while (cursor < end) {
130
+ const line = lineOf(cursor);
131
+ const lineStart = lineStarts[line]!;
132
+ const lineEnd = line + 1 < lineStarts.length ? lineStarts[line + 1]! - 1 : source.length;
133
+ const chars = groupsByLine[line]!;
134
+ const to = Math.min(end, lineEnd) - lineStart;
135
+
136
+ for (let column = cursor - lineStart; column < to; column++) chars[column] = group;
137
+ cursor = lineEnd + 1;
138
+ }
139
+ }
140
+
141
+ return groupsByLine.map((chars) => {
142
+ const spans: SyntaxSpan[] = [];
143
+
144
+ for (let column = 0; column < chars.length; column++) {
145
+ const group = chars[column];
146
+
147
+ if (group === undefined) continue;
148
+ const previous = spans[spans.length - 1];
149
+
150
+ if (previous && previous.group === group && previous.end === column)
151
+ previous.end = column + 1;
152
+ else spans.push({ start: column, end: column + 1, group });
153
+ }
154
+
155
+ return spans;
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Highlight the diff's rows and return the token spans per row index. Rows in a
161
+ * file with no known filetype, and any row while highlighting is still resolving,
162
+ * are absent - the caller renders those unstyled.
163
+ */
164
+ export async function highlightDiffRows(rows: DiffRow[]): Promise<Map<number, SyntaxSpan[]>> {
165
+ const spansByRow = new Map<number, SyntaxSpan[]>();
166
+ const jobs = highlightJobs(rows);
167
+
168
+ if (!jobs.length) return spansByRow;
169
+ const client = getTreeSitterClient();
170
+
171
+ await client.initialize();
172
+ for (const job of jobs) {
173
+ const result = await client.highlightOnce(job.source, job.filetype);
174
+
175
+ if (!result.highlights) continue;
176
+ const lineSpans = spansByLine(job.source, result.highlights);
177
+
178
+ job.rowIndexByLine.forEach((rowIndex, line) => {
179
+ const spans = lineSpans[line];
180
+
181
+ if (spans && spans.length) spansByRow.set(rowIndex, spans);
182
+ });
183
+ }
184
+
185
+ return spansByRow;
186
+ }
@@ -0,0 +1,110 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { diffRows } from "./view-diff";
3
+ import { splitDiffRows, splitRowOffsets, type SplitRow } from "./split-diff";
4
+
5
+ const PATCH = `diff --git a/greet.ts b/greet.ts
6
+ --- a/greet.ts
7
+ +++ b/greet.ts
8
+ @@ -1,4 +1,4 @@
9
+ const name = "world";
10
+ -console.log("hi " + name);
11
+ -console.log("done");
12
+ +console.log("hello " + name);
13
+ +console.log("finished");
14
+ export {};
15
+ `;
16
+
17
+ function pairs(rows: SplitRow[]): SplitRow[] {
18
+ return rows.filter((row) => row.kind === "pair");
19
+ }
20
+
21
+ describe("splitDiffRows", () => {
22
+ test("carries the file and hunk headers through unchanged", () => {
23
+ // Act
24
+ const split = splitDiffRows(diffRows(PATCH));
25
+
26
+ // Assert
27
+ expect(split[0]).toMatchObject({ kind: "file", file: "greet.ts", text: "greet.ts" });
28
+ expect(split[1]?.kind).toBe("hunk");
29
+ });
30
+
31
+ test("a context line sits on both sides with its own line numbers", () => {
32
+ // Act
33
+ const first = pairs(splitDiffRows(diffRows(PATCH)))[0]!;
34
+
35
+ // Assert
36
+ expect(first.left).toMatchObject({ kind: "ctx", lineNumber: 1 });
37
+ expect(first.right).toMatchObject({ kind: "ctx", lineNumber: 1 });
38
+ expect(first.left?.text).toBe(first.right?.text);
39
+ });
40
+
41
+ test("a balanced change block pairs each deletion against an addition", () => {
42
+ // Act - the two deletions zip against the two additions, one per row
43
+ const changed = pairs(splitDiffRows(diffRows(PATCH))).filter(
44
+ (row) => row.left?.kind === "del" || row.right?.kind === "add",
45
+ );
46
+
47
+ // Assert
48
+ expect(changed).toHaveLength(2);
49
+ expect(changed[0]).toMatchObject({
50
+ left: { kind: "del", lineNumber: 2 },
51
+ right: { kind: "add", lineNumber: 2 },
52
+ });
53
+ expect(changed[1]).toMatchObject({
54
+ left: { kind: "del", lineNumber: 3 },
55
+ right: { kind: "add", lineNumber: 3 },
56
+ });
57
+ });
58
+
59
+ test("an unbalanced block blank-fills the shorter side", () => {
60
+ // Arrange - one deletion, three additions
61
+ const patch = `diff --git a/x.ts b/x.ts
62
+ --- a/x.ts
63
+ +++ b/x.ts
64
+ @@ -1,2 +1,4 @@
65
+ keep;
66
+ -old;
67
+ +new1;
68
+ +new2;
69
+ +new3;
70
+ `;
71
+
72
+ // Act
73
+ const changed = pairs(splitDiffRows(diffRows(patch))).filter(
74
+ (row) => row.left?.kind === "del" || row.right?.kind === "add",
75
+ );
76
+
77
+ // Assert - three rows tall, only the first has a left deletion
78
+ expect(changed).toHaveLength(3);
79
+ expect(changed[0]?.left?.kind).toBe("del");
80
+ expect(changed[1]?.left).toBeUndefined();
81
+ expect(changed[2]?.left).toBeUndefined();
82
+ expect(changed.map((row) => row.right?.text.trim())).toEqual(["new1;", "new2;", "new3;"]);
83
+ });
84
+
85
+ test("each side keeps its originating unified row for anchoring", () => {
86
+ // Act
87
+ const base = diffRows(PATCH);
88
+ const changed = pairs(splitDiffRows(base)).find((row) => row.right?.kind === "add")!;
89
+
90
+ // Assert - the split line points back at a row from the base list
91
+ expect(base[changed.right!.rowIndex]).toBe(changed.right!.row);
92
+ });
93
+ });
94
+
95
+ describe("splitRowOffsets", () => {
96
+ test("maps both sides of a pair to the same y and steps a file band by three", () => {
97
+ // Arrange
98
+ const base = diffRows(PATCH);
99
+ const split = splitDiffRows(base);
100
+
101
+ // Act
102
+ const offsets = splitRowOffsets(split);
103
+
104
+ // Assert - the file band is three rows and the hunk one, so the first context pair sits at y 4
105
+ const firstContext = split.find((row) => row.kind === "pair" && row.left?.kind === "ctx")!;
106
+
107
+ expect(offsets.get(firstContext.left!.rowIndex)).toBe(4);
108
+ expect(offsets.get(firstContext.right!.rowIndex)).toBe(4);
109
+ });
110
+ });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Side-by-side diff projection: fold the unified DiffRow list into paired rows,
3
+ * old on the left and new on the right. A context line sits on both sides; a
4
+ * change block zips its deletions against its additions to the taller side, with
5
+ * a blank filler where one side runs out. Each side keeps its originating unified
6
+ * row, so the cursor, anchors, and curation keep working through the base rows.
7
+ */
8
+
9
+ import type { DiffRow } from "./view-diff";
10
+
11
+ /** One code line on one side of the split, carrying its originating unified row. */
12
+ export interface SplitLine {
13
+ row: DiffRow;
14
+ rowIndex: number;
15
+ kind: "ctx" | "del" | "add";
16
+ text: string;
17
+ lineNumber?: number;
18
+ }
19
+
20
+ /** A rendered split row: a file band, a hunk header, or an old|new line pair. */
21
+ export interface SplitRow {
22
+ kind: "file" | "hunk" | "pair";
23
+ file: string;
24
+ text?: string;
25
+ rowIndex?: number;
26
+ left?: SplitLine;
27
+ right?: SplitLine;
28
+ }
29
+
30
+ /** A base row paired with its index in the base list. */
31
+ interface IndexedRow {
32
+ row: DiffRow;
33
+ index: number;
34
+ }
35
+
36
+ function contextLine({ row, index }: IndexedRow, side: "left" | "right"): SplitLine {
37
+ return {
38
+ row,
39
+ rowIndex: index,
40
+ kind: "ctx",
41
+ text: row.text,
42
+ lineNumber: side === "left" ? row.oldLine : row.newLine,
43
+ };
44
+ }
45
+
46
+ function deletionLine({ row, index }: IndexedRow): SplitLine {
47
+ return { row, rowIndex: index, kind: "del", text: row.text, lineNumber: row.oldLine };
48
+ }
49
+
50
+ function additionLine({ row, index }: IndexedRow): SplitLine {
51
+ return { row, rowIndex: index, kind: "add", text: row.text, lineNumber: row.newLine };
52
+ }
53
+
54
+ /** Zip a change block's deletions against its additions, blank-filling the shorter side. */
55
+ function zipChangeBlock(
56
+ deletions: IndexedRow[],
57
+ additions: IndexedRow[],
58
+ file: string,
59
+ ): SplitRow[] {
60
+ const pairs: SplitRow[] = [];
61
+ const height = Math.max(deletions.length, additions.length);
62
+
63
+ for (let index = 0; index < height; index++) {
64
+ const del = deletions[index];
65
+ const add = additions[index];
66
+
67
+ pairs.push({
68
+ kind: "pair",
69
+ file,
70
+ left: del ? deletionLine(del) : undefined,
71
+ right: add ? additionLine(add) : undefined,
72
+ });
73
+ }
74
+
75
+ return pairs;
76
+ }
77
+
78
+ /** Project unified diff rows into side-by-side split rows. */
79
+ export function splitDiffRows(rows: DiffRow[]): SplitRow[] {
80
+ const split: SplitRow[] = [];
81
+ let pendingDeletions: IndexedRow[] = [];
82
+ let pendingAdditions: IndexedRow[] = [];
83
+ let blockFile = "";
84
+
85
+ const flushChangeBlock = (): void => {
86
+ if (pendingDeletions.length === 0 && pendingAdditions.length === 0) return;
87
+ split.push(...zipChangeBlock(pendingDeletions, pendingAdditions, blockFile));
88
+ pendingDeletions = [];
89
+ pendingAdditions = [];
90
+ };
91
+
92
+ rows.forEach((row, index) => {
93
+ if (row.kind === "add") {
94
+ blockFile = row.file;
95
+ pendingAdditions.push({ row, index });
96
+
97
+ return;
98
+ }
99
+ if (row.kind === "del") {
100
+ blockFile = row.file;
101
+ pendingDeletions.push({ row, index });
102
+
103
+ return;
104
+ }
105
+ // any non-change row closes the current change block before it renders
106
+ flushChangeBlock();
107
+ if (row.kind === "file" || row.kind === "hunk") {
108
+ split.push({ kind: row.kind, file: row.file, text: row.text, rowIndex: index });
109
+ } else
110
+ split.push({
111
+ kind: "pair",
112
+ file: row.file,
113
+ left: contextLine({ row, index }, "left"),
114
+ right: contextLine({ row, index }, "right"),
115
+ });
116
+ });
117
+ flushChangeBlock();
118
+
119
+ return split;
120
+ }
121
+
122
+ /**
123
+ * Visual y of each base row index in the split layout, so the cursor scrolls into
124
+ * view. A file band spans three rows (rule, name, rule), a hunk one, a pair one; a
125
+ * pair's two sides share the row, so both base indices map to the same y.
126
+ */
127
+ export function splitRowOffsets(splitRows: SplitRow[]): Map<number, number> {
128
+ const offsets = new Map<number, number>();
129
+ let contentY = 0;
130
+
131
+ for (const row of splitRows) {
132
+ if (row.kind === "file") {
133
+ contentY += 3;
134
+ } else if (row.kind === "hunk") {
135
+ contentY += 1;
136
+ } else {
137
+ if (row.left) offsets.set(row.left.rowIndex, contentY);
138
+ if (row.right) offsets.set(row.right.rowIndex, contentY);
139
+ contentY += 1;
140
+ }
141
+ }
142
+
143
+ return offsets;
144
+ }