@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,1248 @@
1
+ import { expect, test } from "bun:test";
2
+ import React from "react";
3
+ import { testRender } from "@opentui/react/test-utils";
4
+ import {
5
+ SCHEMA_VERSION,
6
+ type Thread,
7
+ type ThreadAgentState,
8
+ type AgentPromptRequest,
9
+ } from "@cueloop/schema";
10
+ import { AgentThreadPane, AgentThreadPrototype } from "./AgentThreadPane";
11
+ import { ThreadFooter } from "./ThreadFooter";
12
+ import { ThreadView, type ThreadViewProps } from "../../markdown/components/ThreadView";
13
+ import { buildDisplay, marksByDisplay } from "../../markdown/view-plan";
14
+ import { DARK } from "../../appearance/theme";
15
+ import {
16
+ locateText,
17
+ dragText,
18
+ waitForText,
19
+ pressKey,
20
+ typeText,
21
+ settle,
22
+ waitForState,
23
+ } from "../../testing/test-support";
24
+ import type { ThreadAgentClient } from "../use-thread-agent";
25
+ import type { EventFrame } from "@cueloop/daemon/client";
26
+
27
+ const thread: Thread = {
28
+ schemaVersion: SCHEMA_VERSION,
29
+ id: "thread",
30
+ workspace: { repoRoot: "/tmp/project", branch: "main" },
31
+ artifact: { type: "plan", content: "Original artifact", meta: {} },
32
+ annotations: [],
33
+ revisions: [],
34
+ status: "pending",
35
+ message: null,
36
+ createdAt: "2026-10-04",
37
+ };
38
+ const noop = () => {};
39
+ const empty: ThreadAgentState = {
40
+ threadId: thread.id,
41
+ phase: { kind: "idle" },
42
+ messages: [],
43
+ tools: [],
44
+ comments: [],
45
+ };
46
+
47
+ test("a failed harness turn displays its error below the conversation", async () => {
48
+ const failure = "OAuth auth derivation failed: missing provider module";
49
+ const { client } = createTestAgentClient({ ...empty, phase: { kind: "failed", error: failure } });
50
+ const setup = await testRender(
51
+ <AgentThreadPane
52
+ thread={thread}
53
+ client={client}
54
+ focused
55
+ theme={DARK}
56
+ onActiveChange={noop}
57
+ onOpenFile={noop}
58
+ >
59
+ {artifactView(thread)}
60
+ </AgentThreadPane>,
61
+ { width: 100, height: 24 },
62
+ );
63
+
64
+ try {
65
+ await waitForText(setup, "Original artifact");
66
+ await settle(setup);
67
+ expect(setup.captureCharFrame()).toContain(failure);
68
+ expect(setup.captureCharFrame()).not.toContain("Thinking");
69
+ } finally {
70
+ setup.renderer.destroy();
71
+ }
72
+ });
73
+
74
+ function createTestAgentClient(initial: ThreadAgentState) {
75
+ let state = initial;
76
+ const prompts: AgentPromptRequest[] = [];
77
+ const listeners = new Set<(event: EventFrame) => void>();
78
+ const client: ThreadAgentClient = {
79
+ onEvent: (listener) => {
80
+ listeners.add(listener);
81
+
82
+ return () => listeners.delete(listener);
83
+ },
84
+ agentGet: async () => state,
85
+ agentPrompt: async (params) => {
86
+ prompts.push(params);
87
+
88
+ if (params.text)
89
+ state = {
90
+ ...state,
91
+ messages: [
92
+ ...state.messages,
93
+ {
94
+ id: `answer-${prompts.length}`,
95
+ role: "agent",
96
+ text: "The timer survives cancellation.",
97
+ complete: true,
98
+ revision: 1,
99
+ },
100
+ ],
101
+ };
102
+
103
+ return state;
104
+ },
105
+ agentCancel: async () => state,
106
+ agentComment: async () => state,
107
+ agentPermission: async () => ({ ...state, phase: { kind: "idle" } }),
108
+ };
109
+
110
+ const publishTestAgentState = (next: ThreadAgentState): void => {
111
+ state = next;
112
+ for (const listener of listeners) listener({ event: "agent.updated", sessionId: thread.id });
113
+ };
114
+
115
+ return { client, prompts, publishTestAgentState };
116
+ }
117
+
118
+ function artifactView(
119
+ value: Thread,
120
+ onReply: (id: string, body: string) => void = noop,
121
+ onUpdateAnnotation: (id: string, body: string) => void = noop,
122
+ onAnnotate: ThreadViewProps["onAnnotate"] = noop,
123
+ observer = false,
124
+ ) {
125
+ const display = buildDisplay(value.artifact.content);
126
+
127
+ return (
128
+ <ThreadView
129
+ session={value}
130
+ display={display}
131
+ marks={marksByDisplay(value.annotations, display)}
132
+ quickActions={[]}
133
+ observer={observer}
134
+ onAnnotate={onAnnotate}
135
+ onReply={onReply}
136
+ onUpdateAnnotation={onUpdateAnnotation}
137
+ onExit={noop}
138
+ />
139
+ );
140
+ }
141
+
142
+ test("typing on the final blank line invokes the agent while the existing footer keeps its action", async () => {
143
+ const { client, prompts } = createTestAgentClient(empty);
144
+ let reviewSubmissions = 0;
145
+ const setup = await testRender(
146
+ <box style={{ flexGrow: 1, flexDirection: "column" }}>
147
+ <AgentThreadPane
148
+ thread={thread}
149
+ client={client}
150
+ focused
151
+ theme={DARK}
152
+ onActiveChange={noop}
153
+ onOpenFile={noop}
154
+ >
155
+ {artifactView(thread)}
156
+ </AgentThreadPane>
157
+ <ThreadFooter repo="project" branch="main" onSubmit={() => reviewSubmissions++} />
158
+ </box>,
159
+ { width: 100, height: 24, kittyKeyboard: true },
160
+ );
161
+
162
+ try {
163
+ await waitForText(setup, "Original artifact");
164
+ expect(setup.captureCharFrame()).not.toContain("Workspace:");
165
+ expect(setup.captureCharFrame()).not.toContain("Ask Agent");
166
+ const artifact = locateText(setup, "Original artifact");
167
+
168
+ await setup.mockMouse.click(artifact.column, artifact.row);
169
+ await typeText(setup, "Ignored unmarked input");
170
+ expect(setup.captureCharFrame()).not.toContain("Ignored unmarked input");
171
+ await setup.mockMouse.click(artifact.column, artifact.row + 2);
172
+ await typeText(setup, "Explain");
173
+ expect(setup.captureCharFrame()).not.toContain("● Explain");
174
+ await pressKey(setup, "RETURN", { meta: true });
175
+ expect(prompts).toHaveLength(0);
176
+ await setup.mockMouse.click(artifact.column, artifact.row);
177
+ await setup.mockMouse.click(artifact.column, artifact.row + 2);
178
+ // Submit the typing burst before waiting for content-change repaint.
179
+ await setup.mockInput.typeText(" retries");
180
+ setup.mockInput.pressKey("RETURN", { ctrl: true });
181
+ await waitForText(setup, "The timer survives cancellation.");
182
+ expect(prompts[0]?.text).toBe("Explain retries");
183
+ expect(prompts[0]?.inputOnly).toBe(true);
184
+ const send = locateText(setup, "Send message (0)");
185
+
186
+ await setup.mockMouse.click(send.column, send.row);
187
+ expect(reviewSubmissions).toBe(1);
188
+ expect(prompts).toHaveLength(1);
189
+ } finally {
190
+ setup.renderer.destroy();
191
+ }
192
+ });
193
+
194
+ test("typing after a submitted original creates a reply instead of editing the frozen comment", async () => {
195
+ const annotated: Thread = {
196
+ ...thread,
197
+ annotations: [
198
+ {
199
+ id: "original",
200
+ kind: "comment",
201
+ body: "Explain the timer",
202
+ anchor: {
203
+ quote: "Original",
204
+ prefix: "",
205
+ suffix: " artifact",
206
+ blockIndex: 0,
207
+ start: 0,
208
+ end: 8,
209
+ },
210
+ createdAt: thread.createdAt,
211
+ },
212
+ ],
213
+ };
214
+ const state: ThreadAgentState = {
215
+ ...empty,
216
+ submissions: [
217
+ {
218
+ id: "turn",
219
+ commentId: "original",
220
+ prompt: "Explain the timer",
221
+ quote: "Original",
222
+ status: "completed",
223
+ },
224
+ ],
225
+ };
226
+ const { client } = createTestAgentClient(state);
227
+ const replies: string[] = [];
228
+ const updates: string[] = [];
229
+ const setup = await testRender(
230
+ <AgentThreadPane
231
+ thread={annotated}
232
+ client={client}
233
+ focused
234
+ theme={DARK}
235
+ onActiveChange={noop}
236
+ onOpenFile={noop}
237
+ >
238
+ {artifactView(
239
+ annotated,
240
+ (_id, body) => replies.push(body),
241
+ (_id, body) => updates.push(body),
242
+ )}
243
+ </AgentThreadPane>,
244
+ { width: 100, height: 24, kittyKeyboard: true },
245
+ );
246
+
247
+ try {
248
+ await waitForText(setup, "✓✓");
249
+ const original = locateText(setup, "Explain the timer");
250
+
251
+ await setup.mockMouse.click(original.column, original.row);
252
+ await typeText(setup, "What about cleanup?");
253
+ await pressKey(setup, "RETURN", { ctrl: true });
254
+ expect(replies).toEqual(["What about cleanup?"]);
255
+ expect(updates).toEqual([]);
256
+ expect(annotated.annotations[0]?.body).toBe("Explain the timer");
257
+ } finally {
258
+ setup.renderer.destroy();
259
+ }
260
+ });
261
+
262
+ test("failed mirrors expose Retry and permissions keep activity at the bottom", async () => {
263
+ const state: ThreadAgentState = {
264
+ ...empty,
265
+ submissions: [
266
+ {
267
+ id: "failed",
268
+ commentId: "original",
269
+ prompt: "Check retries",
270
+ quote: "Original",
271
+ status: "failed",
272
+ },
273
+ ],
274
+ phase: {
275
+ kind: "permission",
276
+ permission: {
277
+ id: "permission",
278
+ title: "Run retry tests",
279
+ options: [{ optionId: "deny", name: "Reject once", kind: "reject_once" }],
280
+ },
281
+ },
282
+ };
283
+ const { client, prompts } = createTestAgentClient(state);
284
+ const setup = await testRender(
285
+ <AgentThreadPane
286
+ thread={thread}
287
+ client={client}
288
+ focused
289
+ theme={DARK}
290
+ onActiveChange={noop}
291
+ onOpenFile={noop}
292
+ >
293
+ {artifactView(thread)}
294
+ </AgentThreadPane>,
295
+ { width: 100, height: 24, kittyKeyboard: true },
296
+ );
297
+
298
+ try {
299
+ await waitForText(setup, "Run retry tests");
300
+ const frame = setup.captureCharFrame();
301
+
302
+ expect(frame.match(/Thinking/g)).toHaveLength(1);
303
+ expect(frame.indexOf("Thinking")).toBeGreaterThan(frame.indexOf("Check retries"));
304
+ const retry = locateText(setup, "Retry");
305
+
306
+ await setup.mockMouse.click(retry.column, retry.row);
307
+ expect(prompts[0]?.retry).toBe("failed");
308
+ expect(prompts[0]?.text).toBe("");
309
+ } finally {
310
+ setup.renderer.destroy();
311
+ }
312
+ });
313
+
314
+ test("three bottom prompts survive delayed acceptance and remain separate submissions", async () => {
315
+ const { client } = createTestAgentClient(empty);
316
+ let release: ((state: ThreadAgentState) => void) | undefined;
317
+ const prompts: string[] = [];
318
+
319
+ client.agentPrompt = async (params) => {
320
+ prompts.push(params.text);
321
+
322
+ if (prompts.length === 1)
323
+ return new Promise((resolve) => {
324
+ release = resolve;
325
+ });
326
+
327
+ return empty;
328
+ };
329
+ const setup = await testRender(
330
+ <AgentThreadPane
331
+ thread={thread}
332
+ client={client}
333
+ focused
334
+ theme={DARK}
335
+ onActiveChange={noop}
336
+ onOpenFile={noop}
337
+ >
338
+ {artifactView(thread)}
339
+ </AgentThreadPane>,
340
+ { width: 100, height: 24, kittyKeyboard: true },
341
+ );
342
+
343
+ try {
344
+ await waitForText(setup, "Original artifact");
345
+ await pressKey(setup, "ARROW_DOWN");
346
+ await pressKey(setup, "ARROW_DOWN");
347
+ await typeText(setup, "First prompt");
348
+ await pressKey(setup, "RETURN", { ctrl: true });
349
+ await typeText(setup, "Second prompt");
350
+ await pressKey(setup, "RETURN", { ctrl: true });
351
+ await typeText(setup, "Third prompt");
352
+ await pressKey(setup, "RETURN", { ctrl: true });
353
+ expect(prompts).toEqual(["First prompt"]);
354
+ release!(empty);
355
+ await settle(setup);
356
+ expect(prompts).toEqual(["First prompt", "Second prompt", "Third prompt"]);
357
+ } finally {
358
+ setup.renderer.destroy();
359
+ }
360
+ });
361
+
362
+ test("typing on an accepted plain prompt cannot edit it or create an unmarked comment", async () => {
363
+ const state: ThreadAgentState = {
364
+ ...empty,
365
+ messages: [
366
+ { id: "prompt", role: "user", text: "Explain retries", complete: true, revision: 1 },
367
+ ],
368
+ submissions: [
369
+ { id: "prompt", commentId: "prompt", prompt: "Explain retries", status: "completed" },
370
+ ],
371
+ };
372
+ const { client } = createTestAgentClient(state);
373
+ const replies: string[] = [];
374
+
375
+ client.agentComment = async (params) => {
376
+ replies.push(params.comment.replyTo ?? "");
377
+
378
+ return state;
379
+ };
380
+ const setup = await testRender(
381
+ <AgentThreadPane
382
+ thread={thread}
383
+ client={client}
384
+ focused
385
+ theme={DARK}
386
+ onActiveChange={noop}
387
+ onOpenFile={noop}
388
+ >
389
+ {artifactView(thread)}
390
+ </AgentThreadPane>,
391
+ { width: 100, height: 24, kittyKeyboard: true },
392
+ );
393
+
394
+ try {
395
+ await waitForText(setup, "Explain retries");
396
+ const prompt = locateText(setup, "Explain retries");
397
+
398
+ await setup.mockMouse.click(prompt.column, prompt.row);
399
+ await typeText(setup, "What about cancellation?");
400
+ await pressKey(setup, "RETURN", { ctrl: true });
401
+ expect(replies).toEqual([]);
402
+ expect(state.messages[0]?.text).toBe("Explain retries");
403
+ expect(setup.captureCharFrame()).not.toContain("What about cancellation?");
404
+ } finally {
405
+ setup.renderer.destroy();
406
+ }
407
+ });
408
+
409
+ for (const editingReply of [false, true]) {
410
+ test(`Option+Enter preserves the exact ${editingReply ? "edited" : "new"} reply for Ctrl+Enter`, async () => {
411
+ const submitted: string[] = [];
412
+ const root = {
413
+ id: "root",
414
+ kind: "comment" as const,
415
+ body: "Submitted question",
416
+ createdAt: "2026-10-04",
417
+ anchor: { quote: "Original", prefix: "", suffix: " artifact" },
418
+ };
419
+ const display = buildDisplay(thread.artifact.content);
420
+
421
+ function ControlledDiscussion(): React.ReactNode {
422
+ const [value, setValue] = React.useState<Thread>({
423
+ ...thread,
424
+ annotations: editingReply
425
+ ? [root, { ...root, id: "reply", replyTo: "root", body: "Editable followup" }]
426
+ : [root],
427
+ });
428
+ const [focused, setFocused] = React.useState<string>();
429
+
430
+ return (
431
+ <ThreadView
432
+ session={value}
433
+ display={display}
434
+ marks={marksByDisplay(value.annotations, display)}
435
+ quickActions={[]}
436
+ observer={false}
437
+ focusedAnnotationId={focused}
438
+ onFocusAnnotation={setFocused}
439
+ isAnnotationReadOnly={(id) => id === "root"}
440
+ onAnnotate={noop}
441
+ onReply={(id, body) => {
442
+ setValue((current) => ({
443
+ ...current,
444
+ annotations: [...current.annotations, { ...root, id: "reply", replyTo: id, body }],
445
+ }));
446
+
447
+ return "reply";
448
+ }}
449
+ onUpdateAnnotation={(id, body) =>
450
+ setValue((current) => ({
451
+ ...current,
452
+ annotations: current.annotations.map((note) =>
453
+ note.id === id ? { ...note, body } : note,
454
+ ),
455
+ }))
456
+ }
457
+ onInvoke={(id) => {
458
+ if (id) submitted.push(id);
459
+ }}
460
+ onExit={noop}
461
+ />
462
+ );
463
+ }
464
+ const setup = await testRender(<ControlledDiscussion />, {
465
+ width: 100,
466
+ height: 24,
467
+ kittyKeyboard: true,
468
+ });
469
+
470
+ try {
471
+ await waitForText(setup, editingReply ? "Editable followup" : "Submitted question");
472
+ const comment = locateText(setup, editingReply ? "Editable followup" : "Submitted question");
473
+
474
+ await setup.mockMouse.click(comment.column, comment.row);
475
+ await typeText(setup, " Why?");
476
+ await pressKey(setup, "RETURN", { meta: true });
477
+ expect(submitted).toEqual([]);
478
+ const saved = locateText(setup, editingReply ? "Editable followup Why?" : "Why?");
479
+
480
+ await setup.mockMouse.click(saved.column, saved.row);
481
+ await pressKey(setup, "RETURN", { ctrl: true });
482
+ expect(submitted).toEqual(["reply"]);
483
+ } finally {
484
+ setup.renderer.destroy();
485
+ }
486
+ });
487
+ }
488
+
489
+ test("Option-saved Changes mirror replies send their stored ID with Ctrl+Enter", async () => {
490
+ const root = {
491
+ id: "changes-note",
492
+ kind: "comment" as const,
493
+ body: "Question on Changes",
494
+ createdAt: thread.createdAt,
495
+ anchor: { quote: "Changed code", prefix: "", suffix: "" },
496
+ };
497
+ const { client, prompts } = createTestAgentClient({
498
+ ...empty,
499
+ submissions: [
500
+ {
501
+ id: "accepted",
502
+ commentId: root.id,
503
+ prompt: root.body,
504
+ quote: root.anchor.quote,
505
+ status: "completed",
506
+ },
507
+ ],
508
+ });
509
+
510
+ function ChangesDiscussion(): React.ReactNode {
511
+ const [value, setValue] = React.useState<Thread>({ ...thread, annotations: [root] });
512
+ const [focused, setFocused] = React.useState<string>();
513
+ const display = buildDisplay(value.artifact.content);
514
+ const saveReply = (id: string, body: string): string => {
515
+ setValue((current) => ({
516
+ ...current,
517
+ annotations: [...current.annotations, { ...root, id: "followup", replyTo: id, body }],
518
+ }));
519
+
520
+ return "followup";
521
+ };
522
+
523
+ return (
524
+ <AgentThreadPane
525
+ thread={value}
526
+ client={client}
527
+ focused
528
+ theme={DARK}
529
+ onActiveChange={noop}
530
+ onOpenFile={noop}
531
+ >
532
+ <ThreadView
533
+ session={value}
534
+ display={display}
535
+ marks={marksByDisplay(value.annotations, display)}
536
+ quickActions={[]}
537
+ observer={false}
538
+ focusedAnnotationId={focused}
539
+ onFocusAnnotation={setFocused}
540
+ onAnnotate={noop}
541
+ onReply={saveReply}
542
+ onUpdateAnnotation={noop}
543
+ onExit={noop}
544
+ />
545
+ </AgentThreadPane>
546
+ );
547
+ }
548
+ const setup = await testRender(<ChangesDiscussion />, {
549
+ width: 100,
550
+ height: 24,
551
+ kittyKeyboard: true,
552
+ });
553
+
554
+ try {
555
+ await waitForText(setup, root.body);
556
+ const mirror = locateText(setup, root.body);
557
+
558
+ await setup.mockMouse.click(mirror.column, mirror.row);
559
+ await typeText(setup, "Follow up on this change");
560
+ await pressKey(setup, "RETURN", { meta: true });
561
+ await waitForText(setup, "Follow up on this change");
562
+ expect(prompts).toHaveLength(0);
563
+ await pressKey(setup, "RETURN", { ctrl: true });
564
+ expect(prompts.map((prompt) => prompt.commentId)).toEqual(["followup"]);
565
+ } finally {
566
+ setup.renderer.destroy();
567
+ }
568
+ });
569
+
570
+ test("Ctrl+Enter submits only the comment just saved, leaving older notes editable", async () => {
571
+ const value: Thread = {
572
+ ...thread,
573
+ annotations: [
574
+ {
575
+ id: "older",
576
+ kind: "comment",
577
+ body: "Older unsent note",
578
+ createdAt: "2026-10-04",
579
+ anchor: { quote: "Original", prefix: "", suffix: " artifact" },
580
+ },
581
+ ],
582
+ };
583
+ const { client, prompts } = createTestAgentClient(empty);
584
+ const display = buildDisplay(value.artifact.content);
585
+ const setup = await testRender(
586
+ <AgentThreadPane
587
+ thread={value}
588
+ theme={DARK}
589
+ focused
590
+ onActiveChange={noop}
591
+ onOpenFile={noop}
592
+ client={client}
593
+ >
594
+ <ThreadView
595
+ session={value}
596
+ display={display}
597
+ marks={marksByDisplay(value.annotations, display)}
598
+ quickActions={[]}
599
+ observer={false}
600
+ onAnnotate={() => "selected"}
601
+ onReply={noop}
602
+ onUpdateAnnotation={noop}
603
+ onExit={noop}
604
+ />
605
+ </AgentThreadPane>,
606
+ { width: 100, height: 24, kittyKeyboard: true },
607
+ );
608
+
609
+ try {
610
+ await waitForText(setup, "Original artifact");
611
+ const artifact = locateText(setup, "Original artifact");
612
+
613
+ await setup.mockMouse.drag(
614
+ artifact.column + 9,
615
+ artifact.row,
616
+ artifact.column + 17,
617
+ artifact.row,
618
+ );
619
+ await typeText(setup, "Only this question");
620
+ await pressKey(setup, "RETURN", { ctrl: true });
621
+ await waitForState(setup, () => prompts.length === 1, "selected comment invocation");
622
+ expect(prompts[0]?.commentId).toBe("selected");
623
+ expect(prompts[0]?.id).toBe(value.id);
624
+ expect(value.annotations[0]?.body).toBe("Older unsent note");
625
+ } finally {
626
+ setup.renderer.destroy();
627
+ }
628
+ });
629
+
630
+ test("Option+Enter saves a comment, Command+Enter leaves its draft, and Ctrl+Enter invokes", async () => {
631
+ const saved: string[] = [];
632
+ let invocations = 0;
633
+ const display = buildDisplay(thread.artifact.content);
634
+ const setup = await testRender(
635
+ <ThreadView
636
+ session={thread}
637
+ display={display}
638
+ marks={marksByDisplay([], display)}
639
+ quickActions={[]}
640
+ observer={false}
641
+ onAnnotate={(_span, body) => {
642
+ saved.push(body);
643
+
644
+ return `saved-${saved.length}`;
645
+ }}
646
+ onReply={noop}
647
+ onUpdateAnnotation={noop}
648
+ onExit={noop}
649
+ onInvoke={() => invocations++}
650
+ />,
651
+ { width: 100, height: 24, kittyKeyboard: true },
652
+ );
653
+
654
+ try {
655
+ await waitForText(setup, "Original artifact");
656
+ const artifact = locateText(setup, "Original artifact");
657
+
658
+ await setup.mockMouse.drag(artifact.column, artifact.row, artifact.column + 8, artifact.row);
659
+ await typeText(setup, "Editable comment");
660
+ setup.mockInput.pressKey("RETURN", { super: true });
661
+ await settle(setup);
662
+ expect(saved).toEqual([]);
663
+ expect(invocations).toBe(0);
664
+ expect(setup.captureCharFrame()).toContain("Editable comment");
665
+ await pressKey(setup, "RETURN", { meta: true });
666
+ expect(saved).toEqual(["Editable comment"]);
667
+ expect(invocations).toBe(0);
668
+ await setup.mockMouse.drag(artifact.column, artifact.row, artifact.column + 8, artifact.row);
669
+ await typeText(setup, "Ask the agent");
670
+ await pressKey(setup, "RETURN", { ctrl: true });
671
+ expect(saved).toEqual(["Editable comment", "Ask the agent"]);
672
+ expect(invocations).toBe(1);
673
+ } finally {
674
+ setup.renderer.destroy();
675
+ }
676
+ });
677
+
678
+ test("comments on a later paragraph of agent output save editable and submit independently", async () => {
679
+ let state: ThreadAgentState = {
680
+ ...empty,
681
+ messages: [
682
+ { id: "earlier", role: "agent", text: "Earlier reply.", complete: true, revision: 1 },
683
+ {
684
+ id: "output",
685
+ role: "agent",
686
+ text: "First paragraph.\n\nThe agent can explain this comment.\n\nLast paragraph.",
687
+ complete: true,
688
+ revision: 2,
689
+ },
690
+ ],
691
+ };
692
+ const { client, prompts } = createTestAgentClient(state);
693
+
694
+ client.agentComment = async ({ comment }) => {
695
+ state = {
696
+ ...state,
697
+ comments: [...state.comments.filter((entry) => entry.id !== comment.id), comment],
698
+ };
699
+
700
+ return state;
701
+ };
702
+ const prompt = client.agentPrompt;
703
+
704
+ client.agentPrompt = async (params) => {
705
+ await prompt(params);
706
+ state = { ...state, comments: state.comments.map((comment) => ({ ...comment, sent: true })) };
707
+
708
+ return state;
709
+ };
710
+ const setup = await testRender(
711
+ <AgentThreadPane
712
+ thread={thread}
713
+ client={client}
714
+ focused
715
+ theme={DARK}
716
+ onActiveChange={noop}
717
+ onOpenFile={noop}
718
+ >
719
+ {artifactView(thread)}
720
+ </AgentThreadPane>,
721
+ { width: 100, height: 30, kittyKeyboard: true },
722
+ );
723
+
724
+ try {
725
+ await waitForText(setup, "The agent can explain");
726
+ await waitForState(
727
+ setup,
728
+ () => setup.renderer.getCursorState().visible,
729
+ "continuation composer ready",
730
+ );
731
+ const output = locateText(setup, "The agent can explain");
732
+
733
+ await setup.mockMouse.drag(output.column, output.row, output.column + 9, output.row);
734
+ await typeText(setup, "Explain this to me");
735
+ await pressKey(setup, "RETURN", { meta: true });
736
+ await waitForState(setup, () => state.comments.length === 1, "agent output comment saved");
737
+ expect(state.comments).toHaveLength(1);
738
+ expect(state.comments[0]?.messageId).toBe("output");
739
+ expect(state.comments[0]?.anchor.quote).toBe("The agent");
740
+ expect(state.comments[0]?.sent).toBe(false);
741
+ expect(prompts).toHaveLength(0);
742
+ expect(setup.captureCharFrame()).toContain("Explain this to me");
743
+ await pressKey(setup, "RETURN", { ctrl: true });
744
+ expect(prompts).toHaveLength(1);
745
+ expect(state.comments[0]?.sent).toBe(true);
746
+ } finally {
747
+ setup.renderer.destroy();
748
+ }
749
+ });
750
+
751
+ for (const policy of [
752
+ { enabled: false, observer: false, pixelPrototype: false },
753
+ { enabled: true, observer: true, pixelPrototype: false },
754
+ ]) {
755
+ test(`disabled or shared agent views retain the original document without opening a harness (${JSON.stringify(policy)})`, async () => {
756
+ let reads = 0;
757
+ const { client } = createTestAgentClient(empty);
758
+
759
+ client.agentGet = async () => {
760
+ reads++;
761
+
762
+ return empty;
763
+ };
764
+ const setup = await testRender(
765
+ <AgentThreadPrototype
766
+ {...policy}
767
+ thread={thread}
768
+ client={client}
769
+ focused
770
+ theme={DARK}
771
+ onActiveChange={noop}
772
+ onOpenFile={noop}
773
+ >
774
+ {artifactView(thread)}
775
+ </AgentThreadPrototype>,
776
+ { width: 100, height: 24 },
777
+ );
778
+
779
+ try {
780
+ await waitForText(setup, "Original artifact");
781
+ expect(reads).toBe(0);
782
+ expect(setup.captureCharFrame()).not.toContain("Thinking");
783
+ } finally {
784
+ setup.renderer.destroy();
785
+ }
786
+ });
787
+ }
788
+
789
+ test("empty Thread typing and accepted prompts use normal paragraph padding without comment cards", async () => {
790
+ const value = { ...thread, artifact: { ...thread.artifact, content: "" } };
791
+ let accepted = empty;
792
+ const { client, prompts } = createTestAgentClient(empty);
793
+
794
+ client.agentPrompt = async (params) => {
795
+ prompts.push(params);
796
+ accepted = {
797
+ ...empty,
798
+ submissions: [
799
+ { id: "plain-prompt", commentId: "plain-prompt", prompt: params.text, status: "completed" },
800
+ ],
801
+ };
802
+
803
+ return accepted;
804
+ };
805
+ const setup = await testRender(
806
+ <AgentThreadPane
807
+ thread={value}
808
+ client={client}
809
+ focused
810
+ theme={DARK}
811
+ onActiveChange={noop}
812
+ onOpenFile={noop}
813
+ >
814
+ {artifactView(value)}
815
+ </AgentThreadPane>,
816
+ { width: 100, height: 24, kittyKeyboard: true },
817
+ );
818
+
819
+ try {
820
+ await settle(setup);
821
+ await pressKey(setup, "ARROW_DOWN");
822
+ await typeText(setup, "Hello from the prompt");
823
+ expect(locateText(setup, "Hello from the prompt")).toMatchObject({ row: 1, column: 2 });
824
+ await pressKey(setup, "RETURN", { ctrl: true });
825
+ await waitForText(setup, "Hello from the prompt");
826
+ expect(prompts).toHaveLength(1);
827
+ expect(locateText(setup, "Hello from the prompt")).toMatchObject({ row: 1, column: 2 });
828
+ expect(setup.captureCharFrame()).not.toContain("●");
829
+ expect(setup.captureCharFrame()).not.toContain("✓");
830
+ } finally {
831
+ setup.renderer.destroy();
832
+ }
833
+ });
834
+
835
+ test("clicking whitespace below the conversation focuses a blinking prompt before typing", async () => {
836
+ const { client, prompts } = createTestAgentClient(empty);
837
+ const setup = await testRender(
838
+ <AgentThreadPane
839
+ thread={thread}
840
+ client={client}
841
+ focused
842
+ theme={DARK}
843
+ onActiveChange={noop}
844
+ onOpenFile={noop}
845
+ >
846
+ {artifactView(thread)}
847
+ </AgentThreadPane>,
848
+ { width: 100, height: 24, kittyKeyboard: true },
849
+ );
850
+
851
+ try {
852
+ await waitForText(setup, "Original artifact");
853
+ const text = locateText(setup, "Original artifact");
854
+
855
+ await setup.mockMouse.click(text.column + 20, text.row + 5);
856
+ await waitForState(
857
+ setup,
858
+ () => {
859
+ const cursor = setup.renderer.getCursorState();
860
+
861
+ return cursor.visible && cursor.blinking;
862
+ },
863
+ "continuation cursor focused",
864
+ );
865
+ expect(setup.renderer.getCursorState()).toMatchObject({ visible: true, blinking: true });
866
+ expect(prompts).toHaveLength(0);
867
+ await typeText(setup, "A prompt from whitespace");
868
+ await pressKey(setup, "RETURN", { ctrl: true });
869
+ expect(prompts[0]?.text).toBe("A prompt from whitespace");
870
+ await waitForText(setup, "The timer survives cancellation.");
871
+ await settle(setup);
872
+ const answer = locateText(setup, "The timer survives cancellation.");
873
+ const cursor = setup.renderer.getCursorState();
874
+
875
+ expect(cursor).toMatchObject({ visible: true, blinking: true, x: 3 });
876
+ expect(cursor.y).toBeGreaterThan(answer.row + 1);
877
+ await typeText(setup, "Continue after the reply");
878
+ await pressKey(setup, "RETURN", { ctrl: true });
879
+ expect(prompts[1]?.text).toBe("Continue after the reply");
880
+ } finally {
881
+ setup.renderer.destroy();
882
+ }
883
+ });
884
+
885
+ test("an arriving reply preserves a bottom draft and submits it as the next prompt", async () => {
886
+ const { client, prompts } = createTestAgentClient(empty);
887
+ let finish: ((state: ThreadAgentState) => void) | undefined;
888
+ const answer: ThreadAgentState = {
889
+ ...empty,
890
+ messages: [
891
+ {
892
+ id: "arriving-answer",
893
+ role: "agent",
894
+ text: "An answer has arrived.",
895
+ complete: true,
896
+ revision: 1,
897
+ },
898
+ ],
899
+ };
900
+
901
+ client.agentPrompt = async (params) => {
902
+ prompts.push(params);
903
+
904
+ if (prompts.length === 1)
905
+ return new Promise<ThreadAgentState>((resolve) => {
906
+ finish = resolve;
907
+ });
908
+
909
+ return answer;
910
+ };
911
+ const setup = await testRender(
912
+ <AgentThreadPane
913
+ thread={thread}
914
+ client={client}
915
+ focused
916
+ theme={DARK}
917
+ onActiveChange={noop}
918
+ onOpenFile={noop}
919
+ >
920
+ {artifactView(thread)}
921
+ </AgentThreadPane>,
922
+ { width: 100, height: 24, kittyKeyboard: true },
923
+ );
924
+
925
+ try {
926
+ await waitForText(setup, "Original artifact");
927
+ const artifact = locateText(setup, "Original artifact");
928
+
929
+ await setup.mockMouse.click(artifact.column + 20, artifact.row + 5);
930
+ await typeText(setup, "First prompt");
931
+ await pressKey(setup, "RETURN", { ctrl: true });
932
+ await setup.mockMouse.click(artifact.column + 20, artifact.row + 5);
933
+ await typeText(setup, "Preserved second prompt");
934
+ finish!(answer);
935
+ await waitForText(setup, "An answer has arrived.");
936
+ expect(setup.captureCharFrame()).toContain("Preserved second prompt");
937
+ await typeText(setup, " after completion");
938
+ await pressKey(setup, "RETURN", { ctrl: true });
939
+ expect(prompts[1]?.text).toBe("Preserved second prompt after completion");
940
+ } finally {
941
+ setup.renderer.destroy();
942
+ }
943
+ });
944
+
945
+ test("clicking the first blank row after a reply opens continuation without losing a draft", async () => {
946
+ const state: ThreadAgentState = {
947
+ ...empty,
948
+ messages: [
949
+ { id: "answer", role: "agent", text: "The final reply.", complete: true, revision: 1 },
950
+ ],
951
+ };
952
+ const { client, prompts } = createTestAgentClient(state);
953
+ const setup = await testRender(
954
+ <AgentThreadPane
955
+ thread={thread}
956
+ client={client}
957
+ focused
958
+ theme={DARK}
959
+ onActiveChange={noop}
960
+ onOpenFile={noop}
961
+ >
962
+ {artifactView(thread)}
963
+ </AgentThreadPane>,
964
+ { width: 100, height: 24, kittyKeyboard: true },
965
+ );
966
+
967
+ try {
968
+ await waitForText(setup, "The final reply.");
969
+ await waitForState(
970
+ setup,
971
+ () => setup.renderer.currentFocusedEditor !== null,
972
+ "initial continuation focus ready",
973
+ );
974
+ await pressKey(setup, "ESCAPE");
975
+ await waitForState(
976
+ setup,
977
+ () => setup.renderer.currentFocusedEditor === null,
978
+ "continuation dismissed",
979
+ );
980
+ const reply = locateText(setup, "The final reply.");
981
+
982
+ await setup.mockMouse.click(reply.column + 5, reply.row + 1);
983
+ await waitForState(
984
+ setup,
985
+ () => {
986
+ const cursor = setup.renderer.getCursorState();
987
+
988
+ return setup.renderer.currentFocusedEditor !== null && cursor.visible && cursor.blinking;
989
+ },
990
+ "continuation cursor focused",
991
+ );
992
+ expect(setup.renderer.getCursorState()).toMatchObject({ visible: true, blinking: true });
993
+ await typeText(setup, "Continue here");
994
+ await setup.mockMouse.click(reply.column + 5, reply.row + 1);
995
+ await waitForState(
996
+ setup,
997
+ () => {
998
+ const cursor = setup.renderer.getCursorState();
999
+
1000
+ return setup.renderer.currentFocusedEditor !== null && cursor.visible && cursor.blinking;
1001
+ },
1002
+ "continuation cursor focused",
1003
+ );
1004
+ expect(setup.renderer.getCursorState()).toMatchObject({ visible: true, blinking: true });
1005
+ await pressKey(setup, "RETURN", { ctrl: true });
1006
+ expect(prompts[0]?.text).toBe("Continue here");
1007
+ } finally {
1008
+ setup.renderer.destroy();
1009
+ }
1010
+ });
1011
+
1012
+ test("a rejected prompt returns visibly to the composer and can be edited before retry", async () => {
1013
+ const { client, prompts } = createTestAgentClient(empty);
1014
+ let reject = true;
1015
+ const acceptPrompt = client.agentPrompt;
1016
+
1017
+ client.agentPrompt = async (params) => {
1018
+ if (reject) {
1019
+ reject = false;
1020
+ throw new Error("Prompt rejected");
1021
+ }
1022
+
1023
+ return acceptPrompt(params);
1024
+ };
1025
+ const setup = await testRender(
1026
+ <AgentThreadPane
1027
+ thread={thread}
1028
+ client={client}
1029
+ focused
1030
+ theme={DARK}
1031
+ onActiveChange={noop}
1032
+ onOpenFile={noop}
1033
+ >
1034
+ {artifactView(thread)}
1035
+ </AgentThreadPane>,
1036
+ { width: 100, height: 24, kittyKeyboard: true },
1037
+ );
1038
+
1039
+ try {
1040
+ await waitForText(setup, "Original artifact");
1041
+ const artifact = locateText(setup, "Original artifact");
1042
+
1043
+ await setup.mockMouse.click(artifact.column, artifact.row + 2);
1044
+ await typeText(setup, "Explain retries");
1045
+ await pressKey(setup, "RETURN", { ctrl: true });
1046
+ await waitForText(setup, "Prompt rejected");
1047
+ expect(setup.captureCharFrame()).toContain("Explain retries");
1048
+ await typeText(setup, " please");
1049
+ await pressKey(setup, "RETURN", { ctrl: true });
1050
+ await waitForText(setup, "The timer survives cancellation.");
1051
+ expect(prompts[0]?.text).toBe("Explain retries please");
1052
+ } finally {
1053
+ setup.renderer.destroy();
1054
+ }
1055
+ });
1056
+
1057
+ test("a failed mutation flush restores the prompt alongside a newer visible draft", async () => {
1058
+ const { client, prompts } = createTestAgentClient(empty);
1059
+ let failFlush: ((reason: Error) => void) | undefined;
1060
+ let flushStarted = false;
1061
+ const failedFlush = new Promise<void>((_resolve, reject) => {
1062
+ failFlush = reject;
1063
+ });
1064
+ const setup = await testRender(
1065
+ <AgentThreadPane
1066
+ thread={thread}
1067
+ client={client}
1068
+ focused
1069
+ theme={DARK}
1070
+ onActiveChange={noop}
1071
+ onOpenFile={noop}
1072
+ flushMutations={() => {
1073
+ if (flushStarted) return Promise.resolve();
1074
+
1075
+ flushStarted = true;
1076
+
1077
+ return failedFlush;
1078
+ }}
1079
+ >
1080
+ {artifactView(thread)}
1081
+ </AgentThreadPane>,
1082
+ { width: 100, height: 24, kittyKeyboard: true },
1083
+ );
1084
+
1085
+ try {
1086
+ await waitForText(setup, "Original artifact");
1087
+ const artifact = locateText(setup, "Original artifact");
1088
+
1089
+ await setup.mockMouse.click(artifact.column, artifact.row + 2);
1090
+ await typeText(setup, "First question");
1091
+ await pressKey(setup, "RETURN", { ctrl: true });
1092
+ await waitForState(setup, () => flushStarted, "mutation flush started");
1093
+ await setup.mockMouse.click(artifact.column, artifact.row + 2);
1094
+ await typeText(setup, "Newer draft");
1095
+ failFlush!(new Error("Mutation flush failed"));
1096
+ await waitForText(setup, "Mutation flush failed");
1097
+ await waitForText(setup, "First question");
1098
+ expect(setup.captureCharFrame()).toContain("Newer draft");
1099
+ await typeText(setup, " edited");
1100
+ await pressKey(setup, "RETURN", { ctrl: true });
1101
+ await waitForText(setup, "The timer survives cancellation.");
1102
+ expect(prompts[0]?.text).toBe("First question\nNewer draft edited");
1103
+ } finally {
1104
+ setup.renderer.destroy();
1105
+ }
1106
+ });
1107
+
1108
+ test("an offline shared agent accepts continuation below its status without model controls", async () => {
1109
+ const state: ThreadAgentState = {
1110
+ ...empty,
1111
+ messages: [
1112
+ { id: "online-answer", role: "agent", text: "An online answer", complete: true, revision: 1 },
1113
+ ],
1114
+ };
1115
+ const { client, prompts, publishTestAgentState } = createTestAgentClient(state);
1116
+ const setup = await testRender(
1117
+ <AgentThreadPane
1118
+ thread={thread}
1119
+ client={client}
1120
+ focused
1121
+ theme={DARK}
1122
+ onActiveChange={noop}
1123
+ onOpenFile={noop}
1124
+ >
1125
+ {artifactView(thread)}
1126
+ </AgentThreadPane>,
1127
+ { width: 80, height: 15, kittyKeyboard: true },
1128
+ );
1129
+
1130
+ try {
1131
+ await waitForText(setup, "An online answer");
1132
+ await waitForState(
1133
+ setup,
1134
+ () => setup.renderer.currentFocusedEditor !== null,
1135
+ "online continuation editor",
1136
+ );
1137
+ publishTestAgentState({ ...state, phase: { kind: "offline" } });
1138
+ await waitForText(setup, "Owner offline");
1139
+ expect(setup.captureCharFrame()).toContain("Original artifact");
1140
+ expect(setup.captureCharFrame()).not.toContain("Thinking");
1141
+ const status = locateText(setup, "Owner offline");
1142
+
1143
+ await setup.mockMouse.click(status.column + 5, status.row + 3);
1144
+ await waitForState(
1145
+ setup,
1146
+ () => setup.renderer.currentFocusedEditor !== null,
1147
+ "offline continuation editor",
1148
+ );
1149
+ await typeText(setup, "Queue until the owner reconnects");
1150
+ await waitForText(setup, "Queue until the owner reconnects");
1151
+ await pressKey(setup, "RETURN", { ctrl: true });
1152
+ expect(prompts[0]?.text).toBe("Queue until the owner reconnects");
1153
+ expect(prompts[0]?.inputOnly).toBe(true);
1154
+ } finally {
1155
+ setup.renderer.destroy();
1156
+ }
1157
+ });
1158
+
1159
+ test("shared viewers see pending activity without owner permission controls", async () => {
1160
+ const { client } = createTestAgentClient({
1161
+ ...empty,
1162
+ phase: {
1163
+ kind: "permission",
1164
+ permission: {
1165
+ id: "owner-permission",
1166
+ title: "Owner approval required",
1167
+ options: [{ optionId: "allow", name: "Allow once", kind: "allow_once" }],
1168
+ },
1169
+ },
1170
+ });
1171
+
1172
+ client.canControlAgent = false;
1173
+ const setup = await testRender(
1174
+ <AgentThreadPane
1175
+ thread={thread}
1176
+ client={client}
1177
+ focused
1178
+ theme={DARK}
1179
+ onActiveChange={noop}
1180
+ onOpenFile={noop}
1181
+ >
1182
+ {artifactView(thread)}
1183
+ </AgentThreadPane>,
1184
+ { width: 80, height: 15 },
1185
+ );
1186
+
1187
+ try {
1188
+ await waitForText(setup, "Thinking");
1189
+ expect(setup.captureCharFrame()).not.toContain("Allow once");
1190
+ expect(setup.captureCharFrame()).not.toContain("Owner approval required");
1191
+ } finally {
1192
+ setup.renderer.destroy();
1193
+ }
1194
+ });
1195
+
1196
+ for (const observer of [false, true]) {
1197
+ test(`shared snapshot preserves artifact annotation permissions (observer=${observer})`, async () => {
1198
+ const value = {
1199
+ ...thread,
1200
+ agentSnapshot: {
1201
+ ...empty,
1202
+ messages: [
1203
+ {
1204
+ id: "answer",
1205
+ role: "agent" as const,
1206
+ text: "Frozen conversation",
1207
+ complete: true,
1208
+ revision: 1,
1209
+ },
1210
+ ],
1211
+ },
1212
+ };
1213
+ const annotations: string[] = [];
1214
+ const setup = await testRender(
1215
+ <AgentThreadPrototype
1216
+ enabled={false}
1217
+ observer={observer}
1218
+ pixelPrototype={false}
1219
+ thread={value}
1220
+ focused
1221
+ theme={DARK}
1222
+ onActiveChange={noop}
1223
+ onOpenFile={noop}
1224
+ >
1225
+ {artifactView(
1226
+ value,
1227
+ noop,
1228
+ noop,
1229
+ (_span, body) => {
1230
+ annotations.push(body);
1231
+ },
1232
+ observer,
1233
+ )}
1234
+ </AgentThreadPrototype>,
1235
+ { width: 100, height: 24, kittyKeyboard: true },
1236
+ );
1237
+
1238
+ try {
1239
+ await waitForText(setup, "Frozen conversation");
1240
+ await dragText(setup, "Original artifact", "Original artifact", "Original artifact".length);
1241
+ await typeText(setup, "Artifact feedback");
1242
+ await pressKey(setup, "RETURN", { meta: true });
1243
+ expect(annotations).toEqual(observer ? [] : ["Artifact feedback"]);
1244
+ } finally {
1245
+ setup.renderer.destroy();
1246
+ }
1247
+ });
1248
+ }