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.
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/cordis.patch.yml +49 -0
- package/lib/client-editor.js +48229 -0
- package/lib/client-mermaid.js +200745 -0
- package/lib/client-registry.js +21867 -0
- package/lib/client-terminal.js +9361 -0
- package/lib/client.js +21867 -0
- package/lib/index.js +4556 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-opens.d.ts +65 -0
- package/lib/types/agent-pty.d.ts +226 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +12 -0
- package/lib/types/client/BrowserView.d.ts +39 -0
- package/lib/types/client/DiffTab.d.ts +6 -0
- package/lib/types/client/DiffView.d.ts +51 -0
- package/lib/types/client/EditorHost.d.ts +13 -0
- package/lib/types/client/FileTree.d.ts +38 -0
- package/lib/types/client/FreeWindow.d.ts +29 -0
- package/lib/types/client/GitView.d.ts +10 -0
- package/lib/types/client/MarkdownHtml.d.ts +32 -0
- package/lib/types/client/OrphanedTab.d.ts +11 -0
- package/lib/types/client/PdfView.d.ts +6 -0
- package/lib/types/client/RenderBoundary.d.ts +32 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +74 -0
- package/lib/types/client/SideChatView.d.ts +16 -0
- package/lib/types/client/Sidebar.d.ts +6 -0
- package/lib/types/client/SubagentView.d.ts +16 -0
- package/lib/types/client/TabBar.d.ts +54 -0
- package/lib/types/client/TerminalView.d.ts +24 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/TreePanel.d.ts +24 -0
- package/lib/types/client/UploadOverlay.d.ts +20 -0
- package/lib/types/client/add-plugin-modal.d.ts +15 -0
- package/lib/types/client/api.d.ts +275 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +17 -0
- package/lib/types/client/browser.d.ts +60 -0
- package/lib/types/client/builtins/index.d.ts +18 -0
- package/lib/types/client/builtins/tabs.d.ts +11 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chunk-loader.d.ts +131 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/mermaid.d.ts +10 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/cm-themes.d.ts +24 -0
- package/lib/types/client/conversation-draft.d.ts +14 -0
- package/lib/types/client/desktop-env.d.ts +35 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/frame-batcher.d.ts +24 -0
- package/lib/types/client/icons.d.ts +59 -0
- package/lib/types/client/image-types.d.ts +3 -0
- package/lib/types/client/ime-guard.d.ts +36 -0
- package/lib/types/client/index.d.ts +25 -0
- package/lib/types/client/intercept.d.ts +46 -0
- package/lib/types/client/lang.d.ts +14 -0
- package/lib/types/client/layout-push.d.ts +15 -0
- package/lib/types/client/lazy-chunk.d.ts +25 -0
- package/lib/types/client/link-intercept.d.ts +42 -0
- package/lib/types/client/locales-ar.d.ts +18 -0
- package/lib/types/client/locales-de.d.ts +4 -0
- package/lib/types/client/locales-fr.d.ts +10 -0
- package/lib/types/client/locales-hi.d.ts +17 -0
- package/lib/types/client/locales-id.d.ts +15 -0
- package/lib/types/client/locales-it.d.ts +8 -0
- package/lib/types/client/locales-ja.d.ts +17 -0
- package/lib/types/client/locales-ko.d.ts +9 -0
- package/lib/types/client/locales-nl.d.ts +16 -0
- package/lib/types/client/locales-pl.d.ts +19 -0
- package/lib/types/client/locales-pt.d.ts +1 -0
- package/lib/types/client/locales-ru.d.ts +13 -0
- package/lib/types/client/locales-sv.d.ts +1 -0
- package/lib/types/client/locales-th.d.ts +17 -0
- package/lib/types/client/locales-tr.d.ts +17 -0
- package/lib/types/client/locales-vi.d.ts +17 -0
- package/lib/types/client/locales-zh-HK.d.ts +32 -0
- package/lib/types/client/locales-zh-MO.d.ts +32 -0
- package/lib/types/client/locales-zh-TW.d.ts +32 -0
- package/lib/types/client/locales.d.ts +452 -0
- package/lib/types/client/markdown-html.d.ts +96 -0
- package/lib/types/client/markdown-images.d.ts +45 -0
- package/lib/types/client/markdown-labels.d.ts +38 -0
- package/lib/types/client/md-toc.d.ts +4 -0
- package/lib/types/client/mermaid-blocks.d.ts +47 -0
- package/lib/types/client/mermaid-sanitize.d.ts +2 -0
- package/lib/types/client/mermaid.d.ts +12 -0
- package/lib/types/client/open-when-sized.d.ts +20 -0
- package/lib/types/client/open-with-settings.d.ts +4 -0
- package/lib/types/client/open-with.d.ts +87 -0
- package/lib/types/client/openpath-intercept.d.ts +85 -0
- package/lib/types/client/paths.d.ts +43 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/pinned.d.ts +86 -0
- package/lib/types/client/plugin-settings.d.ts +8 -0
- package/lib/types/client/plugins-shared.d.ts +27 -0
- package/lib/types/client/plugins-tabs.d.ts +3 -0
- package/lib/types/client/plugins-viewers.d.ts +3 -0
- package/lib/types/client/prefs.d.ts +41 -0
- package/lib/types/client/produced-files.d.ts +29 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +439 -0
- package/lib/types/client/settings-nav-icon.d.ts +19 -0
- package/lib/types/client/shell-presets.d.ts +48 -0
- package/lib/types/client/sidechat-transcript.d.ts +103 -0
- package/lib/types/client/split-pane.d.ts +36 -0
- package/lib/types/client/state.d.ts +432 -0
- package/lib/types/client/subagent-detect.d.ts +54 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -0
- package/lib/types/client/tab-content-memo.d.ts +40 -0
- package/lib/types/client/terminal-font.d.ts +78 -0
- package/lib/types/client/terminal-links.d.ts +119 -0
- package/lib/types/client/theme.d.ts +45 -0
- package/lib/types/client/titlebar-strip.d.ts +23 -0
- package/lib/types/client/upload.d.ts +69 -0
- package/lib/types/client/wco.d.ts +47 -0
- package/lib/types/config.d.ts +64 -0
- package/lib/types/context-types.d.ts +605 -0
- package/lib/types/fs-operations.d.ts +28 -0
- package/lib/types/fs-search.d.ts +24 -0
- package/lib/types/fs-tree.d.ts +51 -0
- package/lib/types/git.d.ts +124 -0
- package/lib/types/html-route.d.ts +59 -0
- package/lib/types/index.d.ts +47 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/jobs-routes.d.ts +45 -0
- package/lib/types/open-external.d.ts +24 -0
- package/lib/types/path-security.d.ts +20 -0
- package/lib/types/prefs-shared.d.ts +236 -0
- package/lib/types/pty-deps.d.ts +78 -0
- package/lib/types/pty-manager.d.ts +168 -0
- package/lib/types/sidechat-core.d.ts +172 -0
- package/lib/types/sidechat-routes.d.ts +30 -0
- package/lib/types/subagent-activity.d.ts +44 -0
- package/lib/types/subagent-live-route.d.ts +44 -0
- package/lib/types/tools.d.ts +34 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +172 -0
- package/scripts/install.ps1 +306 -0
- package/scripts/install.sh +287 -0
- package/src/agent-opens.ts +265 -0
- package/src/agent-pty.ts +527 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +129 -0
- package/src/client/BrowserView.tsx +289 -0
- package/src/client/DiffTab.tsx +114 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +558 -0
- package/src/client/FileTree.tsx +669 -0
- package/src/client/FreeWindow.tsx +337 -0
- package/src/client/GitView.tsx +717 -0
- package/src/client/MarkdownHtml.tsx +296 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/RenderBoundary.tsx +49 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +960 -0
- package/src/client/SideCardSection.tsx +1163 -0
- package/src/client/SideChatView.module.css +437 -0
- package/src/client/SideChatView.tsx +645 -0
- package/src/client/Sidebar.tsx +1370 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +883 -0
- package/src/client/TabBar.tsx +365 -0
- package/src/client/TerminalView.tsx +424 -0
- package/src/client/TextEditor.tsx +468 -0
- package/src/client/TreePanel.tsx +275 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +357 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +61 -0
- package/src/client/browser.ts +167 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +340 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chunk-loader.ts +358 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/mermaid.tsx +10 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/cm-themes.ts +129 -0
- package/src/client/conversation-draft.ts +29 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/desktop-env.ts +72 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +152 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +427 -0
- package/src/client/intercept.tsx +188 -0
- package/src/client/lang.ts +225 -0
- package/src/client/layout-push.ts +24 -0
- package/src/client/layout.css +90 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales-ar.ts +368 -0
- package/src/client/locales-de.ts +353 -0
- package/src/client/locales-fr.ts +360 -0
- package/src/client/locales-hi.ts +367 -0
- package/src/client/locales-id.ts +365 -0
- package/src/client/locales-it.ts +358 -0
- package/src/client/locales-ja.ts +367 -0
- package/src/client/locales-ko.ts +359 -0
- package/src/client/locales-nl.ts +365 -0
- package/src/client/locales-pl.ts +369 -0
- package/src/client/locales-pt.ts +350 -0
- package/src/client/locales-ru.ts +365 -0
- package/src/client/locales-sv.ts +350 -0
- package/src/client/locales-th.ts +367 -0
- package/src/client/locales-tr.ts +367 -0
- package/src/client/locales-vi.ts +367 -0
- package/src/client/locales-zh-HK.ts +382 -0
- package/src/client/locales-zh-MO.ts +382 -0
- package/src/client/locales-zh-TW.ts +382 -0
- package/src/client/locales.ts +894 -0
- package/src/client/markdown-html.ts +331 -0
- package/src/client/markdown-images.ts +137 -0
- package/src/client/markdown-labels.tsx +52 -0
- package/src/client/md-toc.tsx +127 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +406 -0
- package/src/client/open-when-sized.ts +43 -0
- package/src/client/open-with-settings.tsx +135 -0
- package/src/client/open-with.ts +221 -0
- package/src/client/openpath-intercept.ts +143 -0
- package/src/client/paths.ts +63 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/pinned.ts +183 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +96 -0
- package/src/client/plugins-viewers.ts +30 -0
- package/src/client/prefs.ts +210 -0
- package/src/client/produced-files.ts +120 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +859 -0
- package/src/client/settings-nav-icon.ts +45 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +2788 -0
- package/src/client/sidechat-transcript.ts +321 -0
- package/src/client/split-pane.tsx +331 -0
- package/src/client/state.ts +1497 -0
- package/src/client/subagent-detect.ts +137 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/tab-content-memo.ts +55 -0
- package/src/client/terminal-font.ts +224 -0
- package/src/client/terminal-links.ts +194 -0
- package/src/client/theme.ts +110 -0
- package/src/client/titlebar-strip.ts +37 -0
- package/src/client/upload.ts +187 -0
- package/src/client/wco.ts +120 -0
- package/src/config.ts +154 -0
- package/src/context-types.ts +611 -0
- package/src/fs-operations.ts +95 -0
- package/src/fs-search.ts +113 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +446 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1291 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/open-external.ts +90 -0
- package/src/path-security.ts +75 -0
- package/src/prefs-shared.ts +280 -0
- package/src/pty-deps.ts +240 -0
- package/src/pty-manager.ts +376 -0
- package/src/sidechat-core.ts +468 -0
- package/src/sidechat-routes.ts +344 -0
- package/src/subagent-activity.ts +91 -0
- package/src/subagent-live-route.ts +93 -0
- package/src/tools.ts +482 -0
- package/src/trust-fence.ts +84 -0
- 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
|
+
}
|