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

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