@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101

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 (356) 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 +42 -34
  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 +369 -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 +1243 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  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 +20 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1667 -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.agent.test.tsx +50 -0
  119. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  120. package/src/integrations/components/ShareDialog.tsx +574 -0
  121. package/src/integrations/components/share-dialog-store.ts +59 -0
  122. package/src/integrations/editor.test.ts +149 -0
  123. package/src/integrations/editor.ts +160 -0
  124. package/src/integrations/github-identity.test.ts +58 -0
  125. package/src/integrations/github-identity.ts +41 -0
  126. package/src/integrations/herdr.test.ts +43 -0
  127. package/src/integrations/herdr.ts +38 -0
  128. package/src/integrations/integrations.test.ts +41 -0
  129. package/src/integrations/integrations.ts +32 -0
  130. package/src/integrations/pasted-image.test.ts +67 -0
  131. package/src/integrations/pasted-image.ts +60 -0
  132. package/src/integrations/share.ts +238 -0
  133. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  134. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  135. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  136. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  137. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  138. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  139. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  140. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  141. package/src/keyboard/components/NavModeHint.tsx +31 -0
  142. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  143. package/src/keyboard/components/SlashComposer.tsx +87 -0
  144. package/src/keyboard/key-bindings.test.ts +156 -0
  145. package/src/keyboard/key-bindings.ts +459 -0
  146. package/src/keyboard/keymap.test.ts +611 -0
  147. package/src/keyboard/keymap.ts +395 -0
  148. package/src/keyboard/skills.test.ts +74 -0
  149. package/src/keyboard/skills.ts +75 -0
  150. package/src/keyboard/slash-palette.test.ts +103 -0
  151. package/src/keyboard/slash-palette.ts +129 -0
  152. package/src/keyboard/thread-chords.test.ts +79 -0
  153. package/src/keyboard/thread-chords.ts +163 -0
  154. package/src/keyboard/use-shared-keyboard.ts +55 -0
  155. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  156. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  157. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  158. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  159. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  160. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  161. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  162. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  163. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  164. package/src/markdown/components/ThreadView.tsx +560 -0
  165. package/src/markdown/mark-runs.ts +121 -0
  166. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  167. package/src/markdown/markdown-grid.test.ts +101 -0
  168. package/src/markdown/markdown-grid.ts +203 -0
  169. package/src/markdown/markdown-highlight.test.ts +61 -0
  170. package/src/markdown/markdown-highlight.ts +124 -0
  171. package/src/markdown/thread-code-syntax.ts +45 -0
  172. package/src/markdown/use-thread-body-editing.tsx +54 -0
  173. package/src/markdown/view-plan.regression.test.ts +60 -0
  174. package/src/markdown/view-plan.test.ts +545 -0
  175. package/src/markdown/view-plan.ts +1009 -0
  176. package/src/performance/otlp-trace.test.ts +119 -0
  177. package/src/performance/otlp-trace.ts +134 -0
  178. package/src/performance/perf-report.ts +18 -0
  179. package/src/performance/perf-timings.test.ts +18 -0
  180. package/src/performance/perf-timings.ts +39 -0
  181. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  182. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  183. package/src/prototype/prototype-browser.test.ts +81 -0
  184. package/src/prototype/prototype-browser.ts +321 -0
  185. package/src/prototype/prototype-pixels.tsx +35 -0
  186. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  187. package/src/settings/components/SettingsDialog.tsx +183 -0
  188. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  189. package/src/settings/components/SettingsRows.tsx +112 -0
  190. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  192. package/src/settings/components/quick-actions-editor.tsx +222 -0
  193. package/src/settings/config-identity.test.ts +78 -0
  194. package/src/settings/config.test.ts +676 -0
  195. package/src/settings/config.ts +628 -0
  196. package/src/settings/use-settings-dialog.tsx +351 -0
  197. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  198. package/src/stories/stories-app.tsx +196 -0
  199. package/src/stories/stories.test.tsx +74 -0
  200. package/src/stories/story-fixtures.ts +102 -0
  201. package/src/stories/story-tree.test.ts +108 -0
  202. package/src/stories/story.ts +154 -0
  203. package/src/terminal/ansi-screen.test.ts +28 -0
  204. package/src/terminal/ansi-screen.ts +72 -0
  205. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  206. package/src/terminal/components/AnsiScreen.tsx +28 -0
  207. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  208. package/src/terminal/components/terminal-pane.ts +196 -0
  209. package/src/terminal/ghostty-terminal.test.ts +85 -0
  210. package/src/terminal/ghostty-terminal.ts +197 -0
  211. package/src/terminal/kitty-image.test.ts +66 -0
  212. package/src/terminal/kitty-image.ts +137 -0
  213. package/src/terminal/pty.test.ts +81 -0
  214. package/src/terminal/pty.ts +246 -0
  215. package/src/testing/test-support.test.ts +264 -0
  216. package/src/testing/test-support.ts +367 -0
  217. package/src/thread/agent-continuation.test.ts +32 -0
  218. package/src/thread/agent-continuation.ts +13 -0
  219. package/src/thread/agent-transcript.test.ts +161 -0
  220. package/src/thread/agent-transcript.ts +336 -0
  221. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  222. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  223. package/src/thread/components/AgentConfigControls.tsx +114 -0
  224. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  225. package/src/thread/components/AgentThreadPane.test.tsx +935 -0
  226. package/src/thread/components/AgentThreadPane.tsx +377 -0
  227. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  228. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  229. package/src/thread/components/ThreadFooter.tsx +77 -0
  230. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  231. package/src/thread/components/ThreadTree.test.tsx +41 -0
  232. package/src/thread/components/ThreadTree.tsx +267 -0
  233. package/src/thread/components/thread-tree.test.ts +137 -0
  234. package/src/thread/components/thread-tree.ts +112 -0
  235. package/src/thread/thread-controller.curate.test.ts +502 -0
  236. package/src/thread/thread-controller.derivation.test.ts +62 -0
  237. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  238. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  239. package/src/thread/thread-controller.share.test.ts +573 -0
  240. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  241. package/src/thread/thread-controller.tree.test.ts +263 -0
  242. package/src/thread/thread-controller.ts +2026 -0
  243. package/src/thread/tree-view.test.ts +128 -0
  244. package/src/thread/tree-view.ts +146 -0
  245. package/src/thread/use-thread-agent.test.tsx +208 -0
  246. package/src/thread/use-thread-agent.ts +117 -0
  247. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  248. package/src/ui/components/CodeBlock.tsx +80 -0
  249. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  250. package/src/ui/components/ConfirmCard.tsx +151 -0
  251. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  252. package/src/ui/components/ConfirmDialog.tsx +72 -0
  253. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  254. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  255. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  256. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  257. package/src/ui/components/PromptDialog.test.tsx +65 -0
  258. package/src/ui/components/PromptDialog.tsx +99 -0
  259. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  260. package/src/ui/components/RootOverlay.tsx +49 -0
  261. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  262. package/src/ui/components/ScrollArea.tsx +61 -0
  263. package/src/ui/components/Toast.stories.tsx +19 -0
  264. package/src/ui/components/Toast.tsx +54 -0
  265. package/src/ui/components/Tooltip.stories.tsx +26 -0
  266. package/src/ui/components/Tooltip.tsx +72 -0
  267. package/src/ui/components/menu-control.stories.tsx +35 -0
  268. package/src/ui/components/menu-control.tsx +43 -0
  269. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  270. package/src/ui/components/primitives/Button.tsx +57 -0
  271. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  272. package/src/ui/components/primitives/Card.tsx +62 -0
  273. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  274. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  275. package/src/ui/components/primitives/Dialog.tsx +69 -0
  276. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  277. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  278. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  279. package/src/ui/components/primitives/IconButton.tsx +58 -0
  280. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  281. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  282. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  283. package/src/ui/components/primitives/Switch.tsx +32 -0
  284. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  285. package/src/ui/components/primitives/Tabs.tsx +101 -0
  286. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  288. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  289. package/src/ui/components/primitives/Tree.tsx +133 -0
  290. package/src/ui/components/primitives/frame.test.ts +12 -0
  291. package/src/ui/components/primitives/frame.ts +17 -0
  292. package/src/ui/components/primitives/icons.ts +61 -0
  293. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  294. package/src/ui/components/primitives/tree-model.ts +137 -0
  295. package/src/ui/components/syntax-highlight.ts +133 -0
  296. package/src/ui/components/truncate-text.ts +7 -0
  297. package/src/ui/components/truncate-title.test.ts +31 -0
  298. package/src/ui/components/truncate-title.ts +15 -0
  299. package/src/ui/use-frame-measure.ts +74 -0
  300. package/src/ui/use-terminal-virtualizer.ts +259 -0
  301. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  302. package/src/workbench/components/ChangesColumn.tsx +203 -0
  303. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  304. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  305. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  306. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  307. package/src/workbench/components/EditorGrid.tsx +544 -0
  308. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  309. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  310. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  311. package/src/workbench/components/FileContentsView.tsx +100 -0
  312. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  313. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  314. package/src/workbench/components/GridTabContent.tsx +193 -0
  315. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  316. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  317. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  318. package/src/workbench/components/TreePane.stories.tsx +106 -0
  319. package/src/workbench/components/TreePane.tsx +155 -0
  320. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  321. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  322. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  323. package/src/workbench/components/editor-grid.test.ts +184 -0
  324. package/src/workbench/components/editor-grid.ts +281 -0
  325. package/src/workbench/components/file-tree.test.ts +60 -0
  326. package/src/workbench/components/file-tree.ts +112 -0
  327. package/src/workbench/use-changes-workbench.ts +205 -0
  328. package/src/workbench/use-remember-layout.ts +31 -0
  329. package/src/workbench/use-repo-changes.ts +91 -0
  330. package/src/workbench/workbench-snapshot.test.ts +99 -0
  331. package/src/workbench/workbench-snapshot.ts +59 -0
  332. package/src/App.diff.test.tsx +0 -99
  333. package/src/App.export.test.tsx +0 -88
  334. package/src/App.plan-review.test.tsx +0 -259
  335. package/src/App.readonly.test.tsx +0 -148
  336. package/src/App.rendering.test.tsx +0 -108
  337. package/src/App.submit-confirm.test.tsx +0 -183
  338. package/src/App.test.tsx +0 -218
  339. package/src/App.tsx +0 -1249
  340. package/src/config.test.ts +0 -116
  341. package/src/config.ts +0 -150
  342. package/src/editor.ts +0 -34
  343. package/src/herdr.test.ts +0 -68
  344. package/src/herdr.ts +0 -34
  345. package/src/keymap.test.ts +0 -270
  346. package/src/keymap.ts +0 -173
  347. package/src/run.ts +0 -30
  348. package/src/serve.ts +0 -75
  349. package/src/session-controller.ts +0 -398
  350. package/src/syntax.test.ts +0 -41
  351. package/src/syntax.ts +0 -88
  352. package/src/theme.ts +0 -46
  353. package/src/view-diff.test.ts +0 -55
  354. package/src/view-diff.ts +0 -86
  355. package/src/view.test.ts +0 -157
  356. package/src/view.ts +0 -364
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Diff artifact projection: flatten @pierre/diffs' parsed
3
+ * patch model into render rows. Line-anchored annotations use the same
4
+ * quote-primary anchors as plans: quote = the line content, prefix/suffix =
5
+ * the neighbor lines - so the whole anchor/feedback pipeline is shared.
6
+ */
7
+
8
+ import { parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs";
9
+ import {
10
+ annotationTarget,
11
+ isAddressed,
12
+ resolveAnchor,
13
+ type Annotation,
14
+ type Block,
15
+ type Thread,
16
+ } from "@cueloop/schema";
17
+ import { spanRangeInBlock, type TextSpan } from "../annotations/thread-selection";
18
+ import type { Mark } from "../markdown/view-plan";
19
+ import { discussionsFrom } from "../annotations/discussions";
20
+
21
+ // the diff sheet reads marks over rows as a diff concept; re-export so it need not reach into view-plan
22
+ export type { Mark } from "../markdown/view-plan";
23
+
24
+ export type DiffRowKind = "file" | "hunk" | "ctx" | "add" | "del";
25
+
26
+ export interface DiffRow {
27
+ kind: DiffRowKind;
28
+ text: string;
29
+ file: string;
30
+ oldLine?: number;
31
+ newLine?: number;
32
+ }
33
+
34
+ function isCodeRow(row: DiffRow | undefined): row is DiffRow {
35
+ return row !== undefined && row.kind !== "file" && row.kind !== "hunk";
36
+ }
37
+
38
+ export function diffRows(patchText: string): DiffRow[] {
39
+ const rows: DiffRow[] = [];
40
+ const patches = parsePatchFiles(patchText);
41
+
42
+ for (const patch of patches) {
43
+ for (const file of patch.files) {
44
+ rows.push({ kind: "file", text: fileLabel(file), file: file.name });
45
+ for (const hunk of file.hunks) {
46
+ rows.push({ kind: "hunk", text: hunk.hunkSpecs ?? "@@", file: file.name });
47
+ let oldLine = hunk.deletionStart;
48
+ let newLine = hunk.additionStart;
49
+
50
+ for (const segment of hunk.hunkContent) {
51
+ if (segment.type === "context") {
52
+ for (let lineOffset = 0; lineOffset < segment.lines; lineOffset++) {
53
+ rows.push({
54
+ kind: "ctx",
55
+ text: file.additionLines[segment.additionLineIndex + lineOffset] ?? "",
56
+ file: file.name,
57
+ oldLine: oldLine++,
58
+ newLine: newLine++,
59
+ });
60
+ }
61
+ } else {
62
+ for (let lineOffset = 0; lineOffset < segment.deletions; lineOffset++) {
63
+ rows.push({
64
+ kind: "del",
65
+ text: file.deletionLines[segment.deletionLineIndex + lineOffset] ?? "",
66
+ file: file.name,
67
+ oldLine: oldLine++,
68
+ });
69
+ }
70
+ for (let lineOffset = 0; lineOffset < segment.additions; lineOffset++) {
71
+ rows.push({
72
+ kind: "add",
73
+ text: file.additionLines[segment.additionLineIndex + lineOffset] ?? "",
74
+ file: file.name,
75
+ newLine: newLine++,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ return rows;
85
+ }
86
+
87
+ /** Added and removed line counts per file path, for the file band's badge; base rows only. */
88
+ export function fileChangeCounts(
89
+ rows: DiffRow[],
90
+ ): Map<string, { additions: number; deletions: number }> {
91
+ const counts = new Map<string, { additions: number; deletions: number }>();
92
+
93
+ for (const row of rows) {
94
+ if (row.kind !== "add" && row.kind !== "del") continue;
95
+ const entry = counts.get(row.file) ?? { additions: 0, deletions: 0 };
96
+
97
+ if (row.kind === "add") entry.additions += 1;
98
+ else entry.deletions += 1;
99
+ counts.set(row.file, entry);
100
+ }
101
+
102
+ return counts;
103
+ }
104
+
105
+ function fileLabel(file: FileDiffMetadata): string {
106
+ return file.prevName && file.prevName !== file.name
107
+ ? `${file.prevName} → ${file.name}`
108
+ : file.name;
109
+ }
110
+
111
+ /** Row text carries the patch's trailing newline; anchors and rendering strip it. */
112
+ export function diffRowText(row: DiffRow): string {
113
+ return row.text.replace(/\n$/, "");
114
+ }
115
+
116
+ /**
117
+ * The diff rows as anchor blocks, one per row in row order, so a comment span anchors
118
+ * with the same quote/context/position selectors a plan block does. Header rows are
119
+ * blocks too (indices stay 1:1 with the rows) but hold no comment.
120
+ */
121
+ export function diffRowBlocks(rows: DiffRow[]): Block[] {
122
+ return rows.map((row, index) => ({
123
+ kind: "code",
124
+ text: row.kind === "file" || row.kind === "hunk" ? "" : diffRowText(row),
125
+ lineStart: index,
126
+ lineEnd: index,
127
+ }));
128
+ }
129
+
130
+ /**
131
+ * Resolve annotations against the diff rows and group marks per row index, with
132
+ * character ranges and the whole span, so the diff sheet paints and threads them
133
+ * exactly as the plan view does its blocks.
134
+ */
135
+ export function marksByRows(
136
+ annotations: Annotation[],
137
+ rows: DiffRow[],
138
+ focusedId?: string,
139
+ ): Map<number, Mark[]> {
140
+ const blocks = diffRowBlocks(rows);
141
+ const marksByIndex = new Map<number, Mark[]>();
142
+
143
+ for (const annotation of annotations) {
144
+ // an addressed annotation keeps its record but paints no highlight
145
+ if (isAddressed(annotation)) continue;
146
+ const resolved = resolveAnchor(annotation.anchor, blocks);
147
+
148
+ if (!resolved) continue;
149
+ const span: TextSpan = {
150
+ start: { blockIndex: resolved.blockIndex, char: resolved.start },
151
+ end: { blockIndex: resolved.endBlockIndex, char: resolved.end },
152
+ };
153
+
154
+ // a span paints on every row it covers; the mark carries the whole span
155
+ for (let rowIndex = resolved.blockIndex; rowIndex <= resolved.endBlockIndex; rowIndex++) {
156
+ const range = spanRangeInBlock(span, rowIndex, blocks[rowIndex]!.text.length);
157
+
158
+ if (!range) continue;
159
+ const marks = marksByIndex.get(rowIndex) ?? [];
160
+
161
+ marks.push({
162
+ start: range.start,
163
+ end: range.end,
164
+ role: annotation.id === focusedId ? "mark-focus" : "mark-comment",
165
+ annotationId: annotation.id,
166
+ span,
167
+ });
168
+ marksByIndex.set(rowIndex, marks);
169
+ }
170
+ }
171
+
172
+ return marksByIndex;
173
+ }
174
+
175
+ function addResolvedMark(
176
+ marksByIndex: Map<number, Mark[]>,
177
+ annotation: Annotation,
178
+ blocks: Block[],
179
+ startBlockIndex: number,
180
+ endBlockIndex: number,
181
+ focusedId?: string,
182
+ ): void {
183
+ const startAnchor = annotation.reviewComment?.startAnchor ?? annotation.anchor;
184
+ const span: TextSpan = {
185
+ start: {
186
+ blockIndex: startBlockIndex,
187
+ char: Math.min(startAnchor.start ?? 0, blocks[startBlockIndex]!.text.length),
188
+ },
189
+ end: {
190
+ blockIndex: endBlockIndex,
191
+ char: Math.min(
192
+ annotation.anchor.end ?? annotation.anchor.quote.length,
193
+ blocks[endBlockIndex]!.text.length,
194
+ ),
195
+ },
196
+ };
197
+
198
+ for (let rowIndex = startBlockIndex; rowIndex <= endBlockIndex; rowIndex++) {
199
+ const range = spanRangeInBlock(span, rowIndex, blocks[rowIndex]!.text.length);
200
+
201
+ const marksEmptyLine =
202
+ range === null &&
203
+ startBlockIndex === endBlockIndex &&
204
+ rowIndex === startBlockIndex &&
205
+ blocks[rowIndex]!.text.length === 0;
206
+
207
+ if (!range && !marksEmptyLine) continue;
208
+ const marks = marksByIndex.get(rowIndex) ?? [];
209
+
210
+ marks.push({
211
+ start: range?.start ?? 0,
212
+ end: range?.end ?? 0,
213
+ role: annotation.id === focusedId ? "mark-focus" : "mark-comment",
214
+ annotationId: annotation.id,
215
+ span,
216
+ });
217
+ marksByIndex.set(rowIndex, marks);
218
+ }
219
+ }
220
+
221
+ function addReviewFindingMark(
222
+ marksByIndex: Map<number, Mark[]>,
223
+ annotation: Annotation,
224
+ rows: DiffRow[],
225
+ blocks: Block[],
226
+ focusedId?: string,
227
+ ): boolean {
228
+ const finding = annotation.reviewComment;
229
+
230
+ if (!finding || isAddressed(annotation)) return false;
231
+ const end = resolveReviewAnchorRow(rows, annotation.anchor, finding.path, finding.side);
232
+ const start = finding.startAnchor
233
+ ? resolveReviewAnchorRow(rows, finding.startAnchor, finding.path, finding.side)
234
+ : end;
235
+
236
+ if (!start || !end || start.rowIndex > end.rowIndex) return false;
237
+ addResolvedMark(marksByIndex, annotation, blocks, start.rowIndex, end.rowIndex, focusedId);
238
+
239
+ return true;
240
+ }
241
+
242
+ /**
243
+ * A whole file rendered as context rows, so the diff sheet can show and annotate a plain file the
244
+ * same way it does a diff: every line is unchanged, numbered on both sides (the file view collapses
245
+ * that to one number). A trailing newline does not add an empty final row.
246
+ */
247
+ export function fileContentsRows(path: string, contents: string): DiffRow[] {
248
+ const lines = contents.replace(/\n$/, "").split("\n");
249
+
250
+ return lines.map((text, index) => ({
251
+ kind: "ctx",
252
+ text,
253
+ file: path,
254
+ oldLine: index + 1,
255
+ newLine: index + 1,
256
+ }));
257
+ }
258
+
259
+ /** The contiguous row range a file occupies in the aggregate diff, or null when it is not shown. */
260
+ export function fileRowRange(rows: DiffRow[], path: string): { start: number; end: number } | null {
261
+ const start = rows.findIndex((row) => row.file === path);
262
+
263
+ if (start === -1) return null;
264
+ let end = start;
265
+
266
+ while (end < rows.length && rows[end]!.file === path) end++;
267
+
268
+ return { start, end };
269
+ }
270
+
271
+ /**
272
+ * Marks for file-target notes on the aggregate working-tree diff. Each note resolves against only
273
+ * its own file's rows, then the row index and span shift back to aggregate coordinates - so a quote
274
+ * never attaches to the same text in another file or on the opposite diff side.
275
+ */
276
+ export function fileTargetMarks(
277
+ annotations: Annotation[],
278
+ rows: DiffRow[],
279
+ focusedId?: string,
280
+ ): Map<number, Mark[]> {
281
+ const byFile = new Map<string, Annotation[]>();
282
+
283
+ for (const annotation of annotations) {
284
+ const target = annotationTarget(annotation);
285
+
286
+ if (target.kind !== "file") continue;
287
+ const forPath = byFile.get(target.path) ?? [];
288
+
289
+ forPath.push(annotation);
290
+ byFile.set(target.path, forPath);
291
+ }
292
+ const result = new Map<number, Mark[]>();
293
+
294
+ for (const [path, fileAnnotations] of byFile) {
295
+ const range = fileRowRange(rows, path);
296
+
297
+ if (!range) continue;
298
+ const base = range.start;
299
+ const fileRows = rows.slice(range.start, range.end);
300
+ const reviewRootIds = new Set(
301
+ fileAnnotations.flatMap((annotation) =>
302
+ annotation.reviewComment === undefined ? [] : [annotation.id],
303
+ ),
304
+ );
305
+ const ordinaryAnnotations = fileAnnotations.filter(
306
+ (annotation) =>
307
+ annotation.reviewComment === undefined &&
308
+ (annotation.replyTo === undefined || !reviewRootIds.has(annotation.replyTo)),
309
+ );
310
+ const fileMarks = marksByRows(ordinaryAnnotations, fileRows, focusedId);
311
+ const blocks = diffRowBlocks(fileRows);
312
+ const resolvedIds = new Set(
313
+ [...fileMarks.values()]
314
+ .flat()
315
+ .flatMap((mark) => (mark.annotationId === undefined ? [] : [mark.annotationId])),
316
+ );
317
+
318
+ for (const annotation of fileAnnotations) {
319
+ const finding = annotation.reviewComment;
320
+
321
+ if (!finding || isAddressed(annotation)) continue;
322
+ if (addReviewFindingMark(fileMarks, annotation, fileRows, blocks, focusedId)) {
323
+ resolvedIds.add(annotation.id);
324
+ continue;
325
+ }
326
+ const relativeRow = fileRows.findIndex(
327
+ (row) =>
328
+ (finding.side === "RIGHT" ? row.newLine : row.oldLine) === finding.line &&
329
+ row.kind !== "file" &&
330
+ row.kind !== "hunk",
331
+ );
332
+ const fallbackRow = relativeRow === -1 ? 0 : relativeRow;
333
+ const mark: Mark = {
334
+ start: 0,
335
+ end: 0,
336
+ role: annotation.id === focusedId ? "mark-focus" : "mark-comment",
337
+ annotationId: annotation.id,
338
+ outdated: true,
339
+ span: {
340
+ start: { blockIndex: fallbackRow, char: 0 },
341
+ end: { blockIndex: fallbackRow, char: 0 },
342
+ },
343
+ };
344
+
345
+ fileMarks.set(fallbackRow, [...(fileMarks.get(fallbackRow) ?? []), mark]);
346
+ resolvedIds.add(annotation.id);
347
+ }
348
+
349
+ for (const annotation of fileAnnotations) {
350
+ if (!annotation.replyTo || resolvedIds.has(annotation.id) || isAddressed(annotation))
351
+ continue;
352
+ const parent = [...fileMarks.entries()].find(([, marks]) =>
353
+ marks.some((mark) => mark.annotationId === annotation.replyTo),
354
+ );
355
+
356
+ if (!parent) continue;
357
+ const [fallbackRow, parentMarks] = parent;
358
+ const parentMark = parentMarks.find((mark) => mark.annotationId === annotation.replyTo)!;
359
+ const replyMark: Mark = {
360
+ ...parentMark,
361
+ role: annotation.id === focusedId ? "mark-focus" : "mark-comment",
362
+ annotationId: annotation.id,
363
+ outdated: parentMark.outdated,
364
+ };
365
+
366
+ fileMarks.set(fallbackRow, [...parentMarks, replyMark]);
367
+ resolvedIds.add(annotation.id);
368
+ }
369
+
370
+ for (const [relativeRow, marks] of fileMarks) {
371
+ result.set(
372
+ relativeRow + base,
373
+ marks.map((mark) =>
374
+ mark.span
375
+ ? {
376
+ ...mark,
377
+ span: {
378
+ start: { ...mark.span.start, blockIndex: mark.span.start.blockIndex + base },
379
+ end: { ...mark.span.end, blockIndex: mark.span.end.blockIndex + base },
380
+ },
381
+ }
382
+ : mark,
383
+ ),
384
+ );
385
+ }
386
+ }
387
+
388
+ return result;
389
+ }
390
+
391
+ /** A plain diff review's notes anchor to the artifact itself; a working-tree diff's notes target files. */
392
+ export function annotatesArtifact(session: Thread | null): boolean {
393
+ return session?.artifact.type === "diff" && session.artifact.meta.workbench !== true;
394
+ }
395
+
396
+ /**
397
+ * Render the diff from the pinned artifact rather than the live working tree: a plain diff review (the
398
+ * artifact is the diff), or a shared/served snapshot of a workbench thread (the remote has no tree).
399
+ */
400
+ export function readsFrozenDiff(session: Thread | null): boolean {
401
+ return (
402
+ session?.artifact.type === "diff" &&
403
+ (session.artifact.meta.workbench !== true || session.artifact.meta.snapshot === true)
404
+ );
405
+ }
406
+
407
+ /**
408
+ * The marks a Changes surface paints. A plain diff review is the artifact itself, so only its
409
+ * artifact-anchored notes belong; a workbench thread (live or a frozen snapshot) and every other thread
410
+ * show a working-tree diff, whose notes carry a file target and resolve per file - so a refresh never
411
+ * rebinds one across files, and a snapshot still surfaces the feedback already on the thread.
412
+ */
413
+ export function changesMarks(
414
+ session: Thread,
415
+ rows: DiffRow[],
416
+ focusedId?: string,
417
+ ): Map<number, Mark[]> {
418
+ if (annotatesArtifact(session)) {
419
+ const artifactNotes = session.annotations.filter(
420
+ (annotation) => annotationTarget(annotation).kind === "artifact",
421
+ );
422
+ const marks = marksByRows(artifactNotes, rows, focusedId);
423
+
424
+ // Agent PR findings carry a file target so repeated text cannot bind across files.
425
+ // Plain diff comments still use the artifact target for backward compatibility.
426
+ if (session.artifact.meta.pr !== undefined) {
427
+ for (const [row, fileMarks] of fileTargetMarks(session.annotations, rows, focusedId)) {
428
+ marks.set(row, [...(marks.get(row) ?? []), ...fileMarks]);
429
+ }
430
+ }
431
+
432
+ return marks;
433
+ }
434
+
435
+ return fileTargetMarks(session.annotations, rows, focusedId);
436
+ }
437
+
438
+ /**
439
+ * Comments per file path across the diff: each discussion counts its comments (root plus
440
+ * replies) toward the file its span ends in. Feeds the changed-files tree and tab badges, so a
441
+ * file whose tab is closed still shows it carries feedback.
442
+ */
443
+ export function commentCountsByFile(session: Thread, rows: DiffRow[]): Map<string, number> {
444
+ const discussions = discussionsFrom(session, marksByRows(session.annotations, rows));
445
+ const counts = new Map<string, number>();
446
+
447
+ for (const discussion of discussions) {
448
+ const file = rows[discussion.blockIndex]?.file;
449
+
450
+ if (file === undefined) continue;
451
+ counts.set(file, (counts.get(file) ?? 0) + discussion.annotations.length);
452
+ }
453
+
454
+ return counts;
455
+ }
456
+
457
+ /** Quote-primary anchor for a diff row: neighbors as context selectors. */
458
+ export function diffRowAnchor(rows: DiffRow[], rowIndex: number) {
459
+ const row = rows[rowIndex]!;
460
+ const context = diffRowAnchorContext(rows, rowIndex);
461
+
462
+ return {
463
+ quote: diffRowText(row),
464
+ ...context,
465
+ blockIndex: rowIndex,
466
+ endBlockIndex: rowIndex,
467
+ start: 0,
468
+ end: diffRowText(row).length,
469
+ };
470
+ }
471
+
472
+ function diffRowAnchorContext(
473
+ rows: DiffRow[],
474
+ rowIndex: number,
475
+ ): Pick<Annotation["anchor"], "prefix" | "suffix"> {
476
+ const prev = rows[rowIndex - 1];
477
+ const next = rows[rowIndex + 1];
478
+
479
+ return {
480
+ prefix: isCodeRow(prev) ? diffRowText(prev).slice(-24) : "",
481
+ suffix: isCodeRow(next) ? diffRowText(next).slice(0, 24) : "",
482
+ };
483
+ }
484
+
485
+ /** Resolve a publishable PR finding exactly; fuzzy matches must remain visibly outdated. */
486
+ export function resolveReviewAnchorRow(
487
+ rows: DiffRow[],
488
+ anchor: Annotation["anchor"],
489
+ path: string,
490
+ side: "LEFT" | "RIGHT",
491
+ ): { row: DiffRow; rowIndex: number } | undefined {
492
+ const candidates = rows.flatMap((row, rowIndex) => {
493
+ if (
494
+ row.file !== path ||
495
+ diffRowText(row) !== anchor.quote ||
496
+ (side === "RIGHT" ? row.newLine === undefined : row.oldLine === undefined)
497
+ )
498
+ return [];
499
+ const context = diffRowAnchorContext(rows, rowIndex);
500
+
501
+ return [
502
+ {
503
+ row,
504
+ rowIndex,
505
+ score: Number(context.prefix === anchor.prefix) + Number(context.suffix === anchor.suffix),
506
+ },
507
+ ];
508
+ });
509
+
510
+ if (candidates.length === 0) return undefined;
511
+ const bestScore = Math.max(...candidates.map((candidate) => candidate.score));
512
+ const best = candidates.filter((candidate) => candidate.score === bestScore);
513
+
514
+ return best.length === 1 ? best[0] : undefined;
515
+ }
516
+
517
+ /** Location label for the rail and feedback: file:newLine (or old for del). */
518
+ export function diffRowLocation(row: DiffRow): string {
519
+ const line = row.newLine ?? row.oldLine;
520
+
521
+ return line !== undefined ? `${row.file}:${line}` : row.file;
522
+ }
@@ -0,0 +1,121 @@
1
+ /** Pure walk projections: file steps, resume index, viewed counts, notes. */
2
+
3
+ import { describe, expect, test } from "bun:test";
4
+ import type { Annotation } from "@cueloop/schema";
5
+ import { diffRows } from "./view-diff";
6
+ import { firstUnviewedIndex, noteForFile, viewedCount, walkFiles, WALK_PREVIEW_ROWS } from "./walk";
7
+
8
+ const PATCH = `diff --git a/src/a.ts b/src/a.ts
9
+ index 111..222 100644
10
+ --- a/src/a.ts
11
+ +++ b/src/a.ts
12
+ @@ -1,3 +1,4 @@
13
+ export const a = 1;
14
+ -export const b = 2;
15
+ +export const b = 3;
16
+ +export const c = 4;
17
+ diff --git a/src/b.ts b/src/b.ts
18
+ index 333..444 100644
19
+ --- a/src/b.ts
20
+ +++ b/src/b.ts
21
+ @@ -1,2 +1,2 @@
22
+ // header
23
+ -old line
24
+ +new line
25
+ diff --git a/src/c.ts b/src/c.ts
26
+ index 555..666 100644
27
+ --- a/src/c.ts
28
+ +++ b/src/c.ts
29
+ @@ -1,1 +1,8 @@
30
+ -gone
31
+ +one
32
+ +two
33
+ +three
34
+ +four
35
+ +five
36
+ +six
37
+ +seven
38
+ `;
39
+
40
+ const rows = diffRows(PATCH);
41
+ const files = walkFiles(rows);
42
+
43
+ describe("walkFiles", () => {
44
+ test("one step per changed file, in patch order, with counted stats", () => {
45
+ expect(files.map((file) => file.path)).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
46
+ expect(files[0]).toMatchObject({ added: 2, removed: 1 });
47
+ expect(files[1]).toMatchObject({ added: 1, removed: 1 });
48
+ expect(files[2]).toMatchObject({ added: 7, removed: 1 });
49
+ });
50
+
51
+ test("previews carry signed changed lines and cap at the preview budget", () => {
52
+ expect(files[0]!.preview).toEqual([
53
+ { sign: "-", text: "export const b = 2;" },
54
+ { sign: "+", text: "export const b = 3;" },
55
+ { sign: "+", text: "export const c = 4;" },
56
+ ]);
57
+ expect(files[2]!.preview.length).toBe(WALK_PREVIEW_ROWS);
58
+ expect(files[2]!.preview[0]).toEqual({ sign: "-", text: "gone" });
59
+ });
60
+ });
61
+
62
+ describe("firstUnviewedIndex", () => {
63
+ test("starts at the first file when nothing is viewed", () => {
64
+ expect(firstUnviewedIndex(files, new Set())).toBe(0);
65
+ });
66
+
67
+ test("skips viewed files - a resumed review lands on its first gap", () => {
68
+ expect(firstUnviewedIndex(files, new Set(["src/a.ts"]))).toBe(1);
69
+ expect(firstUnviewedIndex(files, new Set(["src/a.ts", "src/b.ts"]))).toBe(2);
70
+ // a viewed file later in the list does not block the earlier gap
71
+ expect(firstUnviewedIndex(files, new Set(["src/c.ts"]))).toBe(0);
72
+ });
73
+
74
+ test("all viewed resumes on the end card, not the first file", () => {
75
+ expect(firstUnviewedIndex(files, new Set(["src/a.ts", "src/b.ts", "src/c.ts"]))).toBe(
76
+ files.length,
77
+ );
78
+ });
79
+ });
80
+
81
+ describe("viewedCount", () => {
82
+ test("counts only paths present in the current file set", () => {
83
+ expect(viewedCount(files, new Set(["src/a.ts", "src/gone-in-rev-2.ts"]))).toBe(1);
84
+ });
85
+ });
86
+
87
+ describe("noteForFile", () => {
88
+ const note = (path: string, body: string): Annotation => ({
89
+ id: `a_${path}`,
90
+ kind: "note",
91
+ anchor: { quote: path, prefix: "", suffix: "" },
92
+ body,
93
+ createdAt: "now",
94
+ });
95
+
96
+ test("finds the note anchored at the file path", () => {
97
+ // Arrange
98
+ const annotations = [
99
+ note("src/a.ts", "Renames b and adds c."),
100
+ note("src/b.ts", "Swaps the line."),
101
+ ];
102
+
103
+ // Assert
104
+ expect(noteForFile(annotations, "src/b.ts")).toBe("Swaps the line.");
105
+ expect(noteForFile(annotations, "src/c.ts")).toBeUndefined();
106
+ });
107
+
108
+ test("ignores non-note annotations that happen to quote the path", () => {
109
+ // Arrange
110
+ const comment: Annotation = {
111
+ id: "a_1",
112
+ kind: "comment",
113
+ anchor: { quote: "src/a.ts", prefix: "", suffix: "" },
114
+ body: "not a note",
115
+ createdAt: "now",
116
+ };
117
+
118
+ // Assert
119
+ expect(noteForFile([comment], "src/a.ts")).toBeUndefined();
120
+ });
121
+ });