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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/annotations/annotation-palette.ts +63 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +383 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +189 -0
  14. package/src/annotations/discussions.ts +78 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +248 -0
  17. package/src/annotations/use-annotation-surface.tsx +1378 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  20. package/src/app/App.deleted-scroll.test.tsx +190 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.harness-settings.test.tsx +77 -0
  25. package/src/app/App.identity.test.tsx +84 -0
  26. package/src/app/App.plan-render.test.tsx +211 -0
  27. package/src/app/App.plan-review.test.tsx +429 -0
  28. package/src/app/App.prototype-markdown.test.tsx +129 -0
  29. package/src/app/App.prototype.test.tsx +176 -0
  30. package/src/app/App.readonly.test.tsx +274 -0
  31. package/src/app/App.regression.test.tsx +99 -0
  32. package/src/app/App.rendering.test.tsx +134 -0
  33. package/src/app/App.share-dialog.test.tsx +177 -0
  34. package/src/app/App.submit-confirm.test.tsx +267 -0
  35. package/src/app/App.test.tsx +497 -0
  36. package/src/app/App.thread-agent.test.ts +20 -0
  37. package/src/app/App.toast.test.tsx +124 -0
  38. package/src/app/App.tsx +1758 -0
  39. package/src/app/App.visual.test.tsx +60 -0
  40. package/src/app/App.walk.test.tsx +300 -0
  41. package/src/app/App.workbench.test.tsx +1152 -0
  42. package/src/app/app-key-state.ts +61 -0
  43. package/src/app/app-screens.tsx +607 -0
  44. package/src/app/app-view-model.ts +178 -0
  45. package/src/app/components/AppHeader.stories.tsx +50 -0
  46. package/src/app/components/AppHeader.tsx +81 -0
  47. package/src/app/components/AppShell.brand.test.tsx +58 -0
  48. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  49. package/src/app/components/AppShell.stories.tsx +89 -0
  50. package/src/app/components/AppShell.tsx +358 -0
  51. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  52. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  53. package/src/app/components/Breadcrumb.tsx +45 -0
  54. package/src/app/components/MenuBar.stories.tsx +28 -0
  55. package/src/app/components/MenuBar.tsx +44 -0
  56. package/src/app/components/PaneGrid.stories.tsx +84 -0
  57. package/src/app/components/PaneGrid.tsx +128 -0
  58. package/src/app/components/PanelColumn.stories.tsx +25 -0
  59. package/src/app/components/PanelColumn.tsx +93 -0
  60. package/src/app/components/ShellHeader.stories.tsx +50 -0
  61. package/src/app/components/ShellHeader.tsx +137 -0
  62. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  63. package/src/app/components/WorkspacePanels.tsx +121 -0
  64. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  65. package/src/app/intent-dispatch.test.ts +729 -0
  66. package/src/app/intent-dispatch.ts +713 -0
  67. package/src/app/launch-layout.test.ts +78 -0
  68. package/src/app/launch-layout.ts +57 -0
  69. package/src/app/ready-signal.test.ts +85 -0
  70. package/src/app/ready-signal.ts +54 -0
  71. package/src/app/run.ts +111 -0
  72. package/src/{serve.test.ts → app/serve.test.ts} +28 -12
  73. package/src/app/serve.ts +111 -0
  74. package/src/app/version.ts +5 -0
  75. package/src/appearance/components/theme-context.stories.tsx +46 -0
  76. package/src/appearance/components/theme-context.tsx +37 -0
  77. package/src/appearance/theme-presets.test.ts +51 -0
  78. package/src/appearance/theme-presets.ts +219 -0
  79. package/src/appearance/theme.ts +112 -0
  80. package/src/appearance/visual/contrast.test.ts +35 -0
  81. package/src/appearance/visual/contrast.ts +33 -0
  82. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  83. package/src/diff/components/AnnotatableFileView.test.tsx +160 -0
  84. package/src/diff/components/AnnotatableFileView.tsx +142 -0
  85. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  86. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  87. package/src/diff/components/DiffContentView.scroll.test.tsx +443 -0
  88. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  89. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  90. package/src/diff/components/DiffContentView.syntax.test.tsx +85 -0
  91. package/src/diff/components/DiffContentView.tsx +1080 -0
  92. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  93. package/src/diff/components/WalkWizard.tsx +146 -0
  94. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  95. package/src/diff/components/diff-content-view-layout.ts +87 -0
  96. package/src/diff/diff-fold.test.ts +78 -0
  97. package/src/diff/diff-fold.ts +130 -0
  98. package/src/diff/diff-hunk-curate.test.ts +315 -0
  99. package/src/diff/diff-hunk-curate.ts +155 -0
  100. package/src/diff/diff-intraline.test.ts +239 -0
  101. package/src/diff/diff-intraline.ts +244 -0
  102. package/src/diff/diff-syntax.test.ts +125 -0
  103. package/src/diff/diff-syntax.ts +194 -0
  104. package/src/diff/split-diff.test.ts +110 -0
  105. package/src/diff/split-diff.ts +150 -0
  106. package/src/diff/view-diff.test.ts +325 -0
  107. package/src/diff/view-diff.ts +540 -0
  108. package/src/diff/walk.test.ts +121 -0
  109. package/src/diff/walk.ts +76 -0
  110. package/src/index.ts +32 -2
  111. package/src/integrations/attribution.test.ts +31 -0
  112. package/src/integrations/attribution.ts +44 -0
  113. package/src/integrations/client-extension-registry.test.tsx +61 -0
  114. package/src/integrations/client-extension-registry.ts +163 -0
  115. package/src/integrations/clipboard.test.ts +23 -0
  116. package/src/integrations/clipboard.ts +34 -0
  117. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  118. package/src/integrations/components/ExtensionZone.tsx +145 -0
  119. package/src/integrations/components/ShareDialog.agent.test.tsx +52 -0
  120. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  121. package/src/integrations/components/ShareDialog.tsx +602 -0
  122. package/src/integrations/components/share-dialog-store.ts +59 -0
  123. package/src/integrations/editor.test.ts +149 -0
  124. package/src/integrations/editor.ts +163 -0
  125. package/src/integrations/github-identity.test.ts +59 -0
  126. package/src/integrations/github-identity.ts +42 -0
  127. package/src/integrations/herdr.test.ts +43 -0
  128. package/src/integrations/herdr.ts +40 -0
  129. package/src/integrations/integrations.test.ts +41 -0
  130. package/src/integrations/integrations.ts +32 -0
  131. package/src/integrations/pasted-image.test.ts +67 -0
  132. package/src/integrations/pasted-image.ts +62 -0
  133. package/src/integrations/share.ts +283 -0
  134. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  135. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  136. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  137. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  138. package/src/keyboard/components/HandleChipsInput.tsx +147 -0
  139. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  140. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  141. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  142. package/src/keyboard/components/NavModeHint.tsx +31 -0
  143. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  144. package/src/keyboard/components/SlashComposer.tsx +90 -0
  145. package/src/keyboard/key-bindings.test.ts +156 -0
  146. package/src/keyboard/key-bindings.ts +462 -0
  147. package/src/keyboard/keymap.test.ts +611 -0
  148. package/src/keyboard/keymap.ts +446 -0
  149. package/src/keyboard/skills.test.ts +74 -0
  150. package/src/keyboard/skills.ts +77 -0
  151. package/src/keyboard/slash-palette.test.ts +103 -0
  152. package/src/keyboard/slash-palette.ts +137 -0
  153. package/src/keyboard/thread-chords.test.ts +79 -0
  154. package/src/keyboard/thread-chords.ts +168 -0
  155. package/src/keyboard/use-shared-keyboard.ts +56 -0
  156. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  157. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  158. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  159. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  160. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  161. package/src/markdown/components/MarkdownThreadEditor.tsx +165 -0
  162. package/src/markdown/components/ThreadView.selection.test.tsx +954 -0
  163. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  164. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  165. package/src/markdown/components/ThreadView.tsx +577 -0
  166. package/src/markdown/mark-runs.ts +125 -0
  167. package/src/markdown/markdown-editor-syntax-style.ts +66 -0
  168. package/src/markdown/markdown-grid.test.ts +101 -0
  169. package/src/markdown/markdown-grid.ts +209 -0
  170. package/src/markdown/markdown-highlight.test.ts +61 -0
  171. package/src/markdown/markdown-highlight.ts +125 -0
  172. package/src/markdown/thread-code-syntax.ts +48 -0
  173. package/src/markdown/use-thread-body-editing.tsx +54 -0
  174. package/src/markdown/view-plan.regression.test.ts +60 -0
  175. package/src/markdown/view-plan.test.ts +545 -0
  176. package/src/markdown/view-plan.ts +1061 -0
  177. package/src/performance/otlp-trace.test.ts +119 -0
  178. package/src/performance/otlp-trace.ts +134 -0
  179. package/src/performance/perf-report.ts +19 -0
  180. package/src/performance/perf-timings.test.ts +18 -0
  181. package/src/performance/perf-timings.ts +40 -0
  182. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  183. package/src/prototype/components/PrototypeContentView.tsx +459 -0
  184. package/src/prototype/prototype-browser.test.ts +81 -0
  185. package/src/prototype/prototype-browser.ts +332 -0
  186. package/src/prototype/prototype-pixels.tsx +35 -0
  187. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  188. package/src/settings/components/SettingsDialog.tsx +186 -0
  189. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  190. package/src/settings/components/SettingsRows.tsx +112 -0
  191. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  192. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  193. package/src/settings/components/quick-actions-editor.tsx +222 -0
  194. package/src/settings/config-identity.test.ts +78 -0
  195. package/src/settings/config.test.ts +754 -0
  196. package/src/settings/config.ts +718 -0
  197. package/src/settings/use-settings-dialog.test.tsx +172 -0
  198. package/src/settings/use-settings-dialog.tsx +465 -0
  199. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  200. package/src/settings/use-thread-harness-setting.ts +86 -0
  201. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  202. package/src/stories/stories-app.tsx +196 -0
  203. package/src/stories/stories.test.tsx +78 -0
  204. package/src/stories/story-fixtures.ts +102 -0
  205. package/src/stories/story-tree.test.ts +108 -0
  206. package/src/stories/story.ts +160 -0
  207. package/src/terminal/ansi-screen.test.ts +28 -0
  208. package/src/terminal/ansi-screen.ts +77 -0
  209. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  210. package/src/terminal/components/AnsiScreen.tsx +28 -0
  211. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  212. package/src/terminal/components/terminal-pane.ts +208 -0
  213. package/src/terminal/ghostty-terminal.test.ts +85 -0
  214. package/src/terminal/ghostty-terminal.ts +207 -0
  215. package/src/terminal/kitty-image.test.ts +66 -0
  216. package/src/terminal/kitty-image.ts +140 -0
  217. package/src/terminal/pty.test.ts +81 -0
  218. package/src/terminal/pty.ts +255 -0
  219. package/src/testing/test-support.test.ts +266 -0
  220. package/src/testing/test-support.ts +424 -0
  221. package/src/thread/agent-continuation.test.ts +34 -0
  222. package/src/thread/agent-continuation.ts +13 -0
  223. package/src/thread/agent-transcript.test.ts +161 -0
  224. package/src/thread/agent-transcript.ts +344 -0
  225. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  226. package/src/thread/components/AgentConfigControls.test.tsx +153 -0
  227. package/src/thread/components/AgentConfigControls.tsx +121 -0
  228. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  229. package/src/thread/components/AgentThreadPane.test.tsx +1248 -0
  230. package/src/thread/components/AgentThreadPane.tsx +458 -0
  231. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  232. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  233. package/src/thread/components/ThreadFooter.tsx +77 -0
  234. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  235. package/src/thread/components/ThreadTree.test.tsx +41 -0
  236. package/src/thread/components/ThreadTree.tsx +268 -0
  237. package/src/thread/components/thread-tree.test.ts +138 -0
  238. package/src/thread/components/thread-tree.ts +112 -0
  239. package/src/thread/thread-controller.curate.test.ts +504 -0
  240. package/src/thread/thread-controller.derivation.test.ts +62 -0
  241. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  242. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  243. package/src/thread/thread-controller.share.test.ts +618 -0
  244. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  245. package/src/thread/thread-controller.tree.test.ts +263 -0
  246. package/src/thread/thread-controller.ts +2151 -0
  247. package/src/thread/tree-view.test.ts +128 -0
  248. package/src/thread/tree-view.ts +150 -0
  249. package/src/thread/use-thread-agent.test.tsx +209 -0
  250. package/src/thread/use-thread-agent.ts +122 -0
  251. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  252. package/src/ui/components/CodeBlock.tsx +82 -0
  253. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  254. package/src/ui/components/ConfirmCard.tsx +151 -0
  255. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  256. package/src/ui/components/ConfirmDialog.tsx +72 -0
  257. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  258. package/src/ui/components/OverlayScrollbar.test.tsx +142 -0
  259. package/src/ui/components/OverlayScrollbar.tsx +134 -0
  260. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  261. package/src/ui/components/PromptDialog.test.tsx +65 -0
  262. package/src/ui/components/PromptDialog.tsx +101 -0
  263. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  264. package/src/ui/components/RootOverlay.tsx +50 -0
  265. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  266. package/src/ui/components/ScrollArea.tsx +63 -0
  267. package/src/ui/components/Toast.stories.tsx +19 -0
  268. package/src/ui/components/Toast.tsx +54 -0
  269. package/src/ui/components/Tooltip.stories.tsx +26 -0
  270. package/src/ui/components/Tooltip.tsx +93 -0
  271. package/src/ui/components/menu-control.stories.tsx +35 -0
  272. package/src/ui/components/menu-control.tsx +43 -0
  273. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  274. package/src/ui/components/primitives/Button.tsx +58 -0
  275. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  276. package/src/ui/components/primitives/Card.tsx +62 -0
  277. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  278. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  279. package/src/ui/components/primitives/Dialog.tsx +69 -0
  280. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  281. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  282. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  283. package/src/ui/components/primitives/IconButton.tsx +58 -0
  284. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  285. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  286. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Switch.tsx +32 -0
  288. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  289. package/src/ui/components/primitives/Tabs.tsx +102 -0
  290. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  291. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  292. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  293. package/src/ui/components/primitives/Tree.tsx +137 -0
  294. package/src/ui/components/primitives/frame.test.ts +12 -0
  295. package/src/ui/components/primitives/frame.ts +17 -0
  296. package/src/ui/components/primitives/icons.ts +61 -0
  297. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  298. package/src/ui/components/primitives/tree-model.ts +144 -0
  299. package/src/ui/components/syntax-highlight.ts +154 -0
  300. package/src/ui/components/truncate-text.ts +7 -0
  301. package/src/ui/components/truncate-title.test.ts +31 -0
  302. package/src/ui/components/truncate-title.ts +16 -0
  303. package/src/ui/use-frame-measure.ts +88 -0
  304. package/src/ui/use-terminal-virtualizer.ts +286 -0
  305. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  306. package/src/workbench/components/ChangesColumn.tsx +210 -0
  307. package/src/workbench/components/ChangesFileTree.test.tsx +184 -0
  308. package/src/workbench/components/EditorGrid.split.test.tsx +119 -0
  309. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  310. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  311. package/src/workbench/components/EditorGrid.tsx +552 -0
  312. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  313. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  314. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  315. package/src/workbench/components/FileContentsView.tsx +104 -0
  316. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  317. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  318. package/src/workbench/components/GridTabContent.tsx +195 -0
  319. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  320. package/src/workbench/components/ProjectTreeView.test.tsx +239 -0
  321. package/src/workbench/components/ProjectTreeView.tsx +159 -0
  322. package/src/workbench/components/TreePane.stories.tsx +106 -0
  323. package/src/workbench/components/TreePane.tsx +157 -0
  324. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  325. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  326. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  327. package/src/workbench/components/editor-grid.test.ts +184 -0
  328. package/src/workbench/components/editor-grid.ts +284 -0
  329. package/src/workbench/components/file-tree.test.ts +60 -0
  330. package/src/workbench/components/file-tree.ts +114 -0
  331. package/src/workbench/use-changes-workbench.ts +210 -0
  332. package/src/workbench/use-remember-layout.ts +33 -0
  333. package/src/workbench/use-repo-changes.ts +93 -0
  334. package/src/workbench/workbench-snapshot.test.ts +99 -0
  335. package/src/workbench/workbench-snapshot.ts +59 -0
  336. package/src/App.diff.test.tsx +0 -99
  337. package/src/App.export.test.tsx +0 -88
  338. package/src/App.plan-review.test.tsx +0 -259
  339. package/src/App.readonly.test.tsx +0 -148
  340. package/src/App.rendering.test.tsx +0 -108
  341. package/src/App.submit-confirm.test.tsx +0 -183
  342. package/src/App.test.tsx +0 -218
  343. package/src/App.tsx +0 -1249
  344. package/src/config.test.ts +0 -116
  345. package/src/config.ts +0 -150
  346. package/src/editor.ts +0 -34
  347. package/src/herdr.test.ts +0 -68
  348. package/src/herdr.ts +0 -34
  349. package/src/keymap.test.ts +0 -270
  350. package/src/keymap.ts +0 -173
  351. package/src/run.ts +0 -30
  352. package/src/serve.ts +0 -75
  353. package/src/session-controller.ts +0 -398
  354. package/src/syntax.test.ts +0 -41
  355. package/src/syntax.ts +0 -88
  356. package/src/theme.ts +0 -46
  357. package/src/view-diff.test.ts +0 -55
  358. package/src/view-diff.ts +0 -86
  359. package/src/view.test.ts +0 -157
  360. package/src/view.ts +0 -364
