@chatcode/cco-better-sidebar 0.18.0-alpha.0

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 (329) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/README_EN.md +563 -0
  4. package/assets/logo.svg +1 -0
  5. package/cordis.patch.yml +79 -0
  6. package/lib/client-editor.js +55744 -0
  7. package/lib/client-mermaid.js +208256 -0
  8. package/lib/client-registry.js +30378 -0
  9. package/lib/client-terminal.js +16894 -0
  10. package/lib/client.js +30378 -0
  11. package/lib/index.js +6897 -0
  12. package/lib/invariant.js +22 -0
  13. package/lib/types/agent-opens.d.ts +65 -0
  14. package/lib/types/agent-pty.d.ts +226 -0
  15. package/lib/types/browser-probe.d.ts +15 -0
  16. package/lib/types/bundle-route.d.ts +12 -0
  17. package/lib/types/chatcode-update.d.ts +47 -0
  18. package/lib/types/client/AccountMenu.d.ts +1 -0
  19. package/lib/types/client/AiRatioSection.d.ts +3 -0
  20. package/lib/types/client/BrowserView.d.ts +39 -0
  21. package/lib/types/client/ChatcodeGate.d.ts +6 -0
  22. package/lib/types/client/CodingPlanModelsSettings.d.ts +9 -0
  23. package/lib/types/client/DiffTab.d.ts +6 -0
  24. package/lib/types/client/DiffView.d.ts +51 -0
  25. package/lib/types/client/EditorHost.d.ts +13 -0
  26. package/lib/types/client/FenceErrorNotice.d.ts +5 -0
  27. package/lib/types/client/FileTree.d.ts +41 -0
  28. package/lib/types/client/FreeWindow.d.ts +29 -0
  29. package/lib/types/client/GitView.d.ts +12 -0
  30. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  31. package/lib/types/client/OrphanedTab.d.ts +11 -0
  32. package/lib/types/client/PdfView.d.ts +6 -0
  33. package/lib/types/client/ReleaseNotesDialog.d.ts +11 -0
  34. package/lib/types/client/RenderBoundary.d.ts +32 -0
  35. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  36. package/lib/types/client/SideCardSection.d.ts +74 -0
  37. package/lib/types/client/Sidebar.d.ts +8 -0
  38. package/lib/types/client/TabBar.d.ts +54 -0
  39. package/lib/types/client/TerminalView.d.ts +24 -0
  40. package/lib/types/client/TextEditor.d.ts +10 -0
  41. package/lib/types/client/TreePanel.d.ts +27 -0
  42. package/lib/types/client/UpdateDialog.d.ts +10 -0
  43. package/lib/types/client/UpdateSettingsPanel.d.ts +1 -0
  44. package/lib/types/client/UpdateSettingsSection.d.ts +10 -0
  45. package/lib/types/client/UploadOverlay.d.ts +20 -0
  46. package/lib/types/client/UsageHeatmapSection.d.ts +4 -0
  47. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  48. package/lib/types/client/api.d.ts +345 -0
  49. package/lib/types/client/binary-download.d.ts +12 -0
  50. package/lib/types/client/breakpoints.d.ts +17 -0
  51. package/lib/types/client/browser.d.ts +60 -0
  52. package/lib/types/client/builtins/index.d.ts +18 -0
  53. package/lib/types/client/builtins/tabs.d.ts +11 -0
  54. package/lib/types/client/builtins/viewers.d.ts +3 -0
  55. package/lib/types/client/chat-presentation.d.ts +13 -0
  56. package/lib/types/client/chunk-loader.d.ts +124 -0
  57. package/lib/types/client/chunks/editor.d.ts +9 -0
  58. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  59. package/lib/types/client/chunks/terminal.d.ts +9 -0
  60. package/lib/types/client/cm-themes.d.ts +24 -0
  61. package/lib/types/client/conversation-draft.d.ts +14 -0
  62. package/lib/types/client/desktop-env.d.ts +35 -0
  63. package/lib/types/client/editor-load.d.ts +66 -0
  64. package/lib/types/client/frame-batcher.d.ts +24 -0
  65. package/lib/types/client/icons.d.ts +66 -0
  66. package/lib/types/client/image-types.d.ts +3 -0
  67. package/lib/types/client/ime-guard.d.ts +36 -0
  68. package/lib/types/client/index.d.ts +19 -0
  69. package/lib/types/client/intercept.d.ts +44 -0
  70. package/lib/types/client/lang.d.ts +14 -0
  71. package/lib/types/client/layout-push.d.ts +15 -0
  72. package/lib/types/client/lazy-chunk.d.ts +25 -0
  73. package/lib/types/client/link-intercept.d.ts +42 -0
  74. package/lib/types/client/locales-ar.d.ts +18 -0
  75. package/lib/types/client/locales-de.d.ts +4 -0
  76. package/lib/types/client/locales-fr.d.ts +10 -0
  77. package/lib/types/client/locales-hi.d.ts +17 -0
  78. package/lib/types/client/locales-id.d.ts +15 -0
  79. package/lib/types/client/locales-it.d.ts +8 -0
  80. package/lib/types/client/locales-ja.d.ts +17 -0
  81. package/lib/types/client/locales-ko.d.ts +9 -0
  82. package/lib/types/client/locales-nl.d.ts +16 -0
  83. package/lib/types/client/locales-pl.d.ts +19 -0
  84. package/lib/types/client/locales-pt.d.ts +1 -0
  85. package/lib/types/client/locales-ru.d.ts +13 -0
  86. package/lib/types/client/locales-sv.d.ts +1 -0
  87. package/lib/types/client/locales-th.d.ts +17 -0
  88. package/lib/types/client/locales-tr.d.ts +17 -0
  89. package/lib/types/client/locales-vi.d.ts +17 -0
  90. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  91. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  92. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  93. package/lib/types/client/locales.d.ts +571 -0
  94. package/lib/types/client/markdown-html.d.ts +96 -0
  95. package/lib/types/client/markdown-images.d.ts +45 -0
  96. package/lib/types/client/markdown-labels.d.ts +21 -0
  97. package/lib/types/client/md-toc.d.ts +4 -0
  98. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  99. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  100. package/lib/types/client/mermaid.d.ts +12 -0
  101. package/lib/types/client/open-when-sized.d.ts +20 -0
  102. package/lib/types/client/open-with-settings.d.ts +4 -0
  103. package/lib/types/client/open-with.d.ts +87 -0
  104. package/lib/types/client/openpath-intercept.d.ts +52 -0
  105. package/lib/types/client/paths.d.ts +43 -0
  106. package/lib/types/client/pdf-types.d.ts +2 -0
  107. package/lib/types/client/pinned.d.ts +86 -0
  108. package/lib/types/client/plugin-settings.d.ts +8 -0
  109. package/lib/types/client/plugins-shared.d.ts +27 -0
  110. package/lib/types/client/plugins-tabs.d.ts +3 -0
  111. package/lib/types/client/plugins-viewers.d.ts +3 -0
  112. package/lib/types/client/prefs.d.ts +41 -0
  113. package/lib/types/client/produced-files.d.ts +29 -0
  114. package/lib/types/client/selection-payload.d.ts +27 -0
  115. package/lib/types/client/service.d.ts +445 -0
  116. package/lib/types/client/session-pins.d.ts +14 -0
  117. package/lib/types/client/settings-nav-icon.d.ts +27 -0
  118. package/lib/types/client/shell-presets.d.ts +48 -0
  119. package/lib/types/client/split-pane.d.ts +38 -0
  120. package/lib/types/client/state.d.ts +480 -0
  121. package/lib/types/client/tab-content-memo.d.ts +40 -0
  122. package/lib/types/client/terminal-font.d.ts +78 -0
  123. package/lib/types/client/terminal-links.d.ts +119 -0
  124. package/lib/types/client/theme.d.ts +45 -0
  125. package/lib/types/client/titlebar-strip.d.ts +23 -0
  126. package/lib/types/client/ui-tweaks/appicons.d.ts +16 -0
  127. package/lib/types/client/ui-tweaks/archive.d.ts +42 -0
  128. package/lib/types/client/ui-tweaks/branding.d.ts +45 -0
  129. package/lib/types/client/ui-tweaks/cachehit.d.ts +41 -0
  130. package/lib/types/client/ui-tweaks/gitbar.d.ts +103 -0
  131. package/lib/types/client/ui-tweaks/index.d.ts +279 -0
  132. package/lib/types/client/ui-tweaks/mcp.d.ts +28 -0
  133. package/lib/types/client/ui-tweaks/notifier.d.ts +125 -0
  134. package/lib/types/client/ui-tweaks/whale.d.ts +55 -0
  135. package/lib/types/client/upload.d.ts +69 -0
  136. package/lib/types/client/useChatcodeAccount.d.ts +10 -0
  137. package/lib/types/client/wco.d.ts +47 -0
  138. package/lib/types/codingplan.d.ts +37 -0
  139. package/lib/types/config.d.ts +95 -0
  140. package/lib/types/context-types.d.ts +540 -0
  141. package/lib/types/fs-operations.d.ts +30 -0
  142. package/lib/types/fs-search.d.ts +24 -0
  143. package/lib/types/fs-tree.d.ts +51 -0
  144. package/lib/types/git.d.ts +124 -0
  145. package/lib/types/html-route.d.ts +59 -0
  146. package/lib/types/index.d.ts +47 -0
  147. package/lib/types/invariant.d.ts +15 -0
  148. package/lib/types/login-route.d.ts +2 -0
  149. package/lib/types/open-external.d.ts +24 -0
  150. package/lib/types/path-security.d.ts +26 -0
  151. package/lib/types/prefs-shared.d.ts +257 -0
  152. package/lib/types/pty-deps.d.ts +79 -0
  153. package/lib/types/pty-manager.d.ts +168 -0
  154. package/lib/types/tools.d.ts +34 -0
  155. package/lib/types/trust-fence.d.ts +24 -0
  156. package/lib/types/ui-tweaks/archive.d.ts +71 -0
  157. package/lib/types/ui-tweaks/config.d.ts +186 -0
  158. package/lib/types/ui-tweaks/git-web.d.ts +43 -0
  159. package/lib/types/ui-tweaks/git.d.ts +311 -0
  160. package/lib/types/ui-tweaks/index.d.ts +17 -0
  161. package/lib/types/ui-tweaks/mcp.d.ts +103 -0
  162. package/lib/types/ui-tweaks/turn-outcome.d.ts +57 -0
  163. package/lib/types/ui-tweaks/web.d.ts +55 -0
  164. package/lib/types/wire.d.ts +38 -0
  165. package/package.json +213 -0
  166. package/scripts/install.ps1 +310 -0
  167. package/scripts/install.sh +287 -0
  168. package/src/agent-opens.ts +265 -0
  169. package/src/agent-pty.ts +527 -0
  170. package/src/browser-probe.ts +26 -0
  171. package/src/bundle-route.ts +129 -0
  172. package/src/chatcode-update.ts +193 -0
  173. package/src/client/AccountMenu.tsx +177 -0
  174. package/src/client/AiRatioSection.module.css +329 -0
  175. package/src/client/AiRatioSection.tsx +292 -0
  176. package/src/client/BrowserView.tsx +289 -0
  177. package/src/client/ChatcodeGate.tsx +35 -0
  178. package/src/client/CodingPlanModelsSettings.module.css +118 -0
  179. package/src/client/CodingPlanModelsSettings.tsx +101 -0
  180. package/src/client/DiffTab.tsx +114 -0
  181. package/src/client/DiffView.tsx +305 -0
  182. package/src/client/EditorHost.tsx +563 -0
  183. package/src/client/FenceErrorNotice.tsx +40 -0
  184. package/src/client/FileTree.tsx +701 -0
  185. package/src/client/FreeWindow.tsx +337 -0
  186. package/src/client/GitView.tsx +722 -0
  187. package/src/client/MarkdownHtml.tsx +296 -0
  188. package/src/client/OrphanedTab.tsx +27 -0
  189. package/src/client/PdfView.tsx +110 -0
  190. package/src/client/ReleaseNotesDialog.tsx +91 -0
  191. package/src/client/RenderBoundary.tsx +49 -0
  192. package/src/client/SandboxStatusBar.tsx +60 -0
  193. package/src/client/SideCardSection.module.css +932 -0
  194. package/src/client/SideCardSection.tsx +1169 -0
  195. package/src/client/Sidebar.tsx +1857 -0
  196. package/src/client/TabBar.tsx +365 -0
  197. package/src/client/TerminalView.tsx +424 -0
  198. package/src/client/TextEditor.tsx +468 -0
  199. package/src/client/TreePanel.tsx +279 -0
  200. package/src/client/UpdateDialog.tsx +186 -0
  201. package/src/client/UpdateSettingsPanel.tsx +29 -0
  202. package/src/client/UpdateSettingsSection.tsx +22 -0
  203. package/src/client/UploadOverlay.tsx +62 -0
  204. package/src/client/UsageHeatmapSection.module.css +440 -0
  205. package/src/client/UsageHeatmapSection.tsx +301 -0
  206. package/src/client/add-plugin-modal.tsx +199 -0
  207. package/src/client/api.ts +433 -0
  208. package/src/client/binary-download.tsx +23 -0
  209. package/src/client/breakpoints.ts +61 -0
  210. package/src/client/browser.ts +167 -0
  211. package/src/client/builtins/index.ts +37 -0
  212. package/src/client/builtins/tabs.tsx +275 -0
  213. package/src/client/builtins/viewers.tsx +125 -0
  214. package/src/client/chat-presentation.ts +95 -0
  215. package/src/client/chunk-loader.ts +350 -0
  216. package/src/client/chunks/editor.tsx +9 -0
  217. package/src/client/chunks/mermaid.tsx +10 -0
  218. package/src/client/chunks/terminal.tsx +9 -0
  219. package/src/client/cm-themes.ts +129 -0
  220. package/src/client/conversation-draft.ts +29 -0
  221. package/src/client/css-modules.d.ts +5 -0
  222. package/src/client/desktop-env.ts +72 -0
  223. package/src/client/editor-load.ts +92 -0
  224. package/src/client/frame-batcher.ts +56 -0
  225. package/src/client/icons.tsx +170 -0
  226. package/src/client/image-types.ts +8 -0
  227. package/src/client/ime-guard.ts +47 -0
  228. package/src/client/index.tsx +501 -0
  229. package/src/client/intercept.tsx +172 -0
  230. package/src/client/lang.ts +225 -0
  231. package/src/client/layout-push.ts +37 -0
  232. package/src/client/layout.css +156 -0
  233. package/src/client/lazy-chunk.tsx +89 -0
  234. package/src/client/link-intercept.ts +73 -0
  235. package/src/client/locales-ar.ts +387 -0
  236. package/src/client/locales-de.ts +372 -0
  237. package/src/client/locales-fr.ts +379 -0
  238. package/src/client/locales-hi.ts +386 -0
  239. package/src/client/locales-id.ts +384 -0
  240. package/src/client/locales-it.ts +377 -0
  241. package/src/client/locales-ja.ts +486 -0
  242. package/src/client/locales-ko.ts +378 -0
  243. package/src/client/locales-nl.ts +384 -0
  244. package/src/client/locales-pl.ts +388 -0
  245. package/src/client/locales-pt.ts +369 -0
  246. package/src/client/locales-ru.ts +384 -0
  247. package/src/client/locales-sv.ts +369 -0
  248. package/src/client/locales-th.ts +386 -0
  249. package/src/client/locales-tr.ts +386 -0
  250. package/src/client/locales-vi.ts +386 -0
  251. package/src/client/locales-zh-HK.ts +401 -0
  252. package/src/client/locales-zh-MO.ts +401 -0
  253. package/src/client/locales-zh-TW.ts +401 -0
  254. package/src/client/locales.ts +1229 -0
  255. package/src/client/markdown-html.ts +331 -0
  256. package/src/client/markdown-images.ts +137 -0
  257. package/src/client/markdown-labels.tsx +31 -0
  258. package/src/client/md-toc.tsx +127 -0
  259. package/src/client/mermaid-blocks.ts +110 -0
  260. package/src/client/mermaid-sanitize.ts +93 -0
  261. package/src/client/mermaid.tsx +406 -0
  262. package/src/client/open-when-sized.ts +43 -0
  263. package/src/client/open-with-settings.tsx +135 -0
  264. package/src/client/open-with.ts +221 -0
  265. package/src/client/openpath-intercept.ts +79 -0
  266. package/src/client/paths.ts +63 -0
  267. package/src/client/pdf-types.ts +4 -0
  268. package/src/client/pinned.ts +184 -0
  269. package/src/client/plugin-settings.ts +43 -0
  270. package/src/client/plugins-shared.ts +29 -0
  271. package/src/client/plugins-tabs.ts +153 -0
  272. package/src/client/plugins-viewers.ts +44 -0
  273. package/src/client/prefs.ts +216 -0
  274. package/src/client/produced-files.ts +120 -0
  275. package/src/client/selection-payload.ts +86 -0
  276. package/src/client/service.ts +873 -0
  277. package/src/client/session-pins.tsx +334 -0
  278. package/src/client/settings-nav-icon.ts +55 -0
  279. package/src/client/shell-presets.ts +83 -0
  280. package/src/client/sidebar.module.css +3607 -0
  281. package/src/client/split-pane.tsx +333 -0
  282. package/src/client/state.ts +1646 -0
  283. package/src/client/tab-content-memo.ts +55 -0
  284. package/src/client/terminal-font.ts +224 -0
  285. package/src/client/terminal-links.ts +194 -0
  286. package/src/client/theme.ts +110 -0
  287. package/src/client/titlebar-strip.ts +37 -0
  288. package/src/client/ui-tweaks/appicons.ts +24 -0
  289. package/src/client/ui-tweaks/archive.tsx +281 -0
  290. package/src/client/ui-tweaks/branding.tsx +232 -0
  291. package/src/client/ui-tweaks/cachehit.tsx +122 -0
  292. package/src/client/ui-tweaks/gitbar.tsx +2559 -0
  293. package/src/client/ui-tweaks/index.tsx +1696 -0
  294. package/src/client/ui-tweaks/mcp.tsx +582 -0
  295. package/src/client/ui-tweaks/notifier.ts +514 -0
  296. package/src/client/ui-tweaks/whale.tsx +171 -0
  297. package/src/client/upload.ts +187 -0
  298. package/src/client/useChatcodeAccount.ts +103 -0
  299. package/src/client/wco.ts +120 -0
  300. package/src/codingplan.ts +146 -0
  301. package/src/config.ts +207 -0
  302. package/src/context-types.ts +559 -0
  303. package/src/fs-operations.ts +97 -0
  304. package/src/fs-search.ts +113 -0
  305. package/src/fs-tree.ts +158 -0
  306. package/src/git.ts +446 -0
  307. package/src/html-route.ts +106 -0
  308. package/src/index.ts +1524 -0
  309. package/src/invariant.ts +32 -0
  310. package/src/login-route.ts +26 -0
  311. package/src/open-external.ts +90 -0
  312. package/src/path-security.ts +81 -0
  313. package/src/prefs-shared.ts +303 -0
  314. package/src/pty-deps.ts +241 -0
  315. package/src/pty-manager.ts +376 -0
  316. package/src/tools.ts +482 -0
  317. package/src/trust-fence.ts +84 -0
  318. package/src/ui-tweaks/archive.ts +293 -0
  319. package/src/ui-tweaks/config.ts +257 -0
  320. package/src/ui-tweaks/git-web.ts +421 -0
  321. package/src/ui-tweaks/git.ts +1524 -0
  322. package/src/ui-tweaks/index.ts +61 -0
  323. package/src/ui-tweaks/mcp.ts +535 -0
  324. package/src/ui-tweaks/turn-outcome.ts +105 -0
  325. package/src/ui-tweaks/web.ts +225 -0
  326. package/src/wire.ts +110 -0
  327. package/vendor/addon-fit.js +2 -0
  328. package/vendor/xterm.css +218 -0
  329. package/vendor/xterm.js +5 -0
