@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101

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 (356) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/annotations/annotation-palette.ts +62 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +369 -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 +1243 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  20. package/src/app/App.deleted-scroll.test.tsx +187 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.identity.test.tsx +84 -0
  25. package/src/app/App.plan-render.test.tsx +209 -0
  26. package/src/app/App.plan-review.test.tsx +428 -0
  27. package/src/app/App.prototype-markdown.test.tsx +128 -0
  28. package/src/app/App.prototype.test.tsx +176 -0
  29. package/src/app/App.readonly.test.tsx +210 -0
  30. package/src/app/App.regression.test.tsx +99 -0
  31. package/src/app/App.rendering.test.tsx +134 -0
  32. package/src/app/App.share-dialog.test.tsx +177 -0
  33. package/src/app/App.submit-confirm.test.tsx +267 -0
  34. package/src/app/App.test.tsx +496 -0
  35. package/src/app/App.thread-agent.test.ts +20 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1667 -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.agent.test.tsx +50 -0
  119. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  120. package/src/integrations/components/ShareDialog.tsx +574 -0
  121. package/src/integrations/components/share-dialog-store.ts +59 -0
  122. package/src/integrations/editor.test.ts +149 -0
  123. package/src/integrations/editor.ts +160 -0
  124. package/src/integrations/github-identity.test.ts +58 -0
  125. package/src/integrations/github-identity.ts +41 -0
  126. package/src/integrations/herdr.test.ts +43 -0
  127. package/src/integrations/herdr.ts +38 -0
  128. package/src/integrations/integrations.test.ts +41 -0
  129. package/src/integrations/integrations.ts +32 -0
  130. package/src/integrations/pasted-image.test.ts +67 -0
  131. package/src/integrations/pasted-image.ts +60 -0
  132. package/src/integrations/share.ts +238 -0
  133. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  134. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  135. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  136. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  137. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  138. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  139. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  140. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  141. package/src/keyboard/components/NavModeHint.tsx +31 -0
  142. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  143. package/src/keyboard/components/SlashComposer.tsx +87 -0
  144. package/src/keyboard/key-bindings.test.ts +156 -0
  145. package/src/keyboard/key-bindings.ts +459 -0
  146. package/src/keyboard/keymap.test.ts +611 -0
  147. package/src/keyboard/keymap.ts +395 -0
  148. package/src/keyboard/skills.test.ts +74 -0
  149. package/src/keyboard/skills.ts +75 -0
  150. package/src/keyboard/slash-palette.test.ts +103 -0
  151. package/src/keyboard/slash-palette.ts +129 -0
  152. package/src/keyboard/thread-chords.test.ts +79 -0
  153. package/src/keyboard/thread-chords.ts +163 -0
  154. package/src/keyboard/use-shared-keyboard.ts +55 -0
  155. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  156. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  157. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  158. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  159. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  160. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  161. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  162. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  163. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  164. package/src/markdown/components/ThreadView.tsx +560 -0
  165. package/src/markdown/mark-runs.ts +121 -0
  166. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  167. package/src/markdown/markdown-grid.test.ts +101 -0
  168. package/src/markdown/markdown-grid.ts +203 -0
  169. package/src/markdown/markdown-highlight.test.ts +61 -0
  170. package/src/markdown/markdown-highlight.ts +124 -0
  171. package/src/markdown/thread-code-syntax.ts +45 -0
  172. package/src/markdown/use-thread-body-editing.tsx +54 -0
  173. package/src/markdown/view-plan.regression.test.ts +60 -0
  174. package/src/markdown/view-plan.test.ts +545 -0
  175. package/src/markdown/view-plan.ts +1009 -0
  176. package/src/performance/otlp-trace.test.ts +119 -0
  177. package/src/performance/otlp-trace.ts +134 -0
  178. package/src/performance/perf-report.ts +18 -0
  179. package/src/performance/perf-timings.test.ts +18 -0
  180. package/src/performance/perf-timings.ts +39 -0
  181. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  182. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  183. package/src/prototype/prototype-browser.test.ts +81 -0
  184. package/src/prototype/prototype-browser.ts +321 -0
  185. package/src/prototype/prototype-pixels.tsx +35 -0
  186. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  187. package/src/settings/components/SettingsDialog.tsx +183 -0
  188. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  189. package/src/settings/components/SettingsRows.tsx +112 -0
  190. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  192. package/src/settings/components/quick-actions-editor.tsx +222 -0
  193. package/src/settings/config-identity.test.ts +78 -0
  194. package/src/settings/config.test.ts +676 -0
  195. package/src/settings/config.ts +628 -0
  196. package/src/settings/use-settings-dialog.tsx +351 -0
  197. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  198. package/src/stories/stories-app.tsx +196 -0
  199. package/src/stories/stories.test.tsx +74 -0
  200. package/src/stories/story-fixtures.ts +102 -0
  201. package/src/stories/story-tree.test.ts +108 -0
  202. package/src/stories/story.ts +154 -0
  203. package/src/terminal/ansi-screen.test.ts +28 -0
  204. package/src/terminal/ansi-screen.ts +72 -0
  205. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  206. package/src/terminal/components/AnsiScreen.tsx +28 -0
  207. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  208. package/src/terminal/components/terminal-pane.ts +196 -0
  209. package/src/terminal/ghostty-terminal.test.ts +85 -0
  210. package/src/terminal/ghostty-terminal.ts +197 -0
  211. package/src/terminal/kitty-image.test.ts +66 -0
  212. package/src/terminal/kitty-image.ts +137 -0
  213. package/src/terminal/pty.test.ts +81 -0
  214. package/src/terminal/pty.ts +246 -0
  215. package/src/testing/test-support.test.ts +264 -0
  216. package/src/testing/test-support.ts +367 -0
  217. package/src/thread/agent-continuation.test.ts +32 -0
  218. package/src/thread/agent-continuation.ts +13 -0
  219. package/src/thread/agent-transcript.test.ts +161 -0
  220. package/src/thread/agent-transcript.ts +336 -0
  221. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  222. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  223. package/src/thread/components/AgentConfigControls.tsx +114 -0
  224. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  225. package/src/thread/components/AgentThreadPane.test.tsx +935 -0
  226. package/src/thread/components/AgentThreadPane.tsx +377 -0
  227. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  228. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  229. package/src/thread/components/ThreadFooter.tsx +77 -0
  230. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  231. package/src/thread/components/ThreadTree.test.tsx +41 -0
  232. package/src/thread/components/ThreadTree.tsx +267 -0
  233. package/src/thread/components/thread-tree.test.ts +137 -0
  234. package/src/thread/components/thread-tree.ts +112 -0
  235. package/src/thread/thread-controller.curate.test.ts +502 -0
  236. package/src/thread/thread-controller.derivation.test.ts +62 -0
  237. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  238. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  239. package/src/thread/thread-controller.share.test.ts +573 -0
  240. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  241. package/src/thread/thread-controller.tree.test.ts +263 -0
  242. package/src/thread/thread-controller.ts +2026 -0
  243. package/src/thread/tree-view.test.ts +128 -0
  244. package/src/thread/tree-view.ts +146 -0
  245. package/src/thread/use-thread-agent.test.tsx +208 -0
  246. package/src/thread/use-thread-agent.ts +117 -0
  247. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  248. package/src/ui/components/CodeBlock.tsx +80 -0
  249. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  250. package/src/ui/components/ConfirmCard.tsx +151 -0
  251. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  252. package/src/ui/components/ConfirmDialog.tsx +72 -0
  253. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  254. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  255. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  256. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  257. package/src/ui/components/PromptDialog.test.tsx +65 -0
  258. package/src/ui/components/PromptDialog.tsx +99 -0
  259. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  260. package/src/ui/components/RootOverlay.tsx +49 -0
  261. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  262. package/src/ui/components/ScrollArea.tsx +61 -0
  263. package/src/ui/components/Toast.stories.tsx +19 -0
  264. package/src/ui/components/Toast.tsx +54 -0
  265. package/src/ui/components/Tooltip.stories.tsx +26 -0
  266. package/src/ui/components/Tooltip.tsx +72 -0
  267. package/src/ui/components/menu-control.stories.tsx +35 -0
  268. package/src/ui/components/menu-control.tsx +43 -0
  269. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  270. package/src/ui/components/primitives/Button.tsx +57 -0
  271. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  272. package/src/ui/components/primitives/Card.tsx +62 -0
  273. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  274. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  275. package/src/ui/components/primitives/Dialog.tsx +69 -0
  276. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  277. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  278. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  279. package/src/ui/components/primitives/IconButton.tsx +58 -0
  280. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  281. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  282. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  283. package/src/ui/components/primitives/Switch.tsx +32 -0
  284. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  285. package/src/ui/components/primitives/Tabs.tsx +101 -0
  286. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  288. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  289. package/src/ui/components/primitives/Tree.tsx +133 -0
  290. package/src/ui/components/primitives/frame.test.ts +12 -0
  291. package/src/ui/components/primitives/frame.ts +17 -0
  292. package/src/ui/components/primitives/icons.ts +61 -0
  293. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  294. package/src/ui/components/primitives/tree-model.ts +137 -0
  295. package/src/ui/components/syntax-highlight.ts +133 -0
  296. package/src/ui/components/truncate-text.ts +7 -0
  297. package/src/ui/components/truncate-title.test.ts +31 -0
  298. package/src/ui/components/truncate-title.ts +15 -0
  299. package/src/ui/use-frame-measure.ts +74 -0
  300. package/src/ui/use-terminal-virtualizer.ts +259 -0
  301. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  302. package/src/workbench/components/ChangesColumn.tsx +203 -0
  303. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  304. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  305. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  306. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  307. package/src/workbench/components/EditorGrid.tsx +544 -0
  308. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  309. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  310. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  311. package/src/workbench/components/FileContentsView.tsx +100 -0
  312. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  313. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  314. package/src/workbench/components/GridTabContent.tsx +193 -0
  315. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  316. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  317. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  318. package/src/workbench/components/TreePane.stories.tsx +106 -0
  319. package/src/workbench/components/TreePane.tsx +155 -0
  320. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  321. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  322. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  323. package/src/workbench/components/editor-grid.test.ts +184 -0
  324. package/src/workbench/components/editor-grid.ts +281 -0
  325. package/src/workbench/components/file-tree.test.ts +60 -0
  326. package/src/workbench/components/file-tree.ts +112 -0
  327. package/src/workbench/use-changes-workbench.ts +205 -0
  328. package/src/workbench/use-remember-layout.ts +31 -0
  329. package/src/workbench/use-repo-changes.ts +91 -0
  330. package/src/workbench/workbench-snapshot.test.ts +99 -0
  331. package/src/workbench/workbench-snapshot.ts +59 -0
  332. package/src/App.diff.test.tsx +0 -99
  333. package/src/App.export.test.tsx +0 -88
  334. package/src/App.plan-review.test.tsx +0 -259
  335. package/src/App.readonly.test.tsx +0 -148
  336. package/src/App.rendering.test.tsx +0 -108
  337. package/src/App.submit-confirm.test.tsx +0 -183
  338. package/src/App.test.tsx +0 -218
  339. package/src/App.tsx +0 -1249
  340. package/src/config.test.ts +0 -116
  341. package/src/config.ts +0 -150
  342. package/src/editor.ts +0 -34
  343. package/src/herdr.test.ts +0 -68
  344. package/src/herdr.ts +0 -34
  345. package/src/keymap.test.ts +0 -270
  346. package/src/keymap.ts +0 -173
  347. package/src/run.ts +0 -30
  348. package/src/serve.ts +0 -75
  349. package/src/session-controller.ts +0 -398
  350. package/src/syntax.test.ts +0 -41
  351. package/src/syntax.ts +0 -88
  352. package/src/theme.ts +0 -46
  353. package/src/view-diff.test.ts +0 -55
  354. package/src/view-diff.ts +0 -86
  355. package/src/view.test.ts +0 -157
  356. package/src/view.ts +0 -364
