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

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 (355) 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 +364 -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 +1238 -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 +12 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1650 -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.stories.tsx +121 -0
  119. package/src/integrations/components/ShareDialog.tsx +550 -0
  120. package/src/integrations/components/share-dialog-store.ts +55 -0
  121. package/src/integrations/editor.test.ts +149 -0
  122. package/src/integrations/editor.ts +160 -0
  123. package/src/integrations/github-identity.test.ts +58 -0
  124. package/src/integrations/github-identity.ts +41 -0
  125. package/src/integrations/herdr.test.ts +43 -0
  126. package/src/integrations/herdr.ts +38 -0
  127. package/src/integrations/integrations.test.ts +41 -0
  128. package/src/integrations/integrations.ts +32 -0
  129. package/src/integrations/pasted-image.test.ts +67 -0
  130. package/src/integrations/pasted-image.ts +60 -0
  131. package/src/integrations/share.ts +238 -0
  132. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  133. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  134. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  135. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  136. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  137. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  138. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  139. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  140. package/src/keyboard/components/NavModeHint.tsx +31 -0
  141. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  142. package/src/keyboard/components/SlashComposer.tsx +87 -0
  143. package/src/keyboard/key-bindings.test.ts +156 -0
  144. package/src/keyboard/key-bindings.ts +459 -0
  145. package/src/keyboard/keymap.test.ts +611 -0
  146. package/src/keyboard/keymap.ts +395 -0
  147. package/src/keyboard/skills.test.ts +74 -0
  148. package/src/keyboard/skills.ts +75 -0
  149. package/src/keyboard/slash-palette.test.ts +103 -0
  150. package/src/keyboard/slash-palette.ts +129 -0
  151. package/src/keyboard/thread-chords.test.ts +79 -0
  152. package/src/keyboard/thread-chords.ts +163 -0
  153. package/src/keyboard/use-shared-keyboard.ts +55 -0
  154. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  155. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  156. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  157. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  158. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  159. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  160. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  161. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  162. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  163. package/src/markdown/components/ThreadView.tsx +560 -0
  164. package/src/markdown/mark-runs.ts +121 -0
  165. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  166. package/src/markdown/markdown-grid.test.ts +101 -0
  167. package/src/markdown/markdown-grid.ts +203 -0
  168. package/src/markdown/markdown-highlight.test.ts +61 -0
  169. package/src/markdown/markdown-highlight.ts +124 -0
  170. package/src/markdown/thread-code-syntax.ts +45 -0
  171. package/src/markdown/use-thread-body-editing.tsx +54 -0
  172. package/src/markdown/view-plan.regression.test.ts +60 -0
  173. package/src/markdown/view-plan.test.ts +545 -0
  174. package/src/markdown/view-plan.ts +1009 -0
  175. package/src/performance/otlp-trace.test.ts +119 -0
  176. package/src/performance/otlp-trace.ts +134 -0
  177. package/src/performance/perf-report.ts +18 -0
  178. package/src/performance/perf-timings.test.ts +18 -0
  179. package/src/performance/perf-timings.ts +39 -0
  180. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  181. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  182. package/src/prototype/prototype-browser.test.ts +81 -0
  183. package/src/prototype/prototype-browser.ts +321 -0
  184. package/src/prototype/prototype-pixels.tsx +35 -0
  185. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  186. package/src/settings/components/SettingsDialog.tsx +183 -0
  187. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  188. package/src/settings/components/SettingsRows.tsx +112 -0
  189. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  190. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  191. package/src/settings/components/quick-actions-editor.tsx +222 -0
  192. package/src/settings/config-identity.test.ts +78 -0
  193. package/src/settings/config.test.ts +676 -0
  194. package/src/settings/config.ts +628 -0
  195. package/src/settings/use-settings-dialog.tsx +351 -0
  196. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  197. package/src/stories/stories-app.tsx +196 -0
  198. package/src/stories/stories.test.tsx +74 -0
  199. package/src/stories/story-fixtures.ts +102 -0
  200. package/src/stories/story-tree.test.ts +108 -0
  201. package/src/stories/story.ts +154 -0
  202. package/src/terminal/ansi-screen.test.ts +28 -0
  203. package/src/terminal/ansi-screen.ts +72 -0
  204. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  205. package/src/terminal/components/AnsiScreen.tsx +28 -0
  206. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  207. package/src/terminal/components/terminal-pane.ts +196 -0
  208. package/src/terminal/ghostty-terminal.test.ts +85 -0
  209. package/src/terminal/ghostty-terminal.ts +197 -0
  210. package/src/terminal/kitty-image.test.ts +66 -0
  211. package/src/terminal/kitty-image.ts +137 -0
  212. package/src/terminal/pty.test.ts +81 -0
  213. package/src/terminal/pty.ts +246 -0
  214. package/src/testing/test-support.test.ts +264 -0
  215. package/src/testing/test-support.ts +367 -0
  216. package/src/thread/agent-continuation.test.ts +32 -0
  217. package/src/thread/agent-continuation.ts +13 -0
  218. package/src/thread/agent-transcript.test.ts +161 -0
  219. package/src/thread/agent-transcript.ts +336 -0
  220. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  221. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  222. package/src/thread/components/AgentConfigControls.tsx +114 -0
  223. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  224. package/src/thread/components/AgentThreadPane.test.tsx +829 -0
  225. package/src/thread/components/AgentThreadPane.tsx +367 -0
  226. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  227. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  228. package/src/thread/components/ThreadFooter.tsx +77 -0
  229. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  230. package/src/thread/components/ThreadTree.test.tsx +41 -0
  231. package/src/thread/components/ThreadTree.tsx +267 -0
  232. package/src/thread/components/thread-tree.test.ts +137 -0
  233. package/src/thread/components/thread-tree.ts +112 -0
  234. package/src/thread/thread-controller.curate.test.ts +502 -0
  235. package/src/thread/thread-controller.derivation.test.ts +62 -0
  236. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  237. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  238. package/src/thread/thread-controller.share.test.ts +573 -0
  239. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  240. package/src/thread/thread-controller.tree.test.ts +263 -0
  241. package/src/thread/thread-controller.ts +2018 -0
  242. package/src/thread/tree-view.test.ts +128 -0
  243. package/src/thread/tree-view.ts +146 -0
  244. package/src/thread/use-thread-agent.test.tsx +208 -0
  245. package/src/thread/use-thread-agent.ts +111 -0
  246. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  247. package/src/ui/components/CodeBlock.tsx +80 -0
  248. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  249. package/src/ui/components/ConfirmCard.tsx +151 -0
  250. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  251. package/src/ui/components/ConfirmDialog.tsx +72 -0
  252. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  253. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  254. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  255. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  256. package/src/ui/components/PromptDialog.test.tsx +65 -0
  257. package/src/ui/components/PromptDialog.tsx +99 -0
  258. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  259. package/src/ui/components/RootOverlay.tsx +49 -0
  260. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  261. package/src/ui/components/ScrollArea.tsx +61 -0
  262. package/src/ui/components/Toast.stories.tsx +19 -0
  263. package/src/ui/components/Toast.tsx +54 -0
  264. package/src/ui/components/Tooltip.stories.tsx +26 -0
  265. package/src/ui/components/Tooltip.tsx +72 -0
  266. package/src/ui/components/menu-control.stories.tsx +35 -0
  267. package/src/ui/components/menu-control.tsx +43 -0
  268. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  269. package/src/ui/components/primitives/Button.tsx +57 -0
  270. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  271. package/src/ui/components/primitives/Card.tsx +62 -0
  272. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  273. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  274. package/src/ui/components/primitives/Dialog.tsx +69 -0
  275. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  276. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  277. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  278. package/src/ui/components/primitives/IconButton.tsx +58 -0
  279. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  280. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  281. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  282. package/src/ui/components/primitives/Switch.tsx +32 -0
  283. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  284. package/src/ui/components/primitives/Tabs.tsx +101 -0
  285. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  286. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  287. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  288. package/src/ui/components/primitives/Tree.tsx +133 -0
  289. package/src/ui/components/primitives/frame.test.ts +12 -0
  290. package/src/ui/components/primitives/frame.ts +17 -0
  291. package/src/ui/components/primitives/icons.ts +61 -0
  292. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  293. package/src/ui/components/primitives/tree-model.ts +137 -0
  294. package/src/ui/components/syntax-highlight.ts +133 -0
  295. package/src/ui/components/truncate-text.ts +7 -0
  296. package/src/ui/components/truncate-title.test.ts +31 -0
  297. package/src/ui/components/truncate-title.ts +15 -0
  298. package/src/ui/use-frame-measure.ts +74 -0
  299. package/src/ui/use-terminal-virtualizer.ts +259 -0
  300. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  301. package/src/workbench/components/ChangesColumn.tsx +203 -0
  302. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  303. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  304. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  305. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  306. package/src/workbench/components/EditorGrid.tsx +544 -0
  307. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  308. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  309. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  310. package/src/workbench/components/FileContentsView.tsx +100 -0
  311. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  312. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  313. package/src/workbench/components/GridTabContent.tsx +193 -0
  314. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  315. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  316. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  317. package/src/workbench/components/TreePane.stories.tsx +106 -0
  318. package/src/workbench/components/TreePane.tsx +155 -0
  319. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  320. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  321. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  322. package/src/workbench/components/editor-grid.test.ts +184 -0
  323. package/src/workbench/components/editor-grid.ts +281 -0
  324. package/src/workbench/components/file-tree.test.ts +60 -0
  325. package/src/workbench/components/file-tree.ts +112 -0
  326. package/src/workbench/use-changes-workbench.ts +205 -0
  327. package/src/workbench/use-remember-layout.ts +31 -0
  328. package/src/workbench/use-repo-changes.ts +91 -0
  329. package/src/workbench/workbench-snapshot.test.ts +99 -0
  330. package/src/workbench/workbench-snapshot.ts +59 -0
  331. package/src/App.diff.test.tsx +0 -99
  332. package/src/App.export.test.tsx +0 -88
  333. package/src/App.plan-review.test.tsx +0 -259
  334. package/src/App.readonly.test.tsx +0 -148
  335. package/src/App.rendering.test.tsx +0 -108
  336. package/src/App.submit-confirm.test.tsx +0 -183
  337. package/src/App.test.tsx +0 -218
  338. package/src/App.tsx +0 -1249
  339. package/src/config.test.ts +0 -116
  340. package/src/config.ts +0 -150
  341. package/src/editor.ts +0 -34
  342. package/src/herdr.test.ts +0 -68
  343. package/src/herdr.ts +0 -34
  344. package/src/keymap.test.ts +0 -270
  345. package/src/keymap.ts +0 -173
  346. package/src/run.ts +0 -30
  347. package/src/serve.ts +0 -75
  348. package/src/session-controller.ts +0 -398
  349. package/src/syntax.test.ts +0 -41
  350. package/src/syntax.ts +0 -88
  351. package/src/theme.ts +0 -46
  352. package/src/view-diff.test.ts +0 -55
  353. package/src/view-diff.ts +0 -86
  354. package/src/view.test.ts +0 -157
  355. package/src/view.ts +0 -364
