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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/annotations/annotation-palette.ts +63 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +383 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +189 -0
  14. package/src/annotations/discussions.ts +78 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +248 -0
  17. package/src/annotations/use-annotation-surface.tsx +1378 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  20. package/src/app/App.deleted-scroll.test.tsx +190 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.harness-settings.test.tsx +77 -0
  25. package/src/app/App.identity.test.tsx +84 -0
  26. package/src/app/App.plan-render.test.tsx +211 -0
  27. package/src/app/App.plan-review.test.tsx +429 -0
  28. package/src/app/App.prototype-markdown.test.tsx +129 -0
  29. package/src/app/App.prototype.test.tsx +176 -0
  30. package/src/app/App.readonly.test.tsx +274 -0
  31. package/src/app/App.regression.test.tsx +99 -0
  32. package/src/app/App.rendering.test.tsx +134 -0
  33. package/src/app/App.share-dialog.test.tsx +177 -0
  34. package/src/app/App.submit-confirm.test.tsx +267 -0
  35. package/src/app/App.test.tsx +497 -0
  36. package/src/app/App.thread-agent.test.ts +20 -0
  37. package/src/app/App.toast.test.tsx +124 -0
  38. package/src/app/App.tsx +1758 -0
  39. package/src/app/App.visual.test.tsx +60 -0
  40. package/src/app/App.walk.test.tsx +300 -0
  41. package/src/app/App.workbench.test.tsx +1153 -0
  42. package/src/app/app-key-state.ts +61 -0
  43. package/src/app/app-screens.tsx +607 -0
  44. package/src/app/app-view-model.ts +178 -0
  45. package/src/app/components/AppHeader.stories.tsx +50 -0
  46. package/src/app/components/AppHeader.tsx +81 -0
  47. package/src/app/components/AppShell.brand.test.tsx +58 -0
  48. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  49. package/src/app/components/AppShell.stories.tsx +89 -0
  50. package/src/app/components/AppShell.tsx +358 -0
  51. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  52. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  53. package/src/app/components/Breadcrumb.tsx +45 -0
  54. package/src/app/components/MenuBar.stories.tsx +28 -0
  55. package/src/app/components/MenuBar.tsx +44 -0
  56. package/src/app/components/PaneGrid.stories.tsx +84 -0
  57. package/src/app/components/PaneGrid.tsx +128 -0
  58. package/src/app/components/PanelColumn.stories.tsx +25 -0
  59. package/src/app/components/PanelColumn.tsx +93 -0
  60. package/src/app/components/ShellHeader.stories.tsx +50 -0
  61. package/src/app/components/ShellHeader.tsx +137 -0
  62. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  63. package/src/app/components/WorkspacePanels.tsx +121 -0
  64. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  65. package/src/app/intent-dispatch.test.ts +729 -0
  66. package/src/app/intent-dispatch.ts +713 -0
  67. package/src/app/launch-layout.test.ts +78 -0
  68. package/src/app/launch-layout.ts +57 -0
  69. package/src/app/ready-signal.test.ts +85 -0
  70. package/src/app/ready-signal.ts +54 -0
  71. package/src/app/run.ts +111 -0
  72. package/src/{serve.test.ts → app/serve.test.ts} +28 -12
  73. package/src/app/serve.ts +111 -0
  74. package/src/app/version.ts +5 -0
  75. package/src/appearance/components/theme-context.stories.tsx +46 -0
  76. package/src/appearance/components/theme-context.tsx +37 -0
  77. package/src/appearance/theme-presets.test.ts +51 -0
  78. package/src/appearance/theme-presets.ts +219 -0
  79. package/src/appearance/theme.ts +112 -0
  80. package/src/appearance/visual/contrast.test.ts +35 -0
  81. package/src/appearance/visual/contrast.ts +33 -0
  82. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  83. package/src/diff/components/AnnotatableFileView.test.tsx +160 -0
  84. package/src/diff/components/AnnotatableFileView.tsx +142 -0
  85. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  86. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  87. package/src/diff/components/DiffContentView.scroll.test.tsx +443 -0
  88. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  89. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  90. package/src/diff/components/DiffContentView.syntax.test.tsx +85 -0
  91. package/src/diff/components/DiffContentView.tsx +1080 -0
  92. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  93. package/src/diff/components/WalkWizard.tsx +146 -0
  94. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  95. package/src/diff/components/diff-content-view-layout.ts +87 -0
  96. package/src/diff/diff-fold.test.ts +78 -0
  97. package/src/diff/diff-fold.ts +130 -0
  98. package/src/diff/diff-hunk-curate.test.ts +315 -0
  99. package/src/diff/diff-hunk-curate.ts +155 -0
  100. package/src/diff/diff-intraline.test.ts +239 -0
  101. package/src/diff/diff-intraline.ts +244 -0
  102. package/src/diff/diff-syntax.test.ts +125 -0
  103. package/src/diff/diff-syntax.ts +194 -0
  104. package/src/diff/split-diff.test.ts +110 -0
  105. package/src/diff/split-diff.ts +150 -0
  106. package/src/diff/view-diff.test.ts +325 -0
  107. package/src/diff/view-diff.ts +540 -0
  108. package/src/diff/walk.test.ts +121 -0
  109. package/src/diff/walk.ts +76 -0
  110. package/src/index.ts +32 -2
  111. package/src/integrations/attribution.test.ts +31 -0
  112. package/src/integrations/attribution.ts +44 -0
  113. package/src/integrations/client-extension-registry.test.tsx +61 -0
  114. package/src/integrations/client-extension-registry.ts +163 -0
  115. package/src/integrations/clipboard.test.ts +23 -0
  116. package/src/integrations/clipboard.ts +34 -0
  117. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  118. package/src/integrations/components/ExtensionZone.tsx +145 -0
  119. package/src/integrations/components/ShareDialog.agent.test.tsx +52 -0
  120. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  121. package/src/integrations/components/ShareDialog.tsx +602 -0
  122. package/src/integrations/components/share-dialog-store.ts +59 -0
  123. package/src/integrations/editor.test.ts +149 -0
  124. package/src/integrations/editor.ts +163 -0
  125. package/src/integrations/github-identity.test.ts +59 -0
  126. package/src/integrations/github-identity.ts +42 -0
  127. package/src/integrations/herdr.test.ts +43 -0
  128. package/src/integrations/herdr.ts +40 -0
  129. package/src/integrations/integrations.test.ts +41 -0
  130. package/src/integrations/integrations.ts +32 -0
  131. package/src/integrations/pasted-image.test.ts +67 -0
  132. package/src/integrations/pasted-image.ts +62 -0
  133. package/src/integrations/share.ts +283 -0
  134. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  135. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  136. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  137. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  138. package/src/keyboard/components/HandleChipsInput.tsx +147 -0
  139. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  140. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  141. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  142. package/src/keyboard/components/NavModeHint.tsx +31 -0
  143. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  144. package/src/keyboard/components/SlashComposer.tsx +90 -0
  145. package/src/keyboard/key-bindings.test.ts +156 -0
  146. package/src/keyboard/key-bindings.ts +462 -0
  147. package/src/keyboard/keymap.test.ts +611 -0
  148. package/src/keyboard/keymap.ts +446 -0
  149. package/src/keyboard/skills.test.ts +74 -0
  150. package/src/keyboard/skills.ts +77 -0
  151. package/src/keyboard/slash-palette.test.ts +103 -0
  152. package/src/keyboard/slash-palette.ts +137 -0
  153. package/src/keyboard/thread-chords.test.ts +79 -0
  154. package/src/keyboard/thread-chords.ts +168 -0
  155. package/src/keyboard/use-shared-keyboard.ts +56 -0
  156. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  157. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  158. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  159. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  160. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  161. package/src/markdown/components/MarkdownThreadEditor.tsx +165 -0
  162. package/src/markdown/components/ThreadView.selection.test.tsx +954 -0
  163. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  164. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  165. package/src/markdown/components/ThreadView.tsx +577 -0
  166. package/src/markdown/mark-runs.ts +125 -0
  167. package/src/markdown/markdown-editor-syntax-style.ts +66 -0
  168. package/src/markdown/markdown-grid.test.ts +101 -0
  169. package/src/markdown/markdown-grid.ts +209 -0
  170. package/src/markdown/markdown-highlight.test.ts +61 -0
  171. package/src/markdown/markdown-highlight.ts +125 -0
  172. package/src/markdown/thread-code-syntax.ts +48 -0
  173. package/src/markdown/use-thread-body-editing.tsx +54 -0
  174. package/src/markdown/view-plan.regression.test.ts +60 -0
  175. package/src/markdown/view-plan.test.ts +545 -0
  176. package/src/markdown/view-plan.ts +1061 -0
  177. package/src/performance/otlp-trace.test.ts +119 -0
  178. package/src/performance/otlp-trace.ts +134 -0
  179. package/src/performance/perf-report.ts +19 -0
  180. package/src/performance/perf-timings.test.ts +18 -0
  181. package/src/performance/perf-timings.ts +40 -0
  182. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  183. package/src/prototype/components/PrototypeContentView.tsx +459 -0
  184. package/src/prototype/prototype-browser.test.ts +81 -0
  185. package/src/prototype/prototype-browser.ts +332 -0
  186. package/src/prototype/prototype-pixels.tsx +35 -0
  187. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  188. package/src/settings/components/SettingsDialog.tsx +186 -0
  189. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  190. package/src/settings/components/SettingsRows.tsx +112 -0
  191. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  192. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  193. package/src/settings/components/quick-actions-editor.tsx +222 -0
  194. package/src/settings/config-identity.test.ts +78 -0
  195. package/src/settings/config.test.ts +771 -0
  196. package/src/settings/config.ts +722 -0
  197. package/src/settings/use-settings-dialog.test.tsx +172 -0
  198. package/src/settings/use-settings-dialog.tsx +465 -0
  199. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  200. package/src/settings/use-thread-harness-setting.ts +86 -0
  201. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  202. package/src/stories/stories-app.tsx +196 -0
  203. package/src/stories/stories.test.tsx +78 -0
  204. package/src/stories/story-fixtures.ts +102 -0
  205. package/src/stories/story-tree.test.ts +108 -0
  206. package/src/stories/story.ts +160 -0
  207. package/src/terminal/ansi-screen.test.ts +28 -0
  208. package/src/terminal/ansi-screen.ts +77 -0
  209. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  210. package/src/terminal/components/AnsiScreen.tsx +28 -0
  211. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  212. package/src/terminal/components/terminal-pane.ts +208 -0
  213. package/src/terminal/ghostty-terminal.test.ts +85 -0
  214. package/src/terminal/ghostty-terminal.ts +207 -0
  215. package/src/terminal/kitty-image.test.ts +66 -0
  216. package/src/terminal/kitty-image.ts +140 -0
  217. package/src/terminal/pty.test.ts +81 -0
  218. package/src/terminal/pty.ts +255 -0
  219. package/src/testing/test-support.test.ts +266 -0
  220. package/src/testing/test-support.ts +449 -0
  221. package/src/thread/agent-continuation.test.ts +34 -0
  222. package/src/thread/agent-continuation.ts +13 -0
  223. package/src/thread/agent-transcript.test.ts +161 -0
  224. package/src/thread/agent-transcript.ts +344 -0
  225. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  226. package/src/thread/components/AgentConfigControls.test.tsx +153 -0
  227. package/src/thread/components/AgentConfigControls.tsx +121 -0
  228. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  229. package/src/thread/components/AgentThreadPane.test.tsx +1248 -0
  230. package/src/thread/components/AgentThreadPane.tsx +458 -0
  231. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  232. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  233. package/src/thread/components/ThreadFooter.tsx +77 -0
  234. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  235. package/src/thread/components/ThreadTree.test.tsx +41 -0
  236. package/src/thread/components/ThreadTree.tsx +268 -0
  237. package/src/thread/components/thread-tree.test.ts +138 -0
  238. package/src/thread/components/thread-tree.ts +112 -0
  239. package/src/thread/thread-controller.curate.test.ts +504 -0
  240. package/src/thread/thread-controller.derivation.test.ts +62 -0
  241. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  242. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  243. package/src/thread/thread-controller.share.test.ts +618 -0
  244. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  245. package/src/thread/thread-controller.tree.test.ts +263 -0
  246. package/src/thread/thread-controller.ts +2151 -0
  247. package/src/thread/tree-view.test.ts +128 -0
  248. package/src/thread/tree-view.ts +150 -0
  249. package/src/thread/use-thread-agent.test.tsx +209 -0
  250. package/src/thread/use-thread-agent.ts +122 -0
  251. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  252. package/src/ui/components/CodeBlock.tsx +82 -0
  253. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  254. package/src/ui/components/ConfirmCard.tsx +151 -0
  255. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  256. package/src/ui/components/ConfirmDialog.tsx +72 -0
  257. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  258. package/src/ui/components/OverlayScrollbar.test.tsx +142 -0
  259. package/src/ui/components/OverlayScrollbar.tsx +134 -0
  260. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  261. package/src/ui/components/PromptDialog.test.tsx +65 -0
  262. package/src/ui/components/PromptDialog.tsx +101 -0
  263. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  264. package/src/ui/components/RootOverlay.tsx +50 -0
  265. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  266. package/src/ui/components/ScrollArea.tsx +63 -0
  267. package/src/ui/components/Toast.stories.tsx +19 -0
  268. package/src/ui/components/Toast.tsx +54 -0
  269. package/src/ui/components/Tooltip.stories.tsx +26 -0
  270. package/src/ui/components/Tooltip.tsx +93 -0
  271. package/src/ui/components/menu-control.stories.tsx +35 -0
  272. package/src/ui/components/menu-control.tsx +43 -0
  273. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  274. package/src/ui/components/primitives/Button.tsx +58 -0
  275. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  276. package/src/ui/components/primitives/Card.tsx +62 -0
  277. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  278. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  279. package/src/ui/components/primitives/Dialog.tsx +69 -0
  280. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  281. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  282. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  283. package/src/ui/components/primitives/IconButton.tsx +58 -0
  284. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  285. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  286. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Switch.tsx +32 -0
  288. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  289. package/src/ui/components/primitives/Tabs.tsx +102 -0
  290. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  291. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  292. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  293. package/src/ui/components/primitives/Tree.tsx +137 -0
  294. package/src/ui/components/primitives/frame.test.ts +12 -0
  295. package/src/ui/components/primitives/frame.ts +17 -0
  296. package/src/ui/components/primitives/icons.ts +61 -0
  297. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  298. package/src/ui/components/primitives/tree-model.ts +144 -0
  299. package/src/ui/components/syntax-highlight.ts +154 -0
  300. package/src/ui/components/truncate-text.ts +7 -0
  301. package/src/ui/components/truncate-title.test.ts +31 -0
  302. package/src/ui/components/truncate-title.ts +16 -0
  303. package/src/ui/use-frame-measure.ts +88 -0
  304. package/src/ui/use-terminal-virtualizer.ts +286 -0
  305. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  306. package/src/workbench/components/ChangesColumn.tsx +210 -0
  307. package/src/workbench/components/ChangesFileTree.test.tsx +184 -0
  308. package/src/workbench/components/EditorGrid.split.test.tsx +119 -0
  309. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  310. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  311. package/src/workbench/components/EditorGrid.tsx +552 -0
  312. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  313. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  314. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  315. package/src/workbench/components/FileContentsView.tsx +104 -0
  316. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  317. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  318. package/src/workbench/components/GridTabContent.tsx +195 -0
  319. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  320. package/src/workbench/components/ProjectTreeView.test.tsx +239 -0
  321. package/src/workbench/components/ProjectTreeView.tsx +159 -0
  322. package/src/workbench/components/TreePane.stories.tsx +106 -0
  323. package/src/workbench/components/TreePane.tsx +157 -0
  324. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  325. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  326. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  327. package/src/workbench/components/editor-grid.test.ts +184 -0
  328. package/src/workbench/components/editor-grid.ts +284 -0
  329. package/src/workbench/components/file-tree.test.ts +60 -0
  330. package/src/workbench/components/file-tree.ts +114 -0
  331. package/src/workbench/use-changes-workbench.ts +210 -0
  332. package/src/workbench/use-remember-layout.ts +33 -0
  333. package/src/workbench/use-repo-changes.ts +93 -0
  334. package/src/workbench/workbench-snapshot.test.ts +99 -0
  335. package/src/workbench/workbench-snapshot.ts +59 -0
  336. package/src/App.diff.test.tsx +0 -99
  337. package/src/App.export.test.tsx +0 -88
  338. package/src/App.plan-review.test.tsx +0 -259
  339. package/src/App.readonly.test.tsx +0 -148
  340. package/src/App.rendering.test.tsx +0 -108
  341. package/src/App.submit-confirm.test.tsx +0 -183
  342. package/src/App.test.tsx +0 -218
  343. package/src/App.tsx +0 -1249
  344. package/src/config.test.ts +0 -116
  345. package/src/config.ts +0 -150
  346. package/src/editor.ts +0 -34
  347. package/src/herdr.test.ts +0 -68
  348. package/src/herdr.ts +0 -34
  349. package/src/keymap.test.ts +0 -270
  350. package/src/keymap.ts +0 -173
  351. package/src/run.ts +0 -30
  352. package/src/serve.ts +0 -75
  353. package/src/session-controller.ts +0 -398
  354. package/src/syntax.test.ts +0 -41
  355. package/src/syntax.ts +0 -88
  356. package/src/theme.ts +0 -46
  357. package/src/view-diff.test.ts +0 -55
  358. package/src/view-diff.ts +0 -86
  359. package/src/view.test.ts +0 -157
  360. package/src/view.ts +0 -364
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The read-only plan render: h1/h2 sit over
3
+ * a rule, links show their label in the link color with the URL concealed,
4
+ * inline code grays with its backticks gone, GFM tables render as an aligned grid
5
+ * and leading YAML frontmatter as a bordered key/value table. Because rendered
6
+ * text now differs from the source, a comment dragged over a concealed-markup
7
+ * passage must still anchor to the source and repaint on the visible label.
8
+ * Char-frame + styled-span assertions over the real App and an in-process daemon.
9
+ */
10
+
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+ import { mkdtempSync, rmSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import React from "react";
16
+ import { DaemonServer } from "@cueloop/daemon";
17
+ import type { Thread } from "@cueloop/schema";
18
+ import { App } from "./App";
19
+ import { DARK } from "../appearance/theme";
20
+ import {
21
+ dragText,
22
+ isolateUserConfig,
23
+ pressKey,
24
+ renderReadyApp,
25
+ settle,
26
+ typeText as type,
27
+ waitForState,
28
+ waitForText,
29
+ } from "../testing/test-support";
30
+
31
+ const PLAN = `---
32
+ title: Migration Plan
33
+ owner: platform team
34
+ ---
35
+
36
+ # Migration Plan
37
+
38
+ ## Rollout
39
+
40
+ Ship it behind a \`flag\` and read [OpenTUI](https://opentui) first.
41
+
42
+ | Name | Size |
43
+ | :--- | ---: |
44
+ | daemon | 12 |
45
+ | client | 7 |
46
+ `;
47
+
48
+ let home: string;
49
+ let server: DaemonServer;
50
+ let session: Thread;
51
+ let restoreUserConfig: () => void;
52
+
53
+ beforeEach(() => {
54
+ home = mkdtempSync(join(tmpdir(), "cueloop-plan-render-"));
55
+ restoreUserConfig = isolateUserConfig(home);
56
+ server = new DaemonServer({ home, idleExitMs: 0 });
57
+ server.start();
58
+ session = server.core.sessionCreate({
59
+ workspace: { repoRoot: "/repo", branch: "main" },
60
+ artifact: {
61
+ type: "plan",
62
+ content: PLAN,
63
+ meta: { title: "Migration Plan", planPath: "plan.md" },
64
+ },
65
+ });
66
+ });
67
+ afterEach(() => {
68
+ restoreUserConfig();
69
+ server.stop();
70
+ rmSync(home, { recursive: true, force: true });
71
+ });
72
+
73
+ type Setup = Awaited<ReturnType<typeof renderPlan>>;
74
+
75
+ async function renderPlan() {
76
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
77
+ width: 90,
78
+ height: 40,
79
+ });
80
+
81
+ await waitForText(setup, "cueloop");
82
+ await settle(setup);
83
+
84
+ return setup;
85
+ }
86
+
87
+ /** Hex foregrounds of every styled span containing the needle. */
88
+ function foregroundsOf(setup: Setup, needle: string): string[] {
89
+ const foregrounds: string[] = [];
90
+
91
+ for (const line of setup.captureSpans().lines) {
92
+ for (const span of line.spans) {
93
+ if (span.fg === undefined || !span.text.includes(needle)) continue;
94
+
95
+ const [red, green, blue] = span.fg.toInts();
96
+
97
+ foregrounds.push(
98
+ "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
99
+ );
100
+ }
101
+ }
102
+
103
+ return foregrounds;
104
+ }
105
+
106
+ /** Hex backgrounds of every styled span containing the needle. */
107
+ function backgroundsOf(setup: Setup, needle: string): string[] {
108
+ const backgrounds: string[] = [];
109
+
110
+ for (const line of setup.captureSpans().lines) {
111
+ for (const span of line.spans) {
112
+ if (!span.text.includes(needle)) continue;
113
+
114
+ const [red, green, blue] = span.bg.toInts();
115
+
116
+ backgrounds.push(
117
+ "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
118
+ );
119
+ }
120
+ }
121
+
122
+ return backgrounds;
123
+ }
124
+
125
+ const THREAD_MARK = "#463852";
126
+
127
+ describe("read-only markdown render", () => {
128
+ test("h1 and h2 sit over a rule", async () => {
129
+ const setup = await renderPlan();
130
+ const rows = setup.captureCharFrame().split("\n");
131
+ // "Migration Plan" also rides the header bar; a heading is any body row directly over a rule
132
+ const ruleBelow = (heading: string): boolean =>
133
+ rows.some((row, index) => row.includes(heading) && (rows[index + 1] ?? "").includes("──"));
134
+
135
+ expect(ruleBelow("Migration Plan")).toBe(true);
136
+ expect(ruleBelow("Rollout")).toBe(true);
137
+ });
138
+
139
+ test("a link shows its label in the link color with the URL concealed", async () => {
140
+ const setup = await renderPlan();
141
+ const frame = setup.captureCharFrame();
142
+
143
+ expect(frame).toContain("OpenTUI");
144
+ expect(frame).not.toContain("https://opentui");
145
+ expect(frame).not.toContain("[OpenTUI]");
146
+ expect(foregroundsOf(setup, "OpenTUI")).toContain(DARK.blue);
147
+ });
148
+
149
+ test("inline code grays with its backticks concealed", async () => {
150
+ const setup = await renderPlan();
151
+ const frame = setup.captureCharFrame();
152
+
153
+ expect(frame).toContain("flag");
154
+ expect(frame).not.toContain("`flag`");
155
+ expect(foregroundsOf(setup, "flag")).toContain(DARK.textDim);
156
+ });
157
+
158
+ test("a table renders as an aligned grid without pipes", async () => {
159
+ const setup = await renderPlan();
160
+ const rows = setup.captureCharFrame().split("\n");
161
+ const header = rows.find((row) => row.includes("Name") && row.includes("Size"))!;
162
+
163
+ expect(header).not.toContain("|");
164
+ // the numeric column is right-aligned, so 12 and 7 line up on their last digit
165
+ const daemon = rows.find((row) => row.includes("daemon"))!;
166
+ const client = rows.find((row) => row.includes("client"))!;
167
+
168
+ expect(daemon.indexOf("12") + 1).toBe(client.indexOf("7"));
169
+ });
170
+
171
+ test("frontmatter renders as a bordered key/value grid", async () => {
172
+ const setup = await renderPlan();
173
+ const frame = setup.captureCharFrame();
174
+
175
+ expect(frame).toContain("┌");
176
+ expect(frame).toContain("└");
177
+ expect(frame).toContain("title");
178
+ expect(frame).toContain("platform team");
179
+ });
180
+ });
181
+
182
+ describe("annotating a grid block", () => {
183
+ test("a comment dragged over a table cell anchors on the grid and marks it", async () => {
184
+ const setup = await renderPlan();
185
+
186
+ // the table renders as a grid, yet it still takes a comment like any other block
187
+ await dragText(setup, "daemon", "daemon", "daemon".length);
188
+ await type(setup, "which daemon?");
189
+ await pressKey(setup, "RETURN", { meta: true });
190
+
191
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
192
+ // the grid carries the mark backdrop, so an annotation resolving into it stays visible
193
+ await waitForState(setup, () => backgroundsOf(setup, "daemon").includes(THREAD_MARK));
194
+ }, 60_000);
195
+ });
196
+
197
+ describe("anchoring over concealed markup", () => {
198
+ test("a comment dragged over a rendered link label anchors to the source and repaints", async () => {
199
+ const setup = await renderPlan();
200
+
201
+ // drag the visible label "OpenTUI" (its brackets and URL are concealed)
202
+ await dragText(setup, "OpenTUI", "OpenTUI", "OpenTUI".length);
203
+ await type(setup, "which build?");
204
+ await pressKey(setup, "RETURN", { meta: true });
205
+
206
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
207
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("OpenTUI");
208
+ // the saved comment keeps the visible label marked
209
+ await waitForState(setup, () => backgroundsOf(setup, "OpenTUI").includes(THREAD_MARK));
210
+ }, 60_000);
211
+ });
@@ -0,0 +1,429 @@
1
+ /** The plan review surface v2: native selection feeds the annotation quote, the inline compose box keeps its anchor painted, the rail edits what the document selects, and edit-exit reconciliation orphans annotations whose passage was removed. Char-frame + styled-span 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, readFileSync, 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 { DEFAULT_QUICK_ACTIONS } from "../settings/config";
12
+ import { slashItemsFrom } from "../keyboard/slash-palette";
13
+ import {
14
+ clickText,
15
+ dragText,
16
+ isolateUserConfig,
17
+ navCommand,
18
+ press,
19
+ pressKey,
20
+ renderReadyApp,
21
+ typeText as type,
22
+ waitForState,
23
+ waitForText,
24
+ waitForTextGone,
25
+ } from "../testing/test-support";
26
+
27
+ const PLAN = `# Migration Plan
28
+
29
+ ## Context
30
+
31
+ The daemon persists sessions to disk atomically.
32
+
33
+ ## Steps
34
+
35
+ - move the store
36
+ - add recovery
37
+ `;
38
+
39
+ let home: string;
40
+ let server: DaemonServer;
41
+ let session: Thread;
42
+ let restoreUserConfig: () => void;
43
+
44
+ beforeEach(() => {
45
+ home = mkdtempSync(join(tmpdir(), "cueloop-plan-review-"));
46
+ restoreUserConfig = isolateUserConfig(home);
47
+ server = new DaemonServer({ home, idleExitMs: 0 });
48
+ server.start();
49
+ session = server.core.sessionCreate({
50
+ workspace: { repoRoot: "/repo", branch: "main" },
51
+ artifact: {
52
+ type: "plan",
53
+ content: PLAN,
54
+ meta: { title: "Migration Plan", planPath: "plan.md", agent: "agent/worker-3" },
55
+ },
56
+ });
57
+ });
58
+ afterEach(() => {
59
+ restoreUserConfig();
60
+ server.stop();
61
+ rmSync(home, { recursive: true, force: true });
62
+ });
63
+
64
+ async function renderApp() {
65
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
66
+ width: 120,
67
+ height: 32,
68
+ });
69
+
70
+ await waitForText(setup, "cueloop");
71
+
72
+ return setup;
73
+ }
74
+
75
+ type Setup = Awaited<ReturnType<typeof renderApp>>;
76
+
77
+ /**
78
+ * Background colors (hex) of every styled span containing the needle - the
79
+ * document highlight and the rail's quote excerpt can both match.
80
+ */
81
+ function backgroundsOf(setup: Setup, needle: string): string[] {
82
+ const backgrounds: string[] = [];
83
+
84
+ for (const line of setup.captureSpans().lines) {
85
+ for (const span of line.spans) {
86
+ if (!span.text.includes(needle)) continue;
87
+
88
+ const [red, green, blue] = span.bg.toInts();
89
+
90
+ backgrounds.push(
91
+ "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
92
+ );
93
+ }
94
+ }
95
+
96
+ return backgrounds;
97
+ }
98
+
99
+ describe("share button", () => {
100
+ test("the owner's plan header shows the Share button next to Edit", async () => {
101
+ // Arrange / Act
102
+ const setup = await renderApp();
103
+
104
+ // Assert
105
+ // both word-buttons ride the header's thread segment, so share sits next to edit
106
+ const headerLine = setup
107
+ .captureCharFrame()
108
+ .split("\n")
109
+ .find((line) => line.includes("edit"));
110
+
111
+ expect(headerLine).toContain("share");
112
+ });
113
+
114
+ test("a read-only viewer (a plan shared over ssh) never sees the Share button", async () => {
115
+ // Arrange / Act
116
+ const viewer = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
117
+ width: 120,
118
+ height: 32,
119
+ });
120
+
121
+ await waitForText(viewer, "cueloop");
122
+
123
+ // Assert
124
+ expect(viewer.captureCharFrame()).not.toContain(" share ");
125
+ });
126
+
127
+ test("a resolved plan hides Edit and Share (no re-sharing a finished review)", async () => {
128
+ // Arrange - resolve the session before opening it
129
+ server.core.sessionSendMessage(session.id, "approved", "");
130
+
131
+ // Act
132
+ const setup = await renderApp();
133
+
134
+ await waitForText(setup, "Migration Plan");
135
+
136
+ // Assert - the owner toolbar is gone once the review is resolved
137
+ const frame = setup.captureCharFrame();
138
+
139
+ expect(frame).not.toContain(" edit ");
140
+ expect(frame).not.toContain(" share ");
141
+ });
142
+ });
143
+
144
+ describe("edit affordance", () => {
145
+ test("the owner sees the Edit button", async () => {
146
+ // Arrange / Act
147
+ const owner = await renderApp();
148
+
149
+ // Assert
150
+ expect(owner.captureCharFrame()).toContain(" edit ");
151
+ });
152
+
153
+ test("a read-only viewer (a plan shared over ssh) never sees the Edit button", async () => {
154
+ // Arrange / Act
155
+ const viewer = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
156
+ width: 120,
157
+ height: 32,
158
+ });
159
+
160
+ await waitForText(viewer, "cueloop");
161
+
162
+ // Assert
163
+ expect(viewer.captureCharFrame()).not.toContain(" edit ");
164
+ });
165
+ });
166
+
167
+ describe("marks feed the comment anchor", () => {
168
+ test("a drag marks a character-precise span and typing anchors the comment to it", async () => {
169
+ // Arrange
170
+ const setup = await renderApp();
171
+
172
+ // Act
173
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
174
+ await type(setup, "Which daemon?");
175
+ await pressKey(setup, "RETURN", { meta: true });
176
+
177
+ // Assert
178
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
179
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("The daemon");
180
+ });
181
+
182
+ test("a drag that ends mid-sentence keeps exactly the dragged words", async () => {
183
+ // Arrange
184
+ const setup = await renderApp();
185
+
186
+ // Act: release one past the last character of "sessions"
187
+ await dragText(setup, "persists", "sessions", "sessions".length);
188
+ await type(setup, "sessions plural?");
189
+ await pressKey(setup, "RETURN", { meta: true });
190
+
191
+ // Assert
192
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
193
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe(
194
+ "persists sessions",
195
+ );
196
+ });
197
+ });
198
+
199
+ /** The thread view's mark backdrop in the dark palette. */
200
+ const THREAD_MARK = "#463852";
201
+
202
+ describe("the mark stays painted while composing", () => {
203
+ test("the mark paints while composing, escape un-paints, save keeps it as the comment's mark", async () => {
204
+ // Arrange
205
+ const setup = await renderApp();
206
+
207
+ // Act
208
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
209
+ await type(setup, "x");
210
+
211
+ // Assert: composing, the marked words carry the mark backdrop
212
+ await waitForText(setup, "● x");
213
+ expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
214
+
215
+ // Act: escape discards the draft and the mark stays for re-typing; the next escape
216
+ // enters nav mode with the mark still held; only an escape in nav drops it
217
+ await press(setup, "escape");
218
+ await waitForTextGone(setup, "● x");
219
+ expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
220
+ await press(setup, "escape");
221
+ await waitForText(setup, "type to leave");
222
+ expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
223
+ await press(setup, "escape");
224
+
225
+ // Assert
226
+ await waitForState(setup, () => !backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
227
+
228
+ // Act: mark again, save
229
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
230
+ await type(setup, "Which daemon?");
231
+ await pressKey(setup, "RETURN", { meta: true });
232
+
233
+ // Assert: the saved comment keeps the passage marked
234
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
235
+ await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
236
+ }, 60_000);
237
+
238
+ test("backspace on an empty draft dismisses the composer back to the mark", async () => {
239
+ // Arrange
240
+ const setup = await renderApp();
241
+
242
+ // Act: mark, type one character, then delete it and backspace again on the now-empty draft
243
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
244
+ await type(setup, "x");
245
+ await waitForText(setup, "● x");
246
+ await press(setup, "backspace");
247
+ await press(setup, "backspace");
248
+
249
+ // Assert: the composer is gone and the mark stays, ready to re-type
250
+ await waitForTextGone(setup, "● x");
251
+ expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
252
+ }, 60_000);
253
+
254
+ test("esc from type mode keeps the mark, so c in nav comments on the marked text", async () => {
255
+ // Arrange
256
+ const setup = await renderApp();
257
+
258
+ // Act: mark, enter nav with esc, open the composer with c, save
259
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
260
+ await navCommand(setup, "c");
261
+ await type(setup, "Which daemon?");
262
+ await waitForText(setup, "● Which daemon?");
263
+ await pressKey(setup, "RETURN", { meta: true });
264
+
265
+ // Assert: the comment anchors to the mark, not to the word under the caret
266
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
267
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("The daemon");
268
+ }, 60_000);
269
+ });
270
+
271
+ describe("compose newline convention", () => {
272
+ test("enter breaks the line; cmd+enter saves the multiline body verbatim", async () => {
273
+ // Arrange
274
+ const setup = await renderApp();
275
+
276
+ await clickText(setup, "daemon");
277
+
278
+ // Act
279
+ await type(setup, "first line");
280
+ await press(setup, "enter");
281
+ await type(setup, "second line");
282
+
283
+ // Assert: still composing, the newline did not save
284
+ expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
285
+
286
+ // Act
287
+ await pressKey(setup, "RETURN", { meta: true });
288
+
289
+ // Assert
290
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
291
+ expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("first line\nsecond line");
292
+ });
293
+
294
+ test("escape cancels the composer without saving", async () => {
295
+ // Arrange
296
+ const setup = await renderApp();
297
+
298
+ await clickText(setup, "daemon");
299
+ await type(setup, "never saved");
300
+ await waitForText(setup, "● never saved");
301
+
302
+ // Act
303
+ await press(setup, "escape");
304
+
305
+ // Assert
306
+ await waitForTextGone(setup, "● never saved");
307
+ expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
308
+ });
309
+ });
310
+
311
+ describe("sheet header", () => {
312
+ test("mirrors the thread name and carries the Edit and Share word-buttons", async () => {
313
+ // Arrange
314
+ const setup = await renderApp();
315
+
316
+ // Assert - the minimal header shows the thread title (mirror) plus the owner
317
+ // actions; the revision and submitter metadata are no longer in the header
318
+ const frame = setup.captureCharFrame();
319
+
320
+ expect(frame).toContain("Migration Plan");
321
+ expect(frame).not.toContain("submitted by");
322
+ expect(frame).not.toContain("rev 1");
323
+ expect(frame).toContain(" edit ");
324
+ expect(frame).toContain(" share ");
325
+ });
326
+ });
327
+
328
+ describe("quick-actions settings editor", () => {
329
+ test("expanding an action edits its system prompt inline and persists it", async () => {
330
+ // Arrange
331
+ const setup = await renderApp();
332
+
333
+ // Act - open Settings from the top-left gear, enter Actions, expand the first action, type
334
+ await setup.mockMouse.click(1, 0);
335
+ await waitForText(setup, "Keybinds");
336
+ await clickText(setup, "settings");
337
+ await clickText(setup, "Actions");
338
+ await clickText(setup, "Zoom out, research in depth");
339
+ await type(setup, "CUSTOM");
340
+ await setup.renderOnce();
341
+
342
+ // Assert - the keystrokes reached the focused input (not swallowed by nav) and persisted
343
+ expect(setup.captureCharFrame()).toContain("CUSTOM");
344
+ await waitForState(setup, () =>
345
+ readFileSync(join(home, "no-config.toml"), "utf8").includes("CUSTOM"),
346
+ );
347
+ });
348
+ });
349
+
350
+ describe("edit-exit reconciliation", () => {
351
+ test("an edit that removes an anchored passage orphans the annotation and shows the banner", async () => {
352
+ // Arrange - anchor a comment to a passage
353
+ const setup = await renderApp();
354
+
355
+ await clickText(setup, "daemon");
356
+ await type(setup, "Anchor me to the doomed passage.");
357
+ await pressKey(setup, "RETURN", { meta: true });
358
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
359
+
360
+ // Act - open the inline editor, replace the whole body (dropping the passage), and leave
361
+ await pressKey(setup, "e", { ctrl: true });
362
+ await waitForText(setup, "save & close");
363
+ await pressKey(setup, "a", { meta: true });
364
+ await type(setup, "A fresh body with no anchored passage.");
365
+ await clickText(setup, "normal");
366
+
367
+ // Assert - the thread view banner reports the orphaned annotation
368
+ await waitForText(setup, "1 annotation no longer match - the passage was removed.");
369
+ // the annotation is NOT deleted: the feedback serializer handles orphans
370
+ expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
371
+ });
372
+ });
373
+
374
+ describe("thread switch preserves an open edit", () => {
375
+ test("clicking another thread while editing saves the body into the leaving thread", async () => {
376
+ // Arrange - a second thread sits in the sidebar to switch to
377
+ server.core.sessionCreate({
378
+ workspace: { repoRoot: "/repo", branch: "main" },
379
+ artifact: {
380
+ type: "plan",
381
+ content: "# Second Thread\n\nAnother body.\n",
382
+ meta: { title: "Second Thread", planPath: "plan.md", agent: "agent/worker-3" },
383
+ },
384
+ });
385
+ const setup = await renderApp();
386
+
387
+ // open the Threads sidebar so the other thread is on screen to click
388
+ await setup.mockMouse.click(4, 0);
389
+ await waitForText(setup, "Second Thread");
390
+
391
+ // Act - open the inline editor, rewrite the body, then click away before an explicit save
392
+ await pressKey(setup, "e", { ctrl: true });
393
+ await waitForText(setup, "save & close");
394
+ await pressKey(setup, "a", { meta: true });
395
+ await type(setup, "Body kept across a thread switch.");
396
+ await clickText(setup, "Second Thread");
397
+
398
+ // Assert - the leaving thread's working copy holds the edit rather than dropping it
399
+ await waitForState(setup, () =>
400
+ (server.core.sessionGet(session.id).workingCopy ?? "").includes(
401
+ "kept across a thread switch",
402
+ ),
403
+ );
404
+ });
405
+ });
406
+
407
+ describe("the quick-action palette", () => {
408
+ test("/ lists the quick actions and a pick inserts the reference, not the body", async () => {
409
+ // Arrange
410
+ const setup = await renderApp();
411
+
412
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
413
+
414
+ // Act - a leading slash opens the palette; step to the second default and pick it
415
+ await type(setup, "/");
416
+ await waitForText(setup, "zoom-out-research-in-depth");
417
+ await pressKey(setup, "ARROW_DOWN");
418
+ await press(setup, "enter");
419
+ await pressKey(setup, "RETURN", { meta: true });
420
+
421
+ // Assert - the comment holds the /name reference, not the expanded body
422
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
423
+ const stored = server.core.sessionGet(session.id);
424
+ const secondName = slashItemsFrom(DEFAULT_QUICK_ACTIONS)[1]!.name;
425
+
426
+ expect(stored.annotations[0]!.kind).toBe("comment");
427
+ expect(stored.annotations[0]!.body.trim()).toBe(`/${secondName}`);
428
+ });
429
+ });