@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,176 @@
1
+ /** Prototype review end to end, in the opt-in EXPERIMENTAL pixel mode ([experimental] prototype_pixels): a click on the rendered page selects a DOM element and opens the compose card directly - the same inline composer plan and diff use - a typed comment saves through the controller on cmd+enter, and the annotation anchors to the element's selector. The headless browser is replaced with a fake so no Chrome or kitty output is needed. Char-frame assertions over the real App and a real in-process daemon. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { DaemonServer } from "@cueloop/daemon";
9
+ import type { Thread } from "@cueloop/schema";
10
+ import { App } from "./App";
11
+ import {
12
+ setPrototypeRendererFactory,
13
+ type PrototypeElement,
14
+ type PrototypeRenderer,
15
+ } from "../prototype/prototype-browser";
16
+ import {
17
+ isolateUserConfig,
18
+ press,
19
+ renderReadyApp,
20
+ settle,
21
+ typeText,
22
+ waitForState,
23
+ waitForText,
24
+ waitForTextGone,
25
+ } from "../testing/test-support";
26
+
27
+ const HTML = "<main><div class='card'><h2>Pro</h2><p>$24/mo</p></div></main>";
28
+
29
+ const FAKE_ELEMENT: PrototypeElement = {
30
+ selector: "main > div.card",
31
+ quote: "Pro $24/mo",
32
+ // mid-page, so the popover anchors well away from the click point below
33
+ box: { x: 440, y: 320, width: 320, height: 220 },
34
+ };
35
+
36
+ let rendered = false;
37
+ let screenshotCount = 0;
38
+ let scrollDeltas: number[] = [];
39
+ const fakeRenderer: PrototypeRenderer = {
40
+ viewport: { width: 1280, height: 800 },
41
+ screenshot: async () => {
42
+ rendered = true;
43
+ screenshotCount += 1;
44
+
45
+ return new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
46
+ },
47
+ elementAt: async () => FAKE_ELEMENT,
48
+ scrollBy: async (deltaY) => {
49
+ scrollDeltas.push(deltaY);
50
+
51
+ return true;
52
+ },
53
+ close: async () => undefined,
54
+ };
55
+
56
+ let home: string;
57
+ let server: DaemonServer;
58
+ let session: Thread;
59
+ let restoreUserConfig: () => void;
60
+
61
+ beforeEach(() => {
62
+ home = mkdtempSync(join(tmpdir(), "cueloop-prototype-"));
63
+ // the pixel mockup is opt-in; these tests exercise that experimental path
64
+ writeFileSync(join(home, "config.toml"), "[experimental]\nprototype_pixels = true\n");
65
+ restoreUserConfig = isolateUserConfig(home, "config.toml");
66
+ server = new DaemonServer({ home, idleExitMs: 0 });
67
+ server.start();
68
+ session = server.core.sessionCreate({
69
+ workspace: { repoRoot: "/repo", branch: "main" },
70
+ artifact: {
71
+ type: "prototype",
72
+ content: HTML,
73
+ meta: { title: "Pricing cards", prototypePath: "/repo/cards.html" },
74
+ },
75
+ });
76
+ rendered = false;
77
+ screenshotCount = 0;
78
+ scrollDeltas = [];
79
+ setPrototypeRendererFactory(async () => fakeRenderer);
80
+ });
81
+ afterEach(() => {
82
+ setPrototypeRendererFactory(null);
83
+ restoreUserConfig();
84
+ server.stop();
85
+ rmSync(home, { recursive: true, force: true });
86
+ });
87
+
88
+ async function renderApp() {
89
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
90
+ width: 120,
91
+ height: 32,
92
+ });
93
+
94
+ await waitForText(setup, "cueloop");
95
+
96
+ return setup;
97
+ }
98
+
99
+ describe("prototype review", () => {
100
+ test("click an element, comment on it, and it saves to the daemon", async () => {
101
+ // Arrange - wait until the fake browser produced its first screenshot, so
102
+ // the sheet is interactive before the click
103
+ const setup = await renderApp();
104
+
105
+ await waitForState(setup, () => rendered);
106
+ await settle(setup);
107
+ // selecting an element must not re-screenshot the page (the compose-open
108
+ // lag regression): the click opens the composer, not a fresh Chromium capture
109
+ const screenshotsAfterLoad = screenshotCount;
110
+
111
+ // Act - a click opens the compose card directly on the element
112
+ await setup.mockMouse.click(6, 6);
113
+ await waitForText(setup, "●");
114
+ expect(screenshotCount).toBe(screenshotsAfterLoad);
115
+ // the keymap-suppression flag must propagate before typing so keys reach the
116
+ // textarea, not the global keymap
117
+ await settle(setup);
118
+ await typeText(setup, "tighten the padding");
119
+ // cmd+enter sends, matching the plan and diff composer; plain enter is a newline
120
+ setup.mockInput.pressKey("RETURN", { meta: true });
121
+
122
+ // Assert - the annotation is stored against the element's selector
123
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
124
+ const stored = server.core.sessionGet(session.id).annotations[0]!;
125
+
126
+ expect(stored.kind).toBe("comment");
127
+ expect(stored.body).toBe("tighten the padding");
128
+ expect(stored.anchor.selector).toBe("main > div.card");
129
+ });
130
+
131
+ test("escape cancels the compose, matching the plan composer", async () => {
132
+ // Arrange
133
+ const setup = await renderApp();
134
+
135
+ await waitForState(setup, () => rendered);
136
+ await settle(setup);
137
+ await setup.mockMouse.click(6, 6);
138
+ await waitForText(setup, "●");
139
+ await settle(setup);
140
+
141
+ // Act
142
+ await press(setup, "escape");
143
+
144
+ // Assert - the composer closes and nothing was saved
145
+ await waitForTextGone(setup, "●");
146
+ expect(server.core.sessionGet(session.id).annotations).toHaveLength(0);
147
+ });
148
+
149
+ test("wheel over the preview scrolls the page down, then up, and clears a selection", async () => {
150
+ // Arrange
151
+ const setup = await renderApp();
152
+
153
+ await waitForState(setup, () => rendered);
154
+ await settle(setup);
155
+
156
+ // open a selection, then scroll: the scroll drops it (it would drift once
157
+ // the page moves under it)
158
+ await setup.mockMouse.click(6, 6);
159
+ await waitForText(setup, "●");
160
+ // scrolling moves the page, so it DOES re-screenshot (unlike a click)
161
+ const screenshotsBeforeScroll = screenshotCount;
162
+
163
+ await setup.mockMouse.scroll(6, 6, "down");
164
+ await waitForState(setup, () => scrollDeltas.length === 1);
165
+ await waitForTextGone(setup, "●");
166
+ await waitForState(setup, () => screenshotCount > screenshotsBeforeScroll);
167
+
168
+ // Act - scroll back up
169
+ await setup.mockMouse.scroll(6, 6, "up");
170
+ await waitForState(setup, () => scrollDeltas.length === 2);
171
+
172
+ // Assert - down was a positive delta, up was negative
173
+ expect(scrollDeltas[0]!).toBeGreaterThan(0);
174
+ expect(scrollDeltas[1]!).toBeLessThan(0);
175
+ });
176
+ });
@@ -0,0 +1,210 @@
1
+ /** Observer mode (tier 2): the App with readOnly renders and navigates like the controller's, but every mutating primitive answers "observer - read-only" and leaves daemon state untouched. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { DaemonServer } from "@cueloop/daemon";
9
+ import { makeAnchor, parseBlocks, type Thread } from "@cueloop/schema";
10
+ import { App } from "./App";
11
+ import {
12
+ clickText,
13
+ dragText,
14
+ frameRow,
15
+ isolateUserConfig,
16
+ navCommand,
17
+ pressKey,
18
+ renderReadyApp,
19
+ typeText,
20
+ waitForText,
21
+ } from "../testing/test-support";
22
+
23
+ const PLAN = `# Migration Plan
24
+
25
+ ## Context
26
+
27
+ The daemon persists sessions to disk atomically.
28
+
29
+ ## Steps
30
+
31
+ - move the store
32
+ - add recovery
33
+ `;
34
+
35
+ let home: string;
36
+ let restoreUserConfig: () => void;
37
+ let server: DaemonServer;
38
+ let session: Thread;
39
+
40
+ beforeEach(() => {
41
+ home = mkdtempSync(join(tmpdir(), "cueloop-observer-"));
42
+ restoreUserConfig = isolateUserConfig(home);
43
+ server = new DaemonServer({ home, idleExitMs: 0 });
44
+ server.start();
45
+ session = server.core.sessionCreate({
46
+ workspace: { repoRoot: "/repo", branch: "main" },
47
+ artifact: {
48
+ type: "plan",
49
+ content: PLAN,
50
+ meta: { title: "Migration Plan", planPath: "plan.md" },
51
+ },
52
+ });
53
+ });
54
+ afterEach(() => {
55
+ restoreUserConfig();
56
+ server.stop();
57
+ rmSync(home, { recursive: true, force: true });
58
+ });
59
+
60
+ async function renderObserver() {
61
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
62
+ width: 120,
63
+ height: 32,
64
+ });
65
+
66
+ await waitForText(setup, "cueloop");
67
+
68
+ return setup;
69
+ }
70
+
71
+ type Setup = Awaited<ReturnType<typeof renderObserver>>;
72
+
73
+ /** Session state that any mutating primitive would change. */
74
+ function snapshot() {
75
+ const stored = server.core.sessionGet(session.id);
76
+
77
+ return {
78
+ annotations: stored.annotations.length,
79
+ workingCopy: stored.workingCopy,
80
+ status: stored.status,
81
+ };
82
+ }
83
+
84
+ describe("observer rendering", () => {
85
+ test("the minimal header mirrors the thread name, with no observer badge", async () => {
86
+ // Arrange
87
+ const setup = await renderObserver();
88
+
89
+ // Assert - the header shows the thread title only; read-only is enforced by
90
+ // the blocked primitives below, not a header badge
91
+ const frame = setup.captureCharFrame();
92
+
93
+ expect(frame).toContain("Migration Plan");
94
+ expect(frame).not.toContain("· observer");
95
+ });
96
+ });
97
+
98
+ describe("observer primitives are blocked", () => {
99
+ const attempts: Array<[string, (setup: Setup) => Promise<void>]> = [
100
+ ["comment (typing)", (setup) => typeText(setup, "c")],
101
+ ["cut (option+x)", (setup) => navCommand(setup, "x")],
102
+ ["edit (ctrl+e)", (setup) => pressKey(setup, "e", { ctrl: true })],
103
+ ["submit (cmd+enter)", (setup) => pressKey(setup, "RETURN", { meta: true })],
104
+ ];
105
+
106
+ for (const [primitive, attempt] of attempts) {
107
+ test(`${primitive} answers observer - read-only and mutates nothing`, async () => {
108
+ const setup = await renderObserver();
109
+ const before = snapshot();
110
+
111
+ await clickText(setup, "daemon");
112
+ await attempt(setup);
113
+ const frame = await waitForText(setup, "observer - read-only");
114
+
115
+ // no draft card and no submit card ever appear
116
+ expect(frame).not.toContain("● c");
117
+ expect(frame).not.toContain("[approve]");
118
+ expect(snapshot()).toEqual(before);
119
+ });
120
+ }
121
+
122
+ test("typing over a marked span is blocked too", async () => {
123
+ // Arrange
124
+ const setup = await renderObserver();
125
+
126
+ // Act
127
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
128
+ await typeText(setup, "c");
129
+
130
+ // Assert
131
+ const frame = await waitForText(setup, "observer - read-only");
132
+
133
+ expect(frame).not.toContain("● c");
134
+ expect(snapshot().annotations).toBe(0);
135
+ });
136
+ });
137
+
138
+ describe("a resolved review is read-only for its owner too", () => {
139
+ test("typing, deleting a card, and editing answer review submitted - read-only", async () => {
140
+ // Arrange: a comment exists and the message is in
141
+ server.core.sessionAnnotate(session.id, {
142
+ id: "a_done",
143
+ kind: "comment",
144
+ anchor: makeAnchor(parseBlocks(PLAN), 2, 0, 10),
145
+ body: "settled",
146
+ });
147
+ server.core.sessionSendMessage(session.id, "approved", "");
148
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
149
+ width: 120,
150
+ height: 32,
151
+ });
152
+
153
+ await waitForText(setup, "settled");
154
+ const before = snapshot();
155
+
156
+ // Act + Assert: a draft never opens
157
+ await clickText(setup, "daemon");
158
+ await typeText(setup, "c");
159
+ await waitForText(setup, "review submitted - read-only");
160
+ expect(setup.captureCharFrame()).not.toContain("● c");
161
+
162
+ // Act + Assert: the focused card cannot be deleted or edited
163
+ await navCommand(setup, "n");
164
+ await pressKey(setup, "BACKSPACE", { meta: true });
165
+ await pressKey(setup, "e", { ctrl: true });
166
+ await navCommand(setup, "x");
167
+ expect(snapshot()).toEqual(before);
168
+ expect(server.core.sessionGet(session.id).annotations).toHaveLength(1);
169
+ });
170
+ });
171
+
172
+ describe("observer navigation still works", () => {
173
+ test("↓ moves the caret between blocks", async () => {
174
+ // Arrange
175
+ const setup = await renderObserver();
176
+
177
+ // Act
178
+ await pressKey(setup, "ARROW_DOWN");
179
+ await pressKey(setup, "ARROW_DOWN");
180
+ await setup.renderOnce();
181
+
182
+ // Assert: the caret cell is painted on the paragraph's row
183
+ const caretRow = setup
184
+ .captureSpans()
185
+ .lines.findIndex((line) =>
186
+ line.spans.some((span) => span.bg.toInts().slice(0, 3).join() === "86,91,104"),
187
+ );
188
+
189
+ expect(caretRow).toBe(frameRow(setup, "persists sessions"));
190
+ });
191
+
192
+ test("option+n focuses annotations made by the controller", async () => {
193
+ // Arrange
194
+ server.core.sessionAnnotate(session.id, {
195
+ id: "a_obs1",
196
+ kind: "comment",
197
+ anchor: makeAnchor(parseBlocks(PLAN), 2, 0, 10),
198
+ body: "From the controller.",
199
+ });
200
+ const setup = await renderObserver();
201
+
202
+ // Act - the observer navigates to the annotation without mutating anything
203
+ await navCommand(setup, "n");
204
+ await setup.renderOnce();
205
+
206
+ // Assert - the observer reads the controller's comment inline in the thread
207
+ await waitForText(setup, "From the controller.");
208
+ expect(snapshot().annotations).toBe(1);
209
+ });
210
+ });
@@ -0,0 +1,99 @@
1
+ /** Regressions from the focus/perf round: ctrl+q must quit from any keyboard state, and clicking a
2
+ * sidebar thread must give it the selected accent at once (the cursor follows the click). */
3
+
4
+ import { afterEach, beforeEach, expect, test } from "bun:test";
5
+ import { mkdtempSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import React from "react";
9
+ import { DaemonServer } from "@cueloop/daemon";
10
+ import { App } from "./App";
11
+ import {
12
+ clickText,
13
+ isolateUserConfig,
14
+ pressKey,
15
+ renderReadyApp,
16
+ waitForText,
17
+ } from "../testing/test-support";
18
+
19
+ const ACCENT_DARK: [number, number, number] = [203, 166, 247];
20
+
21
+ let home: string;
22
+ let restoreUserConfig: () => void;
23
+ let server: DaemonServer;
24
+
25
+ function plan(title: string): void {
26
+ server.core.sessionCreate({
27
+ workspace: { repoRoot: "/repo", branch: "main" },
28
+ artifact: { type: "plan", content: `# ${title}\n`, meta: { title, planPath: "plan.md" } },
29
+ });
30
+ }
31
+
32
+ beforeEach(() => {
33
+ home = mkdtempSync(join(tmpdir(), "cueloop-regression-"));
34
+ restoreUserConfig = isolateUserConfig(home);
35
+ server = new DaemonServer({ home, idleExitMs: 0 });
36
+ server.start();
37
+ plan("First review");
38
+ plan("Second review");
39
+ });
40
+ afterEach(() => {
41
+ restoreUserConfig();
42
+ server.stop();
43
+ rmSync(home, { recursive: true, force: true });
44
+ });
45
+
46
+ /** The text of the sidebar title span currently painted in the accent colour, or undefined. */
47
+ function accentTitle(setup: Awaited<ReturnType<typeof renderReadyApp>>): string | undefined {
48
+ for (const line of setup.captureSpans().lines) {
49
+ for (const span of line.spans) {
50
+ const rgb = span.fg?.toInts();
51
+
52
+ if (
53
+ rgb &&
54
+ rgb[0] === ACCENT_DARK[0] &&
55
+ rgb[1] === ACCENT_DARK[1] &&
56
+ rgb[2] === ACCENT_DARK[2]
57
+ ) {
58
+ const text = span.text.trim();
59
+
60
+ if (text.includes("review")) return text;
61
+ }
62
+ }
63
+ }
64
+
65
+ return undefined;
66
+ }
67
+
68
+ test("ctrl+q quits even when a menu holds the keyboard", async () => {
69
+ let exitCode: number | undefined;
70
+ const setup = await renderReadyApp(<App home={home} onExit={(code) => (exitCode = code)} />, {
71
+ width: 120,
72
+ height: 32,
73
+ });
74
+
75
+ await waitForText(setup, "review");
76
+
77
+ // open the settings dialog from the top-left gear - it takes the keyboard
78
+ await setup.mockMouse.click(1, 0);
79
+ await waitForText(setup, "Keybinds");
80
+
81
+ await pressKey(setup, "q", { ctrl: true });
82
+
83
+ expect(exitCode).toBe(0);
84
+ setup.renderer.destroy();
85
+ });
86
+
87
+ test("clicking a sidebar thread gives it the selected accent at once", async () => {
88
+ const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
89
+
90
+ await waitForText(setup, "Second review");
91
+
92
+ // click whichever thread is not already the selected (accent) one
93
+ const target = accentTitle(setup)?.includes("Second") ? "First review" : "Second review";
94
+
95
+ await clickText(setup, target);
96
+
97
+ expect(accentTitle(setup)).toContain(target.split(" ")[0]!);
98
+ setup.renderer.destroy();
99
+ });
@@ -0,0 +1,134 @@
1
+ /** Plan rendering: code blocks verbatim + contained, spacing never collapses. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { DaemonServer } from "@cueloop/daemon";
9
+ import type { Thread } from "@cueloop/schema";
10
+ import { App } from "./App";
11
+ import { isolateUserConfig, waitForText, renderReadyApp } from "../testing/test-support";
12
+
13
+ const PLAN = `# Render Plan
14
+
15
+ ## Sensor
16
+
17
+ - wire the duck over MQTT
18
+
19
+ \`\`\`tsx
20
+ export function Gate({ full }: { full: number }) {
21
+ const blocked = full < 0.6;
22
+ return <text>{blocked ? "blocked" : "clear"}</text>;
23
+ }
24
+ \`\`\`
25
+
26
+ ## Gate
27
+
28
+ Deploys block below the threshold.
29
+ `;
30
+
31
+ let home: string;
32
+ let restoreUserConfig: () => void;
33
+ let server: DaemonServer;
34
+ let session: Thread;
35
+
36
+ beforeEach(() => {
37
+ home = mkdtempSync(join(tmpdir(), "cueloop-render-"));
38
+ restoreUserConfig = isolateUserConfig(home);
39
+ server = new DaemonServer({ home, idleExitMs: 0 });
40
+ server.start();
41
+ session = server.core.sessionCreate({
42
+ workspace: { repoRoot: "/repo", branch: "main" },
43
+ artifact: { type: "plan", content: PLAN, meta: { title: "Render Plan" } },
44
+ });
45
+ });
46
+ afterEach(() => {
47
+ restoreUserConfig();
48
+ server.stop();
49
+ rmSync(home, { recursive: true, force: true });
50
+ });
51
+
52
+ async function frame(): Promise<string> {
53
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
54
+ width: 120,
55
+ height: 36,
56
+ });
57
+
58
+ await waitForText(setup, "Render Plan");
59
+ // async highlights settle within the visual-idle wait
60
+ await setup.waitForVisualIdle();
61
+
62
+ return setup.captureCharFrame();
63
+ }
64
+
65
+ describe("code blocks", () => {
66
+ test("lines render verbatim: indentation preserved, no word-wrap", async () => {
67
+ // Arrange
68
+ const rendered = await frame();
69
+
70
+ // Assert
71
+ expect(rendered).toContain(" const blocked = full < 0.6;");
72
+ expect(rendered).toContain(' return <text>{blocked ? "blocked" : "clear"}</text>;');
73
+ });
74
+
75
+ test("the block carries its language tag", async () => {
76
+ // Arrange
77
+ const rendered = await frame();
78
+
79
+ // Assert
80
+ expect(rendered).toContain("tsx");
81
+ });
82
+ });
83
+
84
+ describe("block spacing", () => {
85
+ // the review rail paints box edges on the right, so read the document
86
+ // column (everything left of the first rail glyph) before asserting spacing
87
+ const planColumn = (line: string): string => line.split(/[│╭╰]/)[0]!.trimEnd();
88
+
89
+ test("a code block never glues to the list item above it", async () => {
90
+ // Arrange
91
+ const lines = (await frame()).split("\n").map(planColumn);
92
+ const listItemLineIndex = lines.findIndex((line) => line.includes("wire the duck over MQTT"));
93
+ const codeHeader = lines.findIndex((line) => line.trim().startsWith("tsx"));
94
+
95
+ // Assert
96
+ expect(listItemLineIndex).toBeGreaterThan(-1);
97
+ expect(codeHeader).toBeGreaterThan(listItemLineIndex);
98
+ expect(lines.slice(listItemLineIndex + 1, codeHeader).some((line) => line === "")).toBe(true);
99
+ });
100
+
101
+ test("a heading never sits directly on the previous block", async () => {
102
+ // Arrange
103
+ const lines = (await frame()).split("\n").map(planColumn);
104
+ const gate = lines.findIndex((line) => line.trim() === "Gate");
105
+
106
+ // Assert
107
+ expect(gate).toBeGreaterThan(0);
108
+ expect(lines[gate - 1]).toBe("");
109
+ });
110
+
111
+ test("consecutive list items stay tight", async () => {
112
+ // Arrange
113
+ const listPlan = "# T\n\n- one\n- two\n";
114
+ const s2 = server.core.sessionCreate({
115
+ workspace: { repoRoot: "/repo", branch: "main" },
116
+ artifact: { type: "plan", content: listPlan, meta: {} },
117
+ });
118
+ const setup = await renderReadyApp(<App home={home} sessionId={s2.id} />, {
119
+ width: 120,
120
+ height: 30,
121
+ });
122
+
123
+ await waitForText(setup, "one");
124
+
125
+ // Assert
126
+ const lines = setup
127
+ .captureCharFrame()
128
+ .split("\n")
129
+ .map((line) => line.trimEnd());
130
+ const one = lines.findIndex((line) => line.includes("· one"));
131
+
132
+ expect(lines[one + 1]).toContain("· two");
133
+ });
134
+ });