@@ -0,0 +1,267 @@
1
+ /**
2
+ * The sidebar thread tree: a Starred / Projects / Threads listing. Selection stays
3
+ * with the keyboard grammar - the cursor indexes the flat thread order and this
4
+ * component renders the snapshot. A hovered or selected thread reveals a kebab
5
+ * that opens a floating pin / rename / delete menu below the row (so the list
6
+ * never shifts); a long title clips to an ellipsis rather than wrapping. App
7
+ * supplies the surrounding chrome.
8
+ */
9
+
10
+ import React, { useEffect, useRef, useState } from "react";
11
+ import type { BoxRenderable } from "@opentui/core";
12
+ import type { Theme } from "../../appearance/theme";
13
+ import { useComponentTheme } from "../../appearance/components/theme-context";
14
+ import { useFrameMeasure } from "../../ui/use-frame-measure";
15
+ import { useRootOverlay } from "../../ui/components/RootOverlay";
16
+ import { useMenuControl } from "../../ui/components/menu-control";
17
+ import { IconButton } from "../../ui/components/primitives/IconButton";
18
+ import { NERD } from "../../ui/components/primitives/icons";
19
+ import { truncateTitle } from "../../ui/components/truncate-title";
20
+ import { threadTitle, type InboxRow } from "./thread-tree";
21
+
22
+ export interface ThreadTreeProps {
23
+ rows: InboxRow[];
24
+ cursor: number;
25
+ activeId?: string;
26
+ focused?: boolean;
27
+ pinnedIds?: ReadonlySet<string>;
28
+ width?: number;
29
+ onSelect?: (sessionId: string) => void;
30
+ onRequestDelete?: (id: string, title: string) => void;
31
+ onPin?: (id: string) => void;
32
+ onRename?: (id: string, title: string) => void;
33
+ onCreateThread?: () => void;
34
+ theme?: Theme;
35
+ }
36
+
37
+ const MENU_WIDTH = 12;
38
+
39
+ /** Columns per nesting level in the sidebar cascade: section → project → thread. */
40
+ const SIDEBAR_INDENT = 2;
41
+
42
+ function ActionsMenu({
43
+ pinned,
44
+ onPin,
45
+ onRename,
46
+ onDelete,
47
+ tokens,
48
+ }: {
49
+ pinned: boolean;
50
+ onPin?: () => void;
51
+ onRename?: () => void;
52
+ onDelete?: () => void;
53
+ tokens: Theme;
54
+ }): React.ReactNode {
55
+ const item = (label: string, color: string, onPick?: () => void): React.ReactNode =>
56
+ onPick !== undefined ? (
57
+ <box onMouseUp={onPick} style={{ paddingLeft: 1, paddingRight: 1 }}>
58
+ <text fg={color}>{label}</text>
59
+ </box>
60
+ ) : null;
61
+
62
+ return (
63
+ <box
64
+ onMouseUp={(event) => event.stopPropagation()}
65
+ style={{
66
+ width: MENU_WIDTH,
67
+ flexDirection: "column",
68
+ border: true,
69
+ borderStyle: "single",
70
+ borderColor: tokens.border,
71
+ backgroundColor: tokens.elevated,
72
+ }}
73
+ >
74
+ {item(pinned ? "unstar" : "star", tokens.text, onPin)}
75
+ {item("rename", tokens.text, onRename)}
76
+ {item("delete", tokens.text, onDelete)}
77
+ </box>
78
+ );
79
+ }
80
+
81
+ interface ThreadRowProps {
82
+ title: string;
83
+ selected: boolean;
84
+ pinned: boolean;
85
+ depth: number;
86
+ titleWidth: number;
87
+ menuOpen: boolean;
88
+ onToggleMenu: () => void;
89
+ onSelect?: () => void;
90
+ onPin?: () => void;
91
+ onRename?: () => void;
92
+ onDelete?: () => void;
93
+ tokens: Theme;
94
+ theme?: Theme;
95
+ }
96
+
97
+ function ThreadRow(props: ThreadRowProps): React.ReactNode {
98
+ const { title, selected, pinned, depth, titleWidth, menuOpen, onToggleMenu, tokens, theme } =
99
+ props;
100
+ const [hovered, setHovered] = useState(false);
101
+ const rowRef = useRef<BoxRenderable | null>(null);
102
+ const { setOverlay, clearOverlay } = useRootOverlay();
103
+ const anchor = useFrameMeasure(
104
+ () => ({
105
+ x: rowRef.current?.x ?? 0,
106
+ y: rowRef.current?.y ?? 0,
107
+ width: rowRef.current?.width ?? 0,
108
+ }),
109
+ (left, right) => left.x === right.x && left.y === right.y && left.width === right.width,
110
+ { x: 0, y: 0, width: 0 },
111
+ menuOpen,
112
+ );
113
+ const clippedTitle = truncateTitle(title, titleWidth);
114
+ const hasActions =
115
+ props.onPin !== undefined || props.onRename !== undefined || props.onDelete !== undefined;
116
+
117
+ useEffect(() => {
118
+ if (!menuOpen) return;
119
+ // the full-screen box closes the menu on an outside click; the menu drops from under the kebab
120
+ setOverlay(
121
+ "thread-menu",
122
+ <box
123
+ onMouseUp={onToggleMenu}
124
+ style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%" }}
125
+ >
126
+ <box
127
+ style={{
128
+ position: "absolute",
129
+ top: anchor.y + 1,
130
+ left: Math.max(0, anchor.x + anchor.width - MENU_WIDTH),
131
+ }}
132
+ >
133
+ <ActionsMenu
134
+ pinned={pinned}
135
+ onPin={props.onPin}
136
+ onRename={props.onRename}
137
+ onDelete={props.onDelete}
138
+ tokens={tokens}
139
+ />
140
+ </box>
141
+ </box>,
142
+ );
143
+
144
+ return () => clearOverlay("thread-menu");
145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
146
+ }, [menuOpen, anchor, pinned, tokens]);
147
+
148
+ return (
149
+ <box style={{ flexDirection: "column" }} onMouseOut={() => setHovered(false)}>
150
+ <box
151
+ ref={rowRef}
152
+ onMouseUp={props.onSelect}
153
+ onMouseOver={() => setHovered(true)}
154
+ style={{
155
+ flexDirection: "row",
156
+ backgroundColor: selected ? tokens.elevated : hovered ? tokens.panel : undefined,
157
+ }}
158
+ >
159
+ <box style={{ width: depth * SIDEBAR_INDENT, flexShrink: 0 }} />
160
+ <text wrapMode="none" style={{ flexShrink: 1 }}>
161
+ <span fg={selected ? tokens.accent : tokens.textMuted}>{clippedTitle}</span>
162
+ </text>
163
+ <box style={{ flexGrow: 1 }} />
164
+ {(hovered || menuOpen) && hasActions ? (
165
+ <IconButton
166
+ glyph={NERD.kebab}
167
+ active={menuOpen}
168
+ onPress={onToggleMenu}
169
+ marginRight={1}
170
+ theme={theme}
171
+ />
172
+ ) : null}
173
+ </box>
174
+ </box>
175
+ );
176
+ }
177
+
178
+ export function ThreadTree({
179
+ rows,
180
+ cursor,
181
+ activeId,
182
+ focused = false,
183
+ pinnedIds,
184
+ width = 30,
185
+ onSelect,
186
+ onRequestDelete,
187
+ onPin,
188
+ onRename,
189
+ onCreateThread,
190
+ theme,
191
+ }: ThreadTreeProps): React.ReactNode {
192
+ const tokens = useComponentTheme(theme);
193
+ const menuControl = useMenuControl();
194
+ // leave room for the 3-char prefix, the border, and the hover kebab so a row
195
+ // never wraps: the title fades to fit whatever is left
196
+ const titleWidth = Math.max(8, width - 9);
197
+ const closeMenu = (): void => menuControl.closeMenu();
198
+
199
+ const visibleRows =
200
+ onCreateThread && !rows.some((row) => row.id === "section:threads")
201
+ ? [...rows, { kind: "section" as const, id: "section:threads", label: "Threads" }]
202
+ : rows;
203
+
204
+ return (
205
+ <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1 }}>
206
+ {visibleRows.length === 0 ? (
207
+ <text fg={tokens.textDim}>no threads</text>
208
+ ) : (
209
+ visibleRows.map((row, index) => {
210
+ if (row.kind === "section") {
211
+ return (
212
+ <box key={row.id} style={{ flexDirection: "row", marginTop: index > 0 ? 1 : 0 }}>
213
+ <text fg={tokens.textDim}>{row.label}</text>
214
+ {row.id === "section:threads" && onCreateThread ? (
215
+ <>
216
+ <box style={{ flexGrow: 1 }} />
217
+ <IconButton
218
+ glyph="+"
219
+ tip="New Thread"
220
+ onPress={onCreateThread}
221
+ marginRight={1}
222
+ theme={theme}
223
+ />
224
+ </>
225
+ ) : null}
226
+ </box>
227
+ );
228
+ }
229
+
230
+ if (row.kind === "project") {
231
+ return (
232
+ <text key={row.id} wrapMode="none" style={{ marginLeft: SIDEBAR_INDENT }}>
233
+ <span fg={tokens.textMuted}>{row.label}</span>
234
+ </text>
235
+ );
236
+ }
237
+
238
+ const id = row.session.id;
239
+ const title = threadTitle(row.session);
240
+
241
+ return (
242
+ <ThreadRow
243
+ key={row.id}
244
+ title={title}
245
+ selected={
246
+ focused || activeId === undefined ? row.selectionIndex === cursor : activeId === id
247
+ }
248
+ pinned={pinnedIds?.has(id) ?? false}
249
+ depth={row.depth}
250
+ titleWidth={Math.max(8, titleWidth - (row.depth - 1) * SIDEBAR_INDENT)}
251
+ menuOpen={menuControl.openMenuId === `thread:${id}`}
252
+ onToggleMenu={() => menuControl.toggleMenu(`thread:${id}`)}
253
+ onSelect={onSelect ? () => onSelect(id) : undefined}
254
+ onPin={onPin ? () => (onPin(id), closeMenu()) : undefined}
255
+ onRename={onRename ? () => (onRename(id, title), closeMenu()) : undefined}
256
+ onDelete={
257
+ onRequestDelete ? () => (onRequestDelete(id, title), closeMenu()) : undefined
258
+ }
259
+ tokens={tokens}
260
+ theme={theme}
261
+ />
262
+ );
263
+ })
264
+ )}
265
+ </box>
266
+ );
267
+ }
@@ -0,0 +1,137 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Thread, WorkspaceKey } from "@cueloop/schema";
3
+ import { groupInbox, projectName } from "./thread-tree";
4
+
5
+ function session(id: string, title: string, rootCommit?: string, remote?: string): Thread {
6
+ const workspace: WorkspaceKey = { repoRoot: `/home/dev/${id}-checkout`, branch: "main" };
7
+
8
+ if (rootCommit !== undefined) workspace.rootCommit = rootCommit;
9
+ if (remote !== undefined) workspace.remote = remote;
10
+
11
+ return {
12
+ schemaVersion: "1",
13
+ id,
14
+ workspace,
15
+ artifact: { type: "plan", content: `# ${title}\n`, meta: { title } },
16
+ revisions: [],
17
+ annotations: [],
18
+ message: null,
19
+ status: "pending",
20
+ createdAt: "2026-01-01T00:00:00.000Z",
21
+ };
22
+ }
23
+
24
+ describe("projectName", () => {
25
+ test("prefers the remote basename and strips the .git suffix", () => {
26
+ // Arrange
27
+ const workspace: WorkspaceKey = {
28
+ repoRoot: "/tmp/whatever",
29
+ branch: "main",
30
+ remote: "git@github.com:acme/widget.git",
31
+ };
32
+
33
+ // Act
34
+ const name = projectName(workspace);
35
+
36
+ // Assert
37
+ expect(name).toBe("widget");
38
+ });
39
+
40
+ test("falls back to the working-tree folder name when there is no remote", () => {
41
+ // Arrange
42
+ const workspace: WorkspaceKey = { repoRoot: "/home/dev/cueloop", branch: "main" };
43
+
44
+ // Act
45
+ const name = projectName(workspace);
46
+
47
+ // Assert
48
+ expect(name).toBe("cueloop");
49
+ });
50
+ });
51
+
52
+ describe("groupInbox", () => {
53
+ test("splits repo-bound threads into Projects and the rest into Threads", () => {
54
+ // Arrange
55
+ const sessions = [
56
+ session("a", "Read the repo", "root-1", "git@github.com:acme/widget.git"),
57
+ session("b", "A loose idea"),
58
+ ];
59
+
60
+ // Act
61
+ const { rows, ordered } = groupInbox(sessions);
62
+
63
+ // Assert
64
+ const labels = rows.map((row) => `${row.kind}:${"label" in row ? row.label : row.id}`);
65
+
66
+ expect(labels).toEqual([
67
+ "section:Projects",
68
+ "project:widget",
69
+ "thread:a",
70
+ "section:Threads",
71
+ "thread:b",
72
+ ]);
73
+ expect(ordered.map((s) => s.id)).toEqual(["a", "b"]);
74
+ });
75
+
76
+ test("collapses threads sharing a root commit into one project", () => {
77
+ // Arrange
78
+ const sessions = [
79
+ session("a", "First", "root-1", "git@github.com:acme/widget.git"),
80
+ session("b", "Second", "root-1", "git@github.com:acme/widget.git"),
81
+ ];
82
+
83
+ // Act
84
+ const { rows, ordered } = groupInbox(sessions);
85
+
86
+ // Assert
87
+ const projectRows = rows.filter((row) => row.kind === "project");
88
+
89
+ expect(projectRows).toHaveLength(1);
90
+ expect(ordered).toHaveLength(2);
91
+ });
92
+
93
+ test("selectionIndex on each thread matches its position in ordered", () => {
94
+ // Arrange
95
+ const sessions = [session("a", "One", "root-1"), session("b", "Two")];
96
+
97
+ // Act
98
+ const { rows, ordered } = groupInbox(sessions);
99
+
100
+ // Assert
101
+ for (const row of rows) {
102
+ if (row.kind === "thread") expect(ordered[row.selectionIndex]!.id).toBe(row.id);
103
+ }
104
+ });
105
+
106
+ test("omits the Projects section when nothing is repo-bound", () => {
107
+ // Arrange
108
+ const sessions = [session("a", "Loose one"), session("b", "Loose two")];
109
+
110
+ // Act
111
+ const { rows } = groupInbox(sessions);
112
+
113
+ // Assert
114
+ expect(rows.some((row) => row.kind === "section" && row.label === "Projects")).toBe(false);
115
+ expect(rows.some((row) => row.kind === "section" && row.label === "Threads")).toBe(true);
116
+ });
117
+
118
+ test("a pinned thread lifts into a Starred section at the top, out of its normal group", () => {
119
+ // Arrange - one repo-bound thread, one standalone; pin the repo-bound one
120
+ const sessions = [session("a", "Pinned one", "root-1"), session("b", "Loose two")];
121
+
122
+ // Act
123
+ const { rows, ordered } = groupInbox(sessions, new Set(["a"]));
124
+
125
+ // Assert - Starred is the first section and holds thread a; a is gone from Projects
126
+ const sections = rows
127
+ .filter((row) => row.kind === "section")
128
+ .map((row) => (row.kind === "section" ? row.label : ""));
129
+
130
+ expect(sections[0]).toBe("Starred");
131
+ expect(sections).not.toContain("Projects"); // its only member was pinned away
132
+ expect(ordered[0]!.id).toBe("a"); // the cursor walks pinned threads first
133
+ const pinnedThreadIds = rows.filter((row) => row.kind === "thread").map((row) => row.id);
134
+
135
+ expect(pinnedThreadIds).toEqual(["a", "b"]);
136
+ });
137
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Groups the pending inbox into Projects and Threads for the sidebar. Threads
3
+ * whose workspace carries a rootCommit gather under their project (keyed by that
4
+ * commit, so moving or re-cloning the repo never forks it); the rest are
5
+ * standalone Threads. The flat `ordered` sequence is what the inbox cursor
6
+ * walks, so the keyboard grammar keeps a single index across the two groups.
7
+ */
8
+
9
+ import type { Thread, WorkspaceKey } from "@cueloop/schema";
10
+
11
+ /** A repo's display name: the remote basename when present, else the working-tree folder name. */
12
+ export function projectName(workspace: WorkspaceKey): string {
13
+ const { remote, repoRoot } = workspace;
14
+
15
+ if (remote !== undefined) {
16
+ const cleaned = remote.replace(/\.git$/, "").replace(/\/+$/, "");
17
+ const base = cleaned
18
+ .split(/[/:]/)
19
+ .filter((part) => part.length > 0)
20
+ .at(-1);
21
+
22
+ if (base !== undefined) return base;
23
+ }
24
+
25
+ const dir = repoRoot
26
+ .replace(/\/+$/, "")
27
+ .split("/")
28
+ .filter((part) => part.length > 0)
29
+ .at(-1);
30
+
31
+ return dir ?? repoRoot;
32
+ }
33
+
34
+ export type InboxRow =
35
+ | { kind: "section"; id: string; label: string }
36
+ | { kind: "project"; id: string; label: string }
37
+ | { kind: "thread"; id: string; session: Thread; selectionIndex: number; depth: number };
38
+
39
+ export interface GroupedInbox {
40
+ rows: InboxRow[];
41
+ ordered: Thread[];
42
+ }
43
+
44
+ function threadTitle(session: Thread): string {
45
+ return session.artifact.meta.title ?? session.id;
46
+ }
47
+
48
+ interface ProjectGroup {
49
+ name: string;
50
+ sessions: Thread[];
51
+ }
52
+
53
+ /** Group pending sessions into Pinned, then Projects (by root commit), then standalone Threads. */
54
+ export function groupInbox(
55
+ sessions: readonly Thread[],
56
+ pinnedIds?: ReadonlySet<string>,
57
+ ): GroupedInbox {
58
+ const pinned: Thread[] = [];
59
+ const projects = new Map<string, ProjectGroup>();
60
+ const standalone: Thread[] = [];
61
+
62
+ for (const session of sessions) {
63
+ if (pinnedIds?.has(session.id) === true) {
64
+ pinned.push(session);
65
+ continue;
66
+ }
67
+
68
+ const key = session.workspace.rootCommit;
69
+
70
+ if (key === undefined) {
71
+ standalone.push(session);
72
+ continue;
73
+ }
74
+
75
+ const existing = projects.get(key);
76
+
77
+ if (existing !== undefined) existing.sessions.push(session);
78
+ else projects.set(key, { name: projectName(session.workspace), sessions: [session] });
79
+ }
80
+
81
+ const rows: InboxRow[] = [];
82
+ const ordered: Thread[] = [];
83
+ const pushThread = (session: Thread, depth: number): void => {
84
+ rows.push({ kind: "thread", id: session.id, session, selectionIndex: ordered.length, depth });
85
+ ordered.push(session);
86
+ };
87
+
88
+ if (pinned.length > 0) {
89
+ rows.push({ kind: "section", id: "section:pinned", label: "Starred" });
90
+ for (const session of pinned) pushThread(session, 1);
91
+ }
92
+
93
+ const projectEntries = [...projects.entries()].sort((a, b) => a[1].name.localeCompare(b[1].name));
94
+
95
+ if (projectEntries.length > 0) {
96
+ rows.push({ kind: "section", id: "section:projects", label: "Projects" });
97
+ for (const [key, project] of projectEntries) {
98
+ rows.push({ kind: "project", id: `project:${key}`, label: project.name });
99
+ // a project's threads cascade one level under its name
100
+ for (const session of project.sessions) pushThread(session, 2);
101
+ }
102
+ }
103
+
104
+ if (standalone.length > 0) {
105
+ rows.push({ kind: "section", id: "section:threads", label: "Threads" });
106
+ for (const session of standalone) pushThread(session, 1);
107
+ }
108
+
109
+ return { rows, ordered };
110
+ }
111
+
112
+ export { threadTitle };