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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/annotations/annotation-palette.ts +63 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +383 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +189 -0
  14. package/src/annotations/discussions.ts +78 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +248 -0
  17. package/src/annotations/use-annotation-surface.tsx +1378 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  20. package/src/app/App.deleted-scroll.test.tsx +190 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.harness-settings.test.tsx +77 -0
  25. package/src/app/App.identity.test.tsx +84 -0
  26. package/src/app/App.plan-render.test.tsx +211 -0
  27. package/src/app/App.plan-review.test.tsx +429 -0
  28. package/src/app/App.prototype-markdown.test.tsx +129 -0
  29. package/src/app/App.prototype.test.tsx +176 -0
  30. package/src/app/App.readonly.test.tsx +274 -0
  31. package/src/app/App.regression.test.tsx +99 -0
  32. package/src/app/App.rendering.test.tsx +134 -0
  33. package/src/app/App.share-dialog.test.tsx +177 -0
  34. package/src/app/App.submit-confirm.test.tsx +267 -0
  35. package/src/app/App.test.tsx +497 -0
  36. package/src/app/App.thread-agent.test.ts +20 -0
  37. package/src/app/App.toast.test.tsx +124 -0
  38. package/src/app/App.tsx +1758 -0
  39. package/src/app/App.visual.test.tsx +60 -0
  40. package/src/app/App.walk.test.tsx +300 -0
  41. package/src/app/App.workbench.test.tsx +1153 -0
  42. package/src/app/app-key-state.ts +61 -0
  43. package/src/app/app-screens.tsx +607 -0
  44. package/src/app/app-view-model.ts +178 -0
  45. package/src/app/components/AppHeader.stories.tsx +50 -0
  46. package/src/app/components/AppHeader.tsx +81 -0
  47. package/src/app/components/AppShell.brand.test.tsx +58 -0
  48. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  49. package/src/app/components/AppShell.stories.tsx +89 -0
  50. package/src/app/components/AppShell.tsx +358 -0
  51. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  52. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  53. package/src/app/components/Breadcrumb.tsx +45 -0
  54. package/src/app/components/MenuBar.stories.tsx +28 -0
  55. package/src/app/components/MenuBar.tsx +44 -0
  56. package/src/app/components/PaneGrid.stories.tsx +84 -0
  57. package/src/app/components/PaneGrid.tsx +128 -0
  58. package/src/app/components/PanelColumn.stories.tsx +25 -0
  59. package/src/app/components/PanelColumn.tsx +93 -0
  60. package/src/app/components/ShellHeader.stories.tsx +50 -0
  61. package/src/app/components/ShellHeader.tsx +137 -0
  62. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  63. package/src/app/components/WorkspacePanels.tsx +121 -0
  64. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  65. package/src/app/intent-dispatch.test.ts +729 -0
  66. package/src/app/intent-dispatch.ts +713 -0
  67. package/src/app/launch-layout.test.ts +78 -0
  68. package/src/app/launch-layout.ts +57 -0
  69. package/src/app/ready-signal.test.ts +85 -0
  70. package/src/app/ready-signal.ts +54 -0
  71. package/src/app/run.ts +111 -0
  72. package/src/{serve.test.ts → app/serve.test.ts} +28 -12
  73. package/src/app/serve.ts +111 -0
  74. package/src/app/version.ts +5 -0
  75. package/src/appearance/components/theme-context.stories.tsx +46 -0
  76. package/src/appearance/components/theme-context.tsx +37 -0
  77. package/src/appearance/theme-presets.test.ts +51 -0
  78. package/src/appearance/theme-presets.ts +219 -0
  79. package/src/appearance/theme.ts +112 -0
  80. package/src/appearance/visual/contrast.test.ts +35 -0
  81. package/src/appearance/visual/contrast.ts +33 -0
  82. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  83. package/src/diff/components/AnnotatableFileView.test.tsx +160 -0
  84. package/src/diff/components/AnnotatableFileView.tsx +142 -0
  85. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  86. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  87. package/src/diff/components/DiffContentView.scroll.test.tsx +443 -0
  88. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  89. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  90. package/src/diff/components/DiffContentView.syntax.test.tsx +85 -0
  91. package/src/diff/components/DiffContentView.tsx +1080 -0
  92. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  93. package/src/diff/components/WalkWizard.tsx +146 -0
  94. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  95. package/src/diff/components/diff-content-view-layout.ts +87 -0
  96. package/src/diff/diff-fold.test.ts +78 -0
  97. package/src/diff/diff-fold.ts +130 -0
  98. package/src/diff/diff-hunk-curate.test.ts +315 -0
  99. package/src/diff/diff-hunk-curate.ts +155 -0
  100. package/src/diff/diff-intraline.test.ts +239 -0
  101. package/src/diff/diff-intraline.ts +244 -0
  102. package/src/diff/diff-syntax.test.ts +125 -0
  103. package/src/diff/diff-syntax.ts +194 -0
  104. package/src/diff/split-diff.test.ts +110 -0
  105. package/src/diff/split-diff.ts +150 -0
  106. package/src/diff/view-diff.test.ts +325 -0
  107. package/src/diff/view-diff.ts +540 -0
  108. package/src/diff/walk.test.ts +121 -0
  109. package/src/diff/walk.ts +76 -0
  110. package/src/index.ts +32 -2
  111. package/src/integrations/attribution.test.ts +31 -0
  112. package/src/integrations/attribution.ts +44 -0
  113. package/src/integrations/client-extension-registry.test.tsx +61 -0
  114. package/src/integrations/client-extension-registry.ts +163 -0
  115. package/src/integrations/clipboard.test.ts +23 -0
  116. package/src/integrations/clipboard.ts +34 -0
  117. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  118. package/src/integrations/components/ExtensionZone.tsx +145 -0
  119. package/src/integrations/components/ShareDialog.agent.test.tsx +52 -0
  120. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  121. package/src/integrations/components/ShareDialog.tsx +602 -0
  122. package/src/integrations/components/share-dialog-store.ts +59 -0
  123. package/src/integrations/editor.test.ts +149 -0
  124. package/src/integrations/editor.ts +163 -0
  125. package/src/integrations/github-identity.test.ts +59 -0
  126. package/src/integrations/github-identity.ts +42 -0
  127. package/src/integrations/herdr.test.ts +43 -0
  128. package/src/integrations/herdr.ts +40 -0
  129. package/src/integrations/integrations.test.ts +41 -0
  130. package/src/integrations/integrations.ts +32 -0
  131. package/src/integrations/pasted-image.test.ts +67 -0
  132. package/src/integrations/pasted-image.ts +62 -0
  133. package/src/integrations/share.ts +283 -0
  134. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  135. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  136. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  137. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  138. package/src/keyboard/components/HandleChipsInput.tsx +147 -0
  139. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  140. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  141. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  142. package/src/keyboard/components/NavModeHint.tsx +31 -0
  143. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  144. package/src/keyboard/components/SlashComposer.tsx +90 -0
  145. package/src/keyboard/key-bindings.test.ts +156 -0
  146. package/src/keyboard/key-bindings.ts +462 -0
  147. package/src/keyboard/keymap.test.ts +611 -0
  148. package/src/keyboard/keymap.ts +446 -0
  149. package/src/keyboard/skills.test.ts +74 -0
  150. package/src/keyboard/skills.ts +77 -0
  151. package/src/keyboard/slash-palette.test.ts +103 -0
  152. package/src/keyboard/slash-palette.ts +137 -0
  153. package/src/keyboard/thread-chords.test.ts +79 -0
  154. package/src/keyboard/thread-chords.ts +168 -0
  155. package/src/keyboard/use-shared-keyboard.ts +56 -0
  156. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  157. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  158. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  159. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  160. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  161. package/src/markdown/components/MarkdownThreadEditor.tsx +165 -0
  162. package/src/markdown/components/ThreadView.selection.test.tsx +954 -0
  163. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  164. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  165. package/src/markdown/components/ThreadView.tsx +577 -0
  166. package/src/markdown/mark-runs.ts +125 -0
  167. package/src/markdown/markdown-editor-syntax-style.ts +66 -0
  168. package/src/markdown/markdown-grid.test.ts +101 -0
  169. package/src/markdown/markdown-grid.ts +209 -0
  170. package/src/markdown/markdown-highlight.test.ts +61 -0
  171. package/src/markdown/markdown-highlight.ts +125 -0
  172. package/src/markdown/thread-code-syntax.ts +48 -0
  173. package/src/markdown/use-thread-body-editing.tsx +54 -0
  174. package/src/markdown/view-plan.regression.test.ts +60 -0
  175. package/src/markdown/view-plan.test.ts +545 -0
  176. package/src/markdown/view-plan.ts +1061 -0
  177. package/src/performance/otlp-trace.test.ts +119 -0
  178. package/src/performance/otlp-trace.ts +134 -0
  179. package/src/performance/perf-report.ts +19 -0
  180. package/src/performance/perf-timings.test.ts +18 -0
  181. package/src/performance/perf-timings.ts +40 -0
  182. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  183. package/src/prototype/components/PrototypeContentView.tsx +459 -0
  184. package/src/prototype/prototype-browser.test.ts +81 -0
  185. package/src/prototype/prototype-browser.ts +332 -0
  186. package/src/prototype/prototype-pixels.tsx +35 -0
  187. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  188. package/src/settings/components/SettingsDialog.tsx +186 -0
  189. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  190. package/src/settings/components/SettingsRows.tsx +112 -0
  191. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  192. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  193. package/src/settings/components/quick-actions-editor.tsx +222 -0
  194. package/src/settings/config-identity.test.ts +78 -0
  195. package/src/settings/config.test.ts +771 -0
  196. package/src/settings/config.ts +722 -0
  197. package/src/settings/use-settings-dialog.test.tsx +172 -0
  198. package/src/settings/use-settings-dialog.tsx +465 -0
  199. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  200. package/src/settings/use-thread-harness-setting.ts +86 -0
  201. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  202. package/src/stories/stories-app.tsx +196 -0
  203. package/src/stories/stories.test.tsx +78 -0
  204. package/src/stories/story-fixtures.ts +102 -0
  205. package/src/stories/story-tree.test.ts +108 -0
  206. package/src/stories/story.ts +160 -0
  207. package/src/terminal/ansi-screen.test.ts +28 -0
  208. package/src/terminal/ansi-screen.ts +77 -0
  209. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  210. package/src/terminal/components/AnsiScreen.tsx +28 -0
  211. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  212. package/src/terminal/components/terminal-pane.ts +208 -0
  213. package/src/terminal/ghostty-terminal.test.ts +85 -0
  214. package/src/terminal/ghostty-terminal.ts +207 -0
  215. package/src/terminal/kitty-image.test.ts +66 -0
  216. package/src/terminal/kitty-image.ts +140 -0
  217. package/src/terminal/pty.test.ts +81 -0
  218. package/src/terminal/pty.ts +255 -0
  219. package/src/testing/test-support.test.ts +266 -0
  220. package/src/testing/test-support.ts +449 -0
  221. package/src/thread/agent-continuation.test.ts +34 -0
  222. package/src/thread/agent-continuation.ts +13 -0
  223. package/src/thread/agent-transcript.test.ts +161 -0
  224. package/src/thread/agent-transcript.ts +344 -0
  225. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  226. package/src/thread/components/AgentConfigControls.test.tsx +153 -0
  227. package/src/thread/components/AgentConfigControls.tsx +121 -0
  228. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  229. package/src/thread/components/AgentThreadPane.test.tsx +1248 -0
  230. package/src/thread/components/AgentThreadPane.tsx +458 -0
  231. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  232. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  233. package/src/thread/components/ThreadFooter.tsx +77 -0
  234. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  235. package/src/thread/components/ThreadTree.test.tsx +41 -0
  236. package/src/thread/components/ThreadTree.tsx +268 -0
  237. package/src/thread/components/thread-tree.test.ts +138 -0
  238. package/src/thread/components/thread-tree.ts +112 -0
  239. package/src/thread/thread-controller.curate.test.ts +504 -0
  240. package/src/thread/thread-controller.derivation.test.ts +62 -0
  241. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  242. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  243. package/src/thread/thread-controller.share.test.ts +618 -0
  244. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  245. package/src/thread/thread-controller.tree.test.ts +263 -0
  246. package/src/thread/thread-controller.ts +2151 -0
  247. package/src/thread/tree-view.test.ts +128 -0
  248. package/src/thread/tree-view.ts +150 -0
  249. package/src/thread/use-thread-agent.test.tsx +209 -0
  250. package/src/thread/use-thread-agent.ts +122 -0
  251. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  252. package/src/ui/components/CodeBlock.tsx +82 -0
  253. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  254. package/src/ui/components/ConfirmCard.tsx +151 -0
  255. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  256. package/src/ui/components/ConfirmDialog.tsx +72 -0
  257. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  258. package/src/ui/components/OverlayScrollbar.test.tsx +142 -0
  259. package/src/ui/components/OverlayScrollbar.tsx +134 -0
  260. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  261. package/src/ui/components/PromptDialog.test.tsx +65 -0
  262. package/src/ui/components/PromptDialog.tsx +101 -0
  263. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  264. package/src/ui/components/RootOverlay.tsx +50 -0
  265. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  266. package/src/ui/components/ScrollArea.tsx +63 -0
  267. package/src/ui/components/Toast.stories.tsx +19 -0
  268. package/src/ui/components/Toast.tsx +54 -0
  269. package/src/ui/components/Tooltip.stories.tsx +26 -0
  270. package/src/ui/components/Tooltip.tsx +93 -0
  271. package/src/ui/components/menu-control.stories.tsx +35 -0
  272. package/src/ui/components/menu-control.tsx +43 -0
  273. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  274. package/src/ui/components/primitives/Button.tsx +58 -0
  275. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  276. package/src/ui/components/primitives/Card.tsx +62 -0
  277. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  278. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  279. package/src/ui/components/primitives/Dialog.tsx +69 -0
  280. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  281. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  282. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  283. package/src/ui/components/primitives/IconButton.tsx +58 -0
  284. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  285. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  286. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Switch.tsx +32 -0
  288. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  289. package/src/ui/components/primitives/Tabs.tsx +102 -0
  290. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  291. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  292. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  293. package/src/ui/components/primitives/Tree.tsx +137 -0
  294. package/src/ui/components/primitives/frame.test.ts +12 -0
  295. package/src/ui/components/primitives/frame.ts +17 -0
  296. package/src/ui/components/primitives/icons.ts +61 -0
  297. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  298. package/src/ui/components/primitives/tree-model.ts +144 -0
  299. package/src/ui/components/syntax-highlight.ts +154 -0
  300. package/src/ui/components/truncate-text.ts +7 -0
  301. package/src/ui/components/truncate-title.test.ts +31 -0
  302. package/src/ui/components/truncate-title.ts +16 -0
  303. package/src/ui/use-frame-measure.ts +88 -0
  304. package/src/ui/use-terminal-virtualizer.ts +286 -0
  305. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  306. package/src/workbench/components/ChangesColumn.tsx +210 -0
  307. package/src/workbench/components/ChangesFileTree.test.tsx +184 -0
  308. package/src/workbench/components/EditorGrid.split.test.tsx +119 -0
  309. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  310. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  311. package/src/workbench/components/EditorGrid.tsx +552 -0
  312. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  313. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  314. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  315. package/src/workbench/components/FileContentsView.tsx +104 -0
  316. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  317. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  318. package/src/workbench/components/GridTabContent.tsx +195 -0
  319. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  320. package/src/workbench/components/ProjectTreeView.test.tsx +239 -0
  321. package/src/workbench/components/ProjectTreeView.tsx +159 -0
  322. package/src/workbench/components/TreePane.stories.tsx +106 -0
  323. package/src/workbench/components/TreePane.tsx +157 -0
  324. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  325. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  326. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  327. package/src/workbench/components/editor-grid.test.ts +184 -0
  328. package/src/workbench/components/editor-grid.ts +284 -0
  329. package/src/workbench/components/file-tree.test.ts +60 -0
  330. package/src/workbench/components/file-tree.ts +114 -0
  331. package/src/workbench/use-changes-workbench.ts +210 -0
  332. package/src/workbench/use-remember-layout.ts +33 -0
  333. package/src/workbench/use-repo-changes.ts +93 -0
  334. package/src/workbench/workbench-snapshot.test.ts +99 -0
  335. package/src/workbench/workbench-snapshot.ts +59 -0
  336. package/src/App.diff.test.tsx +0 -99
  337. package/src/App.export.test.tsx +0 -88
  338. package/src/App.plan-review.test.tsx +0 -259
  339. package/src/App.readonly.test.tsx +0 -148
  340. package/src/App.rendering.test.tsx +0 -108
  341. package/src/App.submit-confirm.test.tsx +0 -183
  342. package/src/App.test.tsx +0 -218
  343. package/src/App.tsx +0 -1249
  344. package/src/config.test.ts +0 -116
  345. package/src/config.ts +0 -150
  346. package/src/editor.ts +0 -34
  347. package/src/herdr.test.ts +0 -68
  348. package/src/herdr.ts +0 -34
  349. package/src/keymap.test.ts +0 -270
  350. package/src/keymap.ts +0 -173
  351. package/src/run.ts +0 -30
  352. package/src/serve.ts +0 -75
  353. package/src/session-controller.ts +0 -398
  354. package/src/syntax.test.ts +0 -41
  355. package/src/syntax.ts +0 -88
  356. package/src/theme.ts +0 -46
  357. package/src/view-diff.test.ts +0 -55
  358. package/src/view-diff.ts +0 -86
  359. package/src/view.test.ts +0 -157
  360. package/src/view.ts +0 -364
