@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,118 @@
1
+ /**
2
+ * Regression: file tabs keep the width of their name and the strip scrolls horizontally
3
+ * behind the header controls when the row overflows, revealing the active tab - instead of
4
+ * squeezing every tab and truncating its label.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import React from "react";
10
+ import { EditorGrid, visibleTabWindow } from "./EditorGrid";
11
+ import { changesTab, fileTab, makeGroup, type EditorTab } from "./editor-grid";
12
+ import { NERD } from "../../ui/components/primitives/icons";
13
+ import { allowEventLoopUpdates } from "../../testing/test-support";
14
+
15
+ describe("visibleTabWindow", () => {
16
+ const tabs: EditorTab[] = ["aaaa", "bbbb", "cccc", "dddd", "eeee"].map((name) =>
17
+ fileTab(name, name, "diff"),
18
+ );
19
+
20
+ test("an unmeasured strip shows every tab", () => {
21
+ // Arrange / Act / Assert - width 0 means the header clips until it is measured
22
+ expect(visibleTabWindow(tabs, 0, 0, 0)).toEqual({ first: 0, end: tabs.length });
23
+ });
24
+
25
+ test("only the run that fits renders, and the active tab pulls the window to it", () => {
26
+ // Arrange - each tab is 9 cells (4 label + 5 chrome); a 30-cell strip fits about three
27
+ // Act - the last tab is active
28
+ const window = visibleTabWindow(tabs, 0, tabs.length - 1, 30);
29
+
30
+ // Assert - the window ends on the active tab and dropped earlier tabs
31
+ expect(window.end).toBe(tabs.length);
32
+ expect(window.first).toBeGreaterThan(0);
33
+ });
34
+
35
+ test("a tab wider than the strip still shows on its own", () => {
36
+ // Arrange
37
+ const wide = [fileTab("a-very-long-file-name.tsx", "x", "diff")];
38
+
39
+ // Act / Assert
40
+ expect(visibleTabWindow(wide, 0, 0, 10)).toEqual({ first: 0, end: 1 });
41
+ });
42
+
43
+ test("the comment badge counts toward a tab's width, so a commented run does not overflow", () => {
44
+ // Arrange - three 9-cell tabs fit a 27-cell strip exactly with no badge
45
+ const three = ["aaaa", "bbbb", "cccc"].map((name) => fileTab(name, name, "diff"));
46
+
47
+ // Act - without badges all three fit; a ` ● 2` badge on each widens them past the strip
48
+ const bare = visibleTabWindow(three, 0, 0, 27);
49
+ const badged = visibleTabWindow(three, 0, 0, 27, () => 2);
50
+
51
+ // Assert - the badge width narrows the window rather than clipping the last tab
52
+ expect(bare.end).toBe(3);
53
+ expect(badged.end).toBeLessThan(3);
54
+ });
55
+ });
56
+
57
+ const NAMES = [
58
+ "AppShell.tsx",
59
+ "DiffContentView.tsx",
60
+ "IconButton.tsx",
61
+ "EditorGrid.zoom.test.tsx",
62
+ "ThreadView.tsx",
63
+ ];
64
+
65
+ async function headerRow(activeIndex: number): Promise<string> {
66
+ const group = makeGroup([
67
+ changesTab(),
68
+ ...NAMES.map((name) => fileTab(name, `src/${name}`, "diff")),
69
+ ]);
70
+ const tree = { ...group, activeTabId: group.tabs[activeIndex]!.id };
71
+ const setup = await testRender(
72
+ <EditorGrid
73
+ tree={tree}
74
+ focusedGroupId={tree.id}
75
+ onFocusGroup={() => {}}
76
+ onActivateTab={() => {}}
77
+ onCloseTab={() => {}}
78
+ onSplit={() => {}}
79
+ onZoom={() => {}}
80
+ zoomed={false}
81
+ renderTab={() => <text>body</text>}
82
+ />,
83
+ { width: 70, height: 4 },
84
+ );
85
+
86
+ allowEventLoopUpdates();
87
+ await setup.waitForVisualIdle();
88
+ // the strip reveals the active tab on the frame after layout
89
+ await setup.waitForVisualIdle();
90
+ const row = setup.captureCharFrame().split("\n")[0]!;
91
+
92
+ setup.renderer.destroy();
93
+
94
+ return row;
95
+ }
96
+
97
+ describe("editor tab strip", () => {
98
+ test("tabs keep their full names and the controls stay pinned when the row overflows", async () => {
99
+ // Act
100
+ const row = await headerRow(0);
101
+
102
+ // Assert - no squeezed labels: every visible tab reads in full up to the clip edge
103
+ expect(row).toContain("Changes ");
104
+ expect(row).toContain("AppShell.tsx ");
105
+ expect(row).toContain("DiffContentView.tsx ");
106
+ // and the header controls sit at the right edge, past the clipped strip (search is out for now)
107
+ expect(row.trimEnd().endsWith(NERD.zoom)).toBe(true);
108
+ });
109
+
110
+ test("the strip scrolls to reveal the active tab", async () => {
111
+ // Act
112
+ const row = await headerRow(NAMES.length);
113
+
114
+ // Assert - the last tab is on screen, the first has scrolled out behind the left edge
115
+ expect(row).toContain("ThreadView.tsx ");
116
+ expect(row).not.toContain("Changes ");
117
+ });
118
+ });
@@ -0,0 +1,552 @@
1
+ // Renders the Changes pane's editor grid: recursive branches split into rows/columns, each leaf an
2
+ // editor group with its own tab strip and header controls. The caller supplies renderTab so the grid
3
+ // stays layout-only - a tab becomes a diff, a file's contents, or the aggregate Changes view upstream.
4
+
5
+ import React, { useEffect, useRef, useState } from "react";
6
+ import type { BoxRenderable } from "@opentui/core";
7
+ import { isDoubleClick, type ClickStamp } from "../../annotations/thread-selection";
8
+ import { DARK, type Theme } from "../../appearance/theme";
9
+ import { useFrameMeasure } from "../../ui/use-frame-measure";
10
+ import { useSharedKeyboard } from "../../keyboard/use-shared-keyboard";
11
+ import { useRootOverlay } from "../../ui/components/RootOverlay";
12
+ import { useMenuControl } from "../../ui/components/menu-control";
13
+ import { IconButton } from "../../ui/components/primitives/IconButton";
14
+ import { NERD, HEADER_UNDERLINE_CHARS } from "../../ui/components/primitives/icons";
15
+ import {
16
+ countEditorGroups,
17
+ MAX_EDITOR_GROUPS,
18
+ type EditorBranch,
19
+ type EditorGroup,
20
+ type EditorNode,
21
+ type EditorTab,
22
+ type SplitDirection,
23
+ } from "./editor-grid";
24
+
25
+ export interface EditorGridProps {
26
+ tree: EditorNode;
27
+ focusedGroupId: string | null;
28
+ onFocusGroup: (groupId: string) => void;
29
+ onActivateTab: (groupId: string, tabId: string) => void;
30
+ onKeepTab?: (groupId: string, tabId: string) => void;
31
+ onCloseTab: (groupId: string, tabId: string) => void;
32
+ onSplit: (groupId: string, direction: SplitDirection) => void;
33
+ onZoom: () => void;
34
+ zoomed: boolean;
35
+ renderTab: (tab: EditorTab, groupFocused: boolean) => React.ReactNode;
36
+ commentCounts?: ReadonlyMap<string, number>;
37
+ theme?: Theme;
38
+ }
39
+
40
+ const SPLIT_ITEMS: ReadonlyArray<{ label: string; direction: SplitDirection; arrow: string }> = [
41
+ { label: "left", direction: "left", arrow: "←" },
42
+ { label: "right", direction: "right", arrow: "→" },
43
+ { label: "up", direction: "up", arrow: "↑" },
44
+ { label: "down", direction: "down", arrow: "↓" },
45
+ ];
46
+
47
+ const MENU_WIDTH = 12;
48
+ const MIN_GROUP_WIDTH = 24;
49
+ const MIN_GROUP_HEIGHT = 8;
50
+
51
+ function canSplitGroup(width: number, height: number, direction: SplitDirection): boolean {
52
+ return direction === "left" || direction === "right"
53
+ ? width >= MIN_GROUP_WIDTH * 2 + 1
54
+ : height >= MIN_GROUP_HEIGHT * 2 + 1;
55
+ }
56
+
57
+ /** An arrow key maps straight to its split direction; other keys leave the menu untouched. */
58
+ function splitDirectionForKey(name: string): SplitDirection | null {
59
+ const item = SPLIT_ITEMS.find((entry) => entry.direction === name);
60
+
61
+ return item ? item.direction : null;
62
+ }
63
+
64
+ function EditorTabButton({
65
+ tab,
66
+ active,
67
+ commentCount,
68
+ onSelect,
69
+ onKeep,
70
+ onClose,
71
+ tokens,
72
+ }: {
73
+ tab: EditorTab;
74
+ active: boolean;
75
+ /** Comments on this tab's file, shown as a dot-and-count badge; 0 shows nothing. */
76
+ commentCount: number;
77
+ onSelect: () => void;
78
+ onKeep?: () => void;
79
+ onClose: () => void;
80
+ tokens: Theme;
81
+ }): React.ReactNode {
82
+ const [hovered, setHovered] = useState(false);
83
+ const lastClick = useRef<ClickStamp | null>(null);
84
+
85
+ return (
86
+ <box
87
+ id={tab.id}
88
+ onMouseUp={(event) => {
89
+ const stamp = { time: Date.now(), x: event.x, y: event.y };
90
+ const doubleClick = isDoubleClick(lastClick.current, stamp);
91
+
92
+ lastClick.current = stamp;
93
+ onSelect();
94
+
95
+ if (doubleClick && tab.preview) onKeep?.();
96
+ }}
97
+ onMouseOver={() => setHovered(true)}
98
+ onMouseOut={() => setHovered(false)}
99
+ style={{
100
+ flexDirection: "row",
101
+ // a tab keeps the width of its name; the strip scrolls when the row overflows
102
+ flexShrink: 0,
103
+ paddingLeft: 1,
104
+ paddingRight: 1,
105
+ backgroundColor: active ? tokens.background : undefined,
106
+ borderStyle: "single",
107
+ border: ["right"],
108
+ borderColor: tokens.border,
109
+ }}
110
+ >
111
+ <text fg={active ? tokens.textMuted : tokens.textDim} style={{ wrapMode: "none" }}>
112
+ {tab.label}
113
+ </text>
114
+ {commentCount > 0 ? (
115
+ <text
116
+ fg={tokens.textDim}
117
+ style={{ flexShrink: 0, wrapMode: "none" }}
118
+ >{` ● ${commentCount}`}</text>
119
+ ) : null}
120
+ <box
121
+ onMouseUp={(event) => {
122
+ event.stopPropagation();
123
+ onClose();
124
+ }}
125
+ style={{ paddingLeft: 1 }}
126
+ >
127
+ <text fg={tokens.textDim}>{hovered ? NERD.close : " "}</text>
128
+ </box>
129
+ </box>
130
+ );
131
+ }
132
+
133
+ /** Cells a tab occupies: padding, label, the ` ● N` comment badge when present, the close cell, the right rule. */
134
+ export function tabCellWidth(tab: EditorTab, commentCount = 0): number {
135
+ const badge = commentCount > 0 ? 3 + String(commentCount).length : 0;
136
+
137
+ return tab.label.length + 5 + badge;
138
+ }
139
+
140
+ /** The run of tabs `[first, end)` the strip shows. */
141
+ export interface TabWindow {
142
+ first: number;
143
+ end: number;
144
+ }
145
+
146
+ /** The run of tabs that fits the strip and contains the active tab; markers take a cell each side. */
147
+ export function visibleTabWindow(
148
+ tabs: EditorTab[],
149
+ firstVisible: number,
150
+ activeIndex: number,
151
+ stripWidth: number,
152
+ commentCount: (tab: EditorTab) => number = () => 0,
153
+ ): TabWindow {
154
+ // unmeasured strip: show everything, the header clips until the width is known
155
+ if (stripWidth <= 0) return { first: 0, end: tabs.length };
156
+
157
+ const width = (index: number): number => tabCellWidth(tabs[index]!, commentCount(tabs[index]!));
158
+ const fitEnd = (first: number): number => {
159
+ let used = first > 0 ? 1 : 0;
160
+ let end = first;
161
+
162
+ while (end < tabs.length) {
163
+ const remaining = end + 1 < tabs.length ? 1 : 0;
164
+
165
+ if (used + width(end) + remaining > stripWidth) break;
166
+
167
+ used += width(end);
168
+ end++;
169
+ }
170
+
171
+ // always show at least the tab the window starts on, even when it is wider than the strip
172
+ return Math.max(end, Math.min(first + 1, tabs.length));
173
+ };
174
+ let first = Math.min(Math.max(0, firstVisible), Math.max(0, activeIndex));
175
+ let end = fitEnd(first);
176
+
177
+ // the active tab past the right edge pulls the window along until it fits
178
+ while (activeIndex >= end && first < activeIndex) {
179
+ first++;
180
+ end = fitEnd(first);
181
+ }
182
+
183
+ return { first, end };
184
+ }
185
+
186
+ function SplitMenu({
187
+ onPick,
188
+ canPick,
189
+ tokens,
190
+ }: {
191
+ onPick: (direction: SplitDirection) => void;
192
+ canPick: (direction: SplitDirection) => boolean;
193
+ tokens: Theme;
194
+ }): React.ReactNode {
195
+ return (
196
+ <box
197
+ onMouseUp={(event) => event.stopPropagation()}
198
+ style={{
199
+ width: MENU_WIDTH,
200
+ flexDirection: "column",
201
+ borderStyle: "single",
202
+ borderColor: tokens.border,
203
+ backgroundColor: tokens.elevated,
204
+ }}
205
+ >
206
+ {SPLIT_ITEMS.map((item) => (
207
+ <box
208
+ key={item.direction}
209
+ onMouseUp={canPick(item.direction) ? () => onPick(item.direction) : undefined}
210
+ style={{ flexDirection: "row", paddingLeft: 1, paddingRight: 1 }}
211
+ >
212
+ <text fg={canPick(item.direction) ? tokens.text : tokens.textDim}>{item.label}</text>
213
+ <box style={{ flexGrow: 1 }} />
214
+ <text fg={tokens.textDim}>{item.arrow}</text>
215
+ </box>
216
+ ))}
217
+ </box>
218
+ );
219
+ }
220
+
221
+ function EditorGroupPane({
222
+ group,
223
+ props,
224
+ showZoom,
225
+ splitDisabled,
226
+ }: {
227
+ group: EditorGroup;
228
+ props: EditorGridProps;
229
+ showZoom: boolean;
230
+ splitDisabled: boolean;
231
+ }): React.ReactNode {
232
+ const tokens = props.theme ?? DARK;
233
+ const menuControl = useMenuControl();
234
+ const { setOverlay, clearOverlay } = useRootOverlay();
235
+ const menuId = `split:${group.id}`;
236
+ const menuOpen = menuControl.openMenuId === menuId;
237
+ const groupRef = useRef<BoxRenderable | null>(null);
238
+ const groupSize = useFrameMeasure(
239
+ () => ({ width: groupRef.current?.width ?? 0, height: groupRef.current?.height ?? 0 }),
240
+ (left, right) => left.width === right.width && left.height === right.height,
241
+ { width: 0, height: 0 },
242
+ );
243
+ const canSplit = (direction: SplitDirection): boolean =>
244
+ !splitDisabled && canSplitGroup(groupSize.width, groupSize.height, direction);
245
+ const cannotSplit = !SPLIT_ITEMS.some((item) => canSplit(item.direction));
246
+ const splitRef = useRef<BoxRenderable | null>(null);
247
+ const anchor = useFrameMeasure(
248
+ () => ({
249
+ x: splitRef.current?.x ?? 0,
250
+ y: splitRef.current?.y ?? 0,
251
+ width: splitRef.current?.width ?? 0,
252
+ }),
253
+ (left, right) => left.x === right.x && left.y === right.y && left.width === right.width,
254
+ { x: 0, y: 0, width: 0 },
255
+ menuOpen,
256
+ );
257
+ const active = group.tabs.find((tab) => tab.id === group.activeTabId) ?? group.tabs[0];
258
+
259
+ useSharedKeyboard((key) => {
260
+ if (!menuOpen) return;
261
+
262
+ if (key.name === "escape") return menuControl.closeMenu();
263
+
264
+ const direction = splitDirectionForKey(key.name);
265
+
266
+ if (direction && canSplit(direction)) {
267
+ props.onSplit(group.id, direction);
268
+ menuControl.closeMenu();
269
+ }
270
+ });
271
+
272
+ useEffect(() => {
273
+ if (!menuOpen) return;
274
+
275
+ setOverlay(
276
+ "split-menu",
277
+ <box
278
+ onMouseUp={menuControl.closeMenu}
279
+ style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%" }}
280
+ >
281
+ <box
282
+ style={{
283
+ position: "absolute",
284
+ top: anchor.y + 1,
285
+ left: Math.max(0, anchor.x + anchor.width - MENU_WIDTH),
286
+ }}
287
+ >
288
+ <SplitMenu
289
+ onPick={(direction) => {
290
+ if (!canSplit(direction)) return;
291
+
292
+ props.onSplit(group.id, direction);
293
+ menuControl.closeMenu();
294
+ }}
295
+ canPick={canSplit}
296
+ tokens={tokens}
297
+ />
298
+ </box>
299
+ </box>,
300
+ );
301
+
302
+ return () => clearOverlay("split-menu");
303
+ // eslint-disable-next-line react-hooks/exhaustive-deps
304
+ }, [menuOpen, anchor, tokens, groupSize.width, groupSize.height, splitDisabled]);
305
+ // The strip windows its tabs rather than scrolling a nested scrollbox (which stalls the
306
+ // renderer inside this header): tabs keep the width of their names, only the run that fits
307
+ // renders, and the window follows the active tab so a tab opened past the edge is never hidden.
308
+ const stripRef = useRef<BoxRenderable | null>(null);
309
+ const stripWidth = useFrameMeasure(
310
+ () => stripRef.current?.width ?? 0,
311
+ (left, right) => left === right,
312
+ 0,
313
+ );
314
+ const [firstVisible, setFirstVisible] = useState(0);
315
+ // a zoom re-anchors the strip to the leftmost tab, so the new width fills from the left
316
+ // and the step markers disappear when every tab now fits
317
+ const [wasZoomed, setWasZoomed] = useState(props.zoomed);
318
+
319
+ if (wasZoomed !== props.zoomed) {
320
+ setWasZoomed(props.zoomed);
321
+ setFirstVisible(0);
322
+ }
323
+
324
+ const activeIndex = Math.max(
325
+ 0,
326
+ group.tabs.findIndex((tab) => tab.id === active?.id),
327
+ );
328
+ // derived every render: the manual offset is a floor the active tab can pull the window past
329
+ const tabWindow = visibleTabWindow(group.tabs, firstVisible, activeIndex, stripWidth, (tab) =>
330
+ tab.path ? (props.commentCounts?.get(tab.path) ?? 0) : 0,
331
+ );
332
+
333
+ return (
334
+ <box
335
+ id={`editor-group:${group.id}`}
336
+ ref={groupRef}
337
+ onMouseDown={() => props.onFocusGroup(group.id)}
338
+ style={{ flexDirection: "column", flexGrow: 1, flexBasis: 0, minWidth: 0, minHeight: 0 }}
339
+ >
340
+ <box
341
+ customBorderChars={HEADER_UNDERLINE_CHARS}
342
+ style={{
343
+ flexDirection: "row",
344
+ // pin the controls to the right edge so their column never shifts with the container
345
+ // width parity; a lone flexGrow tab strip would drag them a cell on zoom
346
+ justifyContent: "space-between",
347
+ height: 2,
348
+ // match the pane headers: the panel token is transparent in the branded theme, so the
349
+ // header reads as terminal-through rather than a raised box
350
+ backgroundColor: tokens.panel,
351
+ borderStyle: "single",
352
+ border: ["bottom"],
353
+ borderColor: tokens.border,
354
+ }}
355
+ >
356
+ {/* the tab strip windows its tabs: the run that fits renders in full, edge markers step
357
+ the window, and the controls stay pinned on the right */}
358
+ <box
359
+ ref={stripRef}
360
+ style={{ flexDirection: "row", flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }}
361
+ >
362
+ {tabWindow.first > 0 ? (
363
+ <box
364
+ onMouseUp={(event) => {
365
+ event.stopPropagation();
366
+ setFirstVisible(tabWindow.first - 1);
367
+ }}
368
+ style={{ flexShrink: 0 }}
369
+ >
370
+ <text fg={tokens.textDim} selectable={false}>
371
+ {"‹"}
372
+ </text>
373
+ </box>
374
+ ) : null}
375
+ {group.tabs.slice(tabWindow.first, tabWindow.end).map((tab) => (
376
+ <EditorTabButton
377
+ key={tab.id}
378
+ tab={tab}
379
+ active={tab.id === active?.id}
380
+ commentCount={tab.path ? (props.commentCounts?.get(tab.path) ?? 0) : 0}
381
+ onSelect={() => props.onActivateTab(group.id, tab.id)}
382
+ onKeep={() => props.onKeepTab?.(group.id, tab.id)}
383
+ onClose={() => props.onCloseTab(group.id, tab.id)}
384
+ tokens={tokens}
385
+ />
386
+ ))}
387
+ {tabWindow.end < group.tabs.length ? (
388
+ <box
389
+ onMouseUp={(event) => {
390
+ event.stopPropagation();
391
+ setFirstVisible(tabWindow.first + 1);
392
+ }}
393
+ style={{ flexShrink: 0 }}
394
+ >
395
+ <text fg={tokens.textDim} selectable={false}>
396
+ {"›"}
397
+ </text>
398
+ </box>
399
+ ) : null}
400
+ </box>
401
+ <box style={{ flexDirection: "row", flexShrink: 0, paddingLeft: 1, paddingRight: 1 }}>
402
+ {/* search sits out until it does something; split takes its place */}
403
+ <box
404
+ id={`split-button:${group.id}`}
405
+ ref={splitRef}
406
+ style={{ flexShrink: 0, marginRight: showZoom ? 2 : 0 }}
407
+ >
408
+ <IconButton
409
+ glyph="split"
410
+ active={menuOpen}
411
+ disabled={cannotSplit}
412
+ onPress={() => menuControl.toggleMenu(menuId)}
413
+ tip={
414
+ splitDisabled
415
+ ? "Maximum 8 editor groups"
416
+ : cannotSplit
417
+ ? "Tile too small to split"
418
+ : undefined
419
+ }
420
+ theme={tokens}
421
+ />
422
+ </box>
423
+ {showZoom ? (
424
+ <IconButton
425
+ glyph={NERD.zoom}
426
+ active={props.zoomed}
427
+ onPress={props.onZoom}
428
+ tip={props.zoomed ? "Zoom Out" : "Zoom In"}
429
+ theme={tokens}
430
+ />
431
+ ) : null}
432
+ </box>
433
+ </box>
434
+ <box
435
+ style={{ flexGrow: 1, flexBasis: 0, minHeight: 0, minWidth: 0, flexDirection: "column" }}
436
+ >
437
+ {active ? props.renderTab(active, props.focusedGroupId === group.id) : null}
438
+ </box>
439
+ </box>
440
+ );
441
+ }
442
+
443
+ function GridNode({
444
+ node,
445
+ props,
446
+ showZoom,
447
+ splitDisabled,
448
+ }: {
449
+ node: EditorNode;
450
+ props: EditorGridProps;
451
+ showZoom: boolean;
452
+ splitDisabled: boolean;
453
+ }): React.ReactNode {
454
+ if (node.type === "group") {
455
+ return (
456
+ <EditorGroupPane
457
+ group={node}
458
+ props={props}
459
+ showZoom={showZoom}
460
+ splitDisabled={splitDisabled}
461
+ />
462
+ );
463
+ }
464
+
465
+ return (
466
+ <GridBranchPane node={node} props={props} showZoom={showZoom} splitDisabled={splitDisabled} />
467
+ );
468
+ }
469
+
470
+ function GridBranchPane({
471
+ node,
472
+ props,
473
+ showZoom,
474
+ splitDisabled,
475
+ }: {
476
+ node: EditorBranch;
477
+ props: EditorGridProps;
478
+ showZoom: boolean;
479
+ splitDisabled: boolean;
480
+ }): React.ReactNode {
481
+ const tokens = props.theme ?? DARK;
482
+ const horizontal = node.orientation === "horizontal";
483
+ const branchRef = useRef<BoxRenderable | null>(null);
484
+ const branchWidth = useFrameMeasure(
485
+ () => branchRef.current?.width ?? 0,
486
+ (left, right) => left === right,
487
+ 0,
488
+ );
489
+ const leadingWidth = Math.floor((branchWidth - node.children.length + 1) / node.children.length);
490
+
491
+ return (
492
+ <box
493
+ ref={branchRef}
494
+ style={{
495
+ flexDirection: horizontal ? "row" : "column",
496
+ flexGrow: 1,
497
+ flexBasis: 0,
498
+ minWidth: 0,
499
+ minHeight: 0,
500
+ }}
501
+ >
502
+ {node.children.map((child, index) => {
503
+ const fixedWidth = horizontal && index < node.children.length - 1 && leadingWidth > 0;
504
+
505
+ return (
506
+ <React.Fragment key={child.id}>
507
+ {index > 0 ? (
508
+ <box
509
+ style={{
510
+ borderStyle: "single",
511
+ border: [horizontal ? "left" : "top"],
512
+ borderColor: tokens.border,
513
+ }}
514
+ />
515
+ ) : null}
516
+ <box
517
+ style={{
518
+ flexDirection: "column",
519
+ // A fixed whole-cell share for leading horizontal groups leaves the last group
520
+ // the exact remainder. Equal fractional flex shares can overflow the right edge.
521
+ width: fixedWidth ? leadingWidth : undefined,
522
+ flexGrow: fixedWidth ? 0 : 1,
523
+ flexBasis: fixedWidth ? undefined : 0,
524
+ flexShrink: fixedWidth ? 0 : 1,
525
+ minWidth: 0,
526
+ minHeight: 0,
527
+ }}
528
+ >
529
+ <GridNode
530
+ node={child}
531
+ props={props}
532
+ showZoom={showZoom && index === (horizontal ? node.children.length - 1 : 0)}
533
+ splitDisabled={splitDisabled}
534
+ />
535
+ </box>
536
+ </React.Fragment>
537
+ );
538
+ })}
539
+ </box>
540
+ );
541
+ }
542
+
543
+ /** The Changes pane body: the editor grid of groups and splits, each rendering its active tab. */
544
+ export function EditorGrid(props: EditorGridProps): React.ReactNode {
545
+ const splitDisabled = countEditorGroups(props.tree) >= MAX_EDITOR_GROUPS;
546
+
547
+ return (
548
+ <box style={{ flexDirection: "column", flexGrow: 1, minWidth: 0, minHeight: 0 }}>
549
+ <GridNode node={props.tree} props={props} showZoom splitDisabled={splitDisabled} />
550
+ </box>
551
+ );
552
+ }