@cueloop/client 0.1.0-alpha.11 → 0.1.0-alpha.111

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 (360) 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 +63 -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 +383 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +189 -0
  14. package/src/annotations/discussions.ts +78 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +248 -0
  17. package/src/annotations/use-annotation-surface.tsx +1378 -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 +190 -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.harness-settings.test.tsx +77 -0
  25. package/src/app/App.identity.test.tsx +84 -0
  26. package/src/app/App.plan-render.test.tsx +211 -0
  27. package/src/app/App.plan-review.test.tsx +429 -0
  28. package/src/app/App.prototype-markdown.test.tsx +129 -0
  29. package/src/app/App.prototype.test.tsx +176 -0
  30. package/src/app/App.readonly.test.tsx +274 -0
  31. package/src/app/App.regression.test.tsx +99 -0
  32. package/src/app/App.rendering.test.tsx +134 -0
  33. package/src/app/App.share-dialog.test.tsx +177 -0
  34. package/src/app/App.submit-confirm.test.tsx +267 -0
  35. package/src/app/App.test.tsx +497 -0
  36. package/src/app/App.thread-agent.test.ts +20 -0
  37. package/src/app/App.toast.test.tsx +124 -0
  38. package/src/app/App.tsx +1758 -0
  39. package/src/app/App.visual.test.tsx +60 -0
  40. package/src/app/App.walk.test.tsx +300 -0
  41. package/src/app/App.workbench.test.tsx +1153 -0
  42. package/src/app/app-key-state.ts +61 -0
  43. package/src/app/app-screens.tsx +607 -0
  44. package/src/app/app-view-model.ts +178 -0
  45. package/src/app/components/AppHeader.stories.tsx +50 -0
  46. package/src/app/components/AppHeader.tsx +81 -0
  47. package/src/app/components/AppShell.brand.test.tsx +58 -0
  48. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  49. package/src/app/components/AppShell.stories.tsx +89 -0
  50. package/src/app/components/AppShell.tsx +358 -0
  51. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  52. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  53. package/src/app/components/Breadcrumb.tsx +45 -0
  54. package/src/app/components/MenuBar.stories.tsx +28 -0
  55. package/src/app/components/MenuBar.tsx +44 -0
  56. package/src/app/components/PaneGrid.stories.tsx +84 -0
  57. package/src/app/components/PaneGrid.tsx +128 -0
  58. package/src/app/components/PanelColumn.stories.tsx +25 -0
  59. package/src/app/components/PanelColumn.tsx +93 -0
  60. package/src/app/components/ShellHeader.stories.tsx +50 -0
  61. package/src/app/components/ShellHeader.tsx +137 -0
  62. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  63. package/src/app/components/WorkspacePanels.tsx +121 -0
  64. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  65. package/src/app/intent-dispatch.test.ts +729 -0
  66. package/src/app/intent-dispatch.ts +713 -0
  67. package/src/app/launch-layout.test.ts +78 -0
  68. package/src/app/launch-layout.ts +57 -0
  69. package/src/app/ready-signal.test.ts +85 -0
  70. package/src/app/ready-signal.ts +54 -0
  71. package/src/app/run.ts +111 -0
  72. package/src/{serve.test.ts → app/serve.test.ts} +28 -12
  73. package/src/app/serve.ts +111 -0
  74. package/src/app/version.ts +5 -0
  75. package/src/appearance/components/theme-context.stories.tsx +46 -0
  76. package/src/appearance/components/theme-context.tsx +37 -0
  77. package/src/appearance/theme-presets.test.ts +51 -0
  78. package/src/appearance/theme-presets.ts +219 -0
  79. package/src/appearance/theme.ts +112 -0
  80. package/src/appearance/visual/contrast.test.ts +35 -0
  81. package/src/appearance/visual/contrast.ts +33 -0
  82. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  83. package/src/diff/components/AnnotatableFileView.test.tsx +160 -0
  84. package/src/diff/components/AnnotatableFileView.tsx +142 -0
  85. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  86. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  87. package/src/diff/components/DiffContentView.scroll.test.tsx +443 -0
  88. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  89. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  90. package/src/diff/components/DiffContentView.syntax.test.tsx +85 -0
  91. package/src/diff/components/DiffContentView.tsx +1080 -0
  92. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  93. package/src/diff/components/WalkWizard.tsx +146 -0
  94. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  95. package/src/diff/components/diff-content-view-layout.ts +87 -0
  96. package/src/diff/diff-fold.test.ts +78 -0
  97. package/src/diff/diff-fold.ts +130 -0
  98. package/src/diff/diff-hunk-curate.test.ts +315 -0
  99. package/src/diff/diff-hunk-curate.ts +155 -0
  100. package/src/diff/diff-intraline.test.ts +239 -0
  101. package/src/diff/diff-intraline.ts +244 -0
  102. package/src/diff/diff-syntax.test.ts +125 -0
  103. package/src/diff/diff-syntax.ts +194 -0
  104. package/src/diff/split-diff.test.ts +110 -0
  105. package/src/diff/split-diff.ts +150 -0
  106. package/src/diff/view-diff.test.ts +325 -0
  107. package/src/diff/view-diff.ts +540 -0
  108. package/src/diff/walk.test.ts +121 -0
  109. package/src/diff/walk.ts +76 -0
  110. package/src/index.ts +32 -2
  111. package/src/integrations/attribution.test.ts +31 -0
  112. package/src/integrations/attribution.ts +44 -0
  113. package/src/integrations/client-extension-registry.test.tsx +61 -0
  114. package/src/integrations/client-extension-registry.ts +163 -0
  115. package/src/integrations/clipboard.test.ts +23 -0
  116. package/src/integrations/clipboard.ts +34 -0
  117. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  118. package/src/integrations/components/ExtensionZone.tsx +145 -0
  119. package/src/integrations/components/ShareDialog.agent.test.tsx +52 -0
  120. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  121. package/src/integrations/components/ShareDialog.tsx +602 -0
  122. package/src/integrations/components/share-dialog-store.ts +59 -0
  123. package/src/integrations/editor.test.ts +149 -0
  124. package/src/integrations/editor.ts +163 -0
  125. package/src/integrations/github-identity.test.ts +59 -0
  126. package/src/integrations/github-identity.ts +42 -0
  127. package/src/integrations/herdr.test.ts +43 -0
  128. package/src/integrations/herdr.ts +40 -0
  129. package/src/integrations/integrations.test.ts +41 -0
  130. package/src/integrations/integrations.ts +32 -0
  131. package/src/integrations/pasted-image.test.ts +67 -0
  132. package/src/integrations/pasted-image.ts +62 -0
  133. package/src/integrations/share.ts +283 -0
  134. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  135. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  136. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  137. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  138. package/src/keyboard/components/HandleChipsInput.tsx +147 -0
  139. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  140. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  141. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  142. package/src/keyboard/components/NavModeHint.tsx +31 -0
  143. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  144. package/src/keyboard/components/SlashComposer.tsx +90 -0
  145. package/src/keyboard/key-bindings.test.ts +156 -0
  146. package/src/keyboard/key-bindings.ts +462 -0
  147. package/src/keyboard/keymap.test.ts +611 -0
  148. package/src/keyboard/keymap.ts +446 -0
  149. package/src/keyboard/skills.test.ts +74 -0
  150. package/src/keyboard/skills.ts +77 -0
  151. package/src/keyboard/slash-palette.test.ts +103 -0
  152. package/src/keyboard/slash-palette.ts +137 -0
  153. package/src/keyboard/thread-chords.test.ts +79 -0
  154. package/src/keyboard/thread-chords.ts +168 -0
  155. package/src/keyboard/use-shared-keyboard.ts +56 -0
  156. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  157. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  158. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  159. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  160. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  161. package/src/markdown/components/MarkdownThreadEditor.tsx +165 -0
  162. package/src/markdown/components/ThreadView.selection.test.tsx +954 -0
  163. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  164. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  165. package/src/markdown/components/ThreadView.tsx +577 -0
  166. package/src/markdown/mark-runs.ts +125 -0
  167. package/src/markdown/markdown-editor-syntax-style.ts +66 -0
  168. package/src/markdown/markdown-grid.test.ts +101 -0
  169. package/src/markdown/markdown-grid.ts +209 -0
  170. package/src/markdown/markdown-highlight.test.ts +61 -0
  171. package/src/markdown/markdown-highlight.ts +125 -0
  172. package/src/markdown/thread-code-syntax.ts +48 -0
  173. package/src/markdown/use-thread-body-editing.tsx +54 -0
  174. package/src/markdown/view-plan.regression.test.ts +60 -0
  175. package/src/markdown/view-plan.test.ts +545 -0
  176. package/src/markdown/view-plan.ts +1061 -0
  177. package/src/performance/otlp-trace.test.ts +119 -0
  178. package/src/performance/otlp-trace.ts +134 -0
  179. package/src/performance/perf-report.ts +19 -0
  180. package/src/performance/perf-timings.test.ts +18 -0
  181. package/src/performance/perf-timings.ts +40 -0
  182. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  183. package/src/prototype/components/PrototypeContentView.tsx +459 -0
  184. package/src/prototype/prototype-browser.test.ts +81 -0
  185. package/src/prototype/prototype-browser.ts +332 -0
  186. package/src/prototype/prototype-pixels.tsx +35 -0
  187. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  188. package/src/settings/components/SettingsDialog.tsx +186 -0
  189. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  190. package/src/settings/components/SettingsRows.tsx +112 -0
  191. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  192. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  193. package/src/settings/components/quick-actions-editor.tsx +222 -0
  194. package/src/settings/config-identity.test.ts +78 -0
  195. package/src/settings/config.test.ts +771 -0
  196. package/src/settings/config.ts +722 -0
  197. package/src/settings/use-settings-dialog.test.tsx +172 -0
  198. package/src/settings/use-settings-dialog.tsx +465 -0
  199. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  200. package/src/settings/use-thread-harness-setting.ts +86 -0
  201. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  202. package/src/stories/stories-app.tsx +196 -0
  203. package/src/stories/stories.test.tsx +78 -0
  204. package/src/stories/story-fixtures.ts +102 -0
  205. package/src/stories/story-tree.test.ts +108 -0
  206. package/src/stories/story.ts +160 -0
  207. package/src/terminal/ansi-screen.test.ts +28 -0
  208. package/src/terminal/ansi-screen.ts +77 -0
  209. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  210. package/src/terminal/components/AnsiScreen.tsx +28 -0
  211. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  212. package/src/terminal/components/terminal-pane.ts +208 -0
  213. package/src/terminal/ghostty-terminal.test.ts +85 -0
  214. package/src/terminal/ghostty-terminal.ts +207 -0
  215. package/src/terminal/kitty-image.test.ts +66 -0
  216. package/src/terminal/kitty-image.ts +140 -0
  217. package/src/terminal/pty.test.ts +81 -0
  218. package/src/terminal/pty.ts +255 -0
  219. package/src/testing/test-support.test.ts +266 -0
  220. package/src/testing/test-support.ts +449 -0
  221. package/src/thread/agent-continuation.test.ts +34 -0
  222. package/src/thread/agent-continuation.ts +13 -0
  223. package/src/thread/agent-transcript.test.ts +161 -0
  224. package/src/thread/agent-transcript.ts +344 -0
  225. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  226. package/src/thread/components/AgentConfigControls.test.tsx +153 -0
  227. package/src/thread/components/AgentConfigControls.tsx +121 -0
  228. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  229. package/src/thread/components/AgentThreadPane.test.tsx +1248 -0
  230. package/src/thread/components/AgentThreadPane.tsx +458 -0
  231. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  232. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  233. package/src/thread/components/ThreadFooter.tsx +77 -0
  234. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  235. package/src/thread/components/ThreadTree.test.tsx +41 -0
  236. package/src/thread/components/ThreadTree.tsx +268 -0
  237. package/src/thread/components/thread-tree.test.ts +138 -0
  238. package/src/thread/components/thread-tree.ts +112 -0
  239. package/src/thread/thread-controller.curate.test.ts +504 -0
  240. package/src/thread/thread-controller.derivation.test.ts +62 -0
  241. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  242. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  243. package/src/thread/thread-controller.share.test.ts +618 -0
  244. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  245. package/src/thread/thread-controller.tree.test.ts +263 -0
  246. package/src/thread/thread-controller.ts +2151 -0
  247. package/src/thread/tree-view.test.ts +128 -0
  248. package/src/thread/tree-view.ts +150 -0
  249. package/src/thread/use-thread-agent.test.tsx +209 -0
  250. package/src/thread/use-thread-agent.ts +122 -0
  251. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  252. package/src/ui/components/CodeBlock.tsx +82 -0
  253. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  254. package/src/ui/components/ConfirmCard.tsx +151 -0
  255. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  256. package/src/ui/components/ConfirmDialog.tsx +72 -0
  257. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  258. package/src/ui/components/OverlayScrollbar.test.tsx +142 -0
  259. package/src/ui/components/OverlayScrollbar.tsx +134 -0
  260. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  261. package/src/ui/components/PromptDialog.test.tsx +65 -0
  262. package/src/ui/components/PromptDialog.tsx +101 -0
  263. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  264. package/src/ui/components/RootOverlay.tsx +50 -0
  265. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  266. package/src/ui/components/ScrollArea.tsx +63 -0
  267. package/src/ui/components/Toast.stories.tsx +19 -0
  268. package/src/ui/components/Toast.tsx +54 -0
  269. package/src/ui/components/Tooltip.stories.tsx +26 -0
  270. package/src/ui/components/Tooltip.tsx +93 -0
  271. package/src/ui/components/menu-control.stories.tsx +35 -0
  272. package/src/ui/components/menu-control.tsx +43 -0
  273. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  274. package/src/ui/components/primitives/Button.tsx +58 -0
  275. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  276. package/src/ui/components/primitives/Card.tsx +62 -0
  277. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  278. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  279. package/src/ui/components/primitives/Dialog.tsx +69 -0
  280. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  281. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  282. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  283. package/src/ui/components/primitives/IconButton.tsx +58 -0
  284. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  285. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  286. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Switch.tsx +32 -0
  288. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  289. package/src/ui/components/primitives/Tabs.tsx +102 -0
  290. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  291. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  292. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  293. package/src/ui/components/primitives/Tree.tsx +137 -0
  294. package/src/ui/components/primitives/frame.test.ts +12 -0
  295. package/src/ui/components/primitives/frame.ts +17 -0
  296. package/src/ui/components/primitives/icons.ts +61 -0
  297. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  298. package/src/ui/components/primitives/tree-model.ts +144 -0
  299. package/src/ui/components/syntax-highlight.ts +154 -0
  300. package/src/ui/components/truncate-text.ts +7 -0
  301. package/src/ui/components/truncate-title.test.ts +31 -0
  302. package/src/ui/components/truncate-title.ts +16 -0
  303. package/src/ui/use-frame-measure.ts +88 -0
  304. package/src/ui/use-terminal-virtualizer.ts +286 -0
  305. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  306. package/src/workbench/components/ChangesColumn.tsx +210 -0
  307. package/src/workbench/components/ChangesFileTree.test.tsx +184 -0
  308. package/src/workbench/components/EditorGrid.split.test.tsx +119 -0
  309. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  310. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  311. package/src/workbench/components/EditorGrid.tsx +552 -0
  312. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  313. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  314. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  315. package/src/workbench/components/FileContentsView.tsx +104 -0
  316. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  317. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  318. package/src/workbench/components/GridTabContent.tsx +195 -0
  319. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  320. package/src/workbench/components/ProjectTreeView.test.tsx +239 -0
  321. package/src/workbench/components/ProjectTreeView.tsx +159 -0
  322. package/src/workbench/components/TreePane.stories.tsx +106 -0
  323. package/src/workbench/components/TreePane.tsx +157 -0
  324. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  325. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  326. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  327. package/src/workbench/components/editor-grid.test.ts +184 -0
  328. package/src/workbench/components/editor-grid.ts +284 -0
  329. package/src/workbench/components/file-tree.test.ts +60 -0
  330. package/src/workbench/components/file-tree.ts +114 -0
  331. package/src/workbench/use-changes-workbench.ts +210 -0
  332. package/src/workbench/use-remember-layout.ts +33 -0
  333. package/src/workbench/use-repo-changes.ts +93 -0
  334. package/src/workbench/workbench-snapshot.test.ts +99 -0
  335. package/src/workbench/workbench-snapshot.ts +59 -0
  336. package/src/App.diff.test.tsx +0 -99
  337. package/src/App.export.test.tsx +0 -88
  338. package/src/App.plan-review.test.tsx +0 -259
  339. package/src/App.readonly.test.tsx +0 -148
  340. package/src/App.rendering.test.tsx +0 -108
  341. package/src/App.submit-confirm.test.tsx +0 -183
  342. package/src/App.test.tsx +0 -218
  343. package/src/App.tsx +0 -1249
  344. package/src/config.test.ts +0 -116
  345. package/src/config.ts +0 -150
  346. package/src/editor.ts +0 -34
  347. package/src/herdr.test.ts +0 -68
  348. package/src/herdr.ts +0 -34
  349. package/src/keymap.test.ts +0 -270
  350. package/src/keymap.ts +0 -173
  351. package/src/run.ts +0 -30
  352. package/src/serve.ts +0 -75
  353. package/src/session-controller.ts +0 -398
  354. package/src/syntax.test.ts +0 -41
  355. package/src/syntax.ts +0 -88
  356. package/src/theme.ts +0 -46
  357. package/src/view-diff.test.ts +0 -55
  358. package/src/view-diff.ts +0 -86
  359. package/src/view.test.ts +0 -157
  360. package/src/view.ts +0 -364
