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

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