@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,82 @@
1
+ /**
2
+ * Fenced code block: verbatim lines in a contained elevated box, highlighted
3
+ * by the native code renderable (tree-sitter). Unstyled text draws first, so
4
+ * the content never flashes empty while highlights resolve; unknown languages
5
+ * stay unstyled.
6
+ */
7
+
8
+ import React from "react";
9
+ import { createTextAttributes } from "@opentui/core";
10
+ import { useRenderer } from "@opentui/react";
11
+ import type { Theme } from "../../appearance/theme";
12
+ import { useComponentTheme } from "../../appearance/components/theme-context";
13
+ import { filetypeFor, syntaxStyleFor } from "./syntax-highlight";
14
+
15
+ /** A cut code block reads as removed: struck through and grayed, never red. */
16
+ const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
17
+
18
+ export interface CodeBlockProps {
19
+ id?: string;
20
+ language?: string;
21
+ content: string;
22
+ isCursor?: boolean;
23
+ marginTop?: number;
24
+ isAnnotated?: boolean;
25
+ changeTag?: "new" | "edited";
26
+ cut?: boolean;
27
+ theme?: Theme;
28
+ }
29
+
30
+ export function CodeBlock({
31
+ id,
32
+ language,
33
+ content,
34
+ isCursor = false,
35
+ marginTop = 0,
36
+ isAnnotated = false,
37
+ changeTag,
38
+ cut = false,
39
+ theme,
40
+ }: CodeBlockProps): React.ReactNode {
41
+ const tokens = useComponentTheme(theme);
42
+ const renderer = useRenderer();
43
+ const tagColor = changeTag === "new" ? tokens.green : tokens.accent;
44
+
45
+ return (
46
+ <box id={id} style={{ flexDirection: "column", marginTop }}>
47
+ <text>
48
+ <span fg={isCursor ? tokens.accent : tokens.textDim}>{isCursor ? "▎ " : " "}</span>
49
+ <span fg={tokens.textDim}>{language ?? "code"}</span>
50
+ {isAnnotated ? <span fg={tokens.accent}> ◆</span> : null}
51
+ {changeTag ? <span fg={tagColor}> [{changeTag}]</span> : null}
52
+ </text>
53
+ <box
54
+ style={{
55
+ flexDirection: "column",
56
+ backgroundColor: tokens.elevated,
57
+ paddingLeft: 2,
58
+ paddingRight: 2,
59
+ paddingTop: 1,
60
+ paddingBottom: 1,
61
+ marginLeft: 2,
62
+ }}
63
+ >
64
+ {cut ? (
65
+ <text style={{ wrapMode: "none" }} selectable={false}>
66
+ <span fg={tokens.textDim} attributes={CUT_ATTRIBUTES}>
67
+ {content}
68
+ </span>
69
+ </text>
70
+ ) : (
71
+ <code
72
+ content={content}
73
+ filetype={filetypeFor(language)}
74
+ syntaxStyle={syntaxStyleFor(tokens, renderer.nativeScene)}
75
+ selectable={false}
76
+ style={{ wrapMode: "none", fg: tokens.textMuted, bg: tokens.elevated }}
77
+ />
78
+ )}
79
+ </box>
80
+ </box>
81
+ );
82
+ }
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import { DEFAULT_QUICK_ACTIONS } from "../../settings/config";
4
+ import type { Story, StoryMeta } from "../../stories/story";
5
+ import { ConfirmCard } from "./ConfirmCard";
6
+
7
+ export const meta: StoryMeta = { title: "Cards/ConfirmCard" };
8
+
9
+ const callbacks = {
10
+ onInput: () => {},
11
+ onSelectMessage: () => {},
12
+ onSubmit: () => {},
13
+ onCancel: () => {},
14
+ quickActions: DEFAULT_QUICK_ACTIONS,
15
+ };
16
+
17
+ export const ApproveDefault: Story = {
18
+ render: () => <ConfirmCard message="approved" summary="" {...callbacks} />,
19
+ expectedColors: [DARK.green, DARK.accent],
20
+ size: { width: 40, height: 14 },
21
+ };
22
+
23
+ export const RequestChanges: Story = {
24
+ render: () => (
25
+ <ConfirmCard message="changes_requested" summary="Tighten the steps." {...callbacks} />
26
+ ),
27
+ expectedColors: [DARK.red],
28
+ size: { width: 40, height: 14 },
29
+ };
30
+
31
+ export const WithViewedSummary: Story = {
32
+ render: () => (
33
+ <ConfirmCard message="approved" summary="" viewedSummary="2/3 files viewed" {...callbacks} />
34
+ ),
35
+ expectedColors: [DARK.green, DARK.textDim],
36
+ size: { width: 40, height: 14 },
37
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The Submit button expanded into a bordered confirm card at the rail bottom:
3
+ * the message selector (arrow keys or click), the optional summary, and plain
4
+ * word-buttons - key hints live in the status line only. The bordered height
5
+ * derives from the content rows through Card, so layout and render never drift.
6
+ */
7
+
8
+ import React, { useContext } from "react";
9
+ import { useTerminalDimensions } from "@opentui/react";
10
+ import { MESSAGE_OUTCOMES, type MessageOutcome } from "@cueloop/schema";
11
+ import type { Theme } from "../../appearance/theme";
12
+ import type { QuickAction } from "../../settings/config";
13
+ import { SlashSkillsContext } from "../../keyboard/skills";
14
+ import {
15
+ activeSlashToken,
16
+ mergeSlashItems,
17
+ slashFilter,
18
+ slashItemsFrom,
19
+ } from "../../keyboard/slash-palette";
20
+ import { useComponentTheme } from "../../appearance/components/theme-context";
21
+ import { Card } from "./primitives/Card";
22
+ import { composeRowCount } from "../../annotations/components/AnnotationCards";
23
+ import { SlashComposer } from "../../keyboard/components/SlashComposer";
24
+ import { DialogActions } from "./primitives/DialogActions";
25
+
26
+ const SUBMIT_CARD_MAX_WIDTH = 52;
27
+ const PALETTE_WINDOW = 5;
28
+
29
+ /** Selector words in the confirm card - one word per message. */
30
+ export const MESSAGE_OUTCOME_LABEL: Record<MessageOutcome, string> = {
31
+ comment: "comment",
32
+ approved: "approve",
33
+ changes_requested: "changes",
34
+ };
35
+
36
+ export interface ConfirmCardProps {
37
+ message: MessageOutcome;
38
+ summary: string;
39
+ viewedSummary?: string;
40
+ onInput: (summary: string) => void;
41
+ onSelectMessage: (message: MessageOutcome) => void;
42
+ onSubmit: () => void;
43
+ onCancel: () => void;
44
+ quickActions: QuickAction[];
45
+ theme?: Theme;
46
+ }
47
+
48
+ /** 1-row message selector, spacer, summary input, spacer, buttons. */
49
+ const CONFIRM_CONTENT_ROWS = 5;
50
+
51
+ export function outcomeColor(message: MessageOutcome, tokens: Theme): string {
52
+ return message === "approved"
53
+ ? tokens.green
54
+ : message === "changes_requested"
55
+ ? tokens.red
56
+ : tokens.blue;
57
+ }
58
+
59
+ /**
60
+ * The message selector: one row of pressable words in reading order.
61
+ * Selection stays controlled by the grammar (←/→ cycle the message); a click
62
+ * selects directly. Every message keeps its brackets so active color never changes the row's
63
+ * width or moves a neighboring choice.
64
+ */
65
+ function MessageSelector({
66
+ message,
67
+ onSelectMessage,
68
+ theme,
69
+ }: {
70
+ message: MessageOutcome;
71
+ onSelectMessage: (message: MessageOutcome) => void;
72
+ theme?: Theme;
73
+ }): React.ReactNode {
74
+ const tokens = useComponentTheme(theme);
75
+
76
+ return (
77
+ <box style={{ flexDirection: "row", height: 1, width: "100%", justifyContent: "center" }}>
78
+ {MESSAGE_OUTCOMES.map((candidate) => (
79
+ <box
80
+ key={candidate}
81
+ style={{ paddingRight: 1 }}
82
+ onMouseUp={() => onSelectMessage(candidate)}
83
+ >
84
+ <text fg={candidate === message ? outcomeColor(candidate, tokens) : tokens.textDim}>
85
+ {`[${MESSAGE_OUTCOME_LABEL[candidate]}]`}
86
+ </text>
87
+ </box>
88
+ ))}
89
+ </box>
90
+ );
91
+ }
92
+
93
+ export function ConfirmCard({
94
+ message,
95
+ summary,
96
+ viewedSummary,
97
+ onInput,
98
+ onSelectMessage,
99
+ onSubmit,
100
+ onCancel,
101
+ quickActions,
102
+ theme,
103
+ }: ConfirmCardProps): React.ReactNode {
104
+ const tokens = useComponentTheme(theme);
105
+ const skills = useContext(SlashSkillsContext);
106
+ const { width: terminalWidth } = useTerminalDimensions();
107
+ const cardWidth = Math.max(24, Math.min(terminalWidth - 2, SUBMIT_CARD_MAX_WIDTH));
108
+
109
+ const composerRows = composeRowCount(summary, cardWidth - 4);
110
+ const token = activeSlashToken(summary, summary.length);
111
+ const paletteItems =
112
+ token !== null
113
+ ? slashFilter(mergeSlashItems(slashItemsFrom(quickActions), skills), token.slice(1))
114
+ : [];
115
+ const paletteRows =
116
+ paletteItems.length > 0
117
+ ? Math.min(PALETTE_WINDOW, paletteItems.length) +
118
+ (paletteItems.length > PALETTE_WINDOW ? 1 : 0)
119
+ : 0;
120
+ const contentRows =
121
+ CONFIRM_CONTENT_ROWS - 1 + composerRows + paletteRows + (viewedSummary !== undefined ? 2 : 0);
122
+
123
+ return (
124
+ <Card
125
+ contentRows={contentRows}
126
+ width={cardWidth}
127
+ borderColor={tokens.accent}
128
+ backgroundColor={tokens.elevated}
129
+ theme={theme}
130
+ >
131
+ {viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
132
+ {viewedSummary !== undefined ? <box style={{ height: 1 }} /> : null}
133
+ <MessageSelector message={message} onSelectMessage={onSelectMessage} theme={theme} />
134
+ <box style={{ height: 1 }} />
135
+ <SlashComposer
136
+ seed={summary}
137
+ glyph=""
138
+ quickActions={quickActions}
139
+ tokens={tokens}
140
+ placeholder="summary for the agent (optional)"
141
+ onSubmit={(text) => {
142
+ onInput(text);
143
+ onSubmit();
144
+ }}
145
+ onInput={onInput}
146
+ />
147
+ <box style={{ height: 1 }} />
148
+ <DialogActions confirmLabel="send" onConfirm={onSubmit} onCancel={onCancel} theme={theme} />
149
+ </Card>
150
+ );
151
+ }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { ConfirmDialog } from "./ConfirmDialog";
5
+
6
+ export const meta: StoryMeta = { title: "Overlays/ConfirmDialog" };
7
+
8
+ const callbacks = { onConfirm: () => {}, onCancel: () => {} };
9
+
10
+ export const DeleteThread: Story = {
11
+ render: () => (
12
+ <ConfirmDialog
13
+ isOpen
14
+ title=" delete thread "
15
+ message={'Delete "Auth rollout"? This removes the thread.'}
16
+ {...callbacks}
17
+ />
18
+ ),
19
+ expectedColors: [DARK.accent],
20
+ size: { width: 64, height: 16 },
21
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * A centered yes/no confirmation on the shared Dialog overlay. Presentational:
3
+ * the key grammar (⏎ confirm, esc cancel) is driven by the reducer; the buttons
4
+ * carry the same intents for the mouse. Used for destructive actions like the
5
+ * inbox delete.
6
+ */
7
+
8
+ import React from "react";
9
+ import { useTerminalDimensions } from "@opentui/react";
10
+ import type { Theme } from "../../appearance/theme";
11
+ import { useComponentTheme } from "../../appearance/components/theme-context";
12
+ import { Dialog } from "./primitives/Dialog";
13
+ import { DialogActions } from "./primitives/DialogActions";
14
+
15
+ export interface ConfirmDialogProps {
16
+ isOpen: boolean;
17
+ title?: string;
18
+ message: string;
19
+ confirmLabel?: string;
20
+ cancelLabel?: string;
21
+ onConfirm: () => void;
22
+ onCancel: () => void;
23
+ theme?: Theme;
24
+ }
25
+
26
+ export function ConfirmDialog({
27
+ isOpen,
28
+ title,
29
+ message,
30
+ confirmLabel = "delete",
31
+ cancelLabel = "cancel",
32
+ onConfirm,
33
+ onCancel,
34
+ theme,
35
+ }: ConfirmDialogProps): React.ReactNode {
36
+ const tokens = useComponentTheme(theme);
37
+ const { width: terminalWidth } = useTerminalDimensions();
38
+
39
+ if (!isOpen) return null;
40
+
41
+ return (
42
+ <Dialog
43
+ isOpen
44
+ title={title}
45
+ width={Math.min(54, terminalWidth - 6)}
46
+ height={7}
47
+ background={tokens.elevated}
48
+ onDismiss={onCancel}
49
+ theme={theme}
50
+ >
51
+ <box
52
+ style={{
53
+ flexDirection: "column",
54
+ flexGrow: 1,
55
+ paddingLeft: 1,
56
+ paddingRight: 1,
57
+ paddingTop: 1,
58
+ }}
59
+ >
60
+ <text fg={tokens.text}>{message}</text>
61
+ <box style={{ flexGrow: 1 }} />
62
+ <DialogActions
63
+ confirmLabel={confirmLabel}
64
+ cancelLabel={cancelLabel}
65
+ onConfirm={onConfirm}
66
+ onCancel={onCancel}
67
+ theme={theme}
68
+ />
69
+ </box>
70
+ </Dialog>
71
+ );
72
+ }
@@ -0,0 +1,33 @@
1
+ import React, { useRef } from "react";
2
+ import type { ScrollBoxRenderable } from "@opentui/core";
3
+ import { DARK } from "../../appearance/theme";
4
+ import { OverlayScrollbar } from "./OverlayScrollbar";
5
+ import type { Story, StoryMeta } from "../../stories/story";
6
+
7
+ export const meta: StoryMeta = { title: "Chrome/OverlayScrollbar" };
8
+
9
+ function TallSurface(): React.ReactNode {
10
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null);
11
+
12
+ return (
13
+ <box style={{ width: 24, height: 8, flexDirection: "row", backgroundColor: DARK.background }}>
14
+ <scrollbox
15
+ ref={scrollRef}
16
+ style={{ flexGrow: 1 }}
17
+ focused={false}
18
+ verticalScrollbarOptions={{ visible: false }}
19
+ >
20
+ {Array.from({ length: 24 }, (_unused, index) => (
21
+ <text key={index} fg={DARK.textMuted}>{`row ${index + 1}`}</text>
22
+ ))}
23
+ </scrollbox>
24
+ <OverlayScrollbar scrollbox={scrollRef} />
25
+ </box>
26
+ );
27
+ }
28
+
29
+ // At rest the overlay bar is hidden; the thumb only appears while the surface is scrolling.
30
+ export const AtRest: Story = {
31
+ render: () => <TallSurface />,
32
+ size: { width: 24, height: 8 },
33
+ };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The mirrored scrollbar: thumb geometry follows the scroll state, and on a surface it
3
+ * draws as the panel's rightmost column, past the discussion dots - not inside the
4
+ * scrollbox's own viewport row.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
10
+ import React from "react";
11
+ import { DiffContentView } from "../../diff/components/DiffContentView";
12
+ import { diffRows, marksByRows } from "../../diff/view-diff";
13
+ import { allowEventLoopUpdates } from "../../testing/test-support";
14
+ import { fixtureDiffSession } from "../../stories/story-fixtures";
15
+ import { DARK } from "../../appearance/theme";
16
+ import { thumbGeometry } from "./OverlayScrollbar";
17
+
18
+ const noop = (): void => {};
19
+
20
+ function findById(node: Renderable, id: string): Renderable | undefined {
21
+ if (node.id === id) return node;
22
+
23
+ for (const child of node.getChildren()) {
24
+ const found = findById(child, id);
25
+
26
+ if (found) return found;
27
+ }
28
+
29
+ return undefined;
30
+ }
31
+
32
+ function hex(color: { toInts(): [number, number, number, number] }): string {
33
+ const [red, green, blue] = color.toInts();
34
+
35
+ return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
36
+ }
37
+
38
+ describe("thumbGeometry", () => {
39
+ test("nothing to scroll draws no thumb", () => {
40
+ // Arrange / Act / Assert - content fits the viewport
41
+ expect(thumbGeometry({ top: 0, height: 10, viewport: 14 }, 14)).toBeNull();
42
+ });
43
+
44
+ test("the thumb's height is the viewport's share of the content, at least one row", () => {
45
+ // Act
46
+ const tall = thumbGeometry({ top: 0, height: 44, viewport: 14 }, 14);
47
+ const tiny = thumbGeometry({ top: 0, height: 10_000, viewport: 14 }, 14);
48
+
49
+ // Assert
50
+ expect(tall).toEqual({ row: 0, height: 4 });
51
+ expect(tiny?.height).toBe(1);
52
+ });
53
+
54
+ test("the thumb reaches the track's end at the maximum scroll offset", () => {
55
+ // Act - scrolled to the bottom
56
+ const thumb = thumbGeometry({ top: 30, height: 44, viewport: 14 }, 14);
57
+
58
+ // Assert - its last row is the track's last row
59
+ expect(thumb).toEqual({ row: 10, height: 4 });
60
+ });
61
+ });
62
+
63
+ describe("the diff sheet's scrollbar", () => {
64
+ const body = Array.from({ length: 40 }, (_unused, index) => `+line ${index} of the change;`).join(
65
+ "\n",
66
+ );
67
+ const patch = `diff --git a/x.ts b/x.ts\n--- a/x.ts\n+++ b/x.ts\n@@ -0,0 +1,40 @@\n${body}\n`;
68
+
69
+ test("draws as the rightmost column, past the discussion dot", async () => {
70
+ // Arrange - a diff taller than the viewport, with one discussion so the dot rail has a dot
71
+ const rows = diffRows(patch);
72
+ const session = fixtureDiffSession({
73
+ annotations: [
74
+ {
75
+ id: "a1",
76
+ kind: "comment",
77
+ body: "hm",
78
+ anchor: { quote: "line 7 of the change;", prefix: "", suffix: "" },
79
+ createdAt: "2026-01-01T00:00:00Z",
80
+ },
81
+ ],
82
+ });
83
+ const width = 60;
84
+ const setup = await testRender(
85
+ <DiffContentView
86
+ rows={rows}
87
+ session={session}
88
+ marks={marksByRows(session.annotations, rows)}
89
+ quickActions={[]}
90
+ observer={false}
91
+ onAnnotate={noop}
92
+ onReply={noop}
93
+ onUpdateAnnotation={noop}
94
+ onExit={noop}
95
+ />,
96
+ { width, height: 14 },
97
+ );
98
+
99
+ allowEventLoopUpdates();
100
+ await setup.waitForVisualIdle();
101
+ // the overlay bar hides at rest; a scroll reveals the thumb
102
+ const scroller = findById(setup.renderer.root, "diff-scroll");
103
+
104
+ if (!(scroller instanceof ScrollBoxRenderable))
105
+ throw new Error("diff-scroll is not a scrollbox");
106
+
107
+ const countThumbRows = (): number =>
108
+ setup.captureSpans().lines.filter((line) => {
109
+ let column = 0;
110
+
111
+ for (const span of line.spans) {
112
+ column += span.text.length;
113
+
114
+ if (column >= width) return span.bg !== undefined && hex(span.bg) === DARK.textDim;
115
+ }
116
+
117
+ return false;
118
+ }).length;
119
+
120
+ // the overlay bar reveals on a scroll and hides after an idle delay; alternate the offset to
121
+ // re-trigger the reveal until the thumb is captured, so a slow CI settle never races the hide
122
+ let thumbRows = 0;
123
+
124
+ for (let attempt = 0; attempt < 20 && thumbRows === 0; attempt++) {
125
+ scroller.scrollTo(attempt % 2 === 0 ? 4 : 5);
126
+ await setup.waitForVisualIdle();
127
+ thumbRows = countThumbRows();
128
+ }
129
+ const frame = setup.captureCharFrame().split("\n");
130
+
131
+ // Assert - the thumb occupies the last column at the top (scroll offset 0), and the dot sits
132
+ // to its left in the rail, so the order reads content, dots, scrollbar
133
+ expect(thumbRows).toBe(4);
134
+ const dotLine = frame.find((line) => line.includes("○"));
135
+
136
+ expect(dotLine).toBeDefined();
137
+ expect(dotLine!.indexOf("○")).toBeLessThan(width - 1);
138
+ // let the async highlight settle before the renderer goes away
139
+ await new Promise((resolve) => setTimeout(resolve, 100));
140
+ setup.renderer.destroy();
141
+ });
142
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * A one-cell vertical scrollbar that mirrors a scrollbox's scroll state, so the bar can sit
3
+ * where the layout wants it (the panel's rightmost column, past the discussion dots) instead
4
+ * of inside the scrollbox's own viewport row. It is an overlay: the thumb appears while the
5
+ * surface is scrolling and hides once scrolling has been idle, so a resting surface shows none.
6
+ */
7
+
8
+ import React, { useEffect, useRef, useState, type RefObject } from "react";
9
+ import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
10
+ import type { Theme } from "../../appearance/theme";
11
+ import { useFrameMeasure } from "../use-frame-measure";
12
+ import { useComponentTheme } from "../../appearance/components/theme-context";
13
+
14
+ const HIDE_DELAY_MS = 1500;
15
+
16
+ interface ScrollState {
17
+ top: number;
18
+ height: number;
19
+ viewport: number;
20
+ }
21
+
22
+ export interface ThumbGeometry {
23
+ row: number;
24
+ height: number;
25
+ }
26
+
27
+ export function thumbGeometry(state: ScrollState, track: number): ThumbGeometry | null {
28
+ if (track <= 0 || state.height <= state.viewport || state.viewport <= 0) return null;
29
+
30
+ const height = Math.max(1, Math.round((state.viewport / state.height) * track));
31
+ const maxTop = state.height - state.viewport;
32
+ const row = Math.round((state.top / maxTop) * (track - height));
33
+
34
+ return { row: Math.min(track - height, Math.max(0, row)), height };
35
+ }
36
+
37
+ export interface OverlayScrollbarProps {
38
+ scrollbox: RefObject<ScrollBoxRenderable | null>;
39
+ theme?: Theme;
40
+ }
41
+
42
+ export function OverlayScrollbar({ scrollbox, theme }: OverlayScrollbarProps): React.ReactNode {
43
+ const tokens = useComponentTheme(theme);
44
+ const trackRef = useRef<BoxRenderable | null>(null);
45
+ const [visible, setVisible] = useState(false);
46
+ const draggingRef = useRef(false);
47
+ const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
48
+ const prevTopRef = useRef<number | null>(null);
49
+
50
+ const state = useFrameMeasure<ScrollState>(
51
+ () => ({
52
+ top: scrollbox.current?.scrollTop ?? 0,
53
+ height: scrollbox.current?.scrollHeight ?? 0,
54
+ viewport: scrollbox.current?.viewport.height ?? 0,
55
+ }),
56
+ (left, right) =>
57
+ left.top === right.top && left.height === right.height && left.viewport === right.viewport,
58
+ { top: 0, height: 0, viewport: 0 },
59
+ );
60
+ const track = useFrameMeasure(
61
+ () => trackRef.current?.height ?? 0,
62
+ (left, right) => left === right,
63
+ 0,
64
+ );
65
+
66
+ const reveal = (): void => {
67
+ setVisible(true);
68
+
69
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
70
+
71
+ hideTimerRef.current = setTimeout(() => {
72
+ if (!draggingRef.current) setVisible(false);
73
+ }, HIDE_DELAY_MS);
74
+ };
75
+
76
+ // reveal on real scroll movement, never on first mount, then hide once idle
77
+ useEffect(() => {
78
+ if (prevTopRef.current !== null && state.top !== prevTopRef.current) reveal();
79
+
80
+ prevTopRef.current = state.top;
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [state.top]);
83
+
84
+ useEffect(
85
+ () => () => {
86
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
87
+ },
88
+ [],
89
+ );
90
+
91
+ const geometry = thumbGeometry(state, track);
92
+ const thumb = visible ? geometry : null;
93
+
94
+ const scrollToTrackRow = (screenY: number): void => {
95
+ const box = scrollbox.current;
96
+
97
+ if (!box || !geometry) return;
98
+
99
+ const row = screenY - (trackRef.current?.y ?? 0) - Math.floor(geometry.height / 2);
100
+ const ratio = Math.min(1, Math.max(0, row / Math.max(1, track - geometry.height)));
101
+
102
+ box.scrollTo(Math.round(ratio * (state.height - state.viewport)));
103
+ reveal();
104
+ };
105
+
106
+ return (
107
+ <box
108
+ ref={trackRef}
109
+ style={{ width: 1, flexShrink: 0, flexDirection: "column" }}
110
+ onMouseDown={(event) => {
111
+ draggingRef.current = true;
112
+ scrollToTrackRow(event.y);
113
+ }}
114
+ onMouseDrag={(event) => scrollToTrackRow(event.y)}
115
+ onMouseUp={() => {
116
+ draggingRef.current = false;
117
+ reveal();
118
+ }}
119
+ >
120
+ {thumb ? (
121
+ <box
122
+ style={{
123
+ position: "absolute",
124
+ top: thumb.row,
125
+ left: 0,
126
+ width: 1,
127
+ height: thumb.height,
128
+ backgroundColor: tokens.textDim,
129
+ }}
130
+ />
131
+ ) : null}
132
+ </box>
133
+ );
134
+ }