@@ -0,0 +1,497 @@
1
+ /** Virtual-terminal component tests (tier 2): the real App over a real in-process daemon in a temp home. Char-frame assertions + mock keys - the whole review loop drivable without a terminal. */
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 {
13
+ clickText,
14
+ dragText,
15
+ frameRow,
16
+ isolateUserConfig,
17
+ locateText,
18
+ press,
19
+ navCommand,
20
+ pressKey,
21
+ renderReadyApp,
22
+ typeText as type,
23
+ waitForState,
24
+ waitForText,
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
+ Use the **safe** mode.
39
+ `;
40
+
41
+ let home: string;
42
+ let restoreUserConfig: () => void;
43
+ let server: DaemonServer;
44
+ let session: Thread;
45
+
46
+ beforeEach(() => {
47
+ home = mkdtempSync(join(tmpdir(), "cueloop-app-"));
48
+ restoreUserConfig = isolateUserConfig(home);
49
+ server = new DaemonServer({ home, idleExitMs: 0 });
50
+ server.start();
51
+ session = server.core.sessionCreate({
52
+ workspace: { repoRoot: "/repo", branch: "main" },
53
+ artifact: {
54
+ type: "plan",
55
+ content: PLAN,
56
+ meta: { title: "Migration Plan", planPath: "plan.md" },
57
+ },
58
+ });
59
+ });
60
+ afterEach(() => {
61
+ restoreUserConfig();
62
+ server.stop();
63
+ rmSync(home, { recursive: true, force: true });
64
+ });
65
+
66
+ async function renderApp(sessionId?: string) {
67
+ return renderReadyApp(<App home={home} sessionId={sessionId ?? session.id} />, {
68
+ width: 120,
69
+ height: 32,
70
+ });
71
+ }
72
+
73
+ type Setup = Awaited<ReturnType<typeof renderApp>>;
74
+
75
+ function foregroundsOf(setup: Setup, needle: string): string[] {
76
+ const foregrounds: string[] = [];
77
+
78
+ for (const line of setup.captureSpans().lines) {
79
+ for (const span of line.spans) {
80
+ if (!span.text.includes(needle)) continue;
81
+
82
+ const [red, green, blue] = span.fg.toInts();
83
+
84
+ foregrounds.push(
85
+ "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
86
+ );
87
+ }
88
+ }
89
+
90
+ return foregrounds;
91
+ }
92
+
93
+ describe("plan rendering", () => {
94
+ test("renders the plan with headings, list markers, and the footer", async () => {
95
+ // Arrange
96
+ const setup = await renderApp();
97
+
98
+ // Assert
99
+ const frame = setup.captureCharFrame();
100
+
101
+ expect(frame).toContain("Migration Plan");
102
+ expect(frame).toContain("Context");
103
+ expect(frame).toContain("persists sessions to disk atomically");
104
+ expect(frame).toContain("· move the store");
105
+ expect(frame).toContain("Send message (");
106
+ });
107
+
108
+ test("a direct open still populates the Threads sidebar with other pending reviews", async () => {
109
+ // Arrange - a second pending review exists alongside the one opened directly
110
+ server.core.sessionCreate({
111
+ workspace: { repoRoot: "/repo", branch: "main" },
112
+ artifact: { type: "plan", content: "# Other Plan\n", meta: { title: "Other Plan" } },
113
+ });
114
+ const setup = await renderApp();
115
+
116
+ // Act - open the collapsed Threads sidebar (the mirrored panel toggle by the gear)
117
+ await setup.mockMouse.click(4, 0);
118
+
119
+ // Assert - the sidebar lists the other pending review, so you can jump to it
120
+ await waitForText(setup, "Other Plan");
121
+ });
122
+ });
123
+
124
+ describe("thread view grammar", () => {
125
+ test("↓ moves the caret between blocks and typing anchors to the block under it", async () => {
126
+ // Arrange
127
+ const setup = await renderApp();
128
+
129
+ // Act: two blocks down lands on the paragraph; a printable opens a draft there
130
+ await pressKey(setup, "ARROW_DOWN");
131
+ await pressKey(setup, "ARROW_DOWN");
132
+ await type(setup, "x");
133
+
134
+ // Assert: the draft card sits right under the paragraph's row
135
+ await waitForText(setup, "● x");
136
+ expect(frameRow(setup, "● x")).toBeGreaterThan(frameRow(setup, "persists sessions"));
137
+ expect(frameRow(setup, "● x")).toBeLessThan(frameRow(setup, "Steps"));
138
+ });
139
+
140
+ test("comment flow: click a word, type a body, cmd+⏎ saves to the daemon", async () => {
141
+ // Arrange
142
+ const setup = await renderApp();
143
+
144
+ // Act
145
+ await clickText(setup, "daemon");
146
+ await type(setup, "Define atomically.");
147
+ await pressKey(setup, "RETURN", { meta: true });
148
+
149
+ // Assert
150
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
151
+ const stored = server.core.sessionGet(session.id);
152
+
153
+ expect(stored.annotations.length).toBe(1);
154
+ expect(stored.annotations[0]!.body).toBe("Define atomically.");
155
+ expect(stored.annotations[0]!.anchor.quote).toBe("daemon");
156
+ });
157
+
158
+ test("a drag marks the exact span the comment anchors to", async () => {
159
+ // Arrange
160
+ const setup = await renderApp();
161
+
162
+ // Act: from the start of "The" to the end of "daemon"
163
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
164
+ await type(setup, "Which daemon?");
165
+ await pressKey(setup, "RETURN", { meta: true });
166
+
167
+ // Assert
168
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
169
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("The daemon");
170
+ });
171
+
172
+ test("option+x cuts the caret's block into the working copy; option+x restores it", async () => {
173
+ // Arrange
174
+ const setup = await renderApp();
175
+
176
+ await clickText(setup, "move the store");
177
+
178
+ // Act
179
+ await navCommand(setup, "x");
180
+
181
+ // Assert - the cut lands in the working copy
182
+ await waitForState(
183
+ setup,
184
+ () => !(server.core.sessionGet(session.id).workingCopy ?? "").includes("move the store"),
185
+ );
186
+
187
+ // Act
188
+ await navCommand(setup, "x");
189
+
190
+ // Assert
191
+ await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
192
+ });
193
+
194
+ test("Cut removes only the marked characters instead of their whole block", async () => {
195
+ const setup = await renderApp();
196
+
197
+ await dragText(setup, "move the store", "move the store", "move".length);
198
+ await navCommand(setup, "x");
199
+ await waitForState(setup, () =>
200
+ (server.core.sessionGet(session.id).workingCopy ?? "").includes("- the store"),
201
+ );
202
+ const working = server.core.sessionGet(session.id).workingCopy ?? "";
203
+ const textCuts = server.core.sessionGet(session.id).textCuts;
204
+
205
+ expect(working).not.toContain("move the store");
206
+ expect(working).toContain("- the store");
207
+ expect(working).toContain("- add recovery");
208
+ expect(textCuts).toHaveLength(1);
209
+ expect(textCuts?.[0]?.quote).toBe("move");
210
+ expect(foregroundsOf(setup, "the store")).not.toContain(DARK.green);
211
+ expect(setup.captureCharFrame()).not.toContain("selection cut");
212
+
213
+ await pressKey(setup, "x");
214
+ await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
215
+
216
+ expect(server.core.sessionGet(session.id).textCuts).toBeUndefined();
217
+ });
218
+
219
+ test("a large partial Cut keeps its surviving prefix plain in one modified block", async () => {
220
+ const original = "A seeded plan so cueloop dev always has a thread to open.";
221
+ const shortened = server.core.sessionCreate({
222
+ workspace: { repoRoot: "/repo", branch: "main" },
223
+ artifact: { type: "plan", content: original, meta: { title: "Partial Cut" } },
224
+ });
225
+
226
+ server.core.sessionSetWorkingCopy(shortened.id, "A seed");
227
+ const setup = await renderApp(shortened.id);
228
+
229
+ await waitForText(setup, "A seed");
230
+ expect(foregroundsOf(setup, "seed")).toContain(DARK.text);
231
+ expect(foregroundsOf(setup, "seed")).not.toContain(DARK.green);
232
+ });
233
+
234
+ test("Cut maps rendered inline Markdown back to its exact source characters", async () => {
235
+ const setup = await renderApp();
236
+
237
+ await dragText(setup, "safe", "safe", "safe".length);
238
+ await navCommand(setup, "x");
239
+ await waitForState(setup, () =>
240
+ (server.core.sessionGet(session.id).workingCopy ?? "").includes("Use the **** mode."),
241
+ );
242
+
243
+ expect(server.core.sessionGet(session.id).workingCopy).toContain("Use the **** mode.");
244
+ });
245
+
246
+ test("ctrl+e opens the inline editor; the header toggles to normal and leaving tracks the edit", async () => {
247
+ // Arrange
248
+ const setup = await renderApp();
249
+
250
+ // Act - open the inline markdown editor over the thread body
251
+ await pressKey(setup, "e", { ctrl: true });
252
+
253
+ // Assert - the editor owns the pane (its save hint shows) and the header offers the way back
254
+ await waitForText(setup, "save & close");
255
+ expect(setup.captureCharFrame()).toContain("normal");
256
+
257
+ // Act - type into the body, then leave through the header toggle
258
+ await type(setup, "MORE");
259
+ await clickText(setup, "normal");
260
+
261
+ // Assert - the edit is tracked into the working copy, and the read-only view is back
262
+ await waitForState(setup, () =>
263
+ (server.core.sessionGet(session.id).workingCopy ?? "").includes("MORE"),
264
+ );
265
+ });
266
+ });
267
+
268
+ describe("submit", () => {
269
+ test("cmd+⏎ opens the rail confirm card; message + summary resolve the session", async () => {
270
+ // Arrange
271
+ const setup = await renderApp();
272
+
273
+ await clickText(setup, "daemon");
274
+ await type(setup, "Needs a phase list.");
275
+ await pressKey(setup, "RETURN", { meta: true });
276
+ await waitForText(setup, "Needs a phase list");
277
+
278
+ // Act: with no composer open the same chord opens submit
279
+ await pressKey(setup, "RETURN", { meta: true });
280
+
281
+ // Assert - the card opens on the default message, approve
282
+ await waitForText(setup, "[approve]");
283
+
284
+ // Act - cycle to request changes, then send with a summary
285
+ await press(setup, "right");
286
+ await waitForText(setup, "[changes]");
287
+ await type(setup, "Expand the steps.");
288
+ await pressKey(setup, "RETURN", { meta: true });
289
+
290
+ // Assert
291
+ await waitForText(setup, "feedback sent");
292
+ const stored = server.core.sessionGet(session.id);
293
+
294
+ expect(stored.status).toBe("resolved");
295
+ expect(stored.message!.outcome).toBe("changes_requested");
296
+ expect(stored.message!.body).toContain("Needs a phase list.");
297
+ // submit hands the reviewer back to the agent via the completion overlay
298
+ expect(setup.captureCharFrame()).toContain("feedback sent");
299
+ });
300
+
301
+ test("approve via ←/→ message cycling", async () => {
302
+ // Arrange
303
+ const setup = await renderApp();
304
+
305
+ // Act
306
+ await pressKey(setup, "RETURN", { meta: true });
307
+
308
+ // Assert
309
+ await waitForText(setup, "[approve]"); // no pending items → approve default
310
+
311
+ // Act
312
+ await pressKey(setup, "RETURN", { meta: true });
313
+
314
+ // Assert
315
+ await waitForState(setup, () => server.core.sessionGet(session.id).message !== undefined);
316
+ expect(server.core.sessionGet(session.id).message!.outcome).toBe("approved");
317
+ });
318
+ });
319
+
320
+ describe("no-thread shell", () => {
321
+ test("opening with nothing selected lands in the shell with a Welcome tab and the Threads sidebar", async () => {
322
+ // Arrange
323
+ const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
324
+
325
+ // the Welcome playground measures its width before it paints, so wait for its copy, not the header
326
+ await waitForText(setup, "Getting started");
327
+
328
+ // Assert - the same shell header, the disposable Welcome tab, and the pending thread
329
+ const frame = setup.captureCharFrame();
330
+
331
+ expect(frame).toContain("Getting started");
332
+ expect(frame).toContain("Migration Plan");
333
+ expect(frame).not.toContain("· resume"); // the bespoke inbox header is retired
334
+
335
+ // Act
336
+ await press(setup, "enter");
337
+
338
+ // Assert
339
+ await waitForText(setup, "Send message (");
340
+ });
341
+
342
+ test("picking a thread from the no-thread shell keeps the Threads sidebar open", async () => {
343
+ // Arrange - a second pending thread so the sidebar has a row that never shows
344
+ // in the opened thread's own view
345
+ server.core.sessionCreate({
346
+ workspace: { repoRoot: "/repo", branch: "main" },
347
+ artifact: { type: "plan", content: "# Other Plan\n", meta: { title: "Other Plan" } },
348
+ });
349
+ const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
350
+
351
+ await waitForText(setup, "Getting started");
352
+ await waitForText(setup, "Other Plan"); // the sidebar opens by default here
353
+
354
+ // Act - open the thread under the cursor
355
+ await press(setup, "enter");
356
+ await waitForText(setup, "Send message (");
357
+
358
+ // Assert - the sidebar stayed open across the swap: both titles are on screen,
359
+ // and the non-opened one can only come from the still-open sidebar
360
+ const frame = setup.captureCharFrame();
361
+
362
+ expect(frame).toContain("Migration Plan");
363
+ expect(frame).toContain("Other Plan");
364
+ });
365
+
366
+ test("the row kebab menu stars a thread into a Starred section", async () => {
367
+ // Arrange
368
+ const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
369
+
370
+ await waitForText(setup, "Migration Plan");
371
+
372
+ // Act - hover the row to reveal its kebab, open the menu, and pick Star
373
+ const row = locateText(setup, "Migration Plan");
374
+
375
+ await setup.mockMouse.moveTo(row.column, row.row);
376
+ await waitForText(setup, "⋮");
377
+ await clickText(setup, "⋮");
378
+ // "star" alone collides with "Getting started" in the welcome pane, so target the menu via its
379
+ // unique "rename" row - the star action sits directly above it
380
+ await waitForText(setup, "rename");
381
+ const rename = locateText(setup, "rename");
382
+
383
+ await setup.mockMouse.click(rename.column, rename.row - 1);
384
+
385
+ // Assert - a Starred section now holds the thread
386
+ await waitForText(setup, "Starred");
387
+ expect(frameRow(setup, "Migration Plan")).toBeGreaterThan(frameRow(setup, "Starred"));
388
+ });
389
+
390
+ test("a bare launch shows the Welcome surface with the right region collapsed", async () => {
391
+ // Arrange
392
+ const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
393
+
394
+ await waitForText(setup, "Getting started");
395
+
396
+ // Assert - Welcome fills the thread pane (not a Changes tab); the right region stays collapsed
397
+ const frame = setup.captureCharFrame();
398
+
399
+ expect(frame).toContain("Getting started");
400
+ expect(frame).not.toContain("Changes"); // the Changes editor is closed on a bare launch
401
+ expect(frame).toContain("Migration Plan"); // the sidebar lists the pending thread
402
+ });
403
+
404
+ test("the menu opens from the shell gear and escape is not a trap", async () => {
405
+ // Arrange
406
+ const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
407
+
408
+ await waitForText(setup, "Getting started");
409
+
410
+ // Act - open the settings dialog from the top-left gear (the Threads panel header, row 0)
411
+ await setup.mockMouse.click(1, 0);
412
+
413
+ // Assert - the settings dialog with its Keybinds leaf appears
414
+ await waitForText(setup, "Keybinds");
415
+ expect(setup.captureCharFrame()).toContain("settings");
416
+
417
+ // Act - escape closes the menu (not a trap), and the thread nav still works
418
+ await press(setup, "escape");
419
+ await waitForState(setup, () => !setup.captureCharFrame().includes("Keybinds"));
420
+ await press(setup, "enter");
421
+
422
+ // Assert
423
+ await waitForText(setup, "Send message (");
424
+ });
425
+ });
426
+
427
+ describe("the thread view and the menu", () => {
428
+ test("the keybinds dialog lists the thread view grammar, keeps keys away from the text, and closes on escape", async () => {
429
+ // Arrange
430
+ const setup = await renderApp();
431
+
432
+ await waitForText(setup, "The daemon persists");
433
+
434
+ // Act - open the menu from the top-left settings gear, then the keybinds dialog
435
+ await setup.mockMouse.click(1, 0);
436
+ await waitForText(setup, "Keybinds");
437
+ const dropUp = setup.captureCharFrame().split("\n");
438
+ const keybindsRow = dropUp.findIndex((line) => line.includes("Keybinds"));
439
+
440
+ await setup.mockMouse.click(dropUp[keybindsRow]!.indexOf("Keybinds") + 1, keybindsRow);
441
+ await waitForText(setup, "mark text, across blocks");
442
+
443
+ // Assert - the thread grammar, not the plan sheet's
444
+ const dialog = setup.captureCharFrame();
445
+
446
+ expect(dialog).toContain("nav mode");
447
+ expect(dialog).toContain("open selected file or toggle folder");
448
+ expect(dialog).toContain("place the caret");
449
+ expect(dialog).not.toContain("grow/shrink");
450
+
451
+ // Act - a printable behind the dialog must not open a comment; escape closes it
452
+ await press(setup, "x");
453
+ expect(setup.captureCharFrame()).not.toContain("● x");
454
+ await press(setup, "escape");
455
+
456
+ // Assert
457
+ await waitForState(setup, () => !setup.captureCharFrame().includes("mark text, across blocks"));
458
+ expect(setup.captureCharFrame()).not.toContain("● x");
459
+ });
460
+
461
+ test("clicking the Settings nav group is inert and never corrupts the active category", async () => {
462
+ // Arrange - open the settings dialog from the gear
463
+ const setup = await renderApp();
464
+
465
+ await setup.mockMouse.click(1, 0);
466
+ await waitForText(setup, "Keybinds");
467
+
468
+ // the nav folder is the last "settings" on screen (the first is the dialog title)
469
+ const lines = setup.captureCharFrame().split("\n");
470
+ let folderRow = -1;
471
+ let folderColumn = -1;
472
+
473
+ lines.forEach((line, row) => {
474
+ const column = line.indexOf("settings");
475
+
476
+ if (column !== -1) {
477
+ folderColumn = column;
478
+ folderRow = row;
479
+ }
480
+ });
481
+
482
+ // Act - click the synthetic Settings group folder, then press a nav key. The
483
+ // key handler dereferences the active category, so a corrupt id would throw
484
+ // (the global key handler swallows the throw into console.error).
485
+ const keyHandlerErrors: string[] = [];
486
+ const originalConsoleError = console.error;
487
+
488
+ console.error = (...args: unknown[]) => keyHandlerErrors.push(args.map(String).join(" "));
489
+ await setup.mockMouse.click(folderColumn + 1, folderRow);
490
+ await pressKey(setup, "ARROW_DOWN");
491
+ console.error = originalConsoleError;
492
+
493
+ // Assert - no key-handler crash, and the dialog is still up
494
+ expect(keyHandlerErrors.filter((line) => line.includes("keypress handler"))).toEqual([]);
495
+ expect(setup.captureCharFrame()).toContain("Keybinds");
496
+ });
497
+ });
@@ -0,0 +1,20 @@
1
+ import { expect, test } from "bun:test";
2
+ import { canRunThreadAgent } from "./App";
3
+
4
+ test("experimental agent UI requires a local owner and an enabled flag", () => {
5
+ const local = { enabled: true, owner: true, editing: false, shared: false };
6
+
7
+ expect(canRunThreadAgent(local)).toBe(true);
8
+ expect(canRunThreadAgent({ ...local, enabled: false })).toBe(false);
9
+ expect(canRunThreadAgent({ ...local, owner: false })).toBe(false);
10
+ expect(canRunThreadAgent({ ...local, editing: true })).toBe(false);
11
+ expect(canRunThreadAgent({ ...local, shared: true })).toBe(false);
12
+ });
13
+
14
+ test("a per-share agent capability enables collaborator chat without a gateway flag", () => {
15
+ const shared = { enabled: false, owner: false, editing: false, shared: true };
16
+
17
+ expect(canRunThreadAgent(shared)).toBe(false);
18
+ expect(canRunThreadAgent({ ...shared, sharedAgent: true })).toBe(true);
19
+ expect(canRunThreadAgent({ ...shared, sharedAgent: true, editing: true })).toBe(false);
20
+ });
@@ -0,0 +1,124 @@
1
+ /** The share toast is non-modal: while it is up, escape still cancels an open overlay. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, mock, 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 { testRender } from "@opentui/react/test-utils";
9
+ import { DaemonServer } from "@cueloop/daemon";
10
+ import type { Thread } from "@cueloop/schema";
11
+ import { App } from "./App";
12
+ import type { ShareTransport } from "../thread/thread-controller";
13
+ import {
14
+ clickText,
15
+ isolateUserConfig,
16
+ locateText,
17
+ press,
18
+ pressKey,
19
+ typeText,
20
+ waitForText,
21
+ waitForTextGone,
22
+ } from "../testing/test-support";
23
+
24
+ const publishShare = mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true }));
25
+ const shareTransport: ShareTransport = {
26
+ publish: publishShare,
27
+ pull: mock(async () => {
28
+ throw new Error("Unexpected share pull");
29
+ }),
30
+ push: mock(async () => {}),
31
+ watch: () => () => {},
32
+ revoke: async () => {},
33
+ parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
34
+ formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
35
+ collaboratorAnnotations: () => [],
36
+ mergeFromShare: () => ({ annotations: [] }),
37
+ };
38
+
39
+ const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
40
+
41
+ let home: string;
42
+ let restoreUserConfig: () => void;
43
+ let server: DaemonServer;
44
+ let session: Thread;
45
+
46
+ beforeEach(() => {
47
+ home = mkdtempSync(join(tmpdir(), "cueloop-toast-"));
48
+ restoreUserConfig = isolateUserConfig(home);
49
+ server = new DaemonServer({ home, idleExitMs: 0 });
50
+ server.start();
51
+ session = server.core.sessionCreate({
52
+ workspace: { repoRoot: "/repo", branch: "main" },
53
+ artifact: {
54
+ type: "plan",
55
+ content: PLAN,
56
+ meta: { title: "Migration Plan", planPath: "plan.md" },
57
+ },
58
+ });
59
+ });
60
+ afterEach(() => {
61
+ restoreUserConfig();
62
+ server.stop();
63
+ rmSync(home, { recursive: true, force: true });
64
+ });
65
+
66
+ describe("share toast", () => {
67
+ test("clicking outside dismisses the toast while clicking inside keeps it open", async () => {
68
+ const setup = await testRender(
69
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
70
+ { width: 120, height: 32 },
71
+ );
72
+
73
+ await waitForText(setup, "cueloop");
74
+ await pressKey(setup, "s", { ctrl: true });
75
+ await waitForText(setup, "+ new link");
76
+ await press(setup, "enter");
77
+ await press(setup, "enter");
78
+ await waitForText(setup, "link name");
79
+ await press(setup, "enter");
80
+ await waitForText(setup, "link copied");
81
+ await press(setup, "escape");
82
+ await waitForTextGone(setup, "share externally");
83
+ const body = locateText(setup, "ssh p_share01@cueloop.dev");
84
+
85
+ await setup.mockMouse.click(body.column, body.row);
86
+ expect(setup.captureCharFrame()).toContain("link copied");
87
+ await setup.mockMouse.click(1, 1);
88
+ await waitForTextGone(setup, "link copied");
89
+ setup.renderer.destroy();
90
+ });
91
+
92
+ test("escape cancels an open composer even while the toast is up", async () => {
93
+ // Arrange
94
+ const setup = await testRender(
95
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
96
+ {
97
+ width: 120,
98
+ height: 32,
99
+ },
100
+ );
101
+
102
+ await waitForText(setup, "cueloop");
103
+
104
+ // Act: share opens the dialog, the new-link wizard publishes and raises the toast, then a composer under it
105
+ await pressKey(setup, "s", { ctrl: true });
106
+ await waitForText(setup, "+ new link");
107
+ await press(setup, "enter"); // step into the links body
108
+ await press(setup, "enter"); // activate "+ new link" - the wizard opens
109
+ await waitForText(setup, "link name");
110
+ await press(setup, "enter"); // the name field submits and creates the link
111
+ await waitForText(setup, "link copied");
112
+ await press(setup, "escape"); // close the dialog; the non-modal toast stays up
113
+ await waitForTextGone(setup, "share externally");
114
+ await clickText(setup, "daemon");
115
+ await typeText(setup, "x");
116
+ await waitForText(setup, "● x");
117
+
118
+ // Act: escape must reach the composer, not get eaten by the toast
119
+ await press(setup, "escape");
120
+
121
+ // Assert: the composer closed on the first escape
122
+ await waitForTextGone(setup, "● x");
123
+ });
124
+ });