Files
claude-code-haha/desktop/src/components/workbench/WorkbenchPanel.tsx
T
程序员阿江(Relakkes) f5a97ac21c fix(desktop): expand workbench panels into tabs (#730)
Tested: cd desktop && bun run test -- --run src/components/workbench/WorkbenchPanel.test.tsx
Tested: cd desktop && bun run test -- --run src/components/layout/ContentRouter.test.tsx
Tested: cd desktop && bun run test -- --run src/stores/tabStore.test.ts
Tested: bun run check:desktop
Confidence: high
Scope-risk: moderate
2026-06-15 19:41:57 +08:00

122 lines
5.0 KiB
TypeScript

import { FolderOpen, Globe, Maximize2, X } from 'lucide-react'
import { useTranslation } from '../../i18n'
import {
useWorkspacePanelStore,
type WorkbenchMode,
} from '../../stores/workspacePanelStore'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
import { useTabStore } from '../../stores/tabStore'
import { WorkspacePanel } from '../workspace/WorkspacePanel'
import { BrowserSurface } from '../browser/BrowserSurface'
type WorkbenchPanelProps = {
sessionId: string
variant?: 'panel' | 'tab'
onClose?: () => void
}
const MODE_ITEMS: ReadonlyArray<{
mode: WorkbenchMode
labelKey: 'workbench.modeWorkspace' | 'workbench.modeBrowser'
Icon: typeof FolderOpen
}> = [
{ mode: 'workspace', labelKey: 'workbench.modeWorkspace', Icon: FolderOpen },
{ mode: 'browser', labelKey: 'workbench.modeBrowser', Icon: Globe },
]
/**
* Unified right-side "Workbench" panel. Hosts the file workspace and the native
* browser surface behind a single per-session mode switch (file ↔ browser),
* sharing the panel's open state and width via {@link useWorkspacePanelStore}.
*/
export function WorkbenchPanel({ sessionId, variant = 'panel', onClose }: WorkbenchPanelProps) {
const t = useTranslation()
const mode = useWorkspacePanelStore((state) => state.getMode(sessionId))
const setMode = useWorkspacePanelStore((state) => state.setMode)
const closePanel = useWorkspacePanelStore((state) => state.closePanel)
const ensureBlankBrowser = useBrowserPanelStore((state) => state.ensureBlank)
const isTabVariant = variant === 'tab'
const handleModeSelect = (nextMode: WorkbenchMode) => {
if (nextMode === 'browser') {
ensureBlankBrowser(sessionId)
}
setMode(sessionId, nextMode)
}
const handleExpand = () => {
useTabStore.getState().openWorkbenchTab(sessionId, t('workbench.tabTitle'))
}
const handleClose = () => {
if (onClose) {
onClose()
return
}
closePanel(sessionId)
}
return (
<div className="flex h-full min-h-0 w-full flex-col bg-[var(--color-surface)]">
<div className="flex h-10 shrink-0 items-center gap-2 border-b border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-2.5">
<div
role="tablist"
aria-label={t('workbench.modeSwitch')}
className="inline-flex items-center gap-0.5 rounded-[8px] border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5"
>
{MODE_ITEMS.map(({ mode: itemMode, labelKey, Icon }) => {
const isActive = mode === itemMode
return (
<button
key={itemMode}
type="button"
role="tab"
aria-selected={isActive}
onClick={() => handleModeSelect(itemMode)}
className={`inline-flex h-7 items-center gap-1.5 rounded-[6px] px-2.5 text-[12px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 ${
isActive
? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)] shadow-[inset_0_0_0_1px_var(--color-border-focus)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
}`}
>
<Icon size={15} strokeWidth={2} aria-hidden="true" className="shrink-0" />
<span>{t(labelKey)}</span>
</button>
)
})}
</div>
<div className="ml-auto flex shrink-0 items-center gap-1">
{!isTabVariant && (
<button
type="button"
aria-label={t('workbench.expand')}
title={t('workbench.expand')}
onClick={handleExpand}
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-[7px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
>
<Maximize2 size={15} strokeWidth={2} aria-hidden="true" />
</button>
)}
<button
type="button"
aria-label={t('workbench.close')}
onClick={handleClose}
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-[7px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
>
<X size={16} strokeWidth={2} aria-hidden="true" />
</button>
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col">
{mode === 'browser' ? (
<BrowserSurface sessionId={sessionId} />
) : (
<WorkspacePanel sessionId={sessionId} embedded forceVisible={isTabVariant} />
)}
</div>
</div>
)
}