dsh-coding-sidebar 1.0.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 (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +524 -0
  3. package/cordis.patch.yml +49 -0
  4. package/lib/client-editor.js +48229 -0
  5. package/lib/client-mermaid.js +200745 -0
  6. package/lib/client-registry.js +21867 -0
  7. package/lib/client-terminal.js +9361 -0
  8. package/lib/client.js +21867 -0
  9. package/lib/index.js +4556 -0
  10. package/lib/invariant.js +22 -0
  11. package/lib/types/agent-opens.d.ts +65 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +39 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +13 -0
  19. package/lib/types/client/FileTree.d.ts +38 -0
  20. package/lib/types/client/FreeWindow.d.ts +29 -0
  21. package/lib/types/client/GitView.d.ts +10 -0
  22. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  23. package/lib/types/client/OrphanedTab.d.ts +11 -0
  24. package/lib/types/client/PdfView.d.ts +6 -0
  25. package/lib/types/client/RenderBoundary.d.ts +32 -0
  26. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  27. package/lib/types/client/SideCardSection.d.ts +74 -0
  28. package/lib/types/client/SideChatView.d.ts +16 -0
  29. package/lib/types/client/Sidebar.d.ts +6 -0
  30. package/lib/types/client/SubagentView.d.ts +16 -0
  31. package/lib/types/client/TabBar.d.ts +54 -0
  32. package/lib/types/client/TerminalView.d.ts +24 -0
  33. package/lib/types/client/TextEditor.d.ts +10 -0
  34. package/lib/types/client/TreePanel.d.ts +24 -0
  35. package/lib/types/client/UploadOverlay.d.ts +20 -0
  36. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  37. package/lib/types/client/api.d.ts +275 -0
  38. package/lib/types/client/binary-download.d.ts +12 -0
  39. package/lib/types/client/breakpoints.d.ts +17 -0
  40. package/lib/types/client/browser.d.ts +60 -0
  41. package/lib/types/client/builtins/index.d.ts +18 -0
  42. package/lib/types/client/builtins/tabs.d.ts +11 -0
  43. package/lib/types/client/builtins/viewers.d.ts +3 -0
  44. package/lib/types/client/chunk-loader.d.ts +131 -0
  45. package/lib/types/client/chunks/editor.d.ts +9 -0
  46. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  47. package/lib/types/client/chunks/terminal.d.ts +9 -0
  48. package/lib/types/client/cm-themes.d.ts +24 -0
  49. package/lib/types/client/conversation-draft.d.ts +14 -0
  50. package/lib/types/client/desktop-env.d.ts +35 -0
  51. package/lib/types/client/editor-load.d.ts +66 -0
  52. package/lib/types/client/frame-batcher.d.ts +24 -0
  53. package/lib/types/client/icons.d.ts +59 -0
  54. package/lib/types/client/image-types.d.ts +3 -0
  55. package/lib/types/client/ime-guard.d.ts +36 -0
  56. package/lib/types/client/index.d.ts +25 -0
  57. package/lib/types/client/intercept.d.ts +46 -0
  58. package/lib/types/client/lang.d.ts +14 -0
  59. package/lib/types/client/layout-push.d.ts +15 -0
  60. package/lib/types/client/lazy-chunk.d.ts +25 -0
  61. package/lib/types/client/link-intercept.d.ts +42 -0
  62. package/lib/types/client/locales-ar.d.ts +18 -0
  63. package/lib/types/client/locales-de.d.ts +4 -0
  64. package/lib/types/client/locales-fr.d.ts +10 -0
  65. package/lib/types/client/locales-hi.d.ts +17 -0
  66. package/lib/types/client/locales-id.d.ts +15 -0
  67. package/lib/types/client/locales-it.d.ts +8 -0
  68. package/lib/types/client/locales-ja.d.ts +17 -0
  69. package/lib/types/client/locales-ko.d.ts +9 -0
  70. package/lib/types/client/locales-nl.d.ts +16 -0
  71. package/lib/types/client/locales-pl.d.ts +19 -0
  72. package/lib/types/client/locales-pt.d.ts +1 -0
  73. package/lib/types/client/locales-ru.d.ts +13 -0
  74. package/lib/types/client/locales-sv.d.ts +1 -0
  75. package/lib/types/client/locales-th.d.ts +17 -0
  76. package/lib/types/client/locales-tr.d.ts +17 -0
  77. package/lib/types/client/locales-vi.d.ts +17 -0
  78. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  79. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  80. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  81. package/lib/types/client/locales.d.ts +452 -0
  82. package/lib/types/client/markdown-html.d.ts +96 -0
  83. package/lib/types/client/markdown-images.d.ts +45 -0
  84. package/lib/types/client/markdown-labels.d.ts +38 -0
  85. package/lib/types/client/md-toc.d.ts +4 -0
  86. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  87. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  88. package/lib/types/client/mermaid.d.ts +12 -0
  89. package/lib/types/client/open-when-sized.d.ts +20 -0
  90. package/lib/types/client/open-with-settings.d.ts +4 -0
  91. package/lib/types/client/open-with.d.ts +87 -0
  92. package/lib/types/client/openpath-intercept.d.ts +85 -0
  93. package/lib/types/client/paths.d.ts +43 -0
  94. package/lib/types/client/pdf-types.d.ts +2 -0
  95. package/lib/types/client/pinned.d.ts +86 -0
  96. package/lib/types/client/plugin-settings.d.ts +8 -0
  97. package/lib/types/client/plugins-shared.d.ts +27 -0
  98. package/lib/types/client/plugins-tabs.d.ts +3 -0
  99. package/lib/types/client/plugins-viewers.d.ts +3 -0
  100. package/lib/types/client/prefs.d.ts +41 -0
  101. package/lib/types/client/produced-files.d.ts +29 -0
  102. package/lib/types/client/selection-payload.d.ts +27 -0
  103. package/lib/types/client/service.d.ts +439 -0
  104. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  105. package/lib/types/client/shell-presets.d.ts +48 -0
  106. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  107. package/lib/types/client/split-pane.d.ts +36 -0
  108. package/lib/types/client/state.d.ts +432 -0
  109. package/lib/types/client/subagent-detect.d.ts +54 -0
  110. package/lib/types/client/subagent-jobs.d.ts +63 -0
  111. package/lib/types/client/tab-content-memo.d.ts +40 -0
  112. package/lib/types/client/terminal-font.d.ts +78 -0
  113. package/lib/types/client/terminal-links.d.ts +119 -0
  114. package/lib/types/client/theme.d.ts +45 -0
  115. package/lib/types/client/titlebar-strip.d.ts +23 -0
  116. package/lib/types/client/upload.d.ts +69 -0
  117. package/lib/types/client/wco.d.ts +47 -0
  118. package/lib/types/config.d.ts +64 -0
  119. package/lib/types/context-types.d.ts +605 -0
  120. package/lib/types/fs-operations.d.ts +28 -0
  121. package/lib/types/fs-search.d.ts +24 -0
  122. package/lib/types/fs-tree.d.ts +51 -0
  123. package/lib/types/git.d.ts +124 -0
  124. package/lib/types/html-route.d.ts +59 -0
  125. package/lib/types/index.d.ts +47 -0
  126. package/lib/types/invariant.d.ts +15 -0
  127. package/lib/types/jobs-routes.d.ts +45 -0
  128. package/lib/types/open-external.d.ts +24 -0
  129. package/lib/types/path-security.d.ts +20 -0
  130. package/lib/types/prefs-shared.d.ts +236 -0
  131. package/lib/types/pty-deps.d.ts +78 -0
  132. package/lib/types/pty-manager.d.ts +168 -0
  133. package/lib/types/sidechat-core.d.ts +172 -0
  134. package/lib/types/sidechat-routes.d.ts +30 -0
  135. package/lib/types/subagent-activity.d.ts +44 -0
  136. package/lib/types/subagent-live-route.d.ts +44 -0
  137. package/lib/types/tools.d.ts +34 -0
  138. package/lib/types/trust-fence.d.ts +24 -0
  139. package/lib/types/wire.d.ts +38 -0
  140. package/package.json +172 -0
  141. package/scripts/install.ps1 +306 -0
  142. package/scripts/install.sh +287 -0
  143. package/src/agent-opens.ts +265 -0
  144. package/src/agent-pty.ts +527 -0
  145. package/src/browser-probe.ts +26 -0
  146. package/src/bundle-route.ts +129 -0
  147. package/src/client/BrowserView.tsx +289 -0
  148. package/src/client/DiffTab.tsx +114 -0
  149. package/src/client/DiffView.tsx +305 -0
  150. package/src/client/EditorHost.tsx +558 -0
  151. package/src/client/FileTree.tsx +669 -0
  152. package/src/client/FreeWindow.tsx +337 -0
  153. package/src/client/GitView.tsx +717 -0
  154. package/src/client/MarkdownHtml.tsx +296 -0
  155. package/src/client/OrphanedTab.tsx +27 -0
  156. package/src/client/PdfView.tsx +110 -0
  157. package/src/client/RenderBoundary.tsx +49 -0
  158. package/src/client/SandboxStatusBar.tsx +60 -0
  159. package/src/client/SideCardSection.module.css +960 -0
  160. package/src/client/SideCardSection.tsx +1163 -0
  161. package/src/client/SideChatView.module.css +437 -0
  162. package/src/client/SideChatView.tsx +645 -0
  163. package/src/client/Sidebar.tsx +1370 -0
  164. package/src/client/SubagentView.module.css +547 -0
  165. package/src/client/SubagentView.tsx +883 -0
  166. package/src/client/TabBar.tsx +365 -0
  167. package/src/client/TerminalView.tsx +424 -0
  168. package/src/client/TextEditor.tsx +468 -0
  169. package/src/client/TreePanel.tsx +275 -0
  170. package/src/client/UploadOverlay.tsx +62 -0
  171. package/src/client/add-plugin-modal.tsx +199 -0
  172. package/src/client/api.ts +357 -0
  173. package/src/client/binary-download.tsx +23 -0
  174. package/src/client/breakpoints.ts +61 -0
  175. package/src/client/browser.ts +167 -0
  176. package/src/client/builtins/index.ts +37 -0
  177. package/src/client/builtins/tabs.tsx +340 -0
  178. package/src/client/builtins/viewers.tsx +125 -0
  179. package/src/client/chunk-loader.ts +358 -0
  180. package/src/client/chunks/editor.tsx +9 -0
  181. package/src/client/chunks/mermaid.tsx +10 -0
  182. package/src/client/chunks/terminal.tsx +9 -0
  183. package/src/client/cm-themes.ts +129 -0
  184. package/src/client/conversation-draft.ts +29 -0
  185. package/src/client/css-modules.d.ts +5 -0
  186. package/src/client/desktop-env.ts +72 -0
  187. package/src/client/editor-load.ts +92 -0
  188. package/src/client/frame-batcher.ts +56 -0
  189. package/src/client/icons.tsx +152 -0
  190. package/src/client/image-types.ts +8 -0
  191. package/src/client/ime-guard.ts +47 -0
  192. package/src/client/index.tsx +427 -0
  193. package/src/client/intercept.tsx +188 -0
  194. package/src/client/lang.ts +225 -0
  195. package/src/client/layout-push.ts +24 -0
  196. package/src/client/layout.css +90 -0
  197. package/src/client/lazy-chunk.tsx +89 -0
  198. package/src/client/link-intercept.ts +73 -0
  199. package/src/client/locales-ar.ts +368 -0
  200. package/src/client/locales-de.ts +353 -0
  201. package/src/client/locales-fr.ts +360 -0
  202. package/src/client/locales-hi.ts +367 -0
  203. package/src/client/locales-id.ts +365 -0
  204. package/src/client/locales-it.ts +358 -0
  205. package/src/client/locales-ja.ts +367 -0
  206. package/src/client/locales-ko.ts +359 -0
  207. package/src/client/locales-nl.ts +365 -0
  208. package/src/client/locales-pl.ts +369 -0
  209. package/src/client/locales-pt.ts +350 -0
  210. package/src/client/locales-ru.ts +365 -0
  211. package/src/client/locales-sv.ts +350 -0
  212. package/src/client/locales-th.ts +367 -0
  213. package/src/client/locales-tr.ts +367 -0
  214. package/src/client/locales-vi.ts +367 -0
  215. package/src/client/locales-zh-HK.ts +382 -0
  216. package/src/client/locales-zh-MO.ts +382 -0
  217. package/src/client/locales-zh-TW.ts +382 -0
  218. package/src/client/locales.ts +894 -0
  219. package/src/client/markdown-html.ts +331 -0
  220. package/src/client/markdown-images.ts +137 -0
  221. package/src/client/markdown-labels.tsx +52 -0
  222. package/src/client/md-toc.tsx +127 -0
  223. package/src/client/mermaid-blocks.ts +110 -0
  224. package/src/client/mermaid-sanitize.ts +93 -0
  225. package/src/client/mermaid.tsx +406 -0
  226. package/src/client/open-when-sized.ts +43 -0
  227. package/src/client/open-with-settings.tsx +135 -0
  228. package/src/client/open-with.ts +221 -0
  229. package/src/client/openpath-intercept.ts +143 -0
  230. package/src/client/paths.ts +63 -0
  231. package/src/client/pdf-types.ts +4 -0
  232. package/src/client/pinned.ts +183 -0
  233. package/src/client/plugin-settings.ts +43 -0
  234. package/src/client/plugins-shared.ts +29 -0
  235. package/src/client/plugins-tabs.ts +96 -0
  236. package/src/client/plugins-viewers.ts +30 -0
  237. package/src/client/prefs.ts +210 -0
  238. package/src/client/produced-files.ts +120 -0
  239. package/src/client/selection-payload.ts +86 -0
  240. package/src/client/service.ts +859 -0
  241. package/src/client/settings-nav-icon.ts +45 -0
  242. package/src/client/shell-presets.ts +83 -0
  243. package/src/client/sidebar.module.css +2788 -0
  244. package/src/client/sidechat-transcript.ts +321 -0
  245. package/src/client/split-pane.tsx +331 -0
  246. package/src/client/state.ts +1497 -0
  247. package/src/client/subagent-detect.ts +137 -0
  248. package/src/client/subagent-jobs.ts +161 -0
  249. package/src/client/tab-content-memo.ts +55 -0
  250. package/src/client/terminal-font.ts +224 -0
  251. package/src/client/terminal-links.ts +194 -0
  252. package/src/client/theme.ts +110 -0
  253. package/src/client/titlebar-strip.ts +37 -0
  254. package/src/client/upload.ts +187 -0
  255. package/src/client/wco.ts +120 -0
  256. package/src/config.ts +154 -0
  257. package/src/context-types.ts +611 -0
  258. package/src/fs-operations.ts +95 -0
  259. package/src/fs-search.ts +113 -0
  260. package/src/fs-tree.ts +158 -0
  261. package/src/git.ts +446 -0
  262. package/src/html-route.ts +106 -0
  263. package/src/index.ts +1291 -0
  264. package/src/invariant.ts +32 -0
  265. package/src/jobs-routes.ts +257 -0
  266. package/src/open-external.ts +90 -0
  267. package/src/path-security.ts +75 -0
  268. package/src/prefs-shared.ts +280 -0
  269. package/src/pty-deps.ts +240 -0
  270. package/src/pty-manager.ts +376 -0
  271. package/src/sidechat-core.ts +468 -0
  272. package/src/sidechat-routes.ts +344 -0
  273. package/src/subagent-activity.ts +91 -0
  274. package/src/subagent-live-route.ts +93 -0
  275. package/src/tools.ts +482 -0
  276. package/src/trust-fence.ts +84 -0
  277. package/src/wire.ts +100 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * The tab strip of one pane: tabs capped at TAB_MAX_WIDTH (ellipsized),
3
+ * overflow scrolls horizontally, a close button per tab, a four-way split
4
+ * button cluster, and the + menu that opens new tabs (explorer / git /
5
+ * terminal). Tabs are draggable; dropping onto another tab inserts before it,
6
+ * dropping on the strip background appends to this pane. Right-clicking a
7
+ * tab opens the tab context menu (float as a free window / close / close
8
+ * others / close to the left / close to the right, the close ones scoped to
9
+ * this pane).
10
+ */
11
+ import { useEffect, useRef, useState, type ReactNode } from 'react'
12
+ import clsx from 'clsx'
13
+ import {
14
+ IconCloseFill14, IconPlusOutline16, Menu,
15
+ } from '@deepseek-ai/dsh-client-ui-primitives'
16
+ import type { SidebarTab } from './state.ts'
17
+ import { isAgentTabId } from './state.ts'
18
+ import { isPinnedVirtualTab } from './pinned.ts'
19
+ import { IconPinOutline16 } from './icons.tsx'
20
+ import { t } from './locales.ts'
21
+ import css from './sidebar.module.css'
22
+
23
+ /** One + menu option. */
24
+ export interface NewTabOption {
25
+ id: string
26
+ label: string
27
+ disabled?: boolean
28
+ /** Leading icon (Menu row). */
29
+ icon?: ReactNode
30
+ }
31
+
32
+ /** Drag payload for tab moves (HTML5 DnD dataTransfer). */
33
+ export const TAB_DRAG_TYPE = 'application/x-dsh-tab'
34
+
35
+ export interface TabDragPayload {
36
+ tabId: string
37
+ paneId: string
38
+ }
39
+
40
+ export function serializeDrag(payload: TabDragPayload): string {
41
+ return JSON.stringify(payload)
42
+ }
43
+
44
+ export function parseDrag(raw: string): TabDragPayload | null {
45
+ try {
46
+ const parsed = JSON.parse(raw) as TabDragPayload
47
+ if (typeof parsed.tabId === 'string' && typeof parsed.paneId === 'string') return parsed
48
+ return null
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ /** Global tab-drag flag: PDF iframes become non-interactive synchronously. */
55
+ function setTabDragging(active: boolean): void {
56
+ if (active) document.body.setAttribute('data-dsh-tab-dragging', '')
57
+ else document.body.removeAttribute('data-dsh-tab-dragging')
58
+ }
59
+
60
+ export function TabBar(props: {
61
+ paneId: string
62
+ tabs: SidebarTab[]
63
+ active: string | null
64
+ onActivate: (tabId: string) => void
65
+ onClose: (tabId: string) => void
66
+ onNewTab: (optionId: string) => void
67
+ newTabOptions: NewTabOption[]
68
+ /** Drop of a tab from any pane: (payload, insertBeforeTabId | null). */
69
+ onDropTab: (payload: TabDragPayload, before: string | null) => void
70
+ /** Float a tab out as a free window (the tab context menu's entry; the
71
+ * drag-to-conversation gesture is handled at the Sidebar shell level). */
72
+ onFloatTab: (tabId: string) => void
73
+ /**
74
+ * Pin/unpin a terminal tab (v0.17.0+). Called with `'workspace'` or
75
+ * `'global'` to pin (the shell snapshots the home cwd), or `null` to
76
+ * unpin. Non-terminal tabs never trigger this callback. Optional: the
77
+ * menu hides the pin entry when unset (legacy callers).
78
+ */
79
+ onPinTab?: (tabId: string, scope: 'workspace' | 'global' | null) => void
80
+ /** Icon resolver for tab labels (reads from the tab descriptor registry). */
81
+ getTabIcon?: (tab: SidebarTab) => ReactNode
82
+ /** Badge resolver for tab labels (reads the descriptor's `badge`; the
83
+ * resolver returns the rendered pill or null). */
84
+ getTabBadge?: (tab: SidebarTab) => ReactNode
85
+ }) {
86
+ const {
87
+ paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, onFloatTab, onPinTab, getTabIcon, getTabBadge,
88
+ } = props
89
+ const [menuOpen, setMenuOpen] = useState(false)
90
+ // The tab right-click context menu: the target tab plus the cursor
91
+ // position (the portaled Menu anchors there, following GitView/FileTree).
92
+ const [tabMenu, setTabMenu] = useState<{ tabId: string; x: number; y: number } | null>(null)
93
+ const [dragOver, setDragOver] = useState(false)
94
+ const listRef = useRef<HTMLDivElement>(null)
95
+ // The context target's index in the render-time tab snapshot; -1 when the
96
+ // tab disappeared since the menu opened (the menu hides then).
97
+ const tabMenuIndex = tabMenu === null ? -1 : tabs.findIndex(tab => tab.id === tabMenu.tabId)
98
+
99
+ // Middle-click close: the press target is recorded on middle mousedown
100
+ // (preventDefaulted to disarm Chrome's middle-click autoscroll — its
101
+ // indicator is inert here because the strip hides its scrollbar and only
102
+ // the wheel handler scrolls) and the close settles on the first middle
103
+ // mouseup OVER that same tab. Release-position semantics match VS Code
104
+ // (microsoft/vscode#101028) and what users expect from Chrome tabs
105
+ // (crbug/40679924): pressing on a tab and releasing elsewhere cancels the
106
+ // close. The browser dispatches auxclick to the nearest common ancestor of
107
+ // the press/release targets when they differ, so any drift, autoscroll
108
+ // scroll, or tab-list reflow between press and release would otherwise
109
+ // swallow the close; settling on the recorded press target at mouseup
110
+ // keeps release semantics without depending on auxclick delivery.
111
+ const onCloseRef = useRef(onClose)
112
+ const middlePressed = useRef<{ id: string; node: HTMLElement } | null>(null)
113
+ useEffect(() => {
114
+ onCloseRef.current = onClose
115
+ })
116
+ useEffect(() => {
117
+ const onMouseUp = (event: MouseEvent): void => {
118
+ if (event.button !== 1) return
119
+ const pressed = middlePressed.current
120
+ middlePressed.current = null
121
+ // Close only when the release lands on the pressed tab; a drag-away
122
+ // release cancels the press (one-shot per press).
123
+ if (pressed !== null && pressed.node.isConnected && pressed.node.contains(event.target as Node)) {
124
+ onCloseRef.current(pressed.id)
125
+ }
126
+ }
127
+ window.addEventListener('mouseup', onMouseUp)
128
+ return () => { window.removeEventListener('mouseup', onMouseUp) }
129
+ }, [])
130
+
131
+ // Wheel over the strip scrolls the tab row horizontally (a plain mouse
132
+ // wheel emits deltaY, which overflow-x alone never consumes). Bound as a
133
+ // native NON-passive listener: React registers onWheel passively at the
134
+ // root, where preventDefault() is a no-op. Modifier keys keep their native
135
+ // meaning (shift = horizontal scroll, ctrl/cmd = zoom), and a strip that
136
+ // does not overflow leaves the event alone so the page scrolls normally.
137
+ useEffect(() => {
138
+ const el = listRef.current
139
+ if (el === null) return
140
+ const onWheel = (event: WheelEvent): void => {
141
+ if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return
142
+ if (el.scrollWidth <= el.clientWidth) return
143
+ event.preventDefault()
144
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? el.clientWidth : 1
145
+ el.scrollLeft += (event.deltaX + event.deltaY) * unit
146
+ }
147
+ el.addEventListener('wheel', onWheel, { passive: false })
148
+ return () => { el.removeEventListener('wheel', onWheel) }
149
+ }, [])
150
+
151
+ useEffect(() => {
152
+ const clear = (): void => { setTabDragging(false); setDragOver(false) }
153
+ window.addEventListener('dragend', clear, true)
154
+ window.addEventListener('drop', clear, true)
155
+ window.addEventListener('blur', clear)
156
+ return () => {
157
+ window.removeEventListener('dragend', clear, true)
158
+ window.removeEventListener('drop', clear, true)
159
+ window.removeEventListener('blur', clear)
160
+ }
161
+ }, [])
162
+
163
+ return (
164
+ <div
165
+ className={clsx(css.tabBar, dragOver && css.tabBarDrop)}
166
+ onDragOver={(event) => {
167
+ // The strip owns drops on itself (merge into this pane); stopping
168
+ // propagation keeps the pane root from also running its edge-zone
169
+ // handler on the same drop.
170
+ event.preventDefault()
171
+ event.stopPropagation()
172
+ setDragOver(true)
173
+ }}
174
+ onDragLeave={() => { setDragOver(false) }}
175
+ onDrop={(event) => {
176
+ event.preventDefault()
177
+ event.stopPropagation()
178
+ setDragOver(false)
179
+ setTabDragging(false)
180
+ const raw = event.dataTransfer.getData(TAB_DRAG_TYPE)
181
+ const payload = parseDrag(raw)
182
+ if (payload !== null) onDropTab(payload, null)
183
+ }}
184
+ >
185
+ <div ref={listRef} className={css.tabList}>
186
+ {tabs.map(tab => {
187
+ const pinned = isPinnedVirtualTab(tab) || tab.pin !== undefined
188
+ return (
189
+ <div
190
+ key={tab.id}
191
+ className={clsx(css.tab, active === tab.id && css.tabActive, pinned && css.pinnedTab)}
192
+ title={tab.title}
193
+ draggable={!pinned}
194
+ onDragStart={pinned ? undefined : (event) => {
195
+ setTabDragging(true)
196
+ event.dataTransfer.setData(TAB_DRAG_TYPE, serializeDrag({ tabId: tab.id, paneId }))
197
+ event.dataTransfer.effectAllowed = 'move'
198
+ }}
199
+ onDragEnd={() => { setTabDragging(false); setDragOver(false) }}
200
+ onDragOver={(event) => { event.preventDefault(); event.stopPropagation() }}
201
+ onDrop={(event) => {
202
+ if (pinned) { event.stopPropagation(); return }
203
+ event.preventDefault()
204
+ event.stopPropagation()
205
+ setTabDragging(false)
206
+ const raw = event.dataTransfer.getData(TAB_DRAG_TYPE)
207
+ const payload = parseDrag(raw)
208
+ if (payload !== null) onDropTab(payload, tab.id)
209
+ }}
210
+ onClick={() => { onActivate(tab.id) }}
211
+ onMouseDown={(event) => {
212
+ // Middle-click close: record the press target and disarm
213
+ // Chrome's middle-click autoscroll (its indicator is inert
214
+ // here — the strip scrolls via the wheel handler only). The
215
+ // close itself settles on the first middle mouseup over this
216
+ // same tab (window-level), keeping release semantics.
217
+ if (event.button === 1) {
218
+ event.preventDefault()
219
+ middlePressed.current = { id: tab.id, node: event.currentTarget }
220
+ }
221
+ }}
222
+ onContextMenu={(event) => {
223
+ // Take over the browser menu: the tab context menu offers the
224
+ // close operations for this pane. Opening it also dismisses
225
+ // the + menu (only one menu at a time).
226
+ event.preventDefault()
227
+ setMenuOpen(false)
228
+ setTabMenu({ tabId: tab.id, x: event.clientX, y: event.clientY })
229
+ }}
230
+ >
231
+ {pinned && <IconPinOutline16 size={16} />}
232
+ {getTabIcon?.(tab) ?? null}
233
+ {getTabBadge?.(tab) ?? null}
234
+ <span className={css.tabTitle}>{tab.title}</span>
235
+ <button
236
+ type="button"
237
+ className={css.tabClose}
238
+ aria-label={t('close')}
239
+ onClick={(event) => {
240
+ event.stopPropagation()
241
+ onClose(tab.id)
242
+ }}
243
+ >
244
+ <IconCloseFill14 />
245
+ </button>
246
+ </div>
247
+ )
248
+ })}
249
+ {/*
250
+ The + sits immediately after the rightmost tab (sticky at the
251
+ right edge of the scrollport when the tabs overflow, so it stays
252
+ reachable no matter how many tabs are open).
253
+ */}
254
+ <Menu
255
+ open={menuOpen}
256
+ onClose={() => { setMenuOpen(false) }}
257
+ items={newTabOptions.map(option => ({
258
+ id: option.id,
259
+ label: option.label,
260
+ ...(option.disabled === true ? { disabled: true } : {}),
261
+ ...(option.icon !== undefined ? { icon: option.icon } : {}),
262
+ }))}
263
+ onSelect={(id) => {
264
+ onNewTab(id)
265
+ setMenuOpen(false)
266
+ }}
267
+ portal
268
+ align="end"
269
+ anchor={(
270
+ <button
271
+ type="button"
272
+ className={css.tabBarPlus}
273
+ aria-label={t('newTab')}
274
+ title={t('newTab')}
275
+ onClick={() => { setMenuOpen(v => !v); setTabMenu(null) }}
276
+ >
277
+ <IconPlusOutline16 />
278
+ </button>
279
+ )}
280
+ />
281
+ {/*
282
+ The tab context menu, positioned at the right-click cursor (portal
283
+ so the panel's overflow clip cannot crop it). Close operations are
284
+ scoped to THIS pane: "close others/left/right" walk the render-time
285
+ tab snapshot and reuse the per-tab onClose path (which routes
286
+ through the service and releases terminals), so the target tab is
287
+ never closed and the pane never empties mid-loop.
288
+ */}
289
+ <Menu
290
+ open={tabMenu !== null && tabMenuIndex >= 0}
291
+ onClose={() => { setTabMenu(null) }}
292
+ items={(() => {
293
+ // The target tab drives the pin entry's shape: terminal tabs
294
+ // get either a "Pin ▸" submenu (unpinned) or a single "Unpin"
295
+ // row (pinned). Non-terminal tabs and missing onPinTab get no
296
+ // pin entry at all — the menu stays exactly the legacy 5-item
297
+ // shape. Pinned VIRTUAL tabs (injected from other sessions)
298
+ // get a stripped menu: only Unpin + Close (no float, no
299
+ // close-others/left/right — those are pane-scoped operations
300
+ // that don't apply to cross-session virtual tabs).
301
+ const targetTab = tabMenuIndex >= 0 ? tabs[tabMenuIndex] : undefined
302
+ const isTerminal = targetTab?.type === 'terminal'
303
+ const isPinnedVirtual = targetTab !== undefined && isPinnedVirtualTab(targetTab)
304
+ const pinEntries = isTerminal && onPinTab !== undefined
305
+ ? targetTab!.pin !== undefined
306
+ ? [{ id: 'unpin', label: t('unpinTerminal') }]
307
+ : [{
308
+ id: 'pin',
309
+ label: isAgentTabId(targetTab!.id) ? t('pinAgentTerminal') : t('pinTerminal'),
310
+ submenu: [
311
+ { id: 'pinWorkspace', label: t('pinToWorkspace') },
312
+ { id: 'pinGlobal', label: t('pinToGlobal') },
313
+ ],
314
+ }]
315
+ : []
316
+ if (isPinnedVirtual) {
317
+ return [
318
+ ...pinEntries,
319
+ { id: 'close', label: t('close') },
320
+ ]
321
+ }
322
+ return [
323
+ { id: 'float', label: t('moveToFreeWindow') },
324
+ ...pinEntries,
325
+ { id: 'close', label: t('close') },
326
+ { id: 'closeOthers', label: t('closeOtherTabs'), ...(tabs.length <= 1 ? { disabled: true } : {}) },
327
+ { id: 'closeLeft', label: t('closeLeftTabs'), ...(tabMenuIndex <= 0 ? { disabled: true } : {}) },
328
+ { id: 'closeRight', label: t('closeRightTabs'), ...(tabMenuIndex >= tabs.length - 1 ? { disabled: true } : {}) },
329
+ ]
330
+ })()}
331
+ onSelect={(id) => {
332
+ const target = tabMenu
333
+ if (target === null) return
334
+ setTabMenu(null)
335
+ const index = tabs.findIndex(tab => tab.id === target.tabId)
336
+ if (index < 0) return
337
+ if (id === 'float') {
338
+ onFloatTab(target.tabId)
339
+ } else if (id === 'pinWorkspace') {
340
+ onPinTab?.(target.tabId, 'workspace')
341
+ } else if (id === 'pinGlobal') {
342
+ onPinTab?.(target.tabId, 'global')
343
+ } else if (id === 'unpin') {
344
+ onPinTab?.(target.tabId, null)
345
+ } else if (id === 'close') {
346
+ onClose(target.tabId)
347
+ } else if (id === 'closeOthers') {
348
+ for (const tab of tabs) {
349
+ if (tab.id !== target.tabId) onClose(tab.id)
350
+ }
351
+ } else if (id === 'closeLeft') {
352
+ for (const tab of tabs.slice(0, index)) onClose(tab.id)
353
+ } else if (id === 'closeRight') {
354
+ for (const tab of tabs.slice(index + 1)) onClose(tab.id)
355
+ }
356
+ }}
357
+ portal
358
+ align="start"
359
+ getAnchorRect={() => (tabMenu === null ? null : new DOMRect(tabMenu.x, tabMenu.y, 0, 0))}
360
+ anchor={<span />}
361
+ />
362
+ </div>
363
+ </div>
364
+ )
365
+ }