@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,502 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+ import {
3
+ applyTextCuts,
4
+ cutBlock,
5
+ parseBlocks,
6
+ restoreBlock,
7
+ SCHEMA_VERSION,
8
+ type DiffFileContents,
9
+ type HunkRejection,
10
+ type Thread,
11
+ type TextCut,
12
+ } from "@cueloop/schema";
13
+ import { curateDiff } from "@cueloop/daemon/curate";
14
+ import type { ThreadClient } from "@cueloop/daemon/client";
15
+ import { createReviewController } from "./thread-controller";
16
+
17
+ const PATCH = `diff --git a/src/store.ts b/src/store.ts
18
+ index 111..222 100644
19
+ --- a/src/store.ts
20
+ +++ b/src/store.ts
21
+ @@ -1,4 +1,4 @@
22
+ export class Store {
23
+ - private items = [];
24
+ + private items = new Map();
25
+ }
26
+ `;
27
+
28
+ const FILES: DiffFileContents[] = [
29
+ {
30
+ path: "src/store.ts",
31
+ oldContents: "export class Store {\n private items = [];\n}\n",
32
+ newContents: "export class Store {\n private items = new Map();\n}\n",
33
+ status: "modified",
34
+ },
35
+ ];
36
+
37
+ function diffSession(files?: DiffFileContents[]): Thread {
38
+ return {
39
+ schemaVersion: SCHEMA_VERSION,
40
+ id: "ses_diff",
41
+ workspace: { repoRoot: "/repo", branch: "main" },
42
+ artifact: { type: "diff", content: PATCH, meta: {}, files },
43
+ revisions: [{ revision: 1, content: PATCH, submittedAt: "2026-01-01T00:00:00.000Z" }],
44
+ annotations: [],
45
+ message: null,
46
+ status: "pending",
47
+ createdAt: "2026-01-01T00:00:00.000Z",
48
+ };
49
+ }
50
+
51
+ interface WorkingCopySink {
52
+ workingCopy?: string;
53
+ textCuts?: TextCut[];
54
+ }
55
+
56
+ const unimplemented = (member: string) => () =>
57
+ Promise.reject(new Error(`fakeClient does not implement ${member}`));
58
+
59
+ /** A fake client that records the working copy the controller writes. */
60
+ function fakeClient(initial: Thread, sink: WorkingCopySink): ThreadClient {
61
+ let session = initial;
62
+
63
+ return {
64
+ onEvent: () => () => {},
65
+ subscribe: async () => {},
66
+ sessionGet: async () => session,
67
+ sessionList: async () => [session],
68
+ sessionComment: unimplemented("sessionComment"),
69
+ sessionAnnotate: unimplemented("sessionAnnotate"),
70
+ sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
71
+ sessionSetWorkingCopy: mock(
72
+ async (_id: string, content: string | undefined, textCuts?: TextCut[]) => {
73
+ sink.workingCopy = content;
74
+ sink.textCuts = textCuts;
75
+ session = { ...session, workingCopy: content, textCuts };
76
+
77
+ return session;
78
+ },
79
+ ),
80
+ // the daemon's block primitives, stood in for with the same pure helpers it uses
81
+ sessionCutBlock: mock(async (_id: string, blockIndex: number) => {
82
+ const working = session.workingCopy ?? session.artifact.content;
83
+ const content = cutBlock(working, parseBlocks(working)[blockIndex]!);
84
+
85
+ sink.workingCopy = content;
86
+ session = { ...session, workingCopy: content };
87
+
88
+ return session;
89
+ }),
90
+ sessionRestoreBlock: mock(async (_id: string, baseBlockIndex: number, line?: number) => {
91
+ const base = session.artifact.content;
92
+ const working = session.workingCopy ?? base;
93
+ const content = restoreBlock(
94
+ base,
95
+ working,
96
+ parseBlocks(base)[baseBlockIndex]!,
97
+ line ?? working.split("\n").length,
98
+ );
99
+
100
+ sink.workingCopy = content;
101
+ session = { ...session, workingCopy: content };
102
+ if (content === undefined) delete session.workingCopy;
103
+
104
+ return session;
105
+ }),
106
+ // the daemon's curation, stood in for: the record carries the decisions and the patch they leave
107
+ sessionCurate: mock(async (_id: string, rejections: HunkRejection[]) => {
108
+ const content = rejections.length
109
+ ? curateDiff(session.artifact.files!, rejections)
110
+ : undefined;
111
+
112
+ sink.workingCopy = content;
113
+ session = { ...session, workingCopy: content };
114
+ if (rejections.length) session.curation = rejections;
115
+ else delete session.curation;
116
+
117
+ return session;
118
+ }),
119
+ sessionNavigate: unimplemented("sessionNavigate"),
120
+ sessionBranch: unimplemented("sessionBranch"),
121
+ sessionSwitch: unimplemented("sessionSwitch"),
122
+ sessionLabel: unimplemented("sessionLabel"),
123
+ sessionFork: unimplemented("sessionFork"),
124
+ sessionSetViewed: unimplemented("sessionSetViewed"),
125
+ sessionSetTitle: unimplemented("sessionSetTitle"),
126
+ sessionSetShares: unimplemented("sessionSetShares"),
127
+ sessionMergeShared: unimplemented("sessionMergeShared"),
128
+ sessionDelete: unimplemented("sessionDelete"),
129
+ sessionSetSelfName: unimplemented("sessionSetSelfName"),
130
+ sessionSendMessage: unimplemented("sessionSendMessage"),
131
+ close: () => {},
132
+ };
133
+ }
134
+
135
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
136
+
137
+ async function connected(session: Thread) {
138
+ const sink: WorkingCopySink = {};
139
+ const client = fakeClient(session, sink);
140
+ const controller = createReviewController({
141
+ sessionId: session.id,
142
+ openClient: async () => client,
143
+ });
144
+
145
+ controller.connect();
146
+ await tick();
147
+
148
+ return { controller, client, sink };
149
+ }
150
+
151
+ // rows for PATCH: file(0), hunk(1), ctx(2), del(3), add(4), ctx(5)
152
+ describe("diff hunk curation", () => {
153
+ test("rejecting the only change writes an empty curated working copy", async () => {
154
+ // Arrange
155
+ const { controller, sink } = await connected(diffSession(FILES));
156
+
157
+ // Act - reject the change on the added line
158
+ controller.toggleRejectChange(4);
159
+ await tick();
160
+
161
+ // Assert
162
+ expect(controller.getSnapshot().toast).toBeNull();
163
+ expect(sink.workingCopy).toBe("");
164
+ });
165
+
166
+ test("both the deletion and addition rows of the change dim", async () => {
167
+ // Arrange
168
+ const { controller } = await connected(diffSession(FILES));
169
+
170
+ // Act
171
+ controller.toggleRejectChange(4);
172
+ await tick();
173
+
174
+ // Assert - the change spans the del row and the add row
175
+ expect([...controller.rejectedRows()].sort((a, b) => a - b)).toEqual([3, 4]);
176
+ });
177
+
178
+ test("toggling the same change twice restores the full diff", async () => {
179
+ // Arrange
180
+ const { controller, sink } = await connected(diffSession(FILES));
181
+
182
+ // Act - reject then restore
183
+ controller.toggleRejectChange(4);
184
+ await tick();
185
+ controller.toggleRejectChange(4);
186
+ await tick();
187
+
188
+ // Assert - no decisions left, so the working copy reverts to undefined
189
+ expect(controller.getSnapshot().toast).toBeNull();
190
+ expect(sink.workingCopy).toBeUndefined();
191
+ expect(controller.rejectedRows().size).toBe(0);
192
+ });
193
+
194
+ test("whole-hunk reject drops the hunk from the working copy", async () => {
195
+ // Arrange
196
+ const { controller, sink } = await connected(diffSession(FILES));
197
+
198
+ // Act - reject the whole hunk from any of its rows
199
+ controller.toggleRejectHunk(3);
200
+ await tick();
201
+
202
+ // Assert - the single hunk is the whole diff, so nothing remains
203
+ expect(controller.getSnapshot().toast).toBeNull();
204
+ expect(sink.workingCopy).toBe("");
205
+ });
206
+
207
+ test("a rejected change becomes a removal card with a source, id, and preview", async () => {
208
+ // Arrange
209
+ const { controller } = await connected(diffSession(FILES));
210
+
211
+ // Act
212
+ controller.toggleRejectChange(4);
213
+ await tick();
214
+
215
+ // Assert - one item: diff source, revealing the change's row, both lines previewed
216
+ const items = controller.curationItems();
217
+
218
+ expect(items.length).toBe(1);
219
+ expect(items[0]!.source).toBe("diff");
220
+ expect(items[0]!.id).toBe("diff:src/store.ts#0#1");
221
+ expect(items[0]!.revealIndex).toBe(3);
222
+ expect(items[0]!.preview).toEqual([
223
+ "- private items = [];",
224
+ "+ private items = new Map();",
225
+ ]);
226
+ });
227
+
228
+ test("a rejected hunk becomes a removal card with the whole-hunk id", async () => {
229
+ // Arrange
230
+ const { controller } = await connected(diffSession(FILES));
231
+
232
+ // Act
233
+ controller.toggleRejectHunk(3);
234
+ await tick();
235
+
236
+ // Assert - the whole hunk, id diff:path#hunk#hunk
237
+ const items = controller.curationItems();
238
+
239
+ expect(items.length).toBe(1);
240
+ expect(items[0]!.source).toBe("diff");
241
+ expect(items[0]!.id).toBe("diff:src/store.ts#0#hunk");
242
+ });
243
+
244
+ test("restoreCuration drops the diff removal and reverts the working copy", async () => {
245
+ // Arrange - one change rejected
246
+ const { controller, sink } = await connected(diffSession(FILES));
247
+
248
+ controller.toggleRejectChange(4);
249
+ await tick();
250
+ const [item] = controller.curationItems();
251
+
252
+ // Act
253
+ controller.restoreCuration(item!.id);
254
+ await tick();
255
+
256
+ // Assert - the list empties and the working copy reverts to the full diff
257
+ expect(controller.getSnapshot().toast).toBeNull();
258
+ expect(controller.curationItems().length).toBe(0);
259
+ expect(sink.workingCopy).toBeUndefined();
260
+ expect(controller.rejectedRows().size).toBe(0);
261
+ });
262
+
263
+ test("restoreCuration ignores an unknown id", async () => {
264
+ // Arrange - one change rejected
265
+ const { controller } = await connected(diffSession(FILES));
266
+
267
+ controller.toggleRejectChange(4);
268
+ await tick();
269
+
270
+ // Act
271
+ controller.restoreCuration("diff:nope#0#0");
272
+ await tick();
273
+
274
+ // Assert - the item stays
275
+ expect(controller.curationItems().length).toBe(1);
276
+ });
277
+
278
+ test("a second decision before the daemon answers builds on the first", async () => {
279
+ // Arrange
280
+ const { controller, client } = await connected(diffSession(FILES));
281
+
282
+ // Act - reject, then restore, with no answer in between
283
+ controller.toggleRejectChange(4);
284
+ controller.toggleRejectChange(4);
285
+ await tick();
286
+
287
+ // Assert - the second request restores; a stale snapshot would reject again
288
+ expect(client.sessionCurate).toHaveBeenCalledTimes(2);
289
+ expect(client.sessionCurate).toHaveBeenNthCalledWith(1, "ses_diff", [
290
+ { path: "src/store.ts", hunkIndex: 0, changeIndex: 1 },
291
+ ]);
292
+ expect(client.sessionCurate).toHaveBeenNthCalledWith(2, "ses_diff", []);
293
+ });
294
+
295
+ test("curation is disabled without full file contents", async () => {
296
+ // Arrange - a legacy diff with no artifact.files
297
+ const { controller, sink } = await connected(diffSession(undefined));
298
+
299
+ // Act
300
+ controller.toggleRejectChange(4);
301
+ await tick();
302
+
303
+ // Assert
304
+ expect(controller.getSnapshot().status).toContain("hunk curation needs full file contents");
305
+ expect(sink.workingCopy).toBeUndefined();
306
+ });
307
+
308
+ test("curation is disabled when a revision cleared its file snapshots", async () => {
309
+ const { controller, client } = await connected(diffSession([]));
310
+
311
+ controller.toggleRejectChange(4);
312
+ await tick();
313
+
314
+ expect(controller.getSnapshot().status).toContain("hunk curation needs full file contents");
315
+ expect(client.sessionCurate).not.toHaveBeenCalled();
316
+ });
317
+ });
318
+
319
+ const PLAN_CONTENT = "# Title\n\nFirst paragraph.\n\nSecond paragraph.\n";
320
+ // the working copy with the "Second paragraph." block cut out
321
+ const PLAN_CUT = "# Title\n\nFirst paragraph.\n";
322
+
323
+ function planSession(workingCopy?: string): Thread {
324
+ return {
325
+ schemaVersion: SCHEMA_VERSION,
326
+ id: "ses_plan",
327
+ workspace: { repoRoot: "/repo", branch: "main" },
328
+ artifact: { type: "plan", content: PLAN_CONTENT, meta: {} },
329
+ revisions: [{ revision: 1, content: PLAN_CONTENT, submittedAt: "2026-01-01T00:00:00.000Z" }],
330
+ annotations: [],
331
+ message: null,
332
+ status: "pending",
333
+ createdAt: "2026-01-01T00:00:00.000Z",
334
+ workingCopy,
335
+ };
336
+ }
337
+
338
+ describe("plan cut removals", () => {
339
+ test("a cut block becomes a plan removal card previewing its content", async () => {
340
+ // Arrange - a plan whose working copy dropped the second paragraph
341
+ const { controller } = await connected(planSession(PLAN_CUT));
342
+
343
+ // Act
344
+ const items = controller.curationItems();
345
+
346
+ // Assert - one plan-source removal, keyed on the base line range, content previewed
347
+ expect(items.length).toBe(1);
348
+ expect(items[0]!.source).toBe("plan");
349
+ expect(items[0]!.id).toBe("plan:4-4");
350
+ expect(items[0]!.preview).toEqual(["Second paragraph."]);
351
+ });
352
+
353
+ test("restoreCuration re-inserts the cut block and returns to pristine", async () => {
354
+ // Arrange
355
+ const { controller, sink } = await connected(planSession(PLAN_CUT));
356
+ const [item] = controller.curationItems();
357
+
358
+ // Act
359
+ controller.restoreCuration(item!.id);
360
+ await tick();
361
+
362
+ // Assert - restoring the only cut round-trips to the submitted revision
363
+ expect(controller.getSnapshot().toast).toBeNull();
364
+ expect(sink.workingCopy).toBeUndefined();
365
+ });
366
+
367
+ test("a second cut before the daemon answers indexes the already-cut copy", async () => {
368
+ // Arrange - pristine plan: title(0), first(1), second(2)
369
+ const { controller, client, sink } = await connected(planSession());
370
+
371
+ // Act - cut the first paragraph, then the second, with no answer in between
372
+ controller.cut(1);
373
+ controller.cut(2);
374
+ await tick();
375
+
376
+ // Assert - the second paragraph is block 1 once the first is gone
377
+ expect(client.sessionCutBlock).toHaveBeenNthCalledWith(1, "ses_plan", 1);
378
+ expect(client.sessionCutBlock).toHaveBeenNthCalledWith(2, "ses_plan", 1);
379
+ expect(sink.workingCopy).toBe("# Title\n");
380
+ });
381
+
382
+ test("a whole-block Cut composes with an earlier exact Cut", async () => {
383
+ const start = PLAN_CONTENT.indexOf("paragraph.");
384
+ const end = PLAN_CONTENT.indexOf("Second") + "Sec".length;
385
+ const textCuts = [{ start, end, quote: PLAN_CONTENT.slice(start, end) }];
386
+ const session = {
387
+ ...planSession(applyTextCuts(PLAN_CONTENT, textCuts)),
388
+ textCuts,
389
+ };
390
+ const { controller, client, sink } = await connected(session);
391
+
392
+ controller.cut(1);
393
+ await tick();
394
+
395
+ expect(client.sessionSetWorkingCopy).toHaveBeenCalledTimes(1);
396
+ expect(client.sessionCutBlock).not.toHaveBeenCalled();
397
+ expect(sink.textCuts).toHaveLength(1);
398
+ expect(sink.workingCopy).toBe("# Title\nond paragraph.\n");
399
+ });
400
+ });
401
+
402
+ // a file whose hunk touches line 2 only, so lines 1/3/4 are unchanged tail/context
403
+ const TAIL_PATCH = `diff --git a/src/store.ts b/src/store.ts
404
+ --- a/src/store.ts
405
+ +++ b/src/store.ts
406
+ @@ -2,1 +2,1 @@
407
+ - private items = [];
408
+ + private items = new Map();
409
+ `;
410
+
411
+ function tailSession(): Thread {
412
+ return {
413
+ ...diffSession(),
414
+ artifact: {
415
+ type: "diff",
416
+ content: TAIL_PATCH,
417
+ meta: {},
418
+ files: [
419
+ {
420
+ path: "src/store.ts",
421
+ oldContents: "export class Store {\n private items = [];\n}\nexport const x = 1;\n",
422
+ newContents:
423
+ "export class Store {\n private items = new Map();\n}\nexport const x = 1;\n",
424
+ status: "modified",
425
+ },
426
+ ],
427
+ },
428
+ };
429
+ }
430
+
431
+ describe("diff file fold", () => {
432
+ test("added and deleted files with a zero-sided hunk retain their expand action", async () => {
433
+ const patch = [
434
+ "diff --git a/src/new.ts b/src/new.ts",
435
+ "--- /dev/null",
436
+ "+++ b/src/new.ts",
437
+ "@@ -0,0 +1,1 @@",
438
+ "+new line",
439
+ "diff --git a/src/gone.ts b/src/gone.ts",
440
+ "--- a/src/gone.ts",
441
+ "+++ /dev/null",
442
+ "@@ -1,1 +0,0 @@",
443
+ "-old line",
444
+ "",
445
+ ].join("\n");
446
+ const files: DiffFileContents[] = [
447
+ { path: "src/new.ts", oldContents: "", newContents: "new line\n", status: "added" },
448
+ { path: "src/gone.ts", oldContents: "old line\n", newContents: "", status: "deleted" },
449
+ ];
450
+ const session = diffSession(files);
451
+
452
+ session.artifact.content = patch;
453
+ session.revisions[0]!.content = patch;
454
+ const { controller } = await connected(session);
455
+
456
+ expect(controller.canExpandFile("src/new.ts")).toBe(true);
457
+ expect(controller.canExpandFile("src/gone.ts")).toBe(true);
458
+ });
459
+
460
+ test("collapse leaves only the file band; expand restores the body", async () => {
461
+ // Arrange
462
+ const { controller } = await connected(tailSession());
463
+ const bodyRows = () =>
464
+ controller.rows().filter((row) => row.file === "src/store.ts" && row.kind !== "file").length;
465
+
466
+ // Assert - starts unfolded with a body
467
+ expect(bodyRows()).toBeGreaterThan(0);
468
+
469
+ // Act / Assert - collapse hides the body
470
+ controller.setFileCollapsed("src/store.ts", true);
471
+ expect(bodyRows()).toBe(0);
472
+ expect(controller.isFileCollapsed("src/store.ts")).toBe(true);
473
+
474
+ // Act / Assert - unfold brings it back
475
+ controller.setFileCollapsed("src/store.ts", false);
476
+ expect(bodyRows()).toBeGreaterThan(0);
477
+ });
478
+
479
+ test("expand weaves the file's unchanged tail line the hunk never showed", async () => {
480
+ // Arrange
481
+ const { controller } = await connected(tailSession());
482
+ const hasTail = () => controller.rows().some((row) => row.text.includes("export const x = 1"));
483
+
484
+ // Assert - the hunk view does not show the tail line
485
+ expect(controller.canExpandFile("src/store.ts")).toBe(true);
486
+ expect(hasTail()).toBe(false);
487
+
488
+ // Act - weave the full file
489
+ controller.setFileExpanded("src/store.ts", true);
490
+
491
+ // Assert
492
+ expect(hasTail()).toBe(true);
493
+ });
494
+
495
+ test("a diff without full contents cannot be expanded", async () => {
496
+ // Arrange - no artifact.files
497
+ const { controller } = await connected(diffSession(undefined));
498
+
499
+ // Assert
500
+ expect(controller.canExpandFile("src/store.ts")).toBe(false);
501
+ });
502
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { SCHEMA_VERSION, type Annotation, type Thread } from "@cueloop/schema";
3
+ import { sameDerivationInputs } from "./thread-controller";
4
+
5
+ function threadFixture(overrides: Partial<Thread> = {}): Thread {
6
+ return {
7
+ schemaVersion: SCHEMA_VERSION,
8
+ id: "ses_1",
9
+ workspace: { repoRoot: "/repo", branch: "main" },
10
+ artifact: { type: "plan", content: "# Plan\n\nShip it.\n", meta: {} },
11
+ revisions: [
12
+ { revision: 1, content: "# Plan\n\nShip it.\n", submittedAt: "2026-01-01T00:00:00.000Z" },
13
+ ],
14
+ annotations: [],
15
+ message: null,
16
+ status: "pending",
17
+ createdAt: "2026-01-01T00:00:00.000Z",
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ const annotation: Annotation = {
23
+ id: "ann_1",
24
+ kind: "comment",
25
+ anchor: { quote: "Ship it.", prefix: "", suffix: "" },
26
+ body: "nit",
27
+ createdAt: "2026-01-01T00:00:00.000Z",
28
+ };
29
+
30
+ describe("sameDerivationInputs", () => {
31
+ test("an annotation-only update reuses the projection (a fresh record, identical inputs)", () => {
32
+ // Arrange - the daemon re-serializes the whole thread on any change, so the record is all-new
33
+ const before = threadFixture();
34
+ const after = threadFixture({ annotations: [annotation] });
35
+
36
+ // Assert - the artifact and working copy are unchanged, so the projection can be reused
37
+ expect(sameDerivationInputs(before, after)).toBe(true);
38
+ });
39
+
40
+ test("a content edit forces a re-derivation", () => {
41
+ const before = threadFixture();
42
+ const after = threadFixture({
43
+ artifact: { type: "plan", content: "# Plan\n\nShip it soon.\n", meta: {} },
44
+ });
45
+
46
+ expect(sameDerivationInputs(before, after)).toBe(false);
47
+ });
48
+
49
+ test("a working-copy change forces a re-derivation", () => {
50
+ const before = threadFixture();
51
+ const after = threadFixture({ workingCopy: "# Plan\n\nShip it.\n\nedited\n" });
52
+
53
+ expect(sameDerivationInputs(before, after)).toBe(false);
54
+ });
55
+
56
+ test("a different thread is never a reuse", () => {
57
+ const before = threadFixture();
58
+ const after = threadFixture({ id: "ses_2" });
59
+
60
+ expect(sameDerivationInputs(before, after)).toBe(false);
61
+ });
62
+ });