@@ -0,0 +1,42 @@
1
+ import { expect, test } from "bun:test";
2
+ import React from "react";
3
+ import { testRender } from "@opentui/react/test-utils";
4
+ import { CommentRow } from "./AnnotationCards";
5
+ import { DARK } from "../../appearance/theme";
6
+ import { TooltipProvider } from "../../ui/components/Tooltip";
7
+ import { locateText, settle } from "../../testing/test-support";
8
+
9
+ test("submitted comment checks stay visible on hover and navigate to the reply", async () => {
10
+ let navigations = 0;
11
+ const setup = await testRender(
12
+ <TooltipProvider>
13
+ <CommentRow
14
+ annotation={{
15
+ id: "submitted",
16
+ kind: "comment",
17
+ body: "Explain the change",
18
+ anchor: { quote: "change", prefix: "", suffix: "", blockIndex: 0, start: 0, end: 6 },
19
+ createdAt: "2026-10-04",
20
+ }}
21
+ tokens={DARK}
22
+ action={{ label: "View reply", run: () => navigations++ }}
23
+ />
24
+ </TooltipProvider>,
25
+ { width: 60, height: 8 },
26
+ );
27
+
28
+ try {
29
+ await settle(setup);
30
+ const checks = locateText(setup, "✓✓");
31
+
32
+ await setup.mockMouse.moveTo(checks.column, checks.row);
33
+ await settle(setup);
34
+ expect(setup.captureCharFrame()).toContain("✓✓");
35
+ expect(setup.captureCharFrame()).toContain("View reply");
36
+ expect(setup.captureCharFrame()).not.toContain("↓");
37
+ await setup.mockMouse.click(checks.column, checks.row);
38
+ expect(navigations).toBe(1);
39
+ } finally {
40
+ setup.renderer.destroy();
41
+ }
42
+ });
@@ -0,0 +1,383 @@
1
+ /**
2
+ * The inline comment pieces every annotated surface renders: the composer (a
3
+ * textarea in a card row), the "/" actions-and-skills palette below it, the
4
+ * edge-segmented discussion card, and one comment row. The plan thread view
5
+ * and the diff sheet both draw their comments with these, so a discussion looks
6
+ * and behaves the same on prose and on code.
7
+ */
8
+
9
+ import React, { useContext, useLayoutEffect, useRef, useState } from "react";
10
+ import { usePaste, useRenderer } from "@opentui/react";
11
+ import type {
12
+ BoxRenderable,
13
+ KeyBinding,
14
+ MouseEvent as TerminalMouseEvent,
15
+ TextareaRenderable,
16
+ } from "@opentui/core";
17
+ import type { Annotation } from "@cueloop/schema";
18
+ import type { Theme } from "../../appearance/theme";
19
+ import { useTooltip } from "../../ui/components/Tooltip";
20
+ import { lighten } from "../annotation-palette";
21
+ import { useFrameMeasure } from "../../ui/use-frame-measure";
22
+ import { imagePlaceholder, looksLikeBinaryPaste } from "../../integrations/pasted-image";
23
+ import { skillReferenceRanges, type SlashItem } from "../../keyboard/slash-palette";
24
+ import { PaletteNamesContext } from "../../keyboard/skills";
25
+ import { referenceStyleFor } from "../../ui/components/syntax-highlight";
26
+
27
+ /* -------------------------------------------------------------- composer */
28
+
29
+ // Option+Enter saves a comment; Ctrl+Enter may additionally invoke the agent.
30
+ // Plain Enter breaks the line. Command+Enter belongs to the terminal.
31
+ const COMPOSE_KEY_BINDINGS: KeyBinding[] = [
32
+ { name: "return", super: true, action: "submit" },
33
+ { name: "return", meta: true, action: "submit" },
34
+ { name: "return", ctrl: true, action: "submit" },
35
+ { name: "return", action: "newline" },
36
+ { name: "return", shift: true, action: "newline" },
37
+ ];
38
+
39
+ export function composeRowCount(text: string, contentWidth: number): number {
40
+ const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
41
+ let visualRowCount = 0;
42
+
43
+ for (const line of text.split("\n")) {
44
+ visualRowCount += Math.max(1, Math.ceil(line.length / usableWidth));
45
+ }
46
+
47
+ // no cap: the card is the discussion, and folding is its containment
48
+ return Math.max(1, visualRowCount);
49
+ }
50
+
51
+ export function Composer({
52
+ seed,
53
+ glyph,
54
+ tokens,
55
+ onSave,
56
+ onReady,
57
+ onInput,
58
+ placeholder,
59
+ agentEnabled = false,
60
+ focusRequest,
61
+ }: {
62
+ seed: string;
63
+ glyph: string | null;
64
+ tokens: Theme;
65
+ onSave: (body: string) => void;
66
+ onReady: (readText: () => string) => void;
67
+ onInput: (text: string, caret: number) => void;
68
+ placeholder?: string;
69
+ agentEnabled?: boolean;
70
+ focusRequest?: number;
71
+ }): React.ReactNode {
72
+ const renderer = useRenderer();
73
+ const editorRef = useRef<TextareaRenderable | null>(null);
74
+ const pastedImageCount = useRef(0);
75
+ const [rows, setRows] = useState(1);
76
+ // action and skill names whose "/name" references paint in the accent color
77
+ const referenceNames = useContext(PaletteNamesContext);
78
+
79
+ usePaste((event) => {
80
+ const editor = editorRef.current;
81
+
82
+ if (!editor || !editor.focused || !looksLikeBinaryPaste(event.bytes)) return;
83
+
84
+ event.preventDefault();
85
+ pastedImageCount.current += 1;
86
+ editor.editBuffer.insertText(imagePlaceholder(pastedImageCount.current));
87
+ });
88
+
89
+ const paintReferences = (editor: TextareaRenderable): void => {
90
+ const { styleId } = referenceStyleFor(tokens, renderer.nativeScene);
91
+
92
+ editor.editBuffer.clearAllHighlights();
93
+ for (const range of skillReferenceRanges(editor.plainText, referenceNames)) {
94
+ editor.editBuffer.addHighlightByCharRange({ ...range, styleId });
95
+ }
96
+ };
97
+
98
+ // synchronously at commit, before the next tty key is read: signalling readiness in a
99
+ // post-paint effect lets a fast burst race the mount, splitting input between the seed
100
+ // buffer and this textarea out of order (#365)
101
+ useLayoutEffect(() => {
102
+ const editor = editorRef.current;
103
+
104
+ if (!editor) return;
105
+
106
+ editor.cursorOffset = seed.length;
107
+ editor.editBuffer.setSyntaxStyle(referenceStyleFor(tokens, renderer.nativeScene).style);
108
+ paintReferences(editor);
109
+ setRows(composeRowCount(editor.plainText, editor.width));
110
+ onReady(() => editorRef.current?.plainText ?? "");
111
+ // eslint-disable-next-line react-hooks/exhaustive-deps
112
+ }, []);
113
+ useLayoutEffect(() => {
114
+ editorRef.current?.focus();
115
+ }, [focusRequest]);
116
+
117
+ return (
118
+ <box style={{ flexDirection: "row" }}>
119
+ {glyph !== null ? (
120
+ <text selectable={false} fg={tokens.text} style={{ flexShrink: 0 }}>{`${glyph} `}</text>
121
+ ) : null}
122
+ <textarea
123
+ ref={editorRef}
124
+ focused
125
+ initialValue={seed}
126
+ placeholder={placeholder}
127
+ placeholderColor={tokens.textDim}
128
+ cursorStyle={{ style: "block", blinking: true }}
129
+ keyBindings={
130
+ agentEnabled
131
+ ? COMPOSE_KEY_BINDINGS.filter((binding) => !binding.super)
132
+ : COMPOSE_KEY_BINDINGS
133
+ }
134
+ onSubmit={() => onSave(editorRef.current?.plainText ?? "")}
135
+ onContentChange={() => {
136
+ const editor = editorRef.current;
137
+
138
+ if (!editor) return;
139
+
140
+ paintReferences(editor);
141
+ setRows(composeRowCount(editor.plainText, editor.width));
142
+ onInput(editor.plainText, editor.cursorOffset);
143
+ }}
144
+ style={{
145
+ height: rows,
146
+ flexGrow: 1,
147
+ backgroundColor: "transparent",
148
+ focusedBackgroundColor: "transparent",
149
+ textColor: tokens.text,
150
+ focusedTextColor: tokens.text,
151
+ }}
152
+ />
153
+ </box>
154
+ );
155
+ }
156
+
157
+ /* ---------------------------------------------------------- slash palette */
158
+
159
+ function SlashList({
160
+ items,
161
+ selected,
162
+ tokens,
163
+ }: {
164
+ items: SlashItem[];
165
+ selected: number;
166
+ tokens: Theme;
167
+ }): React.ReactNode {
168
+ const WINDOW = 5;
169
+ const start = Math.max(0, Math.min(selected - 2, items.length - WINDOW));
170
+ const visible = items.slice(start, start + WINDOW);
171
+
172
+ if (items.length === 0) return <text fg={tokens.textDim}>no matching actions</text>;
173
+
174
+ const nameColumn = Math.min(36, Math.max(16, ...visible.map((item) => item.name.length + 5)));
175
+
176
+ return (
177
+ <box style={{ flexDirection: "column" }}>
178
+ {visible.map((item, offset) => {
179
+ const index = start + offset;
180
+ const isSelected = index === selected;
181
+ const name = `${isSelected ? "→ " : " "}/${item.name}`.padEnd(nameColumn);
182
+
183
+ return (
184
+ <text key={item.name} style={{ flexShrink: 0, wrapMode: "none" }}>
185
+ <span fg={isSelected ? tokens.accent : tokens.textMuted}>{name}</span>
186
+ <span fg={tokens.textDim}>{item.description}</span>
187
+ </text>
188
+ );
189
+ })}
190
+ {items.length > WINDOW ? (
191
+ <text fg={tokens.textDim}>{`(${selected + 1}/${items.length})`}</text>
192
+ ) : null}
193
+ </box>
194
+ );
195
+ }
196
+
197
+ /** Below the composer: the actions/skills list while the caret is on a "/word", else nothing. */
198
+ export function ComposerPalette({
199
+ slashActive,
200
+ slashItems,
201
+ slashIndex,
202
+ tokens,
203
+ }: {
204
+ slashActive: boolean;
205
+ slashItems: SlashItem[];
206
+ slashIndex: number;
207
+ tokens: Theme;
208
+ }): React.ReactNode {
209
+ if (!slashActive) return null;
210
+
211
+ return (
212
+ <box style={{ flexDirection: "column", marginLeft: 3 }}>
213
+ <SlashList
214
+ items={slashItems}
215
+ selected={Math.min(slashIndex, Math.max(0, slashItems.length - 1))}
216
+ tokens={tokens}
217
+ />
218
+ </box>
219
+ );
220
+ }
221
+
222
+ /* ------------------------------------------------------------ discussion card */
223
+
224
+ /** One card row group with its own accent-edge color, one per voice. */
225
+ export interface EdgeSegment {
226
+ color: string;
227
+ node: React.ReactNode;
228
+ }
229
+
230
+ /**
231
+ * The comment-card frame. The accent edge is drawn by hand so that (a) its
232
+ * corner cells are half-height glyphs ("╷"/"╵") terminating flush with the
233
+ * hairline rows, and (b) each voice's rows carry their own edge color with
234
+ * zero gap. Focus lightens every segment; the hairlines never change.
235
+ */
236
+ export function DiscussionCard({
237
+ segments,
238
+ tokens,
239
+ focused = false,
240
+ onFocus,
241
+ }: {
242
+ segments: EdgeSegment[];
243
+ tokens: Theme;
244
+ focused?: boolean;
245
+ onFocus?: () => void;
246
+ }): React.ReactNode {
247
+ const segmentRefs = useRef<Array<BoxRenderable | null>>([]);
248
+ const noHeights: number[] = [];
249
+ const heights = useFrameMeasure(
250
+ () => segmentRefs.current.map((renderable) => renderable?.height ?? 0),
251
+ (left, right) => left.join() === right.join(),
252
+ noHeights,
253
+ );
254
+
255
+ const edgeColor = (color: string): string => (focused ? lighten(color) : color);
256
+ const glyphs = focused
257
+ ? { top: "╻", middle: "┃", bottom: "╹" }
258
+ : { top: "╷", middle: "│", bottom: "╵" };
259
+ const edgeRows: Array<{ glyph: string; color: string }> = [
260
+ {
261
+ glyph: glyphs.top,
262
+ color: edgeColor(segments[0]?.color ?? tokens.border),
263
+ },
264
+ ];
265
+
266
+ for (let index = 0; index < segments.length; index++) {
267
+ const segmentRows = Math.max(1, heights[index] ?? 1);
268
+
269
+ for (let row = 0; row < segmentRows; row++) {
270
+ edgeRows.push({
271
+ glyph: glyphs.middle,
272
+ color: edgeColor(segments[index]!.color),
273
+ });
274
+ }
275
+ }
276
+ edgeRows.push({
277
+ glyph: glyphs.bottom,
278
+ color: edgeColor(segments[segments.length - 1]?.color ?? tokens.border),
279
+ });
280
+
281
+ return (
282
+ <box style={{ flexDirection: "row", marginTop: 0, marginLeft: 2 }}>
283
+ <text selectable={false} style={{ flexShrink: 0, width: 1 }}>
284
+ {edgeRows.map((row, index) => (
285
+ <span key={index} fg={row.color}>
286
+ {index < edgeRows.length - 1 ? `${row.glyph}\n` : row.glyph}
287
+ </span>
288
+ ))}
289
+ </text>
290
+ <box
291
+ onMouseDown={onFocus}
292
+ style={{
293
+ flexGrow: 1,
294
+ flexDirection: "column",
295
+ border: ["top", "right", "bottom"],
296
+ borderStyle: "single",
297
+ borderColor: tokens.border,
298
+ backgroundColor: "transparent",
299
+ paddingLeft: 1,
300
+ paddingRight: 1,
301
+ }}
302
+ >
303
+ {segments.map((segment, index) => (
304
+ <box
305
+ key={index}
306
+ ref={(renderable: BoxRenderable | null) => {
307
+ segmentRefs.current[index] = renderable;
308
+ }}
309
+ style={{ flexDirection: "column" }}
310
+ >
311
+ {segment.node}
312
+ </box>
313
+ ))}
314
+ </box>
315
+ </box>
316
+ );
317
+ }
318
+
319
+ export function CommentRow({
320
+ annotation,
321
+ tokens,
322
+ authorLabel,
323
+ action,
324
+ onFocus,
325
+ }: {
326
+ annotation: Annotation;
327
+ tokens: Theme;
328
+ /** The author's resolved display name, shown as a tooltip when the dot is hovered. */
329
+ authorLabel?: string;
330
+ action?: { label: string; run: () => void };
331
+ onFocus?: () => void;
332
+ }): React.ReactNode {
333
+ // own comments (no author) wear the filled dot, collaborators the outline
334
+ const own = annotation.author === undefined;
335
+ const glyph = own ? "●" : "○";
336
+ const glyphColor = annotation.orphan ? tokens.warning : own ? tokens.text : tokens.textMuted;
337
+ const { showTooltip, hideTooltip } = useTooltip();
338
+ const dotHover = authorLabel
339
+ ? {
340
+ onMouseOver: (event: TerminalMouseEvent) => showTooltip(authorLabel, event.x, event.y),
341
+ onMouseOut: hideTooltip,
342
+ }
343
+ : {};
344
+
345
+ return (
346
+ <box
347
+ style={{ flexDirection: "column" }}
348
+ onMouseDown={(event) => {
349
+ if (!onFocus) return;
350
+
351
+ event.stopPropagation();
352
+ onFocus();
353
+ }}
354
+ >
355
+ {annotation.body.split("\n").map((line, lineIndex) => (
356
+ <box key={lineIndex} style={{ flexDirection: "row" }}>
357
+ <box style={{ flexShrink: 0 }} {...(lineIndex === 0 ? dotHover : {})}>
358
+ <text selectable={false} fg={glyphColor}>
359
+ {lineIndex === 0 ? `${glyph} ` : " "}
360
+ </text>
361
+ </box>
362
+ <text fg={tokens.text} style={{ wrapMode: "word", flexGrow: 1, flexShrink: 1 }}>
363
+ {line}
364
+ </text>
365
+ {lineIndex === 0 && action ? (
366
+ <text
367
+ fg={tokens.blue}
368
+ onMouseUp={action.run}
369
+ onMouseOver={(event: TerminalMouseEvent) => {
370
+ showTooltip(action.label, event.x, event.y);
371
+ }}
372
+ onMouseOut={() => {
373
+ hideTooltip();
374
+ }}
375
+ >
376
+ {action.label === "Retry" ? "Retry" : "✓✓"}
377
+ </text>
378
+ ) : null}
379
+ </box>
380
+ ))}
381
+ </box>
382
+ );
383
+ }
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ import type { Annotation } from "@cueloop/schema";
3
+ import { DARK } from "../../appearance/theme";
4
+ import type { Discussion } from "../discussions";
5
+ import type { TextSpan } from "../thread-selection";
6
+ import { RootOverlayProvider } from "../../ui/components/RootOverlay";
7
+ import { DiscussionMarkerRail } from "./DiscussionMarkerRail";
8
+ import type { Story, StoryMeta } from "../../stories/story";
9
+
10
+ export const meta: StoryMeta = { title: "Chrome/DiscussionMarkerRail" };
11
+
12
+ function comment(id: string, body: string): Annotation {
13
+ return {
14
+ id,
15
+ kind: "comment",
16
+ anchor: { quote: body, prefix: "", suffix: "" },
17
+ body,
18
+ createdAt: "2026-09-08T00:00:00.000Z",
19
+ };
20
+ }
21
+
22
+ function discussion(row: number, body: string): Discussion {
23
+ const span: TextSpan = { start: { blockIndex: row, char: 0 }, end: { blockIndex: row, char: 4 } };
24
+
25
+ return {
26
+ key: `${row}`,
27
+ rootId: `a${row}`,
28
+ blockIndex: row,
29
+ span,
30
+ annotations: [comment(`a${row}`, body)],
31
+ };
32
+ }
33
+
34
+ const discussions = [
35
+ discussion(2, "Map needs an eviction story."),
36
+ discussion(5, "why a Map here?"),
37
+ discussion(9, "name this constant"),
38
+ ];
39
+
40
+ // The centered dot column: one dot per discussion, drawn on a surface's right edge.
41
+ export const Dots: Story = {
42
+ render: () => (
43
+ <RootOverlayProvider>
44
+ <box
45
+ style={{ width: 24, height: 10, flexDirection: "row", backgroundColor: DARK.background }}
46
+ >
47
+ <box style={{ flexGrow: 1 }} />
48
+ <DiscussionMarkerRail discussions={discussions} spanQuote={() => "x"} onJump={() => {}} />
49
+ </box>
50
+ </RootOverlayProvider>
51
+ ),
52
+ expectedColors: [DARK.textDim],
53
+ size: { width: 24, height: 10 },
54
+ };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The right-edge discussion navigator: a vertically-centered column of dots, one
3
+ * per discussion, that highlights on hover, shows a floating preview, and jumps
4
+ * on click. The plan thread view and each diff editor group render one on their
5
+ * right edge, so comment navigation reads the same on prose and on code. The
6
+ * preview draws at the app root (OpenTUI has no z-index) so no pane border slices it.
7
+ */
8
+
9
+ import React, { useEffect, useRef, useState, type RefObject } from "react";
10
+ import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
11
+ import type { Theme } from "../../appearance/theme";
12
+ import type { TextSpan } from "../thread-selection";
13
+ import type { Discussion } from "../discussions";
14
+ import { useFrameMeasure } from "../../ui/use-frame-measure";
15
+ import { useComponentTheme } from "../../appearance/components/theme-context";
16
+ import { useRootOverlay } from "../../ui/components/RootOverlay";
17
+ import { OverlayScrollbar } from "../../ui/components/OverlayScrollbar";
18
+
19
+ interface HoveredMarker {
20
+ key: string;
21
+ screenY: number;
22
+ anchorX: number;
23
+ }
24
+
25
+ function ScrollMarkers({
26
+ discussions,
27
+ hovered,
28
+ focused,
29
+ tokens,
30
+ onHover,
31
+ onJump,
32
+ }: {
33
+ discussions: Discussion[];
34
+ hovered: string | null;
35
+ focused: string | null;
36
+ tokens: Theme;
37
+ onHover: (marker: HoveredMarker | null) => void;
38
+ onJump: (key: string) => void;
39
+ }): React.ReactNode {
40
+ const railRef = useRef<BoxRenderable | null>(null);
41
+ const height = useFrameMeasure(
42
+ () => railRef.current?.height ?? 0,
43
+ (left, right) => left === right,
44
+ 0,
45
+ );
46
+
47
+ const firstRow = Math.max(0, Math.floor((height - discussions.length) / 2));
48
+ const rowFor = (index: number): number => firstRow + index;
49
+ const indexAt = (row: number): number | null => {
50
+ const index = row - firstRow;
51
+
52
+ return index >= 0 && index < discussions.length ? index : null;
53
+ };
54
+ const hoveredIndex = discussions.findIndex((discussion) => discussion.key === hovered);
55
+ const focusedIndex = discussions.findIndex((discussion) => discussion.key === focused);
56
+ const isActive = (index: number): boolean => index === hoveredIndex || index === focusedIndex;
57
+ const colorFor = (index: number): string => {
58
+ if (isActive(index)) return tokens.text;
59
+
60
+ if (hoveredIndex >= 0 && discussions.length >= 5 && Math.abs(index - hoveredIndex) === 1) {
61
+ return tokens.textMuted;
62
+ }
63
+
64
+ return tokens.textDim;
65
+ };
66
+
67
+ return (
68
+ <box
69
+ ref={railRef}
70
+ style={{ width: 3, flexShrink: 0, flexDirection: "column" }}
71
+ onMouseMove={(event) => {
72
+ const railY = railRef.current?.y ?? 0;
73
+ const index = indexAt(event.y - railY);
74
+
75
+ onHover(
76
+ index === null
77
+ ? null
78
+ : {
79
+ key: discussions[index]!.key,
80
+ // screen coordinates so the preview can render at the app root, above every pane rule
81
+ screenY: railY + rowFor(index),
82
+ anchorX: railRef.current?.x ?? 0,
83
+ },
84
+ );
85
+ }}
86
+ onMouseOut={() => onHover(null)}
87
+ onMouseDown={(event) => {
88
+ const index = indexAt(event.y - (railRef.current?.y ?? 0));
89
+
90
+ if (index !== null) onJump(discussions[index]!.key);
91
+ }}
92
+ >
93
+ {discussions.map((discussion, index) => (
94
+ <text
95
+ key={discussion.key}
96
+ selectable={false}
97
+ style={{ position: "absolute", top: rowFor(index), left: 0 }}
98
+ fg={colorFor(index)}
99
+ >
100
+ {(isActive(index) ? "●" : "○").padStart(2)}
101
+ </text>
102
+ ))}
103
+ </box>
104
+ );
105
+ }
106
+
107
+ export interface DiscussionMarkerRailProps {
108
+ discussions: Discussion[];
109
+ spanQuote: (span: TextSpan) => string;
110
+ focusedKey?: string | null;
111
+ onJump: (key: string) => void;
112
+ scrollbox?: RefObject<ScrollBoxRenderable | null>;
113
+ theme?: Theme;
114
+ }
115
+
116
+ export function DiscussionMarkerRail({
117
+ discussions,
118
+ spanQuote,
119
+ focusedKey = null,
120
+ onJump,
121
+ scrollbox,
122
+ theme,
123
+ }: DiscussionMarkerRailProps): React.ReactNode {
124
+ const tokens = useComponentTheme(theme);
125
+ const [hovered, setHovered] = useState<HoveredMarker | null>(null);
126
+ const { setOverlay, clearOverlay } = useRootOverlay();
127
+
128
+ const preview = (): React.ReactNode => {
129
+ if (hovered === null) return null;
130
+
131
+ const discussion = discussions.find((candidate) => candidate.key === hovered.key);
132
+
133
+ if (!discussion) return null;
134
+
135
+ const quote = spanQuote(discussion.span);
136
+ const lastComment = discussion.annotations.at(-1)!;
137
+
138
+ return (
139
+ <box
140
+ style={{
141
+ position: "absolute",
142
+ top: Math.max(0, hovered.screenY - 1),
143
+ left: Math.max(0, hovered.anchorX - 48),
144
+ width: 48,
145
+ flexDirection: "column",
146
+ border: true,
147
+ borderStyle: "single",
148
+ borderColor: tokens.border,
149
+ backgroundColor: tokens.elevated,
150
+ paddingLeft: 1,
151
+ paddingRight: 1,
152
+ }}
153
+ >
154
+ <text fg={tokens.textDim}>{`"${quote.slice(0, 42)}"`}</text>
155
+ <text fg={tokens.text}>
156
+ {`${lastComment.author === undefined ? "●" : "○"} ${lastComment.body}`.slice(0, 44)}
157
+ </text>
158
+ <text fg={tokens.textDim}>
159
+ {`${discussion.annotations.length} comment${
160
+ discussion.annotations.length === 1 ? "" : "s"
161
+ } · click to jump`}
162
+ </text>
163
+ </box>
164
+ );
165
+ };
166
+
167
+ // render the preview at the app root (above every pane rule); OpenTUI has no z-index, so an
168
+ // absolute box inside this pane would be sliced by the next pane's border
169
+ useEffect(() => {
170
+ setOverlay("discussion-preview", preview());
171
+
172
+ return () => clearOverlay("discussion-preview");
173
+ // eslint-disable-next-line react-hooks/exhaustive-deps
174
+ }, [hovered, discussions, tokens]);
175
+
176
+ return (
177
+ <>
178
+ <ScrollMarkers
179
+ discussions={discussions}
180
+ hovered={hovered?.key ?? null}
181
+ focused={focusedKey}
182
+ tokens={tokens}
183
+ onHover={setHovered}
184
+ onJump={onJump}
185
+ />
186
+ {scrollbox ? <OverlayScrollbar scrollbox={scrollbox} theme={theme} /> : null}
187
+ </>
188
+ );
189
+ }