@@ -0,0 +1,618 @@
1
+ import { beforeEach, describe, expect, mock, test, type Mock } from "bun:test";
2
+ import { ManualClock } from "@opentui/core/testing";
3
+ import { SCHEMA_VERSION, type Annotation, type SharePolicy, type Thread } from "@cueloop/schema";
4
+ import type { ThreadClient } from "@cueloop/daemon/client";
5
+ import {
6
+ createReviewController,
7
+ SHARE_RECONNECT_MAX_MS,
8
+ SHARE_RECONNECT_MIN_MS,
9
+ type ShareTransport,
10
+ } from "./thread-controller";
11
+ import {
12
+ snapshotThreadConversation,
13
+ mergeFromShare,
14
+ type ShareWatchHandlers,
15
+ } from "../integrations/share";
16
+
17
+ const publishShare = mock(async (_session: Thread) => ({
18
+ line: "ssh p_abc123xy@cueloop.dev",
19
+ copied: true,
20
+ }));
21
+ let remote: Thread;
22
+ const pullShare = mock(async () => remote);
23
+ const pushShare = mock(
24
+ async (
25
+ _shareId: string,
26
+ _annotations: Array<Omit<Annotation, "createdAt">>,
27
+ _policy?: SharePolicy,
28
+ ) => {},
29
+ );
30
+
31
+ const revokeShare = mock(async (_shareId: string) => {});
32
+
33
+ const shareTransport: ShareTransport = {
34
+ publish: publishShare,
35
+ pull: pullShare,
36
+ push: pushShare,
37
+ watch: () => () => {},
38
+ revoke: revokeShare,
39
+ parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
40
+ formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
41
+ collaboratorAnnotations: (session) => session.annotations.filter((entry) => entry.author),
42
+ mergeFromShare,
43
+ };
44
+
45
+ function sessionFixture(overrides: Partial<Thread> = {}): Thread {
46
+ return {
47
+ schemaVersion: SCHEMA_VERSION,
48
+ id: "ses_1",
49
+ workspace: { repoRoot: "/repo", branch: "main" },
50
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
51
+ revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00.000Z" }],
52
+ annotations: [],
53
+ message: null,
54
+ status: "pending",
55
+ createdAt: "2026-01-01T00:00:00.000Z",
56
+ ...overrides,
57
+ };
58
+ }
59
+
60
+ function annotation(id: string, author: string): Annotation {
61
+ return {
62
+ id,
63
+ kind: "comment",
64
+ anchor: { quote: "Plan", prefix: "", suffix: "" },
65
+ body: "note",
66
+ author,
67
+ createdAt: "2026-01-01T00:00:00.000Z",
68
+ };
69
+ }
70
+
71
+ interface FakeThreadClient extends ThreadClient {
72
+ sessionComment: Mock<ThreadClient["sessionComment"]>;
73
+ }
74
+
75
+ const unimplemented = (member: string) => () =>
76
+ Promise.reject(new Error(`fakeClient does not implement ${member}`));
77
+
78
+ function fakeClient(session: Thread): FakeThreadClient {
79
+ return {
80
+ onEvent: () => () => {},
81
+ subscribe: async () => {},
82
+ sessionGet: async () => session,
83
+ sessionList: async () => [session],
84
+ sessionComment: mock<ThreadClient["sessionComment"]>(async () => session),
85
+ sessionAnnotate: unimplemented("sessionAnnotate"),
86
+ sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
87
+ sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
88
+ sessionCutBlock: unimplemented("sessionCutBlock"),
89
+ sessionRestoreBlock: unimplemented("sessionRestoreBlock"),
90
+ sessionCurate: unimplemented("sessionCurate"),
91
+ sessionNavigate: unimplemented("sessionNavigate"),
92
+ sessionBranch: unimplemented("sessionBranch"),
93
+ sessionSwitch: unimplemented("sessionSwitch"),
94
+ sessionLabel: unimplemented("sessionLabel"),
95
+ sessionFork: unimplemented("sessionFork"),
96
+ sessionSetViewed: unimplemented("sessionSetViewed"),
97
+ sessionSetTitle: unimplemented("sessionSetTitle"),
98
+ sessionSetShares: mock(
99
+ async (_id: string, shares: import("@cueloop/schema").ShareLink[]) => (
100
+ (session.shares = shares),
101
+ session
102
+ ),
103
+ ),
104
+ sessionMergeShared: mock(async (_id: string, incoming: { annotations: Annotation[] }) => {
105
+ const known = new Set(session.annotations.map((existing) => existing.id));
106
+
107
+ for (const note of incoming.annotations)
108
+ if (!known.has(note.id)) session.annotations.push(note);
109
+
110
+ return session;
111
+ }),
112
+ sessionDelete: unimplemented("sessionDelete"),
113
+ sessionSetSelfName: unimplemented("sessionSetSelfName"),
114
+ sessionSendMessage: unimplemented("sessionSendMessage"),
115
+ close: () => {},
116
+ };
117
+ }
118
+
119
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
120
+
121
+ async function connectedController(
122
+ session: Thread,
123
+ clock?: ManualClock,
124
+ transport: ShareTransport = shareTransport,
125
+ ): Promise<{ controller: ReturnType<typeof createReviewController>; client: FakeThreadClient }> {
126
+ const client = fakeClient(session);
127
+ const controller = createReviewController({
128
+ sessionId: session.id,
129
+ openClient: async () => client,
130
+ clock,
131
+ shareTransport: transport,
132
+ });
133
+
134
+ controller.connect();
135
+ await tick();
136
+
137
+ return { controller, client };
138
+ }
139
+
140
+ describe("share", () => {
141
+ test("publishes a public link and records it on the session", async () => {
142
+ // Arrange
143
+ const { controller, client } = await connectedController(sessionFixture());
144
+
145
+ // Act
146
+ controller.share();
147
+ await tick();
148
+
149
+ // Assert - a public link (no auth) is persisted with the returned id
150
+ expect(client.sessionSetShares).toHaveBeenCalledWith("ses_1", [
151
+ { id: "p_abc123xy", name: undefined, requireAuth: false, allowlist: [], shareBranch: "main" },
152
+ ]);
153
+ });
154
+
155
+ test("surfaces the ssh line as a toast, not an inline status", async () => {
156
+ // Arrange
157
+ const { controller } = await connectedController(sessionFixture());
158
+
159
+ // Act
160
+ controller.share();
161
+ await tick();
162
+
163
+ // Assert
164
+ expect(controller.getSnapshot().toast).toEqual({
165
+ body: "ssh p_abc123xy@cueloop.dev",
166
+ title: "link copied",
167
+ });
168
+ expect(controller.getSnapshot().status).not.toContain("ssh p_abc123xy@cueloop.dev");
169
+ });
170
+ });
171
+
172
+ describe("updateShareLink policy", () => {
173
+ test("pushes the allowlist up to the gateway when the plan is already shared", async () => {
174
+ // Arrange
175
+ pushShare.mockClear();
176
+ const { controller } = await connectedController(
177
+ sessionFixture({ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }] }),
178
+ );
179
+
180
+ // Act
181
+ controller.updateShareLink("p_abc123xy", { requireAuth: true, allowlist: ["octocat"] });
182
+ await tick();
183
+
184
+ // Assert - the existing link must enforce the new access, so it rides the push
185
+ expect(pushShare.mock.calls[0]?.[0]).toBe("p_abc123xy");
186
+ expect(pushShare.mock.calls[0]?.[2]).toEqual({ requireAuth: true, allowlist: ["octocat"] });
187
+ });
188
+
189
+ test("does not push when the plan was never shared", async () => {
190
+ // Arrange
191
+ pushShare.mockClear();
192
+ const { controller } = await connectedController(sessionFixture());
193
+
194
+ // Act
195
+ controller.updateShareLink("p_abc123xy", { requireAuth: true, allowlist: ["octocat"] });
196
+ await tick();
197
+
198
+ // Assert
199
+ expect(pushShare).not.toHaveBeenCalled();
200
+ });
201
+ });
202
+
203
+ describe("pullShared", () => {
204
+ test("unions collaborator notes and identities in when the plan was shared", async () => {
205
+ // Arrange
206
+ remote = sessionFixture({
207
+ annotations: [annotation("a1", "SHA256:mate")],
208
+ participants: [{ id: "SHA256:mate", provider: "ssh", name: "Sam" }],
209
+ });
210
+ const { controller, client } = await connectedController(
211
+ sessionFixture({ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }] }),
212
+ );
213
+
214
+ // Act
215
+ controller.pullShared();
216
+ await tick();
217
+
218
+ // Assert
219
+ expect(pullShare).toHaveBeenCalledWith("p_abc123xy");
220
+ expect(client.sessionMergeShared).toHaveBeenCalledWith("ses_1", {
221
+ annotations: remote.annotations,
222
+ participants: remote.participants,
223
+ });
224
+ });
225
+
226
+ test("does nothing for a plan that was never shared", async () => {
227
+ // Arrange
228
+ pullShare.mockClear();
229
+ const { controller, client } = await connectedController(sessionFixture());
230
+
231
+ // Act
232
+ controller.pullShared();
233
+ await tick();
234
+
235
+ // Assert
236
+ expect(pullShare).not.toHaveBeenCalled();
237
+ expect(client.sessionMergeShared).not.toHaveBeenCalled();
238
+ });
239
+ });
240
+
241
+ describe("mirror on annotate", () => {
242
+ test("pushes an edited note up when the plan is shared", async () => {
243
+ // Arrange
244
+ pushShare.mockClear();
245
+ const { controller } = await connectedController(
246
+ sessionFixture({
247
+ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }],
248
+ annotations: [annotation("a1", "SHA256:me")],
249
+ }),
250
+ );
251
+
252
+ // Act
253
+ controller.updateAnnotation("a1", "revised body");
254
+ await tick();
255
+
256
+ // Assert
257
+ expect(pushShare).toHaveBeenCalledTimes(1);
258
+ expect(pushShare.mock.calls[0]?.[0]).toBe("p_abc123xy");
259
+ expect(pushShare.mock.calls[0]?.[1]).toEqual([
260
+ {
261
+ id: "a1",
262
+ kind: "comment",
263
+ anchor: { quote: "Plan", prefix: "", suffix: "" },
264
+ body: "revised body",
265
+ },
266
+ ]);
267
+ });
268
+
269
+ test("does not push when the plan was never shared", async () => {
270
+ // Arrange
271
+ pushShare.mockClear();
272
+ const { controller } = await connectedController(
273
+ sessionFixture({ annotations: [annotation("a1", "SHA256:me")] }),
274
+ );
275
+
276
+ // Act
277
+ controller.updateAnnotation("a1", "revised body");
278
+ await tick();
279
+
280
+ // Assert
281
+ expect(pushShare).not.toHaveBeenCalled();
282
+ });
283
+
284
+ test("does not push when the local write is rejected", async () => {
285
+ // Arrange - the daemon write fails (e.g. a resolved session)
286
+ pushShare.mockClear();
287
+ const { controller, client } = await connectedController(
288
+ sessionFixture({
289
+ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }],
290
+ annotations: [annotation("a1", "SHA256:me")],
291
+ }),
292
+ );
293
+
294
+ client.sessionComment.mockImplementationOnce(async () => {
295
+ throw new Error("session is resolved");
296
+ });
297
+
298
+ // Act
299
+ controller.updateAnnotation("a1", "revised body");
300
+ await tick();
301
+
302
+ // Assert - a failed local write must never leak to the share
303
+ expect(pushShare).not.toHaveBeenCalled();
304
+ });
305
+ });
306
+
307
+ describe("reply", () => {
308
+ test("a reply shares the root's anchor, names it in replyTo, and mirrors up", async () => {
309
+ // Arrange
310
+ pushShare.mockClear();
311
+ const { controller, client } = await connectedController(
312
+ sessionFixture({
313
+ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }],
314
+ annotations: [annotation("a1", "SHA256:ana")],
315
+ }),
316
+ );
317
+
318
+ // Act
319
+ const id = controller.reply("a1", "agreed");
320
+
321
+ await tick();
322
+
323
+ // Assert
324
+ const wire = client.sessionComment.mock.calls.at(-1)![1];
325
+
326
+ expect(wire).toEqual({
327
+ id: id!,
328
+ kind: "comment",
329
+ anchor: { quote: "Plan", prefix: "", suffix: "" },
330
+ body: "agreed",
331
+ replyTo: "a1",
332
+ });
333
+ expect(pushShare.mock.calls[0]?.[1]).toEqual([wire]);
334
+ });
335
+
336
+ test("editing a reply keeps its reply link", async () => {
337
+ // Arrange
338
+ const { controller, client } = await connectedController(
339
+ sessionFixture({
340
+ annotations: [
341
+ annotation("a1", "SHA256:ana"),
342
+ { ...annotation("a2", "SHA256:me"), replyTo: "a1" },
343
+ ],
344
+ }),
345
+ );
346
+
347
+ // Act
348
+ controller.updateAnnotation("a2", "revised reply");
349
+ await tick();
350
+
351
+ // Assert
352
+ expect(client.sessionComment.mock.calls.at(-1)?.[1]).toMatchObject({
353
+ id: "a2",
354
+ body: "revised reply",
355
+ replyTo: "a1",
356
+ });
357
+ });
358
+
359
+ test("a reply to a reply hangs off the discussion's root comment", async () => {
360
+ // Arrange
361
+ const { controller, client } = await connectedController(
362
+ sessionFixture({
363
+ annotations: [
364
+ annotation("a1", "SHA256:ana"),
365
+ { ...annotation("a2", "SHA256:bob"), replyTo: "a1" },
366
+ ],
367
+ }),
368
+ );
369
+
370
+ // Act
371
+ controller.reply("a2", "same thread");
372
+ await tick();
373
+
374
+ // Assert
375
+ expect(client.sessionComment.mock.calls.at(-1)?.[1]).toMatchObject({ replyTo: "a1" });
376
+ });
377
+
378
+ test("replying to an unknown annotation does nothing", async () => {
379
+ // Arrange
380
+ const { controller, client } = await connectedController(sessionFixture());
381
+
382
+ // Act + Assert
383
+ expect(controller.reply("missing", "x")).toBeUndefined();
384
+ expect(client.sessionComment).not.toHaveBeenCalled();
385
+ });
386
+ });
387
+
388
+ describe("startShareSync", () => {
389
+ /** A fake watch stream: records handlers so a test can push a session or drop the link. */
390
+ const streams: Array<{ shareId: string; handlers: ShareWatchHandlers; stopped: boolean }> = [];
391
+ const watchShare = mock((shareId: string, handlers: ShareWatchHandlers) => {
392
+ const stream = { shareId, handlers, stopped: false };
393
+
394
+ streams.push(stream);
395
+
396
+ return () => {
397
+ stream.stopped = true;
398
+ };
399
+ });
400
+ const liveTransport: ShareTransport = { ...shareTransport, watch: watchShare };
401
+
402
+ beforeEach(() => {
403
+ streams.length = 0;
404
+ watchShare.mockClear();
405
+ pullShare.mockClear();
406
+ remote = sessionFixture({ annotations: [] });
407
+ });
408
+
409
+ test("connect pulls once to catch up, then follows the stream and merges each session it delivers", async () => {
410
+ // Arrange
411
+ const clock = new ManualClock();
412
+ const { controller, client } = await connectedController(
413
+ sessionFixture({ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }] }),
414
+ clock,
415
+ liveTransport,
416
+ );
417
+
418
+ // Act
419
+ controller.startShareSync();
420
+ await tick();
421
+
422
+ // Assert - one catch-up pull, one open stream on the share
423
+ expect(pullShare).toHaveBeenCalledTimes(1);
424
+ expect(streams).toHaveLength(1);
425
+ expect(streams[0]!.shareId).toBe("p_abc123xy");
426
+
427
+ // Act - the gateway pushes a change carrying Ana's note
428
+ streams[0]!.handlers.onSession(
429
+ sessionFixture({ annotations: [annotation("a9", "SHA256:ana")] }),
430
+ );
431
+ await tick();
432
+
433
+ // Assert - merged without another pull
434
+ expect(client.sessionMergeShared).toHaveBeenLastCalledWith(
435
+ "ses_1",
436
+ expect.objectContaining({
437
+ annotations: [expect.objectContaining({ id: "a9", author: "SHA256:ana" })],
438
+ }),
439
+ );
440
+ expect(pullShare).toHaveBeenCalledTimes(1);
441
+ });
442
+
443
+ test("a dropped stream reconnects with doubling backoff and catches up on each connect", async () => {
444
+ // Arrange
445
+ const clock = new ManualClock();
446
+ const { controller } = await connectedController(
447
+ sessionFixture({ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }] }),
448
+ clock,
449
+ liveTransport,
450
+ );
451
+
452
+ controller.startShareSync();
453
+ await tick();
454
+
455
+ // Act - the link drops: nothing until the floor delay has passed
456
+ streams[0]!.handlers.onClose("ssh exited 255");
457
+ clock.advance(SHARE_RECONNECT_MIN_MS - 1);
458
+ await tick();
459
+ expect(streams).toHaveLength(1);
460
+ clock.advance(1);
461
+ await tick();
462
+
463
+ // Assert - reconnected, with a fresh catch-up pull
464
+ expect(streams).toHaveLength(2);
465
+ expect(pullShare).toHaveBeenCalledTimes(2);
466
+
467
+ // Act - drops again: the wait doubles
468
+ streams[1]!.handlers.onClose("ssh exited 255");
469
+ clock.advance(SHARE_RECONNECT_MIN_MS);
470
+ await tick();
471
+ expect(streams).toHaveLength(2);
472
+ clock.advance(SHARE_RECONNECT_MIN_MS);
473
+ await tick();
474
+ expect(streams).toHaveLength(3);
475
+
476
+ // Act - a delivered session resets the backoff to the floor
477
+ streams[2]!.handlers.onSession(remote);
478
+ streams[2]!.handlers.onClose("ssh exited 255");
479
+ clock.advance(SHARE_RECONNECT_MIN_MS);
480
+ await tick();
481
+
482
+ // Assert
483
+ expect(streams).toHaveLength(4);
484
+ });
485
+
486
+ test("stop closes the stream and a late close never reconnects", async () => {
487
+ // Arrange
488
+ const clock = new ManualClock();
489
+ const { controller } = await connectedController(
490
+ sessionFixture({ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }] }),
491
+ clock,
492
+ liveTransport,
493
+ );
494
+ const stop = controller.startShareSync();
495
+
496
+ await tick();
497
+
498
+ // Act
499
+ stop();
500
+ streams[0]!.handlers.onClose("killed");
501
+ clock.advance(SHARE_RECONNECT_MAX_MS * 2);
502
+ await tick();
503
+
504
+ // Assert
505
+ expect(streams[0]!.stopped).toBe(true);
506
+ expect(streams).toHaveLength(1);
507
+ });
508
+
509
+ test("a restart replaces the stream instead of doubling it", async () => {
510
+ // Arrange
511
+ const clock = new ManualClock();
512
+ const { controller } = await connectedController(
513
+ sessionFixture({ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }] }),
514
+ clock,
515
+ liveTransport,
516
+ );
517
+
518
+ controller.startShareSync();
519
+ await tick();
520
+
521
+ // Act
522
+ controller.startShareSync();
523
+ await tick();
524
+ streams[0]!.handlers.onClose("stale");
525
+ clock.advance(SHARE_RECONNECT_MAX_MS * 2);
526
+ await tick();
527
+
528
+ // Assert - the first stream was stopped and its close re-armed nothing
529
+ expect(streams[0]!.stopped).toBe(true);
530
+ expect(streams).toHaveLength(2);
531
+ expect(streams[1]!.stopped).toBe(false);
532
+ });
533
+ });
534
+
535
+ describe("revoke", () => {
536
+ test("unshare revokes the gateway blob for the shared thread", async () => {
537
+ revokeShare.mockClear();
538
+ const { controller } = await connectedController(
539
+ sessionFixture({ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }] }),
540
+ );
541
+
542
+ controller.unshare();
543
+ await tick();
544
+
545
+ expect(revokeShare).toHaveBeenCalledWith("p_abc123xy");
546
+ });
547
+
548
+ test("unshare on an unshared thread is a no-op", async () => {
549
+ revokeShare.mockClear();
550
+ const { controller } = await connectedController(sessionFixture());
551
+
552
+ controller.unshare();
553
+ await tick();
554
+
555
+ expect(revokeShare).not.toHaveBeenCalled();
556
+ });
557
+
558
+ test("deleting a shared thread revokes its share first", async () => {
559
+ revokeShare.mockClear();
560
+ const session = sessionFixture({
561
+ shares: [{ id: "p_abc123xy", requireAuth: false, allowlist: [] }],
562
+ });
563
+ const client = fakeClient(session);
564
+
565
+ client.sessionDelete = mock(async () => {});
566
+ const controller = createReviewController({
567
+ sessionId: session.id,
568
+ openClient: async () => client,
569
+ shareTransport,
570
+ });
571
+
572
+ controller.connect();
573
+ await tick();
574
+ controller.deleteSession(session.id);
575
+ await tick();
576
+
577
+ expect(revokeShare).toHaveBeenCalledWith("p_abc123xy");
578
+ expect(client.sessionDelete).toHaveBeenCalledWith(session.id);
579
+ });
580
+ });
581
+
582
+ test("share publication captures an empty artifact's separate conversation without execution records", async () => {
583
+ const thread = sessionFixture({ artifact: { type: "plan", content: "", meta: {} } });
584
+ const { controller, client } = await connectedController(thread);
585
+ const state = {
586
+ threadId: thread.id,
587
+ phase: { kind: "idle" as const },
588
+ harness: { id: "pi", label: "pi", sessionId: "private-session" },
589
+ messages: [
590
+ {
591
+ id: "answer",
592
+ role: "agent" as const,
593
+ text: "Visible conversation",
594
+ complete: true,
595
+ revision: 1,
596
+ },
597
+ ],
598
+ comments: [],
599
+ tools: [],
600
+ promptOperations: [
601
+ { operationId: "private-operation", fingerprint: "private-hash", result: [] },
602
+ ],
603
+ };
604
+
605
+ client.agentGet = async () => state;
606
+ publishShare.mockClear();
607
+ controller.share();
608
+ await tick();
609
+ await tick();
610
+ const shared = publishShare.mock.calls[0]![0];
611
+
612
+ expect(shared.agentSnapshot?.messages).toEqual(state.messages);
613
+ expect(shared.agentSnapshot?.harness).toEqual({ id: "pi", label: "pi" });
614
+ expect(shared.agentSnapshot?.promptOperations).toBeUndefined();
615
+ expect(thread.agentSnapshot).toBeUndefined();
616
+ expect(await snapshotThreadConversation(thread, {})).toBe(thread);
617
+ controller.close();
618
+ });