@@ -0,0 +1,128 @@
1
+ /** The default prototype is a Markdown design doc: with no experimental flag it renders through the thread/markdown path (not the kitty pixel view), its headings and fenced sections show as text, and a quote-anchored comment lands on a line - the same as a plan. A real in-process daemon; char-frame assertions over the real App. */
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 { DARK } from "../appearance/theme";
12
+ import { isolateUserConfig, renderReadyApp, settle, waitForText } from "../testing/test-support";
13
+
14
+ const DOC = `# PromoCard
15
+
16
+ A promotional card for a growth surface.
17
+
18
+ ## API
19
+
20
+ \`\`\`ts
21
+ interface PromoCardProps {
22
+ title: string
23
+ onDismiss?: () => void
24
+ }
25
+ \`\`\`
26
+
27
+ ## Composition
28
+
29
+ \`\`\`
30
+ PromoCard
31
+ └─ TextGroup reuse: Text
32
+ \`\`\`
33
+ `;
34
+
35
+ let home: string;
36
+ let server: DaemonServer;
37
+ let session: Thread;
38
+ let restoreUserConfig: () => void;
39
+
40
+ beforeEach(() => {
41
+ home = mkdtempSync(join(tmpdir(), "cueloop-prototype-md-"));
42
+ restoreUserConfig = isolateUserConfig(home);
43
+ server = new DaemonServer({ home, idleExitMs: 0 });
44
+ server.start();
45
+ session = server.core.sessionCreate({
46
+ workspace: { repoRoot: "/repo", branch: "promo-card" },
47
+ artifact: { type: "prototype", content: DOC, meta: { title: "PromoCard", agent: "pi" } },
48
+ });
49
+ });
50
+ afterEach(() => {
51
+ restoreUserConfig();
52
+ server.stop();
53
+ rmSync(home, { recursive: true, force: true });
54
+ });
55
+
56
+ describe("markdown prototype review", () => {
57
+ test("syntax-highlights TypeScript fences without changing their text", async () => {
58
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
59
+ width: 120,
60
+ height: 40,
61
+ });
62
+
63
+ await waitForText(setup, "PromoCardProps");
64
+ let keywordHighlighted = false;
65
+
66
+ for (let attempt = 0; attempt < 60 && !keywordHighlighted; attempt++) {
67
+ await setup.renderOnce();
68
+ await new Promise((resolve) => setTimeout(resolve, 20));
69
+ const accentText = setup
70
+ .captureSpans()
71
+ .lines.flatMap((line) =>
72
+ line.spans
73
+ .filter((span) => {
74
+ const [red, green, blue] = span.fg.toInts();
75
+
76
+ return (
77
+ `#${[red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("")}` ===
78
+ DARK.accent
79
+ );
80
+ })
81
+ .map((span) => span.text),
82
+ )
83
+ .join("");
84
+
85
+ keywordHighlighted = accentText.includes("interface");
86
+ }
87
+
88
+ expect(keywordHighlighted).toBe(true);
89
+ expect(setup.captureCharFrame()).toContain("interface PromoCardProps {");
90
+ }, 25000);
91
+
92
+ test("renders as a markdown design doc, not the pixel view", async () => {
93
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
94
+ width: 120,
95
+ height: 40,
96
+ });
97
+
98
+ await waitForText(setup, "PromoCard");
99
+ await settle(setup);
100
+ const frame = setup.captureCharFrame();
101
+
102
+ // the markdown path renders the doc's headings and interface as text
103
+ expect(frame).toContain("API");
104
+ expect(frame).toContain("PromoCardProps");
105
+ expect(frame).toContain("Composition");
106
+ // and never falls into the pixel view's status line
107
+ expect(frame).not.toContain("rendering prototype");
108
+ expect(frame).not.toContain("graphics terminal");
109
+ });
110
+
111
+ test("a quote-anchored comment stores against the doc, like a plan", async () => {
112
+ server.core.sessionAnnotate(session.id, {
113
+ id: "own_1",
114
+ kind: "comment",
115
+ anchor: { quote: "onDismiss?: () => void", prefix: "", suffix: "" },
116
+ body: "make dismiss required for a modal surface?",
117
+ });
118
+
119
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
120
+ width: 120,
121
+ height: 40,
122
+ });
123
+
124
+ await waitForText(setup, "make dismiss required");
125
+ expect(server.core.sessionGet(session.id).annotations).toHaveLength(1);
126
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.selector).toBeUndefined();
127
+ });
128
+ });
@@ -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
+ });