@cueloop/client 0.1.0-alpha.0 → 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 (345) 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 +41 -27
  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/App.autoclose.test.tsx +144 -0
  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 -80
  333. package/src/App.readonly.test.tsx +0 -148
  334. package/src/App.test.tsx +0 -217
  335. package/src/App.tsx +0 -0
  336. package/src/config.test.ts +0 -116
  337. package/src/config.ts +0 -113
  338. package/src/editor.ts +0 -34
  339. package/src/run.ts +0 -28
  340. package/src/serve.ts +0 -75
  341. package/src/theme.ts +0 -46
  342. package/src/view-diff.test.ts +0 -55
  343. package/src/view-diff.ts +0 -86
  344. package/src/view.test.ts +0 -157
  345. package/src/view.ts +0 -0
@@ -0,0 +1,358 @@
1
+ // The one app shell: a single header row over four full-height panes - Threads, Thread, Changes,
2
+ // Project - divided by straight rules; each header cell's thin bottom rule shares the divider gray so
3
+ // the whole header underline reads as one line joining the side rules.
4
+ // The right region (Changes + Project) toggles as a unit: the Project pane is the right sidebar and is
5
+ // always present when the region is on, Changes rides on top of it, and a thin rail holds the sidebar
6
+ // toggle when the region is closed. Each pane owns its own header controls; the thread header never does.
7
+
8
+ import React, { useEffect, useRef } from "react";
9
+ import { useTerminalDimensions } from "@opentui/react";
10
+ import type { BoxRenderable } from "@opentui/core";
11
+ import { DARK, type Theme } from "../../appearance/theme";
12
+ import { useFrameMeasure } from "../../ui/use-frame-measure";
13
+ import { truncateTitle } from "../../ui/components/truncate-title";
14
+ import { PanelColumn } from "./PanelColumn";
15
+ import { IconButton } from "../../ui/components/primitives/IconButton";
16
+ import { NERD, HEADER_UNDERLINE_CHARS } from "../../ui/components/primitives/icons";
17
+ import { TooltipProvider } from "../../ui/components/Tooltip";
18
+ import { RootOverlayProvider } from "../../ui/components/RootOverlay";
19
+ import type { ExtensionUIContext } from "@cueloop/extension-api/client";
20
+ import { ClientExtensionRegistry } from "../../integrations/client-extension-registry";
21
+ import {
22
+ ExtensionZone,
23
+ useClientExtensionSnapshot,
24
+ } from "../../integrations/components/ExtensionZone";
25
+ import { WorkspacePanels } from "./WorkspacePanels";
26
+
27
+ const EMPTY_EXTENSION_REGISTRY = new ClientExtensionRegistry();
28
+ const EMPTY_EXTENSION_CONTEXT: ExtensionUIContext = { workspace: null, threadId: null };
29
+
30
+ interface ExtensionInputs {
31
+ registry: ClientExtensionRegistry;
32
+ context: ExtensionUIContext;
33
+ }
34
+
35
+ function extensionInputs(
36
+ registry?: ClientExtensionRegistry,
37
+ context?: ExtensionUIContext,
38
+ ): ExtensionInputs {
39
+ return {
40
+ registry: registry ?? EMPTY_EXTENSION_REGISTRY,
41
+ context: context ?? EMPTY_EXTENSION_CONTEXT,
42
+ };
43
+ }
44
+
45
+ export type ProjectPanelMode = "changes" | "tree";
46
+
47
+ /** The thread title on one line: it shrinks with the header and tails off in an ellipsis rather than wrapping. */
48
+ function HeaderTitle({ title, color }: { title: string; color: string }): React.ReactNode {
49
+ const boxRef = useRef<BoxRenderable | null>(null);
50
+ const width = useFrameMeasure(
51
+ () => boxRef.current?.width ?? 0,
52
+ (left, right) => left === right,
53
+ 0,
54
+ );
55
+ const clipped = width > 0 ? truncateTitle(title, width) : title;
56
+
57
+ return (
58
+ <box ref={boxRef} style={{ flexShrink: 1, minWidth: 0 }}>
59
+ <text fg={color} wrapMode="none">
60
+ {clipped}
61
+ </text>
62
+ </box>
63
+ );
64
+ }
65
+
66
+ export interface AppShellProps {
67
+ sidebarOpen: boolean;
68
+ onToggleSidebar: () => void;
69
+ onOpenMenu: () => void;
70
+ threadsPanel: React.ReactNode;
71
+ threadTitle: string;
72
+ threadActions?: React.ReactNode;
73
+ threadPanel: React.ReactNode;
74
+ changesOpen: boolean;
75
+ projectOpen: boolean;
76
+ onToggleChanges: () => void;
77
+ onToggleProject: () => void;
78
+ onToggleRight: () => void;
79
+ projectMode: ProjectPanelMode;
80
+ changesPanel?: React.ReactNode;
81
+ changesFooter?: React.ReactNode;
82
+ projectPanel: React.ReactNode;
83
+ zoomHideThread?: boolean;
84
+ footer?: React.ReactNode;
85
+ children?: React.ReactNode;
86
+ theme?: Theme;
87
+ threadsWidth?: number;
88
+ projectWidth?: number;
89
+ onFocusPane?: (pane: FocusPane) => void;
90
+ focusedPane?: FocusPane;
91
+ extensionRegistry?: ClientExtensionRegistry;
92
+ extensionContext?: ExtensionUIContext;
93
+ onExtensionError?: (message: string) => void;
94
+ }
95
+
96
+ export type FocusPane = "threads" | "thread" | "changes" | "project";
97
+
98
+ /** The right region's reserved width: the open project pane, nothing when the region is closed, else the collapsed rail. */
99
+ function rightRegionColumns(
100
+ projectOpen: boolean,
101
+ projectWidth: number,
102
+ rightRegionClosed: boolean,
103
+ collapsedRailWidth: number,
104
+ ): number {
105
+ if (projectOpen) return projectWidth;
106
+
107
+ return rightRegionClosed ? 0 : collapsedRailWidth;
108
+ }
109
+
110
+ /** The Thread header's right cluster: the owner actions, plus the reopen control when the region is closed. */
111
+ function threadHeaderRight(
112
+ threadActions: React.ReactNode,
113
+ reopenControl: React.ReactNode,
114
+ showReopen: boolean,
115
+ ): React.ReactNode {
116
+ if (!showReopen) return threadActions;
117
+
118
+ return (
119
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
120
+ {threadActions ? <box style={{ paddingRight: 2 }}>{threadActions}</box> : null}
121
+ {reopenControl}
122
+ </box>
123
+ );
124
+ }
125
+
126
+ export function AppShell({
127
+ sidebarOpen,
128
+ onToggleSidebar,
129
+ onOpenMenu,
130
+ threadsPanel,
131
+ threadTitle,
132
+ threadActions,
133
+ threadPanel,
134
+ changesOpen,
135
+ projectOpen,
136
+ onToggleChanges,
137
+ onToggleProject,
138
+ onToggleRight,
139
+ projectMode,
140
+ changesPanel,
141
+ changesFooter,
142
+ projectPanel,
143
+ zoomHideThread,
144
+ footer,
145
+ children,
146
+ theme,
147
+ threadsWidth = 30,
148
+ projectWidth = 32,
149
+ onFocusPane,
150
+ focusedPane,
151
+ extensionRegistry,
152
+ extensionContext,
153
+ onExtensionError,
154
+ }: AppShellProps): React.ReactNode {
155
+ const { registry, context } = extensionInputs(extensionRegistry, extensionContext);
156
+ const extensionSnapshot = useClientExtensionSnapshot(registry);
157
+
158
+ useEffect(() => {
159
+ if (extensionSnapshot.lastError) onExtensionError?.(extensionSnapshot.lastError);
160
+ }, [extensionSnapshot.lastError, onExtensionError]);
161
+
162
+ const tokens = theme ?? DARK;
163
+ const { width: terminalWidth } = useTerminalDimensions();
164
+ // with both panels closed the right region collapses to nothing: the reopen toggle
165
+ // rides the thread header, and the middle reclaims the width the rail would have taken
166
+ const rightRegionClosed = !projectOpen && !changesOpen;
167
+ // Thread and Changes would otherwise split the middle as two flexBasis-0 items, and Yoga adds the
168
+ // Changes left border on top of its share: at even widths the halves come out fractional, the
169
+ // Changes side rounds up, and the row overflows a cell under the Project border. Sizing the Thread
170
+ // pane to a whole number leaves Changes the lone flex item, which lays out exactly.
171
+ const collapsedRailWidth = 4;
172
+ const rightRegionWidth = rightRegionColumns(
173
+ projectOpen,
174
+ projectWidth,
175
+ rightRegionClosed,
176
+ collapsedRailWidth,
177
+ );
178
+ const middleWidth = terminalWidth - (sidebarOpen ? threadsWidth : 0) - rightRegionWidth;
179
+ const threadPaneWidth =
180
+ changesOpen && !zoomHideThread ? Math.max(20, Math.floor(middleWidth / 2)) : undefined;
181
+
182
+ // gear + sidebar toggle + product mark: global chrome, in the Threads header when open, else at the
183
+ // left of the Thread header when the Threads pane is collapsed
184
+ const brandChrome = (
185
+ <box style={{ flexDirection: "row", flexShrink: 0 }}>
186
+ <box onMouseUp={onOpenMenu} style={{ paddingRight: 2 }}>
187
+ <text fg={tokens.textMuted}>{NERD.settings}</text>
188
+ </box>
189
+ <IconButton
190
+ glyph={sidebarOpen ? NERD.sidebarLeft : NERD.sidebarLeftOff}
191
+ onPress={onToggleSidebar}
192
+ tip="Toggle Threads"
193
+ marginRight={2}
194
+ theme={tokens}
195
+ />
196
+ <text fg={tokens.accent}>cueloop</text>
197
+ </box>
198
+ );
199
+
200
+ // the reopen control - a divider tick then the toggle - shared by the collapsed
201
+ // rail and, when the rail is suppressed, the thread header
202
+ const reopenRightControl = (
203
+ <>
204
+ <text fg={tokens.border}>{"│"}</text>
205
+ <IconButton
206
+ glyph={NERD.sidebarRightOff}
207
+ onPress={onToggleRight}
208
+ tip="Toggle Sidebar"
209
+ marginLeft={1}
210
+ theme={tokens}
211
+ />
212
+ </>
213
+ );
214
+
215
+ return (
216
+ <box
217
+ style={{
218
+ flexDirection: "column",
219
+ width: "100%",
220
+ height: "100%",
221
+ backgroundColor: tokens.background,
222
+ }}
223
+ >
224
+ <RootOverlayProvider>
225
+ <TooltipProvider theme={tokens}>
226
+ <box style={{ flexGrow: 1, flexDirection: "row" }}>
227
+ {sidebarOpen ? (
228
+ <PanelColumn
229
+ width={threadsWidth}
230
+ border="right"
231
+ header={brandChrome}
232
+ onFocus={() => onFocusPane?.("threads")}
233
+ theme={tokens}
234
+ >
235
+ {threadsPanel}
236
+ <ExtensionZone
237
+ zone="threads.sidebar"
238
+ registry={registry}
239
+ context={context}
240
+ onError={onExtensionError}
241
+ />
242
+ </PanelColumn>
243
+ ) : null}
244
+ {!zoomHideThread ? (
245
+ <PanelColumn
246
+ width={threadPaneWidth}
247
+ header={
248
+ <box style={{ flexDirection: "row", minWidth: 0 }}>
249
+ {!sidebarOpen ? (
250
+ <box style={{ paddingRight: 2, flexShrink: 0 }}>{brandChrome}</box>
251
+ ) : null}
252
+ {threadTitle ? (
253
+ <HeaderTitle title={threadTitle} color={tokens.textDim} />
254
+ ) : null}
255
+ </box>
256
+ }
257
+ // with the region closed, the reopen toggle rides the thread header instead of an empty column
258
+ headerRight={threadHeaderRight(
259
+ extensionSnapshot.actions.length > 0 ? (
260
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
261
+ {threadActions}
262
+ <ExtensionZone
263
+ zone="thread.header"
264
+ registry={registry}
265
+ context={context}
266
+ onError={onExtensionError}
267
+ />
268
+ </box>
269
+ ) : (
270
+ threadActions
271
+ ),
272
+ reopenRightControl,
273
+ rightRegionClosed,
274
+ )}
275
+ onFocus={() => onFocusPane?.("thread")}
276
+ theme={tokens}
277
+ >
278
+ {threadPanel}
279
+ </PanelColumn>
280
+ ) : null}
281
+ {changesOpen ? (
282
+ <box
283
+ onMouseDown={() => onFocusPane?.("changes")}
284
+ style={{
285
+ flexDirection: "column",
286
+ flexGrow: 1,
287
+ flexBasis: 0,
288
+ minWidth: 0,
289
+ borderStyle: "single",
290
+ // zoom drops the Thread pane, whose sidebar rule already divides here, so drop this
291
+ // one then to avoid a double border
292
+ border: zoomHideThread ? [] : ["left"],
293
+ borderColor: tokens.border,
294
+ }}
295
+ >
296
+ <box style={{ flexGrow: 1, minHeight: 0 }}>{changesPanel}</box>
297
+ {zoomHideThread ? changesFooter : null}
298
+ </box>
299
+ ) : null}
300
+ {projectOpen ? (
301
+ <WorkspacePanels
302
+ width={projectWidth}
303
+ projectMode={projectMode}
304
+ projectPanel={projectPanel}
305
+ onToggleChanges={onToggleChanges}
306
+ onToggleProject={onToggleProject}
307
+ onToggleRight={onToggleRight}
308
+ onFocus={() => onFocusPane?.("project")}
309
+ focused={focusedPane === "project"}
310
+ registry={registry}
311
+ context={context}
312
+ theme={tokens}
313
+ onExtensionError={onExtensionError}
314
+ />
315
+ ) : rightRegionClosed ? null : (
316
+ // Changes open but Project closed: a divider tick above the continuous header underline holds the reopen toggle;
317
+ // the rule lives in the header only, never running the pane's full height. The extra column
318
+ // of right padding keeps the reopen icon off the terminal's last column, which squeezes it
319
+ <box style={{ flexDirection: "column", width: 4 }}>
320
+ <box
321
+ customBorderChars={HEADER_UNDERLINE_CHARS}
322
+ style={{
323
+ flexDirection: "row",
324
+ alignItems: "center",
325
+ height: 2,
326
+ paddingRight: 1,
327
+ backgroundColor: tokens.panel,
328
+ borderStyle: "single",
329
+ border: ["bottom"],
330
+ borderColor: tokens.border,
331
+ }}
332
+ >
333
+ {reopenRightControl}
334
+ </box>
335
+ </box>
336
+ )}
337
+ </box>
338
+ {footer !== undefined ? (
339
+ <box
340
+ style={{
341
+ flexDirection: "row",
342
+ height: 1,
343
+ paddingLeft: 1,
344
+ paddingRight: 1,
345
+ borderStyle: "single",
346
+ border: ["top"],
347
+ borderColor: tokens.border,
348
+ }}
349
+ >
350
+ {footer}
351
+ </box>
352
+ ) : null}
353
+ {children}
354
+ </TooltipProvider>
355
+ </RootOverlayProvider>
356
+ </box>
357
+ );
358
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Regression: the Changes header controls keep their column when zoom hides the Thread
3
+ * pane. Thread and Changes used to split the middle as two flexBasis-0 items, and Yoga
4
+ * added the Changes left border on top of its share, so at even terminal widths the
5
+ * Changes pane overflowed a cell under the Project border and the zoom icon appeared to
6
+ * jump left on zoom. The shell now sizes the Thread pane to a whole number.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { testRender } from "@opentui/react/test-utils";
11
+ import React from "react";
12
+ import { AppShell } from "./AppShell";
13
+ import { EditorGrid } from "../../workbench/components/EditorGrid";
14
+ import { changesTab, makeGroup, splitGroup } from "../../workbench/components/editor-grid";
15
+ import { NERD } from "../../ui/components/primitives/icons";
16
+ import { allowEventLoopUpdates, settle } from "../../testing/test-support";
17
+
18
+ const grid = makeGroup([changesTab()]);
19
+
20
+ async function zoomIconColumn(
21
+ width: number,
22
+ zoomed: boolean,
23
+ sidebarOpen: boolean,
24
+ splitEditor = false,
25
+ ): Promise<number> {
26
+ const editor = splitEditor
27
+ ? splitGroup(grid, grid.id, "right")
28
+ : { tree: grid, focusGroupId: grid.id };
29
+ const setup = await testRender(
30
+ <AppShell
31
+ sidebarOpen={sidebarOpen}
32
+ onToggleSidebar={() => {}}
33
+ onOpenMenu={() => {}}
34
+ threadsPanel={<text>threads</text>}
35
+ threadTitle="thread"
36
+ threadPanel={<text>body</text>}
37
+ changesOpen
38
+ projectOpen
39
+ onToggleChanges={() => {}}
40
+ onToggleProject={() => {}}
41
+ onToggleRight={() => {}}
42
+ projectMode="changes"
43
+ zoomHideThread={zoomed}
44
+ projectPanel={<text>project</text>}
45
+ changesPanel={
46
+ <EditorGrid
47
+ tree={editor.tree}
48
+ focusedGroupId={editor.focusGroupId}
49
+ onFocusGroup={() => {}}
50
+ onActivateTab={() => {}}
51
+ onCloseTab={() => {}}
52
+ onSplit={() => {}}
53
+ onZoom={() => {}}
54
+ zoomed={zoomed}
55
+ renderTab={() => <text>diff</text>}
56
+ />
57
+ }
58
+ />,
59
+ { width, height: 8 },
60
+ );
61
+
62
+ // Split widths commit through React after the frame measurement. Renderer idle alone
63
+ // can still expose the initial fractional split, before its whole-cell widths land.
64
+ await settle(setup);
65
+ const column = setup.captureCharFrame().split("\n")[0]!.indexOf(NERD.zoom);
66
+
67
+ setup.renderer.destroy();
68
+
69
+ return column;
70
+ }
71
+
72
+ describe("zoom icon in the shell", () => {
73
+ // even widths are where the fractional split used to overflow; odd widths never did
74
+ for (const width of [160, 161]) {
75
+ for (const sidebarOpen of [false, true]) {
76
+ test(`stays in its column when zoom toggles (width ${width}, sidebar ${sidebarOpen ? "open" : "closed"})`, async () => {
77
+ // Act
78
+ const unzoomed = await zoomIconColumn(width, false, sidebarOpen);
79
+ const zoomed = await zoomIconColumn(width, true, sidebarOpen);
80
+
81
+ // Assert
82
+ expect(unzoomed).toBeGreaterThan(0);
83
+ expect(zoomed).toBe(unzoomed);
84
+ });
85
+ }
86
+ }
87
+
88
+ test("the footer rides the Changes pane only while the Thread pane is zoomed away", async () => {
89
+ async function footerVisible(zoomed: boolean): Promise<boolean> {
90
+ const setup = await testRender(
91
+ <AppShell
92
+ sidebarOpen={false}
93
+ onToggleSidebar={() => {}}
94
+ onOpenMenu={() => {}}
95
+ threadsPanel={<text>threads</text>}
96
+ threadTitle="thread"
97
+ threadPanel={<text>body</text>}
98
+ changesOpen
99
+ projectOpen
100
+ onToggleChanges={() => {}}
101
+ onToggleProject={() => {}}
102
+ onToggleRight={() => {}}
103
+ projectMode="changes"
104
+ zoomHideThread={zoomed}
105
+ projectPanel={<text>project</text>}
106
+ changesFooter={<text>send message</text>}
107
+ changesPanel={<text>diff</text>}
108
+ />,
109
+ { width: 160, height: 8 },
110
+ );
111
+
112
+ allowEventLoopUpdates();
113
+ await setup.waitForVisualIdle();
114
+ const shown = setup.captureCharFrame().includes("send message");
115
+
116
+ setup.renderer.destroy();
117
+
118
+ return shown;
119
+ }
120
+
121
+ // Act
122
+ const [zoomed, unzoomed] = await Promise.all([footerVisible(true), footerVisible(false)]);
123
+
124
+ // Assert - the Thread pane carries the footer normally, so it must not double up in Changes
125
+ expect(zoomed).toBe(true);
126
+ expect(unzoomed).toBe(false);
127
+ });
128
+
129
+ test("the icon clears the Project border by its padding in both states", async () => {
130
+ // Act
131
+ const setupColumns = await Promise.all([
132
+ zoomIconColumn(160, false, false),
133
+ zoomIconColumn(160, true, false),
134
+ ]);
135
+
136
+ // Assert - width 160, project pane 32: the border sits at 128 and the glyph one padding cell
137
+ // before it, the same inset the right-sidebar icon keeps from its own edge
138
+ for (const column of setupColumns) expect(128 - column).toBe(2);
139
+ });
140
+
141
+ test("a split editor keeps the zoom icon clear of the Project border", async () => {
142
+ const columns = await Promise.all([
143
+ zoomIconColumn(160, false, false, true),
144
+ zoomIconColumn(160, true, false, true),
145
+ ]);
146
+
147
+ for (const column of columns) expect(128 - column).toBe(2);
148
+ });
149
+ });
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { Breadcrumb } from "./Breadcrumb";
5
+
6
+ export const meta: StoryMeta = { title: "Chrome/Breadcrumb" };
7
+
8
+ export const SessionHeader: Story = {
9
+ render: () => (
10
+ <Breadcrumb
11
+ items={[
12
+ { label: "cueloop", tone: "accent" },
13
+ { label: "Migration Plan · rev 2", tone: "dim" },
14
+ { label: "resolved: approved", tone: "green" },
15
+ ]}
16
+ />
17
+ ),
18
+ expectedColors: [DARK.accent, DARK.green],
19
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * One-row breadcrumb trail: toned segments joined by " · " on the panel
3
+ * surface. The app header is a breadcrumb (product · artifact · revision)
4
+ * followed by state badges; each item picks a semantic tone.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { Theme } from "../../appearance/theme";
9
+ import { useComponentTheme } from "../../appearance/components/theme-context";
10
+
11
+ export type BreadcrumbTone = "accent" | "dim" | "text" | "green";
12
+
13
+ export interface BreadcrumbItem {
14
+ label: string;
15
+ tone?: BreadcrumbTone;
16
+ }
17
+
18
+ export interface BreadcrumbProps {
19
+ items: BreadcrumbItem[];
20
+ theme?: Theme;
21
+ }
22
+
23
+ export function Breadcrumb({ items, theme }: BreadcrumbProps): React.ReactNode {
24
+ const tokens = useComponentTheme(theme);
25
+ const toneColor = (tone?: BreadcrumbTone): string =>
26
+ tone === "accent"
27
+ ? tokens.accent
28
+ : tone === "green"
29
+ ? tokens.green
30
+ : tone === "text"
31
+ ? tokens.text
32
+ : tokens.textDim;
33
+
34
+ return (
35
+ <box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1, flexDirection: "row" }}>
36
+ <text fg={tokens.text}>
37
+ {items.map((item, index) => (
38
+ <span key={index} fg={toneColor(item.tone)}>
39
+ {index === 0 ? item.label : ` · ${item.label}`}
40
+ </span>
41
+ ))}
42
+ </text>
43
+ </box>
44
+ );
45
+ }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { MenuBar } from "./MenuBar";
5
+
6
+ export const meta: StoryMeta = { title: "Chrome/MenuBar" };
7
+
8
+ const callbacks = { onSettings: () => {}, onKeybinds: () => {} };
9
+
10
+ function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNode {
11
+ return (
12
+ <box
13
+ style={{ flexDirection: "column", width: "100%", height: "100%", justifyContent: "flex-end" }}
14
+ >
15
+ {children}
16
+ </box>
17
+ );
18
+ }
19
+
20
+ export const Open: Story = {
21
+ render: () => (
22
+ <BottomFrame>
23
+ <MenuBar open {...callbacks} />
24
+ </BottomFrame>
25
+ ),
26
+ expectedColors: [DARK.text],
27
+ size: { width: 60, height: 8 },
28
+ };
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import type { Theme } from "../../appearance/theme";
3
+ import { useComponentTheme } from "../../appearance/components/theme-context";
4
+ import { FRAME_BORDER_STYLE } from "../../ui/components/primitives/frame";
5
+
6
+ export interface MenuBarProps {
7
+ open: boolean;
8
+ onSettings: () => void;
9
+ onKeybinds: () => void;
10
+ theme?: Theme;
11
+ }
12
+
13
+ const DROPDOWN_OPTION_ROWS = 2;
14
+
15
+ /** The settings gear's drop-down: Settings and Keybinds, below the top-left gear. */
16
+ export function MenuBar({ open, onSettings, onKeybinds, theme }: MenuBarProps): React.ReactNode {
17
+ const tokens = useComponentTheme(theme);
18
+ const dropdownHeight = DROPDOWN_OPTION_ROWS + 2;
19
+
20
+ if (!open) return null;
21
+
22
+ return (
23
+ <box
24
+ style={{
25
+ position: "absolute",
26
+ left: 1,
27
+ top: 2,
28
+ width: 16,
29
+ height: dropdownHeight,
30
+ border: true,
31
+ borderStyle: FRAME_BORDER_STYLE,
32
+ borderColor: tokens.text,
33
+ flexDirection: "column",
34
+ }}
35
+ >
36
+ <box onMouseUp={onSettings}>
37
+ <text fg={tokens.text}> Settings</text>
38
+ </box>
39
+ <box onMouseUp={onKeybinds}>
40
+ <text fg={tokens.text}> Keybinds</text>
41
+ </box>
42
+ </box>
43
+ );
44
+ }