@cueloop/client 0.1.0-alpha.10 → 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 (355) 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 +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.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 +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 -88
  333. package/src/App.plan-review.test.tsx +0 -259
  334. package/src/App.readonly.test.tsx +0 -148
  335. package/src/App.rendering.test.tsx +0 -108
  336. package/src/App.submit-confirm.test.tsx +0 -183
  337. package/src/App.test.tsx +0 -218
  338. package/src/App.tsx +0 -1249
  339. package/src/config.test.ts +0 -116
  340. package/src/config.ts +0 -150
  341. package/src/editor.ts +0 -34
  342. package/src/herdr.test.ts +0 -68
  343. package/src/herdr.ts +0 -34
  344. package/src/keymap.test.ts +0 -270
  345. package/src/keymap.ts +0 -173
  346. package/src/run.ts +0 -30
  347. package/src/serve.ts +0 -75
  348. package/src/session-controller.ts +0 -398
  349. package/src/syntax.test.ts +0 -41
  350. package/src/syntax.ts +0 -88
  351. package/src/theme.ts +0 -46
  352. package/src/view-diff.test.ts +0 -55
  353. package/src/view-diff.ts +0 -86
  354. package/src/view.test.ts +0 -157
  355. package/src/view.ts +0 -364
@@ -0,0 +1,174 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+ import {
3
+ SCHEMA_VERSION,
4
+ type DiffFileContents,
5
+ type HunkRejection,
6
+ type Thread,
7
+ } from "@cueloop/schema";
8
+ import { curateDiff } from "@cueloop/daemon/curate";
9
+ import type { ThreadClient } from "@cueloop/daemon/client";
10
+ import { createReviewController } from "./thread-controller";
11
+
12
+ const PATCH_A = `diff --git a/src/a.ts b/src/a.ts
13
+ index 111..222 100644
14
+ --- a/src/a.ts
15
+ +++ b/src/a.ts
16
+ @@ -1,3 +1,3 @@
17
+ export class A {
18
+ - value = 1;
19
+ + value = 2;
20
+ }
21
+ `;
22
+
23
+ const PATCH_B = `diff --git a/src/b.ts b/src/b.ts
24
+ index 333..444 100644
25
+ --- a/src/b.ts
26
+ +++ b/src/b.ts
27
+ @@ -1,3 +1,3 @@
28
+ export class B {
29
+ - flag = false;
30
+ + flag = true;
31
+ }
32
+ `;
33
+
34
+ function diffSession(id: string, patch: string, files: DiffFileContents[]): Thread {
35
+ return {
36
+ schemaVersion: SCHEMA_VERSION,
37
+ id,
38
+ workspace: { repoRoot: "/repo", branch: "main" },
39
+ artifact: { type: "diff", content: patch, meta: { title: id }, files },
40
+ revisions: [{ revision: 1, content: patch, submittedAt: "2026-01-01T00:00:00.000Z" }],
41
+ annotations: [],
42
+ message: null,
43
+ status: "pending",
44
+ createdAt: "2026-01-01T00:00:00.000Z",
45
+ };
46
+ }
47
+
48
+ const FILES_A: DiffFileContents[] = [
49
+ {
50
+ path: "src/a.ts",
51
+ oldContents: "export class A {\n value = 1;\n}\n",
52
+ newContents: "export class A {\n value = 2;\n}\n",
53
+ status: "modified",
54
+ },
55
+ ];
56
+ const FILES_B: DiffFileContents[] = [
57
+ {
58
+ path: "src/b.ts",
59
+ oldContents: "export class B {\n flag = false;\n}\n",
60
+ newContents: "export class B {\n flag = true;\n}\n",
61
+ status: "modified",
62
+ },
63
+ ];
64
+
65
+ const unimplemented = (member: string) => () =>
66
+ Promise.reject(new Error(`fakeClient does not implement ${member}`));
67
+
68
+ /** A fake client over a fixed set of sessions, so open(id) can switch between them from the inbox. */
69
+ function fakeClient(sessions: Thread[]): ThreadClient {
70
+ const byId = new Map(sessions.map((session) => [session.id, session]));
71
+
72
+ return {
73
+ onEvent: () => () => {},
74
+ subscribe: async () => {},
75
+ sessionGet: async (id: string) => byId.get(id)!,
76
+ sessionList: async () => [...byId.values()],
77
+ sessionCurate: mock(async (id: string, rejections: HunkRejection[]) => {
78
+ const session = byId.get(id)!;
79
+ const content = rejections.length
80
+ ? curateDiff(session.artifact.files!, rejections)
81
+ : undefined;
82
+ const next = {
83
+ ...session,
84
+ workingCopy: content,
85
+ curation: rejections.length ? rejections : undefined,
86
+ };
87
+
88
+ byId.set(id, next);
89
+
90
+ return next;
91
+ }),
92
+ sessionComment: unimplemented("sessionComment"),
93
+ sessionAnnotate: unimplemented("sessionAnnotate"),
94
+ sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
95
+ sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
96
+ sessionCutBlock: unimplemented("sessionCutBlock"),
97
+ sessionRestoreBlock: unimplemented("sessionRestoreBlock"),
98
+ sessionNavigate: unimplemented("sessionNavigate"),
99
+ sessionBranch: unimplemented("sessionBranch"),
100
+ sessionSwitch: unimplemented("sessionSwitch"),
101
+ sessionLabel: unimplemented("sessionLabel"),
102
+ sessionFork: unimplemented("sessionFork"),
103
+ sessionSetViewed: unimplemented("sessionSetViewed"),
104
+ sessionSetTitle: unimplemented("sessionSetTitle"),
105
+ sessionSetShares: unimplemented("sessionSetShares"),
106
+ sessionMergeShared: unimplemented("sessionMergeShared"),
107
+ sessionDelete: unimplemented("sessionDelete"),
108
+ sessionSetSelfName: unimplemented("sessionSetSelfName"),
109
+ sessionSendMessage: unimplemented("sessionSendMessage"),
110
+ close: () => {},
111
+ };
112
+ }
113
+
114
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
115
+
116
+ // rows for each single-change patch: file(0), hunk(1), ctx(2), del(3), add(4), ctx(5)
117
+ describe("switching threads keeps the cached projection curatable", () => {
118
+ test("curation still works after switching away and back to a diff thread", async () => {
119
+ // Arrange - two diff threads in one inbox
120
+ const controller = createReviewController({
121
+ sessionId: "ses_a",
122
+ openClient: async () =>
123
+ fakeClient([
124
+ diffSession("ses_a", PATCH_A, FILES_A),
125
+ diffSession("ses_b", PATCH_B, FILES_B),
126
+ ]),
127
+ });
128
+
129
+ controller.connect();
130
+ await tick();
131
+
132
+ // Act - visit B, then return to A (A's projection now comes from the per-thread cache)
133
+ controller.open("ses_b");
134
+ await tick();
135
+ controller.open("ses_a");
136
+ await tick();
137
+
138
+ // Assert - the returned thread's file model parses lazily and curation lands
139
+ controller.toggleRejectChange(4);
140
+ await tick();
141
+
142
+ expect(controller.getSnapshot().toast).toBeNull();
143
+ expect(controller.getSnapshot().session?.workingCopy).toBe("");
144
+ });
145
+
146
+ test("returning to a diff thread shows the daemon's latest, not the stale sidebar copy", async () => {
147
+ // The sidebar list trails the daemon for a hot-reloading diff (its session.updated
148
+ // refreshes the open thread, not the list). Returning to it must revalidate.
149
+ const staleA = diffSession("ses_a", PATCH_A, FILES_A);
150
+ const freshA = diffSession("ses_a", PATCH_B, FILES_B);
151
+ const other = diffSession("ses_b", PATCH_B, FILES_B);
152
+ const base = fakeClient([staleA, other]);
153
+ const controller = createReviewController({
154
+ sessionId: "ses_a",
155
+ openClient: async () => ({
156
+ ...base,
157
+ // the sidebar keeps the stale copy; the daemon serves the fresh one
158
+ sessionList: async () => [staleA, other],
159
+ sessionGet: async (id: string) => (id === "ses_a" ? freshA : other),
160
+ }),
161
+ });
162
+
163
+ controller.connect();
164
+ await tick();
165
+
166
+ // Visit B, then return to A - open() paints the stale sidebar copy, then revalidates
167
+ controller.open("ses_b");
168
+ await tick();
169
+ controller.open("ses_a");
170
+ await tick();
171
+
172
+ expect(controller.getSnapshot().session?.artifact.content).toBe(PATCH_B);
173
+ });
174
+ });
@@ -0,0 +1,263 @@
1
+ /**
2
+ * The controller's tree primitives over a fake client: a move shows its path
3
+ * before the daemon answers, refusals surface as status, a fork opens the fork,
4
+ * and fork-and-share shares the fork while the current session stays put.
5
+ */
6
+
7
+ import { describe, expect, mock, test } from "bun:test";
8
+ import {
9
+ appendEntry,
10
+ createBranch,
11
+ historyFromLinear,
12
+ SCHEMA_VERSION,
13
+ switchBranch,
14
+ type Annotation,
15
+ type Thread,
16
+ } from "@cueloop/schema";
17
+ import type { ThreadClient } from "@cueloop/daemon/client";
18
+ import { createReviewController, type ShareTransport } from "./thread-controller";
19
+ import { mergeFromShare } from "../integrations/share";
20
+
21
+ const AT = "2026-01-01T00:00:00.000Z";
22
+
23
+ function annotation(id: string, body: string): Annotation {
24
+ return {
25
+ id,
26
+ kind: "comment",
27
+ anchor: { quote: "Plan", prefix: "", suffix: "" },
28
+ body,
29
+ createdAt: AT,
30
+ };
31
+ }
32
+
33
+ /** A plan with one comment on main and its history. */
34
+ function sessionFixture(): Thread {
35
+ const session: Thread = {
36
+ schemaVersion: SCHEMA_VERSION,
37
+ id: "ses_1",
38
+ workspace: { repoRoot: "/repo", branch: "main" },
39
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
40
+ revisions: [{ revision: 1, content: "# Plan\n", submittedAt: AT }],
41
+ annotations: [annotation("a1", "first")],
42
+ message: null,
43
+ status: "pending",
44
+ createdAt: AT,
45
+ };
46
+
47
+ session.history = historyFromLinear(session);
48
+
49
+ return session;
50
+ }
51
+
52
+ const publish = mock(async (session: Thread) => ({
53
+ line: `ssh p_${session.id}@cueloop.dev`,
54
+ copied: true,
55
+ }));
56
+ const shareTransport: ShareTransport = {
57
+ publish,
58
+ pull: mock(async () => sessionFixture()),
59
+ push: mock(async () => {}),
60
+ watch: () => () => {},
61
+ revoke: async () => {},
62
+ parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
63
+ formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
64
+ collaboratorAnnotations: (session) => session.annotations.filter((entry) => entry.author),
65
+ mergeFromShare,
66
+ };
67
+
68
+ const unimplemented = (member: string) => () =>
69
+ Promise.reject(new Error(`fakeClient does not implement ${member}`));
70
+
71
+ /** Tree requests never resolve here: what the controller shows is its own guess. */
72
+ const pending = () => new Promise<Thread>(() => {});
73
+
74
+ function fakeClient(session: Thread) {
75
+ return {
76
+ onEvent: () => () => {},
77
+ subscribe: async () => {},
78
+ sessionGet: async () => session,
79
+ sessionList: async () => [session],
80
+ sessionComment: unimplemented("sessionComment"),
81
+ sessionAnnotate: unimplemented("sessionAnnotate"),
82
+ sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
83
+ sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
84
+ sessionCutBlock: unimplemented("sessionCutBlock"),
85
+ sessionRestoreBlock: unimplemented("sessionRestoreBlock"),
86
+ sessionCurate: unimplemented("sessionCurate"),
87
+ sessionNavigate: mock(pending),
88
+ sessionBranch: mock(pending),
89
+ sessionSwitch: mock(pending),
90
+ sessionLabel: mock(pending),
91
+ sessionFork: mock(async (id: string) => ({
92
+ ...sessionFixture(),
93
+ id: `${id}_fork`,
94
+ parentSessionId: id,
95
+ })),
96
+ sessionSetViewed: unimplemented("sessionSetViewed"),
97
+ sessionSetTitle: unimplemented("sessionSetTitle"),
98
+ sessionSetShares: mock(async () => session),
99
+ sessionMergeShared: unimplemented("sessionMergeShared"),
100
+ sessionDelete: unimplemented("sessionDelete"),
101
+ sessionSetSelfName: unimplemented("sessionSetSelfName"),
102
+ sessionSendMessage: unimplemented("sessionSendMessage"),
103
+ close: () => {},
104
+ } satisfies ThreadClient;
105
+ }
106
+
107
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
108
+
109
+ async function connected(session = sessionFixture()) {
110
+ const client = fakeClient(session);
111
+ const controller = createReviewController({
112
+ sessionId: session.id,
113
+ openClient: async () => client,
114
+ shareTransport,
115
+ });
116
+
117
+ controller.connect();
118
+ await tick();
119
+
120
+ return { controller, client, session };
121
+ }
122
+
123
+ describe("read-only guards", () => {
124
+ test("an observer's rename and delete never reach the daemon", async () => {
125
+ // Arrange - a read-only controller over a client whose mutations would throw
126
+ const session = sessionFixture();
127
+ const client = fakeClient(session);
128
+ const controller = createReviewController({
129
+ sessionId: session.id,
130
+ openClient: async () => client,
131
+ readOnly: true,
132
+ shareTransport,
133
+ });
134
+
135
+ controller.connect();
136
+ await tick();
137
+
138
+ // Act - the sidebar's rename and delete, invoked directly
139
+ controller.renameSession(session.id, "hijacked");
140
+ controller.deleteSession(session.id);
141
+ await tick();
142
+
143
+ // Assert - both answer read-only and the throwing client stubs were never called
144
+ expect(controller.getSnapshot().status).toBe("observer - read-only");
145
+ });
146
+ });
147
+
148
+ describe("tree primitives", () => {
149
+ test("a move back shows the earlier path at once and asks the daemon for the same move", async () => {
150
+ // Arrange
151
+ const { controller, client, session } = await connected();
152
+ const revision = session.history!.entries[0]!.id;
153
+
154
+ // Act
155
+ controller.goToEntry(revision, "too soon");
156
+
157
+ // Assert: the comment is off the shown path before any answer; the summary travels
158
+ expect(controller.getSnapshot().session!.annotations).toEqual([]);
159
+ expect(controller.getSnapshot().session!.shelvedAnnotations?.map((entry) => entry.id)).toEqual([
160
+ "a1",
161
+ ]);
162
+ expect(controller.treeRows().find((row) => row.glyph === "↩")?.isCurrentTip).toBe(true);
163
+ expect(client.sessionNavigate).toHaveBeenCalledWith("ses_1", revision, "too soon", undefined);
164
+ });
165
+
166
+ test("a move on another branch is one request that names the branch", async () => {
167
+ // Arrange: alt with its own comment entry; the owner stands on main
168
+ const session = sessionFixture();
169
+ let history = createBranch(session.history!, "alt");
170
+
171
+ history = appendEntry(history, { type: "comment", annotationId: "b1", createdAt: AT }).history;
172
+ history = appendEntry(history, { type: "comment", annotationId: "b2", createdAt: AT }).history;
173
+ session.history = switchBranch(history, "main");
174
+ session.shelvedAnnotations = [annotation("b1", "alt one"), annotation("b2", "alt two")];
175
+ const { controller, client } = await connected(session);
176
+ const altFirst = history.entries.find(
177
+ (entry) => entry.type === "comment" && entry.annotationId === "b1",
178
+ )!;
179
+
180
+ // Act
181
+ controller.goToEntry(altFirst.id);
182
+
183
+ // Assert
184
+ expect(client.sessionSwitch).not.toHaveBeenCalled();
185
+ expect(client.sessionNavigate).toHaveBeenCalledWith("ses_1", altFirst.id, undefined, "alt");
186
+ expect(controller.getSnapshot().session!.annotations.map((entry) => entry.id)).toEqual([
187
+ "a1",
188
+ "b1",
189
+ ]);
190
+ });
191
+
192
+ test("a refused move gives way to the daemon's record", async () => {
193
+ // Arrange: the daemon refuses; a re-read hands back the untouched record
194
+ const session = sessionFixture();
195
+ const { controller, client } = await connected(session);
196
+
197
+ client.sessionNavigate.mockImplementationOnce(() => Promise.reject(new Error("resolved")));
198
+
199
+ // Act
200
+ controller.goToEntry(session.history!.entries[0]!.id);
201
+ const guessed = controller.getSnapshot().session!.annotations.length;
202
+
203
+ await tick();
204
+
205
+ // Assert: the guess showed, then the record came back
206
+ expect(guessed).toBe(0);
207
+ expect(controller.getSnapshot().session!.annotations.map((entry) => entry.id)).toEqual(["a1"]);
208
+ expect(controller.getSnapshot().status).toBe("resolved");
209
+ });
210
+
211
+ test("branch and label refuse an empty name and a taken branch name", async () => {
212
+ // Arrange
213
+ const { controller, client } = await connected();
214
+
215
+ // Act
216
+ controller.branch(" ");
217
+ controller.branch("main");
218
+ controller.labelTip("");
219
+
220
+ // Assert
221
+ expect(client.sessionBranch).not.toHaveBeenCalled();
222
+ expect(client.sessionLabel).not.toHaveBeenCalled();
223
+ expect(controller.getSnapshot().status).toBe("a checkpoint needs a name");
224
+ });
225
+
226
+ test("a branch shows up on the tip and becomes the current branch", async () => {
227
+ // Arrange
228
+ const { controller, client } = await connected();
229
+
230
+ // Act
231
+ controller.branch("alt");
232
+
233
+ // Assert
234
+ expect(controller.getSnapshot().session!.history!.branch).toBe("alt");
235
+ expect(controller.treeRows().find((row) => row.isCurrentTip)?.tips).toEqual(["main", "alt"]);
236
+ expect(client.sessionBranch).toHaveBeenCalledWith("ses_1", "alt");
237
+ });
238
+
239
+ test("fork opens the fork; fork-and-share shares the fork and stays on the original", async () => {
240
+ // Arrange
241
+ const forked = await connected();
242
+ const shared = await connected();
243
+
244
+ // Act
245
+ forked.controller.fork();
246
+ shared.controller.forkAndShare();
247
+ await tick();
248
+ await tick();
249
+
250
+ // Assert
251
+ expect(forked.controller.getSnapshot().session!.id).toBe("ses_1_fork");
252
+ expect(shared.controller.getSnapshot().session!.id).toBe("ses_1");
253
+ expect(publish).toHaveBeenCalledWith(
254
+ expect.objectContaining({ id: "ses_1_fork" }),
255
+ {},
256
+ { requireAuth: false, allowlist: [], shareBranch: "main" },
257
+ );
258
+ expect(shared.client.sessionSetShares).toHaveBeenCalledWith("ses_1_fork", [
259
+ { id: "p_ses_1_fork", requireAuth: false, allowlist: [], shareBranch: "main" },
260
+ ]);
261
+ expect(shared.controller.getSnapshot().toast?.title).toBe("fork shared - link copied");
262
+ });
263
+ });