@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,268 @@
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
+
120
+ // the full-screen box closes the menu on an outside click; the menu drops from under the kebab
121
+ setOverlay(
122
+ "thread-menu",
123
+ <box
124
+ onMouseUp={onToggleMenu}
125
+ style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%" }}
126
+ >
127
+ <box
128
+ style={{
129
+ position: "absolute",
130
+ top: anchor.y + 1,
131
+ left: Math.max(0, anchor.x + anchor.width - MENU_WIDTH),
132
+ }}
133
+ >
134
+ <ActionsMenu
135
+ pinned={pinned}
136
+ onPin={props.onPin}
137
+ onRename={props.onRename}
138
+ onDelete={props.onDelete}
139
+ tokens={tokens}
140
+ />
141
+ </box>
142
+ </box>,
143
+ );
144
+
145
+ return () => clearOverlay("thread-menu");
146
+ // eslint-disable-next-line react-hooks/exhaustive-deps
147
+ }, [menuOpen, anchor, pinned, tokens]);
148
+
149
+ return (
150
+ <box style={{ flexDirection: "column" }} onMouseOut={() => setHovered(false)}>
151
+ <box
152
+ ref={rowRef}
153
+ onMouseUp={props.onSelect}
154
+ onMouseOver={() => setHovered(true)}
155
+ style={{
156
+ flexDirection: "row",
157
+ backgroundColor: selected ? tokens.elevated : hovered ? tokens.panel : undefined,
158
+ }}
159
+ >
160
+ <box style={{ width: depth * SIDEBAR_INDENT, flexShrink: 0 }} />
161
+ <text wrapMode="none" style={{ flexShrink: 1 }}>
162
+ <span fg={selected ? tokens.accent : tokens.textMuted}>{clippedTitle}</span>
163
+ </text>
164
+ <box style={{ flexGrow: 1 }} />
165
+ {(hovered || menuOpen) && hasActions ? (
166
+ <IconButton
167
+ glyph={NERD.kebab}
168
+ active={menuOpen}
169
+ onPress={onToggleMenu}
170
+ marginRight={1}
171
+ theme={theme}
172
+ />
173
+ ) : null}
174
+ </box>
175
+ </box>
176
+ );
177
+ }
178
+
179
+ export function ThreadTree({
180
+ rows,
181
+ cursor,
182
+ activeId,
183
+ focused = false,
184
+ pinnedIds,
185
+ width = 30,
186
+ onSelect,
187
+ onRequestDelete,
188
+ onPin,
189
+ onRename,
190
+ onCreateThread,
191
+ theme,
192
+ }: ThreadTreeProps): React.ReactNode {
193
+ const tokens = useComponentTheme(theme);
194
+ const menuControl = useMenuControl();
195
+ // leave room for the 3-char prefix, the border, and the hover kebab so a row
196
+ // never wraps: the title fades to fit whatever is left
197
+ const titleWidth = Math.max(8, width - 9);
198
+ const closeMenu = (): void => menuControl.closeMenu();
199
+
200
+ const visibleRows =
201
+ onCreateThread && !rows.some((row) => row.id === "section:threads")
202
+ ? [...rows, { kind: "section" as const, id: "section:threads", label: "Threads" }]
203
+ : rows;
204
+
205
+ return (
206
+ <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1 }}>
207
+ {visibleRows.length === 0 ? (
208
+ <text fg={tokens.textDim}>no threads</text>
209
+ ) : (
210
+ visibleRows.map((row, index) => {
211
+ if (row.kind === "section") {
212
+ return (
213
+ <box key={row.id} style={{ flexDirection: "row", marginTop: index > 0 ? 1 : 0 }}>
214
+ <text fg={tokens.textDim}>{row.label}</text>
215
+ {row.id === "section:threads" && onCreateThread ? (
216
+ <>
217
+ <box style={{ flexGrow: 1 }} />
218
+ <IconButton
219
+ glyph="+"
220
+ tip="New Thread"
221
+ onPress={onCreateThread}
222
+ marginRight={1}
223
+ theme={theme}
224
+ />
225
+ </>
226
+ ) : null}
227
+ </box>
228
+ );
229
+ }
230
+
231
+ if (row.kind === "project") {
232
+ return (
233
+ <text key={row.id} wrapMode="none" style={{ marginLeft: SIDEBAR_INDENT }}>
234
+ <span fg={tokens.textMuted}>{row.label}</span>
235
+ </text>
236
+ );
237
+ }
238
+
239
+ const id = row.session.id;
240
+ const title = threadTitle(row.session);
241
+
242
+ return (
243
+ <ThreadRow
244
+ key={row.id}
245
+ title={title}
246
+ selected={
247
+ focused || activeId === undefined ? row.selectionIndex === cursor : activeId === id
248
+ }
249
+ pinned={pinnedIds?.has(id) ?? false}
250
+ depth={row.depth}
251
+ titleWidth={Math.max(8, titleWidth - (row.depth - 1) * SIDEBAR_INDENT)}
252
+ menuOpen={menuControl.openMenuId === `thread:${id}`}
253
+ onToggleMenu={() => menuControl.toggleMenu(`thread:${id}`)}
254
+ onSelect={onSelect ? () => onSelect(id) : undefined}
255
+ onPin={onPin ? () => (onPin(id), closeMenu()) : undefined}
256
+ onRename={onRename ? () => (onRename(id, title), closeMenu()) : undefined}
257
+ onDelete={
258
+ onRequestDelete ? () => (onRequestDelete(id, title), closeMenu()) : undefined
259
+ }
260
+ tokens={tokens}
261
+ theme={theme}
262
+ />
263
+ );
264
+ })
265
+ )}
266
+ </box>
267
+ );
268
+ }
@@ -0,0 +1,138 @@
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
+
10
+ if (remote !== undefined) workspace.remote = remote;
11
+
12
+ return {
13
+ schemaVersion: "1",
14
+ id,
15
+ workspace,
16
+ artifact: { type: "plan", content: `# ${title}\n`, meta: { title } },
17
+ revisions: [],
18
+ annotations: [],
19
+ message: null,
20
+ status: "pending",
21
+ createdAt: "2026-01-01T00:00:00.000Z",
22
+ };
23
+ }
24
+
25
+ describe("projectName", () => {
26
+ test("prefers the remote basename and strips the .git suffix", () => {
27
+ // Arrange
28
+ const workspace: WorkspaceKey = {
29
+ repoRoot: "/tmp/whatever",
30
+ branch: "main",
31
+ remote: "git@github.com:acme/widget.git",
32
+ };
33
+
34
+ // Act
35
+ const name = projectName(workspace);
36
+
37
+ // Assert
38
+ expect(name).toBe("widget");
39
+ });
40
+
41
+ test("falls back to the working-tree folder name when there is no remote", () => {
42
+ // Arrange
43
+ const workspace: WorkspaceKey = { repoRoot: "/home/dev/cueloop", branch: "main" };
44
+
45
+ // Act
46
+ const name = projectName(workspace);
47
+
48
+ // Assert
49
+ expect(name).toBe("cueloop");
50
+ });
51
+ });
52
+
53
+ describe("groupInbox", () => {
54
+ test("splits repo-bound threads into Projects and the rest into Threads", () => {
55
+ // Arrange
56
+ const sessions = [
57
+ session("a", "Read the repo", "root-1", "git@github.com:acme/widget.git"),
58
+ session("b", "A loose idea"),
59
+ ];
60
+
61
+ // Act
62
+ const { rows, ordered } = groupInbox(sessions);
63
+
64
+ // Assert
65
+ const labels = rows.map((row) => `${row.kind}:${"label" in row ? row.label : row.id}`);
66
+
67
+ expect(labels).toEqual([
68
+ "section:Projects",
69
+ "project:widget",
70
+ "thread:a",
71
+ "section:Threads",
72
+ "thread:b",
73
+ ]);
74
+ expect(ordered.map((s) => s.id)).toEqual(["a", "b"]);
75
+ });
76
+
77
+ test("collapses threads sharing a root commit into one project", () => {
78
+ // Arrange
79
+ const sessions = [
80
+ session("a", "First", "root-1", "git@github.com:acme/widget.git"),
81
+ session("b", "Second", "root-1", "git@github.com:acme/widget.git"),
82
+ ];
83
+
84
+ // Act
85
+ const { rows, ordered } = groupInbox(sessions);
86
+
87
+ // Assert
88
+ const projectRows = rows.filter((row) => row.kind === "project");
89
+
90
+ expect(projectRows).toHaveLength(1);
91
+ expect(ordered).toHaveLength(2);
92
+ });
93
+
94
+ test("selectionIndex on each thread matches its position in ordered", () => {
95
+ // Arrange
96
+ const sessions = [session("a", "One", "root-1"), session("b", "Two")];
97
+
98
+ // Act
99
+ const { rows, ordered } = groupInbox(sessions);
100
+
101
+ // Assert
102
+ for (const row of rows) {
103
+ if (row.kind === "thread") expect(ordered[row.selectionIndex]!.id).toBe(row.id);
104
+ }
105
+ });
106
+
107
+ test("omits the Projects section when nothing is repo-bound", () => {
108
+ // Arrange
109
+ const sessions = [session("a", "Loose one"), session("b", "Loose two")];
110
+
111
+ // Act
112
+ const { rows } = groupInbox(sessions);
113
+
114
+ // Assert
115
+ expect(rows.some((row) => row.kind === "section" && row.label === "Projects")).toBe(false);
116
+ expect(rows.some((row) => row.kind === "section" && row.label === "Threads")).toBe(true);
117
+ });
118
+
119
+ test("a pinned thread lifts into a Starred section at the top, out of its normal group", () => {
120
+ // Arrange - one repo-bound thread, one standalone; pin the repo-bound one
121
+ const sessions = [session("a", "Pinned one", "root-1"), session("b", "Loose two")];
122
+
123
+ // Act
124
+ const { rows, ordered } = groupInbox(sessions, new Set(["a"]));
125
+
126
+ // Assert - Starred is the first section and holds thread a; a is gone from Projects
127
+ const sections = rows
128
+ .filter((row) => row.kind === "section")
129
+ .map((row) => (row.kind === "section" ? row.label : ""));
130
+
131
+ expect(sections[0]).toBe("Starred");
132
+ expect(sections).not.toContain("Projects"); // its only member was pinned away
133
+ expect(ordered[0]!.id).toBe("a"); // the cursor walks pinned threads first
134
+ const pinnedThreadIds = rows.filter((row) => row.kind === "thread").map((row) => row.id);
135
+
136
+ expect(pinnedThreadIds).toEqual(["a", "b"]);
137
+ });
138
+ });
@@ -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 };