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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/annotations/annotation-palette.ts +62 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +369 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
  14. package/src/annotations/discussions.ts +77 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +242 -0
  17. package/src/annotations/use-annotation-surface.tsx +1243 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  20. package/src/app/App.deleted-scroll.test.tsx +187 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.identity.test.tsx +84 -0
  25. package/src/app/App.plan-render.test.tsx +209 -0
  26. package/src/app/App.plan-review.test.tsx +428 -0
  27. package/src/app/App.prototype-markdown.test.tsx +128 -0
  28. package/src/app/App.prototype.test.tsx +176 -0
  29. package/src/app/App.readonly.test.tsx +210 -0
  30. package/src/app/App.regression.test.tsx +99 -0
  31. package/src/app/App.rendering.test.tsx +134 -0
  32. package/src/app/App.share-dialog.test.tsx +177 -0
  33. package/src/app/App.submit-confirm.test.tsx +267 -0
  34. package/src/app/App.test.tsx +496 -0
  35. package/src/app/App.thread-agent.test.ts +20 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1667 -0
  38. package/src/app/App.visual.test.tsx +59 -0
  39. package/src/app/App.walk.test.tsx +299 -0
  40. package/src/app/App.workbench.test.tsx +1149 -0
  41. package/src/app/app-key-state.ts +61 -0
  42. package/src/app/app-screens.tsx +606 -0
  43. package/src/app/app-view-model.ts +168 -0
  44. package/src/app/components/AppHeader.stories.tsx +50 -0
  45. package/src/app/components/AppHeader.tsx +81 -0
  46. package/src/app/components/AppShell.brand.test.tsx +58 -0
  47. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  48. package/src/app/components/AppShell.stories.tsx +89 -0
  49. package/src/app/components/AppShell.tsx +358 -0
  50. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  51. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  52. package/src/app/components/Breadcrumb.tsx +45 -0
  53. package/src/app/components/MenuBar.stories.tsx +28 -0
  54. package/src/app/components/MenuBar.tsx +44 -0
  55. package/src/app/components/PaneGrid.stories.tsx +84 -0
  56. package/src/app/components/PaneGrid.tsx +128 -0
  57. package/src/app/components/PanelColumn.stories.tsx +25 -0
  58. package/src/app/components/PanelColumn.tsx +93 -0
  59. package/src/app/components/ShellHeader.stories.tsx +50 -0
  60. package/src/app/components/ShellHeader.tsx +137 -0
  61. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  62. package/src/app/components/WorkspacePanels.tsx +120 -0
  63. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  64. package/src/app/intent-dispatch.test.ts +728 -0
  65. package/src/app/intent-dispatch.ts +687 -0
  66. package/src/app/launch-layout.test.ts +78 -0
  67. package/src/app/launch-layout.ts +57 -0
  68. package/src/app/ready-signal.test.ts +85 -0
  69. package/src/app/ready-signal.ts +52 -0
  70. package/src/app/run.ts +109 -0
  71. package/src/{serve.test.ts → app/serve.test.ts} +24 -12
  72. package/src/app/serve.ts +111 -0
  73. package/src/app/version.ts +5 -0
  74. package/src/appearance/components/theme-context.stories.tsx +46 -0
  75. package/src/appearance/components/theme-context.tsx +37 -0
  76. package/src/appearance/theme-presets.test.ts +51 -0
  77. package/src/appearance/theme-presets.ts +219 -0
  78. package/src/appearance/theme.ts +112 -0
  79. package/src/appearance/visual/contrast.test.ts +35 -0
  80. package/src/appearance/visual/contrast.ts +33 -0
  81. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  82. package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
  83. package/src/diff/components/AnnotatableFileView.tsx +141 -0
  84. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  85. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  86. package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
  87. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  88. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  89. package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
  90. package/src/diff/components/DiffContentView.tsx +1055 -0
  91. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  92. package/src/diff/components/WalkWizard.tsx +146 -0
  93. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  94. package/src/diff/components/diff-content-view-layout.ts +84 -0
  95. package/src/diff/diff-fold.test.ts +78 -0
  96. package/src/diff/diff-fold.ts +126 -0
  97. package/src/diff/diff-hunk-curate.test.ts +315 -0
  98. package/src/diff/diff-hunk-curate.ts +152 -0
  99. package/src/diff/diff-intraline.test.ts +239 -0
  100. package/src/diff/diff-intraline.ts +238 -0
  101. package/src/diff/diff-syntax.test.ts +125 -0
  102. package/src/diff/diff-syntax.ts +186 -0
  103. package/src/diff/split-diff.test.ts +110 -0
  104. package/src/diff/split-diff.ts +144 -0
  105. package/src/diff/view-diff.test.ts +325 -0
  106. package/src/diff/view-diff.ts +522 -0
  107. package/src/diff/walk.test.ts +121 -0
  108. package/src/diff/walk.ts +73 -0
  109. package/src/index.ts +31 -2
  110. package/src/integrations/attribution.test.ts +31 -0
  111. package/src/integrations/attribution.ts +42 -0
  112. package/src/integrations/client-extension-registry.test.tsx +61 -0
  113. package/src/integrations/client-extension-registry.ts +159 -0
  114. package/src/integrations/clipboard.test.ts +23 -0
  115. package/src/integrations/clipboard.ts +33 -0
  116. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  117. package/src/integrations/components/ExtensionZone.tsx +145 -0
  118. package/src/integrations/components/ShareDialog.agent.test.tsx +50 -0
  119. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  120. package/src/integrations/components/ShareDialog.tsx +574 -0
  121. package/src/integrations/components/share-dialog-store.ts +59 -0
  122. package/src/integrations/editor.test.ts +149 -0
  123. package/src/integrations/editor.ts +160 -0
  124. package/src/integrations/github-identity.test.ts +58 -0
  125. package/src/integrations/github-identity.ts +41 -0
  126. package/src/integrations/herdr.test.ts +43 -0
  127. package/src/integrations/herdr.ts +38 -0
  128. package/src/integrations/integrations.test.ts +41 -0
  129. package/src/integrations/integrations.ts +32 -0
  130. package/src/integrations/pasted-image.test.ts +67 -0
  131. package/src/integrations/pasted-image.ts +60 -0
  132. package/src/integrations/share.ts +238 -0
  133. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  134. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  135. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  136. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  137. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  138. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  139. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  140. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  141. package/src/keyboard/components/NavModeHint.tsx +31 -0
  142. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  143. package/src/keyboard/components/SlashComposer.tsx +87 -0
  144. package/src/keyboard/key-bindings.test.ts +156 -0
  145. package/src/keyboard/key-bindings.ts +459 -0
  146. package/src/keyboard/keymap.test.ts +611 -0
  147. package/src/keyboard/keymap.ts +395 -0
  148. package/src/keyboard/skills.test.ts +74 -0
  149. package/src/keyboard/skills.ts +75 -0
  150. package/src/keyboard/slash-palette.test.ts +103 -0
  151. package/src/keyboard/slash-palette.ts +129 -0
  152. package/src/keyboard/thread-chords.test.ts +79 -0
  153. package/src/keyboard/thread-chords.ts +163 -0
  154. package/src/keyboard/use-shared-keyboard.ts +55 -0
  155. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  156. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  157. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  158. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  159. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  160. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  161. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  162. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  163. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  164. package/src/markdown/components/ThreadView.tsx +560 -0
  165. package/src/markdown/mark-runs.ts +121 -0
  166. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  167. package/src/markdown/markdown-grid.test.ts +101 -0
  168. package/src/markdown/markdown-grid.ts +203 -0
  169. package/src/markdown/markdown-highlight.test.ts +61 -0
  170. package/src/markdown/markdown-highlight.ts +124 -0
  171. package/src/markdown/thread-code-syntax.ts +45 -0
  172. package/src/markdown/use-thread-body-editing.tsx +54 -0
  173. package/src/markdown/view-plan.regression.test.ts +60 -0
  174. package/src/markdown/view-plan.test.ts +545 -0
  175. package/src/markdown/view-plan.ts +1009 -0
  176. package/src/performance/otlp-trace.test.ts +119 -0
  177. package/src/performance/otlp-trace.ts +134 -0
  178. package/src/performance/perf-report.ts +18 -0
  179. package/src/performance/perf-timings.test.ts +18 -0
  180. package/src/performance/perf-timings.ts +39 -0
  181. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  182. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  183. package/src/prototype/prototype-browser.test.ts +81 -0
  184. package/src/prototype/prototype-browser.ts +321 -0
  185. package/src/prototype/prototype-pixels.tsx +35 -0
  186. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  187. package/src/settings/components/SettingsDialog.tsx +183 -0
  188. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  189. package/src/settings/components/SettingsRows.tsx +112 -0
  190. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  192. package/src/settings/components/quick-actions-editor.tsx +222 -0
  193. package/src/settings/config-identity.test.ts +78 -0
  194. package/src/settings/config.test.ts +676 -0
  195. package/src/settings/config.ts +628 -0
  196. package/src/settings/use-settings-dialog.tsx +351 -0
  197. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  198. package/src/stories/stories-app.tsx +196 -0
  199. package/src/stories/stories.test.tsx +74 -0
  200. package/src/stories/story-fixtures.ts +102 -0
  201. package/src/stories/story-tree.test.ts +108 -0
  202. package/src/stories/story.ts +154 -0
  203. package/src/terminal/ansi-screen.test.ts +28 -0
  204. package/src/terminal/ansi-screen.ts +72 -0
  205. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  206. package/src/terminal/components/AnsiScreen.tsx +28 -0
  207. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  208. package/src/terminal/components/terminal-pane.ts +196 -0
  209. package/src/terminal/ghostty-terminal.test.ts +85 -0
  210. package/src/terminal/ghostty-terminal.ts +197 -0
  211. package/src/terminal/kitty-image.test.ts +66 -0
  212. package/src/terminal/kitty-image.ts +137 -0
  213. package/src/terminal/pty.test.ts +81 -0
  214. package/src/terminal/pty.ts +246 -0
  215. package/src/testing/test-support.test.ts +264 -0
  216. package/src/testing/test-support.ts +367 -0
  217. package/src/thread/agent-continuation.test.ts +32 -0
  218. package/src/thread/agent-continuation.ts +13 -0
  219. package/src/thread/agent-transcript.test.ts +161 -0
  220. package/src/thread/agent-transcript.ts +336 -0
  221. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  222. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  223. package/src/thread/components/AgentConfigControls.tsx +114 -0
  224. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  225. package/src/thread/components/AgentThreadPane.test.tsx +935 -0
  226. package/src/thread/components/AgentThreadPane.tsx +377 -0
  227. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  228. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  229. package/src/thread/components/ThreadFooter.tsx +77 -0
  230. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  231. package/src/thread/components/ThreadTree.test.tsx +41 -0
  232. package/src/thread/components/ThreadTree.tsx +267 -0
  233. package/src/thread/components/thread-tree.test.ts +137 -0
  234. package/src/thread/components/thread-tree.ts +112 -0
  235. package/src/thread/thread-controller.curate.test.ts +502 -0
  236. package/src/thread/thread-controller.derivation.test.ts +62 -0
  237. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  238. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  239. package/src/thread/thread-controller.share.test.ts +573 -0
  240. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  241. package/src/thread/thread-controller.tree.test.ts +263 -0
  242. package/src/thread/thread-controller.ts +2026 -0
  243. package/src/thread/tree-view.test.ts +128 -0
  244. package/src/thread/tree-view.ts +146 -0
  245. package/src/thread/use-thread-agent.test.tsx +208 -0
  246. package/src/thread/use-thread-agent.ts +117 -0
  247. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  248. package/src/ui/components/CodeBlock.tsx +80 -0
  249. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  250. package/src/ui/components/ConfirmCard.tsx +151 -0
  251. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  252. package/src/ui/components/ConfirmDialog.tsx +72 -0
  253. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  254. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  255. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  256. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  257. package/src/ui/components/PromptDialog.test.tsx +65 -0
  258. package/src/ui/components/PromptDialog.tsx +99 -0
  259. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  260. package/src/ui/components/RootOverlay.tsx +49 -0
  261. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  262. package/src/ui/components/ScrollArea.tsx +61 -0
  263. package/src/ui/components/Toast.stories.tsx +19 -0
  264. package/src/ui/components/Toast.tsx +54 -0
  265. package/src/ui/components/Tooltip.stories.tsx +26 -0
  266. package/src/ui/components/Tooltip.tsx +72 -0
  267. package/src/ui/components/menu-control.stories.tsx +35 -0
  268. package/src/ui/components/menu-control.tsx +43 -0
  269. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  270. package/src/ui/components/primitives/Button.tsx +57 -0
  271. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  272. package/src/ui/components/primitives/Card.tsx +62 -0
  273. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  274. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  275. package/src/ui/components/primitives/Dialog.tsx +69 -0
  276. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  277. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  278. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  279. package/src/ui/components/primitives/IconButton.tsx +58 -0
  280. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  281. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  282. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  283. package/src/ui/components/primitives/Switch.tsx +32 -0
  284. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  285. package/src/ui/components/primitives/Tabs.tsx +101 -0
  286. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  288. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  289. package/src/ui/components/primitives/Tree.tsx +133 -0
  290. package/src/ui/components/primitives/frame.test.ts +12 -0
  291. package/src/ui/components/primitives/frame.ts +17 -0
  292. package/src/ui/components/primitives/icons.ts +61 -0
  293. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  294. package/src/ui/components/primitives/tree-model.ts +137 -0
  295. package/src/ui/components/syntax-highlight.ts +133 -0
  296. package/src/ui/components/truncate-text.ts +7 -0
  297. package/src/ui/components/truncate-title.test.ts +31 -0
  298. package/src/ui/components/truncate-title.ts +15 -0
  299. package/src/ui/use-frame-measure.ts +74 -0
  300. package/src/ui/use-terminal-virtualizer.ts +259 -0
  301. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  302. package/src/workbench/components/ChangesColumn.tsx +203 -0
  303. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  304. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  305. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  306. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  307. package/src/workbench/components/EditorGrid.tsx +544 -0
  308. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  309. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  310. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  311. package/src/workbench/components/FileContentsView.tsx +100 -0
  312. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  313. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  314. package/src/workbench/components/GridTabContent.tsx +193 -0
  315. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  316. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  317. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  318. package/src/workbench/components/TreePane.stories.tsx +106 -0
  319. package/src/workbench/components/TreePane.tsx +155 -0
  320. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  321. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  322. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  323. package/src/workbench/components/editor-grid.test.ts +184 -0
  324. package/src/workbench/components/editor-grid.ts +281 -0
  325. package/src/workbench/components/file-tree.test.ts +60 -0
  326. package/src/workbench/components/file-tree.ts +112 -0
  327. package/src/workbench/use-changes-workbench.ts +205 -0
  328. package/src/workbench/use-remember-layout.ts +31 -0
  329. package/src/workbench/use-repo-changes.ts +91 -0
  330. package/src/workbench/workbench-snapshot.test.ts +99 -0
  331. package/src/workbench/workbench-snapshot.ts +59 -0
  332. package/src/App.diff.test.tsx +0 -99
  333. package/src/App.export.test.tsx +0 -88
  334. package/src/App.plan-review.test.tsx +0 -259
  335. package/src/App.readonly.test.tsx +0 -148
  336. package/src/App.rendering.test.tsx +0 -108
  337. package/src/App.submit-confirm.test.tsx +0 -183
  338. package/src/App.test.tsx +0 -218
  339. package/src/App.tsx +0 -1249
  340. package/src/config.test.ts +0 -116
  341. package/src/config.ts +0 -150
  342. package/src/editor.ts +0 -34
  343. package/src/herdr.test.ts +0 -68
  344. package/src/herdr.ts +0 -34
  345. package/src/keymap.test.ts +0 -270
  346. package/src/keymap.ts +0 -173
  347. package/src/run.ts +0 -30
  348. package/src/serve.ts +0 -75
  349. package/src/session-controller.ts +0 -398
  350. package/src/syntax.test.ts +0 -41
  351. package/src/syntax.ts +0 -88
  352. package/src/theme.ts +0 -46
  353. package/src/view-diff.test.ts +0 -55
  354. package/src/view-diff.ts +0 -86
  355. package/src/view.test.ts +0 -157
  356. package/src/view.ts +0 -364
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Guided-walk projections: pure helpers over the diff row model - no
3
+ * IO, no React. The walk steps through the changed files in patch order;
4
+ * which files were seen lives on the session record (viewedPaths), which
5
+ * file is on screen is view state in the session controller.
6
+ */
7
+
8
+ import type { Annotation } from "@cueloop/schema";
9
+ import type { DiffRow } from "./view-diff";
10
+
11
+ /** One wizard step: a changed file with its stats and a short diff preview. */
12
+ export interface WalkFile {
13
+ path: string;
14
+ added: number;
15
+ removed: number;
16
+ preview: { sign: "+" | "-"; text: string }[];
17
+ }
18
+
19
+ /** Preview lines per card: enough to recognize the change, never a full diff. */
20
+ export const WALK_PREVIEW_ROWS = 5;
21
+
22
+ /** Collapse the diff rows into one walk step per changed file, in patch order. */
23
+ export function walkFiles(rows: DiffRow[]): WalkFile[] {
24
+ const files: WalkFile[] = [];
25
+ const byPath = new Map<string, WalkFile>();
26
+
27
+ for (const row of rows) {
28
+ let file = byPath.get(row.file);
29
+
30
+ if (!file) {
31
+ file = { path: row.file, added: 0, removed: 0, preview: [] };
32
+ byPath.set(row.file, file);
33
+ files.push(file);
34
+ }
35
+ if (row.kind !== "add" && row.kind !== "del") continue;
36
+ if (row.kind === "add") file.added += 1;
37
+ else file.removed += 1;
38
+ if (file.preview.length < WALK_PREVIEW_ROWS) {
39
+ file.preview.push({
40
+ sign: row.kind === "add" ? "+" : "-",
41
+ text: row.text.replace(/\n$/, ""),
42
+ });
43
+ }
44
+ }
45
+
46
+ return files;
47
+ }
48
+
49
+ /**
50
+ * Where the walk resumes: the first file not yet marked viewed. All files
51
+ * viewed = the end card (index === files.length), so a finished walk reopens
52
+ * on its summary instead of restarting.
53
+ */
54
+ export function firstUnviewedIndex(files: WalkFile[], viewedPaths: ReadonlySet<string>): number {
55
+ const index = files.findIndex((file) => !viewedPaths.has(file.path));
56
+
57
+ return index === -1 ? files.length : index;
58
+ }
59
+
60
+ /** Viewed count against the CURRENT file set - stale paths never inflate it. */
61
+ export function viewedCount(files: WalkFile[], viewedPaths: ReadonlySet<string>): number {
62
+ return files.filter((file) => viewedPaths.has(file.path)).length;
63
+ }
64
+
65
+ /**
66
+ * The agent's note for a file: an annotation with kind "note" whose anchor
67
+ * quote is the file path (the note contract in the shared review core).
68
+ */
69
+ export function noteForFile(annotations: Annotation[], path: string): string | undefined {
70
+ return annotations.find(
71
+ (annotation) => annotation.kind === "note" && annotation.anchor.quote === path,
72
+ )?.body;
73
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,31 @@
1
- export { runClient, type RunClientOptions } from "./run";
2
- export { serveClient, type ServeHandle, type ServeOptions } from "./serve";
1
+ export { runClient, type RunClientOptions } from "./app/run";
2
+ export { snapshotWorkbench, type RepoDiff } from "./workbench/workbench-snapshot";
3
+ export {
4
+ defaultLayout,
5
+ pairLayout,
6
+ planLayout,
7
+ pullRequestReviewLayout,
8
+ reviewLayout,
9
+ type LaunchLayout,
10
+ } from "./app/launch-layout";
11
+ export {
12
+ diffRowAnchor,
13
+ diffRows,
14
+ diffRowText,
15
+ fileRowRange,
16
+ resolveReviewAnchorRow,
17
+ type DiffRow,
18
+ } from "./diff/view-diff";
19
+ export { loadConfig, type ReviewWorkspaceMode } from "./settings/config";
20
+ export { serveClient, type ServeHandle, type ServeOptions } from "./app/serve";
21
+ export { App, type AppProps } from "./app/App";
22
+ export {
23
+ collaboratorAnnotations,
24
+ mergeFromShare,
25
+ publishShare,
26
+ pullShare,
27
+ pushShare,
28
+ shareIdFromLine,
29
+ type ShareResult,
30
+ type ShareTarget,
31
+ } from "./integrations/share";
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Identity } from "@cueloop/schema";
3
+ import { ANONYMOUS_LABEL, resolveDisplayName, shortHandle } from "./attribution";
4
+
5
+ describe(shortHandle, () => {
6
+ test("shortens an ssh fingerprint to a stable handle", () => {
7
+ // Arrange / Act / Assert
8
+ expect(shortHandle("SHA256:1a2b3c4d5e6f")).toBe("1a2b3c4d");
9
+ expect(shortHandle("SHA256:1a2b3c4d5e6f")).toBe(shortHandle("SHA256:1a2b3c4d5e6f"));
10
+ });
11
+ });
12
+
13
+ describe(resolveDisplayName, () => {
14
+ const named: Identity[] = [{ id: "SHA256:abc", provider: "ssh", name: "Al" }];
15
+ const unnamed: Identity[] = [{ id: "SHA256:abc", provider: "ssh" }];
16
+
17
+ test("a local rename override wins over everything", () => {
18
+ // Arrange / Act
19
+ const label = resolveDisplayName("SHA256:abc", named, { "SHA256:abc": "Alex" });
20
+
21
+ // Assert
22
+ expect(label).toBe("Alex");
23
+ });
24
+
25
+ test("falls back to name, then anonymous, then the short handle", () => {
26
+ // Arrange / Act / Assert
27
+ expect(resolveDisplayName("SHA256:abc", named, {})).toBe("Al");
28
+ expect(resolveDisplayName("SHA256:abc", unnamed, {})).toBe(ANONYMOUS_LABEL);
29
+ expect(resolveDisplayName("SHA256:1a2b3c4d5e", [], {})).toBe("1a2b3c4d");
30
+ });
31
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Attribution labels for annotations. An annotation's `author` is an identity
3
+ * id (an SSH fingerprint today). The rail resolves it against the share's
4
+ * participant registry and the planner's local rename overrides; ADR 0006.
5
+ */
6
+
7
+ import type { Identity } from "@cueloop/schema";
8
+
9
+ const FINGERPRINT_PREFIX = "SHA256:";
10
+
11
+ /** Shown for a known participant who chose not to give a name. */
12
+ export const ANONYMOUS_LABEL = "-- anonymous --";
13
+
14
+ /** A short, stable handle from a raw author id when no name is known. */
15
+ export function shortHandle(authorId: string): string {
16
+ const base = authorId.startsWith(FINGERPRINT_PREFIX)
17
+ ? authorId.slice(FINGERPRINT_PREFIX.length)
18
+ : authorId;
19
+
20
+ return base.slice(0, 8) || authorId;
21
+ }
22
+
23
+ /**
24
+ * The rail label for an author id: a local rename wins, then the collaborator's
25
+ * own name, then anonymous for a known-but-unnamed participant, then a short
26
+ * handle for an id we hold no identity for.
27
+ */
28
+ export function resolveDisplayName(
29
+ authorId: string,
30
+ participants: Identity[] | undefined,
31
+ overrides: Record<string, string>,
32
+ ): string {
33
+ const override = overrides[authorId];
34
+
35
+ if (override) return override;
36
+ const identity = participants?.find((participant) => participant.id === authorId);
37
+
38
+ if (identity?.name) return identity.name;
39
+ if (identity) return ANONYMOUS_LABEL;
40
+
41
+ return shortHandle(authorId);
42
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import React from "react";
3
+ import { ClientExtensionRegistry } from "./client-extension-registry";
4
+
5
+ const Component = () => <></>;
6
+
7
+ describe("client extension registry", () => {
8
+ test("registers each contribution and removes it on disposal", async () => {
9
+ const registry = new ClientExtensionRegistry();
10
+ const before = registry.snapshot();
11
+ let notifications = 0;
12
+ const unsubscribe = registry.subscribe(() => notifications++);
13
+ const loaded = await registry.load("example", (api) => {
14
+ api.registerSection({ id: "summary", zone: "threads.sidebar", title: "Summary", Component });
15
+ api.registerAction({ id: "open", zone: "thread.header", label: "Open", onPress() {} });
16
+ api.registerView({ id: "details", zone: "workspace.panels", title: "Details", Component });
17
+ });
18
+
19
+ expect(registry.snapshot()).not.toBe(before);
20
+ expect(registry.snapshot().sections.map(({ key }) => key)).toEqual(["example:summary"]);
21
+ expect(registry.snapshot().actions.map(({ key }) => key)).toEqual(["example:open"]);
22
+ expect(registry.snapshot().views.map(({ key }) => key)).toEqual(["example:details"]);
23
+ loaded.dispose();
24
+ unsubscribe();
25
+ expect(registry.snapshot().sections).toEqual([]);
26
+ expect(registry.snapshot().actions).toEqual([]);
27
+ expect(registry.snapshot().views).toEqual([]);
28
+ expect(notifications).toBe(6);
29
+ });
30
+
31
+ test("rolls back a factory that fails after registering", async () => {
32
+ const registry = new ClientExtensionRegistry();
33
+
34
+ await registry.load("broken", (api) => {
35
+ api.registerSection({ id: "summary", zone: "threads.sidebar", title: "Summary", Component });
36
+ throw new Error("boom");
37
+ });
38
+
39
+ expect(registry.snapshot().sections).toEqual([]);
40
+ expect(registry.errors[0]).toContain("boom");
41
+ });
42
+
43
+ test("a disposed registration cannot remove a later registration with the same ID", async () => {
44
+ const registry = new ClientExtensionRegistry();
45
+ let first: { dispose(): void } | undefined;
46
+
47
+ await registry.load("example", (api) => {
48
+ first = api.registerSection({
49
+ id: "summary",
50
+ zone: "threads.sidebar",
51
+ title: "Old",
52
+ Component,
53
+ });
54
+ first.dispose();
55
+ api.registerSection({ id: "summary", zone: "threads.sidebar", title: "New", Component });
56
+ });
57
+ first?.dispose();
58
+
59
+ expect(registry.snapshot().sections[0]?.value.title).toBe("New");
60
+ });
61
+ });
@@ -0,0 +1,159 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import * as React from "react";
3
+ import { useKeyboard } from "@opentui/react";
4
+ import * as v from "valibot";
5
+ import { discoverInstalledExtensionPackages } from "@cueloop/extension-api/installed-packages";
6
+ import type {
7
+ ClientExtensionAPI,
8
+ ClientExtensionFactory,
9
+ ExtensionUIDisposable,
10
+ ThreadHeaderAction,
11
+ ThreadSidebarSection,
12
+ WorkspacePanelView,
13
+ } from "@cueloop/extension-api/client";
14
+
15
+ interface OwnedContribution<T> {
16
+ key: string;
17
+ value: T;
18
+ }
19
+
20
+ /** A stable snapshot lets React subscribe without rebuilding contribution arrays on every render. */
21
+ export interface ClientExtensionSnapshot {
22
+ sections: readonly OwnedContribution<ThreadSidebarSection>[];
23
+ actions: readonly OwnedContribution<ThreadHeaderAction>[];
24
+ views: readonly OwnedContribution<WorkspacePanelView>[];
25
+ lastError: string | null;
26
+ }
27
+
28
+ /** Client extension registrations are isolated by package and disposed on load failure. */
29
+ export class ClientExtensionRegistry {
30
+ private readonly listeners = new Set<() => void>();
31
+ private readonly sections = new Map<string, ThreadSidebarSection>();
32
+ private readonly actions = new Map<string, ThreadHeaderAction>();
33
+ private readonly views = new Map<string, WorkspacePanelView>();
34
+ private current: ClientExtensionSnapshot = {
35
+ sections: [],
36
+ actions: [],
37
+ views: [],
38
+ lastError: null,
39
+ };
40
+ readonly errors: string[] = [];
41
+
42
+ readonly subscribe = (listener: () => void): (() => void) => {
43
+ this.listeners.add(listener);
44
+
45
+ return () => this.listeners.delete(listener);
46
+ };
47
+
48
+ readonly snapshot = (): ClientExtensionSnapshot => this.current;
49
+
50
+ private changed(): void {
51
+ this.current = {
52
+ sections: [...this.sections].map(([key, value]) => ({ key, value })),
53
+ actions: [...this.actions].map(([key, value]) => ({ key, value })),
54
+ views: [...this.views].map(([key, value]) => ({ key, value })),
55
+ lastError: this.errors.at(-1) ?? null,
56
+ };
57
+ for (const listener of this.listeners) listener();
58
+ }
59
+
60
+ private add<T extends { id: string; zone: string }>(
61
+ map: Map<string, T>,
62
+ owner: string,
63
+ contribution: T,
64
+ zone: string,
65
+ ): ExtensionUIDisposable {
66
+ if (!/^[a-z][a-z0-9-]*$/.test(contribution.id))
67
+ throw new Error(`Client extension invalid contribution ID: ${contribution.id}`);
68
+ if (contribution.zone !== zone)
69
+ throw new Error(`Client extension ${owner}:${contribution.id} must use zone ${zone}`);
70
+ const key = `${owner}:${contribution.id}`;
71
+
72
+ if (this.sections.has(key) || this.actions.has(key) || this.views.has(key))
73
+ throw new Error(`Client extension duplicate contribution: ${key}`);
74
+ map.set(key, contribution);
75
+ this.changed();
76
+
77
+ return {
78
+ dispose: () => {
79
+ if (map.get(key) === contribution) {
80
+ map.delete(key);
81
+ this.changed();
82
+ }
83
+ },
84
+ };
85
+ }
86
+
87
+ reportError(message: string): void {
88
+ this.errors.push(message);
89
+ this.changed();
90
+ }
91
+
92
+ /** Load one client factory and remove all of its registrations if it fails. */
93
+ async load(owner: string, factory: ClientExtensionFactory): Promise<ExtensionUIDisposable> {
94
+ const registrations: ExtensionUIDisposable[] = [];
95
+ const api: ClientExtensionAPI = {
96
+ react: React,
97
+ useKeyboard,
98
+ registerSection: (section) => {
99
+ const registration = this.add(this.sections, owner, section, "threads.sidebar");
100
+
101
+ registrations.push(registration);
102
+
103
+ return registration;
104
+ },
105
+ registerAction: (action) => {
106
+ const registration = this.add(this.actions, owner, action, "thread.header");
107
+
108
+ registrations.push(registration);
109
+
110
+ return registration;
111
+ },
112
+ registerView: (view) => {
113
+ const registration = this.add(this.views, owner, view, "workspace.panels");
114
+
115
+ registrations.push(registration);
116
+
117
+ return registration;
118
+ },
119
+ };
120
+
121
+ try {
122
+ await factory(api);
123
+ } catch (error) {
124
+ for (const registration of registrations) registration.dispose();
125
+ this.reportError(`Client extension ${owner}: ${String(error)}`);
126
+ }
127
+
128
+ return {
129
+ dispose: () => {
130
+ for (const registration of registrations) registration.dispose();
131
+ },
132
+ };
133
+ }
134
+ }
135
+
136
+ /** Import only the client entry points of installed extension packages. */
137
+ export async function loadInstalledClientExtensions(
138
+ registry: ClientExtensionRegistry,
139
+ installRoot?: string,
140
+ ): Promise<void> {
141
+ const discovery = discoverInstalledExtensionPackages(installRoot);
142
+
143
+ for (const error of discovery.errors) registry.reportError(error);
144
+ for (const extension of discovery.packages) {
145
+ if (!extension.clientEntry) continue;
146
+
147
+ try {
148
+ const module: unknown = await import(pathToFileURL(extension.clientEntry).href);
149
+ const parsed = v.safeParse(v.object({ default: v.function() }), module);
150
+
151
+ if (!parsed.success) throw new Error("client entry point must export a default factory");
152
+ await registry.load(extension.name, async (api) => {
153
+ await parsed.output.default(api);
154
+ });
155
+ } catch (error) {
156
+ registry.reportError(`Client extension ${extension.name}: ${String(error)}`);
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { clipboardCommands } from "./clipboard";
3
+
4
+ describe(clipboardCommands, () => {
5
+ test("uses pbcopy on macOS", () => {
6
+ // Assert
7
+ expect(clipboardCommands("darwin")).toEqual([["pbcopy"]]);
8
+ });
9
+
10
+ test("uses clip.exe on Windows", () => {
11
+ // Assert
12
+ expect(clipboardCommands("win32")).toEqual([["clip.exe"]]);
13
+ });
14
+
15
+ test("prefers wayland, then X11 tools on Linux", () => {
16
+ // Act
17
+ const commands = clipboardCommands("linux");
18
+
19
+ // Assert
20
+ expect(commands[0]).toEqual(["wl-copy"]);
21
+ expect(commands.map((command) => command[0])).toEqual(["wl-copy", "xclip", "xsel"]);
22
+ });
23
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Best-effort system clipboard. Each platform has its own tool; we try them in
3
+ * order and report whether one worked, so the caller can fall back to printing
4
+ * the line (the over-SSH case, where no clipboard tool exists). Mirrors how
5
+ * herdr confirms a copy.
6
+ */
7
+
8
+ /** The clipboard-write commands to try, most-preferred first, for this platform. */
9
+ export function clipboardCommands(platform: NodeJS.Platform = process.platform): string[][] {
10
+ if (platform === "darwin") return [["pbcopy"]];
11
+ if (platform === "win32") return [["clip.exe"]];
12
+
13
+ return [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]];
14
+ }
15
+
16
+ /** Copy `text` to the clipboard; false when no tool is available or all fail. */
17
+ export async function copyToClipboard(text: string): Promise<boolean> {
18
+ for (const command of clipboardCommands()) {
19
+ try {
20
+ const proc = Bun.spawn(command, {
21
+ stdin: Buffer.from(text),
22
+ stdout: "ignore",
23
+ stderr: "ignore",
24
+ });
25
+
26
+ if ((await proc.exited) === 0) return true;
27
+ } catch {
28
+ // tool not installed - try the next one
29
+ }
30
+ }
31
+
32
+ return false;
33
+ }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import { ClientExtensionRegistry } from "../client-extension-registry";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { ExtensionZone } from "./ExtensionZone";
5
+
6
+ export const meta: StoryMeta = { title: "Extensions/ExtensionZone" };
7
+
8
+ const registry = new ClientExtensionRegistry();
9
+
10
+ void registry.load("example", (api) => {
11
+ api.registerSection({
12
+ id: "summary",
13
+ zone: "threads.sidebar",
14
+ title: "Summary",
15
+ Component: () => <text>Two pending Threads</text>,
16
+ });
17
+ });
18
+
19
+ export const SidebarSection: Story = {
20
+ render: () => (
21
+ <ExtensionZone
22
+ zone="threads.sidebar"
23
+ registry={registry}
24
+ context={{ workspace: "/repo", threadId: null }}
25
+ />
26
+ ),
27
+ size: { width: 32, height: 8 },
28
+ };
@@ -0,0 +1,145 @@
1
+ import React, { useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import type { ExtensionUIContext } from "@cueloop/extension-api/client";
3
+ import type {
4
+ ClientExtensionRegistry,
5
+ ClientExtensionSnapshot,
6
+ } from "../client-extension-registry";
7
+ import { useComponentTheme } from "../../appearance/components/theme-context";
8
+ import { IconButton } from "../../ui/components/primitives/IconButton";
9
+ import { NERD } from "../../ui/components/primitives/icons";
10
+
11
+ /** Subscribe to the client extension registry without coupling extensions to React state. */
12
+ export function useClientExtensionSnapshot(
13
+ registry: ClientExtensionRegistry,
14
+ ): ClientExtensionSnapshot {
15
+ return useSyncExternalStore(registry.subscribe, registry.snapshot, registry.snapshot);
16
+ }
17
+
18
+ interface VisibleContributions<T> {
19
+ items: T[];
20
+ errors: string[];
21
+ }
22
+
23
+ /** Evaluate visibility without letting one extension remove another's UI. */
24
+ export function visibleContributions<
25
+ T extends {
26
+ key: string;
27
+ value: { when?: (context: ExtensionUIContext) => boolean };
28
+ },
29
+ >(contributions: readonly T[], context: ExtensionUIContext): VisibleContributions<T> {
30
+ const items: T[] = [];
31
+ const errors: string[] = [];
32
+
33
+ for (const contribution of contributions) {
34
+ try {
35
+ if (!contribution.value.when || contribution.value.when(context)) items.push(contribution);
36
+ } catch (error) {
37
+ errors.push(`Extension ${contribution.key} visibility failed: ${String(error)}`);
38
+ }
39
+ }
40
+
41
+ return { items, errors };
42
+ }
43
+
44
+ type VisibilityContribution = {
45
+ key: string;
46
+ value: { when?: (context: ExtensionUIContext) => boolean };
47
+ };
48
+
49
+ class ExtensionRenderBoundary extends React.Component<
50
+ { children: React.ReactNode; onError?: (message: string) => void },
51
+ { failed: boolean }
52
+ > {
53
+ state = { failed: false };
54
+
55
+ static getDerivedStateFromError() {
56
+ return { failed: true };
57
+ }
58
+
59
+ componentDidCatch(error: Error): void {
60
+ console.error("Client extension rendering failed", error);
61
+ this.props.onError?.(`Extension rendering failed: ${error.message}`);
62
+ }
63
+
64
+ render(): React.ReactNode {
65
+ if (this.state.failed) return <text>Extension failed</text>;
66
+
67
+ return this.props.children;
68
+ }
69
+ }
70
+
71
+ /** Render host-framed sections, host buttons, or the selected workspace view. */
72
+ export function ExtensionZone(props: {
73
+ zone: "threads.sidebar" | "thread.header" | "workspace.panels";
74
+ registry: ClientExtensionRegistry;
75
+ context: ExtensionUIContext;
76
+ selectedViewId?: string | null;
77
+ onError?: (message: string) => void;
78
+ }): React.ReactNode {
79
+ const { zone, registry, context, selectedViewId, onError } = props;
80
+ const snapshot = useClientExtensionSnapshot(registry);
81
+ const theme = useComponentTheme();
82
+ const { workspace, threadId } = context;
83
+ const visibility = useMemo(() => {
84
+ const contributions: readonly VisibilityContribution[] =
85
+ zone === "threads.sidebar"
86
+ ? snapshot.sections
87
+ : zone === "thread.header"
88
+ ? snapshot.actions
89
+ : [];
90
+
91
+ return visibleContributions(contributions, { workspace, threadId });
92
+ }, [zone, snapshot, workspace, threadId]);
93
+
94
+ useEffect(() => {
95
+ for (const error of visibility.errors) onError?.(error);
96
+ }, [visibility, onError]);
97
+
98
+ const visibleKeys = new Set(visibility.items.map(({ key }) => key));
99
+
100
+ if (zone === "threads.sidebar") {
101
+ return snapshot.sections
102
+ .filter(({ key }) => visibleKeys.has(key))
103
+ .map(({ key, value }) => (
104
+ <box key={key} style={{ flexDirection: "column", maxHeight: 8, flexShrink: 0 }}>
105
+ <text fg={theme.textDim}>{value.title}</text>
106
+ <ExtensionRenderBoundary onError={onError}>
107
+ {React.createElement(value.Component, { context })}
108
+ </ExtensionRenderBoundary>
109
+ </box>
110
+ ));
111
+ }
112
+
113
+ if (zone === "thread.header") {
114
+ return snapshot.actions
115
+ .filter(({ key }) => visibleKeys.has(key))
116
+ .map(({ key, value }) => (
117
+ <IconButton
118
+ key={key}
119
+ glyph={value.glyph ?? NERD.kebab}
120
+ tip={value.label}
121
+ onPress={() => {
122
+ try {
123
+ void Promise.resolve(value.onPress(context)).catch((error) => {
124
+ console.error(`Client extension action failed: ${key}`, error);
125
+ onError?.(`Extension action failed: ${value.label}`);
126
+ });
127
+ } catch (error) {
128
+ console.error(`Client extension action failed: ${key}`, error);
129
+ onError?.(`Extension action failed: ${value.label}`);
130
+ }
131
+ }}
132
+ marginRight={1}
133
+ theme={theme}
134
+ />
135
+ ));
136
+ }
137
+
138
+ const selected = snapshot.views.find(({ key }) => key === selectedViewId);
139
+
140
+ return selected ? (
141
+ <ExtensionRenderBoundary key={selected.key} onError={onError}>
142
+ {React.createElement(selected.value.Component, { context })}
143
+ </ExtensionRenderBoundary>
144
+ ) : null;
145
+ }
@@ -0,0 +1,50 @@
1
+ import { expect, test } from "bun:test";
2
+ import React from "react";
3
+ import { testRender } from "@opentui/react/test-utils";
4
+ import { DARK } from "../../appearance/theme";
5
+ import { press, waitForText } from "../../testing/test-support";
6
+ import { ShareDialog } from "./ShareDialog";
7
+ import { newWizardDraft, shareDialogStore } from "./share-dialog-store";
8
+ import type { NewShareLink } from "../../thread/thread-controller";
9
+
10
+ for (const enabled of [false, true])
11
+ test(`share agent toggle is ${enabled ? "opt-in" : "hidden"}`, async () => {
12
+ const created: NewShareLink[] = [];
13
+
14
+ shareDialogStore.getState().reset();
15
+ shareDialogStore.getState().openWizard(newWizardDraft("Review"));
16
+ const setup = await testRender(
17
+ <ShareDialog
18
+ isOpen
19
+ isOwner
20
+ agentAvailable={enabled}
21
+ links={[]}
22
+ threadName="Review"
23
+ theme={DARK}
24
+ onCreateLink={(input) => created.push(input)}
25
+ onUpdateLink={() => {}}
26
+ onDeleteLink={() => {}}
27
+ onCopyLink={() => {}}
28
+ onClose={() => {}}
29
+ />,
30
+ { width: 100, height: 26 },
31
+ );
32
+
33
+ try {
34
+ await waitForText(setup, "link name");
35
+ expect(shareDialogStore.getState().wizard?.agentEnabled ?? false).toBe(false);
36
+ expect(setup.captureCharFrame().includes("Allow agent messages")).toBe(enabled);
37
+ await press(setup, "down");
38
+ await press(setup, "down");
39
+ if (enabled) {
40
+ await press(setup, " ");
41
+ await press(setup, "down");
42
+ }
43
+ await press(setup, "enter");
44
+ expect(created).toHaveLength(1);
45
+ expect(created[0]?.agentEnabled).toBe(enabled ? true : undefined);
46
+ } finally {
47
+ setup.renderer.destroy();
48
+ shareDialogStore.getState().reset();
49
+ }
50
+ });