@@ -0,0 +1,333 @@
1
+ /**
2
+ * The split-pane workbench: renders the recursive split tree. A split lays
3
+ * children out row- or column-wise with draggable dividers (fractional
4
+ * sizes); a leaf renders its tab strip plus the active tab's content.
5
+ *
6
+ * Splitting is VSCode-style DRAG-TO-EDGE, not buttons: while dragging a tab
7
+ * over a pane, a drop overlay shows five zones — four edges (left/right/up/
8
+ * down) that split the pane with the tab in a fresh leaf, and the center
9
+ * that merges the tab into the pane. The tree and all operations live in
10
+ * state.ts; this file is pure presentation over them.
11
+ */
12
+ import { Fragment, useEffect, useRef, useState } from 'react'
13
+ import type { ReactNode } from 'react'
14
+ import clsx from 'clsx'
15
+ import type { SidebarState, SidebarTab, SplitNode } from './state.ts'
16
+ import type { DropZone } from './state.ts'
17
+ import { TabBar, type NewTabOption, parseDrag, type TabDragPayload } from './TabBar.tsx'
18
+ import { createFrameBatcher } from './frame-batcher.ts'
19
+ import css from './sidebar.module.css'
20
+
21
+ /** Actions the workbench needs (bound to the store by the sidebar shell). */
22
+ export interface WorkbenchActions {
23
+ closeTab: (paneId: string, tabId: string) => void
24
+ activateTab: (paneId: string, tabId: string) => void
25
+ /** Make a pane the target of newly opened tabs (click focus). */
26
+ focusPane: (paneId: string) => void
27
+ /** VSCode drag gesture: edge → split the target pane, center → merge. */
28
+ moveTabToEdge: (payload: TabDragPayload, toPane: string, zone: DropZone) => void
29
+ /** Reorder within a pane (drop onto another tab inserts before it). */
30
+ moveTabBefore: (payload: TabDragPayload, toPane: string, beforeTabId: string) => void
31
+ resizeSplit: (splitId: string, index: number, deltaFrac: number) => void
32
+ /** Float a docked tab out as a free window (tab context menu entry). */
33
+ floatTab: (tabId: string) => void
34
+ /**
35
+ * Pin/unpin a terminal tab (v0.17.0+). The shell snapshots the home cwd
36
+ * at pin time; null clears the pin. Optional: when undefined the tab
37
+ * context menu hides the pin entry (legacy callers).
38
+ */
39
+ pinTab?: (tabId: string, scope: 'workspace' | 'global' | null) => void
40
+ }
41
+
42
+ /** One divider: pointer-capture drag translating px deltas into fractions.
43
+ * Deltas are incremental — each move reports the displacement since the
44
+ * previous move — because the store adds every reported delta to the pane
45
+ * sizes; a cumulative (since-pointer-down) delta would be re-added on each
46
+ * move and the divider would run away from the cursor.
47
+ *
48
+ * The moves are BATCHED per frame (createFrameBatcher): a pointer stream
49
+ * fires faster than the display refresh, and applying each move is a store
50
+ * reduce that re-renders both workbenches (terminals, editors, trees) per
51
+ * event — the visible drag lag on slower CPUs (#315). The batch accumulates
52
+ * the incremental deltas in a ref and applies the summed fraction at most
53
+ * once per frame; the sum equals what the per-event application would have
54
+ * produced (the reducer clamps each application, and at a settled position
55
+ * a clamped sum is clamped to the same boundary), so the result is
56
+ * indistinguishable at rest and at most one frame behind the cursor.
57
+ */
58
+ function Divider(props: { dir: 'row' | 'col'; onResize: (deltaFrac: number) => void }) {
59
+ const { dir, onResize } = props
60
+ // `last` is the previous pointer position while dragging; `size` is the
61
+ // container size at pointer-down (constant during the drag), used to
62
+ // normalize the px delta into a fraction.
63
+ const last = useRef({ x: 0, y: 0, size: 0 })
64
+ const [dragging, setDragging] = useState(false)
65
+ const pendingDelta = useRef(0)
66
+ const batcher = useRef(createFrameBatcher()).current
67
+ useEffect(() => () => batcher.dispose(), [batcher])
68
+
69
+ return (
70
+ <div
71
+ className={clsx(css.divider, dir === 'row' ? css.dividerRow : css.dividerCol, dragging && css.dividerActive)}
72
+ onPointerDown={(event) => {
73
+ event.preventDefault()
74
+ event.currentTarget.setPointerCapture(event.pointerId)
75
+ const box = event.currentTarget.parentElement?.getBoundingClientRect()
76
+ last.current = {
77
+ x: event.clientX,
78
+ y: event.clientY,
79
+ size: box === undefined ? 1 : (dir === 'row' ? box.width : box.height),
80
+ }
81
+ setDragging(true)
82
+ }}
83
+ onPointerMove={(event) => {
84
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return
85
+ const delta = dir === 'row' ? event.clientX - last.current.x : event.clientY - last.current.y
86
+ pendingDelta.current += delta
87
+ batcher.schedule(() => {
88
+ const accumulated = pendingDelta.current
89
+ pendingDelta.current = 0
90
+ if (accumulated !== 0) onResize(accumulated / Math.max(1, last.current.size))
91
+ })
92
+ last.current.x = event.clientX
93
+ last.current.y = event.clientY
94
+ }}
95
+ onPointerUp={(event) => {
96
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return
97
+ // Flush any pending baked delta so the final pointer position lands
98
+ // (the release event is not a move; dropping it would lose the tail).
99
+ batcher.flushNow()
100
+ event.currentTarget.releasePointerCapture(event.pointerId)
101
+ setDragging(false)
102
+ }}
103
+ />
104
+ )
105
+ }
106
+
107
+ /** Map a pointer position inside a pane to the VSCode drop zone (25% edges). */
108
+ function zoneAt(event: React.DragEvent, pane: HTMLElement): DropZone {
109
+ const rect = pane.getBoundingClientRect()
110
+ if (rect.width === 0 || rect.height === 0) return 'center'
111
+ const x = (event.clientX - rect.left) / rect.width
112
+ const y = (event.clientY - rect.top) / rect.height
113
+ if (x < 0.25) return 'left'
114
+ if (x > 0.75) return 'right'
115
+ if (y < 0.25) return 'up'
116
+ if (y > 0.75) return 'down'
117
+ return 'center'
118
+ }
119
+
120
+ /** The icon of one openable type card (mirror of the + menu options). */
121
+ /**
122
+ * An empty pane's welcome cards: the openable types as cards, clicked to
123
+ * open (instead of a bare "this pane is empty" message).
124
+ */
125
+ function PaneEmptyCards(props: {
126
+ newTabOptions: NewTabOption[]
127
+ onNewTab: (optionId: string) => void
128
+ }) {
129
+ const { newTabOptions, onNewTab } = props
130
+ return (
131
+ <div className={css.paneEmptyCards}>
132
+ {newTabOptions.map(option => (
133
+ <button
134
+ key={option.id}
135
+ type="button"
136
+ className={css.paneCard}
137
+ disabled={option.disabled === true}
138
+ title={option.label}
139
+ onClick={() => { onNewTab(option.id) }}
140
+ >
141
+ {option.icon ?? null}
142
+ <span>{option.label}</span>
143
+ </button>
144
+ ))}
145
+ </div>
146
+ )
147
+ }
148
+
149
+ /** A leaf: tab strip + active content + VSCode-style drop target for tabs. */
150
+ function LeafView(props: {
151
+ leaf: { id: string; tabs: SidebarTab[]; active: string | null }
152
+ newTabOptions: NewTabOption[]
153
+ actions: WorkbenchActions
154
+ onNewTab: (optionId: string) => void
155
+ renderTab: (tab: SidebarTab, active: boolean, paneId: string) => ReactNode
156
+ getTabIcon?: (tab: SidebarTab) => ReactNode
157
+ getTabBadge?: (tab: SidebarTab) => ReactNode
158
+ }) {
159
+ const { leaf, newTabOptions, actions, onNewTab, renderTab, getTabIcon, getTabBadge } = props
160
+ const [dropZone, setDropZone] = useState<DropZone | null>(null)
161
+ const activeTab = leaf.tabs.find(tab => tab.id === leaf.active) ?? leaf.tabs[leaf.tabs.length - 1]
162
+
163
+ useEffect(() => {
164
+ const clear = (): void => { setDropZone(null) }
165
+ window.addEventListener('dragend', clear, true)
166
+ window.addEventListener('drop', clear, true)
167
+ window.addEventListener('blur', clear)
168
+ return () => {
169
+ window.removeEventListener('dragend', clear, true)
170
+ window.removeEventListener('drop', clear, true)
171
+ window.removeEventListener('blur', clear)
172
+ }
173
+ }, [])
174
+
175
+ return (
176
+ <div
177
+ className={clsx(css.pane, dropZone !== null && css.paneDrop)}
178
+ data-dsh-pane={leaf.id}
179
+ onPointerDown={() => { actions.focusPane(leaf.id) }}
180
+ onDragOver={(event) => {
181
+ event.preventDefault()
182
+ const zone = zoneAt(event, event.currentTarget)
183
+ setDropZone(zone)
184
+ }}
185
+ onDragLeave={(event) => {
186
+ // Only clear when the pointer left the pane entirely (not onto a child).
187
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
188
+ setDropZone(null)
189
+ }
190
+ }}
191
+ onDrop={(event) => {
192
+ event.preventDefault()
193
+ const zone = dropZone ?? zoneAt(event, event.currentTarget)
194
+ setDropZone(null)
195
+ const payload = parseDrag(event.dataTransfer.getData('application/x-dsh-tab'))
196
+ if (payload !== null) actions.moveTabToEdge(payload, leaf.id, zone)
197
+ }}
198
+ >
199
+ {dropZone !== null && <div className={clsx(css.dropOverlay, css[`drop${dropZone[0]!.toUpperCase()}${dropZone.slice(1)}`])} />}
200
+ {/*
201
+ The tab strip renders even for an empty pane: the + menu must stay
202
+ reachable when the pane has no tabs (fresh split, or the last tab was
203
+ dragged out), so a new tab can always be created or dragged in.
204
+ */}
205
+ <TabBar
206
+ paneId={leaf.id}
207
+ tabs={leaf.tabs}
208
+ active={leaf.active}
209
+ onActivate={(tabId) => { actions.activateTab(leaf.id, tabId) }}
210
+ onClose={(tabId) => { actions.closeTab(leaf.id, tabId) }}
211
+ onNewTab={onNewTab}
212
+ newTabOptions={newTabOptions}
213
+ getTabIcon={getTabIcon}
214
+ getTabBadge={getTabBadge}
215
+ onDropTab={(payload, before) => {
216
+ if (before === null) actions.moveTabToEdge(payload, leaf.id, 'center')
217
+ else actions.moveTabBefore(payload, leaf.id, before)
218
+ }}
219
+ onFloatTab={actions.floatTab}
220
+ onPinTab={actions.pinTab}
221
+ />
222
+ {leaf.tabs.length > 0 ? (
223
+ /*
224
+ Every tab stays MOUNTED (inactive ones hidden), so switching tabs
225
+ never tears down the content: a terminal keeps its pty connection
226
+ and scrollback, an editor keeps its CodeMirror view and unsaved
227
+ draft, explorer/git keep their loaded data. The unmount (and the
228
+ terminal's close frame) happens only when a tab is truly closed.
229
+ */
230
+ <div className={css.paneContent}>
231
+ {leaf.tabs.map(tab => (
232
+ <div
233
+ key={tab.id}
234
+ className={clsx(css.paneTab, tab.id !== activeTab?.id && css.paneTabHidden)}
235
+ >
236
+ {renderTab(tab, tab.id === activeTab?.id, leaf.id)}
237
+ </div>
238
+ ))}
239
+ </div>
240
+ ) : (
241
+ <PaneEmptyCards newTabOptions={newTabOptions} onNewTab={onNewTab} />
242
+ )}
243
+ </div>
244
+ )
245
+ }
246
+
247
+ /** Recursive node renderer. */
248
+ function NodeView(props: {
249
+ node: SplitNode
250
+ state: SidebarState
251
+ newTabOptions: NewTabOption[]
252
+ actions: WorkbenchActions
253
+ onNewTab: (optionId: string) => void
254
+ renderTab: (tab: SidebarTab, active: boolean, paneId: string) => ReactNode
255
+ getTabIcon?: (tab: SidebarTab) => ReactNode
256
+ getTabBadge?: (tab: SidebarTab) => ReactNode
257
+ }) {
258
+ const { node, state, newTabOptions, actions, onNewTab, renderTab, getTabIcon, getTabBadge } = props
259
+ if (node.kind === 'leaf') {
260
+ return (
261
+ <LeafView
262
+ leaf={node}
263
+ newTabOptions={newTabOptions}
264
+ actions={actions}
265
+ onNewTab={onNewTab}
266
+ renderTab={renderTab}
267
+ getTabIcon={getTabIcon}
268
+ getTabBadge={getTabBadge}
269
+ />
270
+ )
271
+ }
272
+ const isRow = node.dir === 'row'
273
+ return (
274
+ <div className={clsx(css.split, isRow ? css.splitRow : css.splitCol)}>
275
+ {node.children.map((child, index) => (
276
+ <Fragment key={child.id}>
277
+ {index > 0 && (
278
+ <Divider
279
+ dir={node.dir}
280
+ onResize={(deltaFrac) => { actions.resizeSplit(node.id, index - 1, deltaFrac) }}
281
+ />
282
+ )}
283
+ <div
284
+ className={css.splitChild}
285
+ style={{ flexGrow: node.sizes[index], flexBasis: 0, minWidth: 0, minHeight: 0 }}
286
+ >
287
+ <NodeView
288
+ node={child}
289
+ state={state}
290
+ newTabOptions={newTabOptions}
291
+ actions={actions}
292
+ onNewTab={onNewTab}
293
+ renderTab={renderTab}
294
+ getTabIcon={getTabIcon}
295
+ getTabBadge={getTabBadge}
296
+ />
297
+ </div>
298
+ </Fragment>
299
+ ))}
300
+ </div>
301
+ )
302
+ }
303
+
304
+ /** The workbench: the split tree filling the sidebar body. `tree` selects
305
+ * which tree renders (the right panel's by default; the bottom panel passes
306
+ * `state.bottomSplits` — the actions route by pane id, so one action set
307
+ * serves both). */
308
+ export function Workbench(props: {
309
+ state: SidebarState
310
+ tree?: SplitNode
311
+ newTabOptions: NewTabOption[]
312
+ actions: WorkbenchActions
313
+ onNewTab: (optionId: string) => void
314
+ renderTab: (tab: SidebarTab, active: boolean, paneId: string) => ReactNode
315
+ getTabIcon?: (tab: SidebarTab) => ReactNode
316
+ getTabBadge?: (tab: SidebarTab) => ReactNode
317
+ }) {
318
+ const { state, tree, newTabOptions, actions, onNewTab, renderTab, getTabIcon, getTabBadge } = props
319
+ return (
320
+ <div className={css.workbench}>
321
+ <NodeView
322
+ node={tree ?? state.splits}
323
+ state={state}
324
+ newTabOptions={newTabOptions}
325
+ actions={actions}
326
+ onNewTab={onNewTab}
327
+ renderTab={renderTab}
328
+ getTabIcon={getTabIcon}
329
+ getTabBadge={getTabBadge}
330
+ />
331
+ </div>
332
+ )
333
+ }