import type { ContextMenuItem, PreviewSessionSnapshot } from "@t3tools/contracts";import { getTerminalLabel } from "@t3tools/shared/terminalLabels";import { ClipboardList, FileDiff, Files, Globe2, Plus, TerminalSquare, X } from "lucide-react";import { type MouseEvent as ReactMouseEvent, type ReactElement, type ReactNode, useCallback, useEffect, useRef, useState,} from "react"; import { isElectron } from "~/env";import type { RightPanelSurface } from "~/rightPanelStore";import { cn } from "~/lib/utils";import { readLocalApi } from "~/localApi";import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu";import { ScrollArea } from "~/components/ui/scroll-area";import { faviconUrlForOrigin } from "~/lib/favicon";import { useTheme } from "~/hooks/useTheme";import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; import { PreviewPanelShell, type PreviewPanelMode } from "./preview/PreviewPanelShell";import { PierreEntryIcon } from "./chat/PierreEntryIcon"; interface RightPanelTabsProps { mode: PreviewPanelMode; maximized?: boolean; layoutControls?: ReactNode; surfaces: readonly RightPanelSurface[]; activeSurfaceId: string | null; pendingSurfaceIds: ReadonlySet<string>; previewSessions: Readonly<Record<string, PreviewSessionSnapshot>>; terminalLabelsById: ReadonlyMap<string, string>; onActivate: (surface: RightPanelSurface) => void; onCloseSurface: (surface: RightPanelSurface) => void; onCloseOtherSurfaces: (surface: RightPanelSurface) => void; onCloseSurfacesToRight: (surface: RightPanelSurface) => void; onCloseAllSurfaces: () => void; onCopyFilePath: (relativePath: string) => void; onAddBrowser: () => void; onAddTerminal: () => void; onAddDiff: () => void; onAddFiles: () => void; browserAvailable: boolean; diffAvailable: boolean; filesAvailable: boolean; children: ReactNode;} const SURFACE_DISABLED_REASONS = { browser: "Browser previews are only available in the T3 Code desktop app.", files: "Files are only available when a project is open.", diff: "Diff is only available for server threads in Git repositories.",} as const; type TabContextMenuAction = "copy-path" | "close" | "close-others" | "close-to-right" | "close-all"; function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) { return ( <Tooltip> <TooltipTrigger render={props.trigger} /> <TooltipPopup side="top">{props.reason}</TooltipPopup> </Tooltip> );} function SurfaceMenuItem(props: { available: boolean; disabledReason?: string; onClick: () => void; children: ReactNode;}) { const item = ( <MenuItem className={!props.available ? "data-disabled:pointer-events-auto" : undefined} onClick={props.onClick} disabled={!props.available} > {props.children} </MenuItem> ); if (props.available || !props.disabledReason) return item; return <DisabledReasonTooltip reason={props.disabledReason} trigger={item} />;} function RightPanelEmptyState(props: { onAddBrowser: () => void; onAddTerminal: () => void; onAddDiff: () => void; onAddFiles: () => void; browserAvailable: boolean; diffAvailable: boolean; filesAvailable: boolean;}) { const actions = [ { label: "Browser", description: "Open a local app or URL.", icon: Globe2, available: props.browserAvailable, disabledReason: SURFACE_DISABLED_REASONS.browser, onClick: props.onAddBrowser, }, { label: "Terminal", description: "Start a shell in this workspace.", icon: TerminalSquare, available: true, disabledReason: null, onClick: props.onAddTerminal, }, { label: "Files", description: "Browse and read workspace files.", icon: Files, available: props.filesAvailable, disabledReason: SURFACE_DISABLED_REASONS.files, onClick: props.onAddFiles, }, { label: "Diff", description: "Review changes in this thread.", icon: FileDiff, available: props.diffAvailable, disabledReason: SURFACE_DISABLED_REASONS.diff, onClick: props.onAddDiff, }, ] as const; return ( <div className="flex min-h-0 flex-1 items-center justify-center p-6"> <div className="w-full max-w-xl"> <div className="mb-5 text-center"> <h3 className="text-sm font-medium text-foreground">Open a surface</h3> <p className="mt-1 text-xs text-muted-foreground"> Choose what to show in the right panel. </p> </div> <div className="grid grid-cols-2 gap-2"> {actions.map((action) => { const Icon = action.icon; const content = ( <> <Icon className="mb-3 size-5" /> <span className="text-sm font-medium">{action.label}</span> <span className="mt-1 text-xs leading-relaxed text-muted-foreground"> {action.description} </span> </> ); if (action.available) { return ( <button key={action.label} type="button" onClick={action.onClick} className="flex min-h-28 w-full flex-col items-start rounded-lg border border-border/80 bg-card p-4 text-left transition hover:border-border hover:bg-accent/60 dark:border-transparent dark:shadow-none dark:inset-ring-1 dark:inset-ring-white/5" > {content} </button> ); } const disabledCard = ( <button type="button" className="flex min-h-28 w-full cursor-not-allowed flex-col items-start rounded-lg border border-border/80 bg-card p-4 text-left opacity-40 dark:border-transparent dark:shadow-none dark:inset-ring-1 dark:inset-ring-white/5" aria-disabled="true" > {content} </button> ); return ( <DisabledReasonTooltip key={action.label} reason={action.disabledReason} trigger={disabledCard} /> ); })} </div> </div> </div> );} function surfaceTitle( surface: RightPanelSurface, sessions: Readonly<Record<string, PreviewSessionSnapshot>>, terminalLabelsById: ReadonlyMap<string, string>,): string { switch (surface.kind) { case "diff": return "Diff"; case "files": return "Files"; case "file": return surface.relativePath.slice(surface.relativePath.lastIndexOf("/") + 1); case "terminal": return ( terminalLabelsById.get(surface.activeTerminalId) ?? getTerminalLabel(surface.activeTerminalId) ); case "plan": return "Plan"; case "preview": { const snapshot = surface.resourceId ? sessions[surface.resourceId] : null; if (!snapshot || snapshot.navStatus._tag === "Idle") return "Browser"; if (snapshot.navStatus.title.trim().length > 0) return snapshot.navStatus.title; try { return new URL(snapshot.navStatus.url).host || "Browser"; } catch { return "Browser"; } } }} function PreviewFavicon({ url }: { url: string | null }) { const faviconUrl = faviconUrlForOrigin(url, 32); const [failedUrl, setFailedUrl] = useState<string | null>(null); if (!faviconUrl || failedUrl === faviconUrl) return <Globe2 className="size-3.5 shrink-0" />; return ( <img src={faviconUrl} alt="" aria-hidden draggable={false} className="size-3.5 shrink-0 rounded-sm" onError={() => setFailedUrl(faviconUrl)} /> );} function SurfaceIcon({ surface, sessions, theme,}: { surface: RightPanelSurface; sessions: Readonly<Record<string, PreviewSessionSnapshot>>; theme: "light" | "dark";}) { switch (surface.kind) { case "preview": { const snapshot = surface.resourceId ? sessions[surface.resourceId] : null; const url = !snapshot || snapshot.navStatus._tag === "Idle" ? null : snapshot.navStatus.url; return <PreviewFavicon url={url} />; } case "diff": return <FileDiff className="size-3.5 shrink-0" />; case "files": return <Files className="size-3.5 shrink-0" />; case "file": return ( <PierreEntryIcon pathValue={surface.relativePath} kind="file" theme={theme} className="size-3.5" /> ); case "terminal": return <TerminalSquare className="size-3.5 shrink-0" />; case "plan": return <ClipboardList className="size-3.5 shrink-0" />; }} export function RightPanelTabs(props: RightPanelTabsProps) { const ownsDesktopTitleBar = isElectron && props.mode === "inline"; const { resolvedTheme } = useTheme(); const tabListRef = useRef<HTMLDivElement>(null); const handleTabContextMenu = useCallback( async (event: ReactMouseEvent, surface: RightPanelSurface) => { event.preventDefault(); event.stopPropagation(); const api = readLocalApi(); if (!api) return; const surfaceIndex = props.surfaces.findIndex((entry) => entry.id === surface.id); if (surfaceIndex < 0) return; const items: ContextMenuItem<TabContextMenuAction>[] = []; if (surface.kind === "file") { items.push({ id: "copy-path", label: "Copy path" }); } items.push( { id: "close", label: "Close" }, { id: "close-others", label: "Close others", disabled: props.surfaces.length <= 1, }, { id: "close-to-right", label: "Close to the right", disabled: surfaceIndex >= props.surfaces.length - 1, }, { id: "close-all", label: "Close all", disabled: props.surfaces.length === 0, }, ); const action = await api.contextMenu.show(items, { x: event.clientX, y: event.clientY }); switch (action) { case "copy-path": if (surface.kind === "file") props.onCopyFilePath(surface.relativePath); break; case "close": props.onCloseSurface(surface); break; case "close-others": props.onCloseOtherSurfaces(surface); break; case "close-to-right": props.onCloseSurfacesToRight(surface); break; case "close-all": props.onCloseAllSurfaces(); break; case null: break; } }, [props], ); const handleTabMouseDown = useCallback((event: ReactMouseEvent) => { if (event.button !== 1) return; event.preventDefault(); }, []); const handleTabAuxClick = useCallback( (event: ReactMouseEvent, surface: RightPanelSurface) => { if (event.button !== 1) return; event.preventDefault(); event.stopPropagation(); props.onCloseSurface(surface); }, [props], ); useEffect(() => { const activeTab = tabListRef.current?.querySelector<HTMLElement>("[data-active-tab='true']"); activeTab?.scrollIntoView({ block: "nearest", inline: "nearest" }); }, [props.activeSurfaceId]); return ( <PreviewPanelShell mode={props.mode} {...(props.maximized !== undefined ? { maximized: props.maximized } : {})} > <div className={cn( "workspace-topbar gap-1 pl-2", props.mode === "inline" ? "pr-28" : "pr-3", ownsDesktopTitleBar && "wco:pr-[calc(var(--workspace-native-controls-inset)+6rem)]", props.mode === "inline" && props.maximized && COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, )} data-right-panel-tabbar > <ScrollArea ref={tabListRef} hideScrollbars scrollFade className={cn("min-w-0 flex-1 rounded-none", ownsDesktopTitleBar && "drag-region")} data-right-panel-tab-list > <div className="flex h-full w-max min-w-full items-center gap-1"> {props.surfaces.map((surface) => { const active = surface.id === props.activeSurfaceId; const pending = props.pendingSurfaceIds.has(surface.id); const title = surfaceTitle(surface, props.previewSessions, props.terminalLabelsById); return ( <div key={surface.id} data-active-tab={active} onMouseDown={handleTabMouseDown} onAuxClick={(event) => handleTabAuxClick(event, surface)} onContextMenu={(event) => void handleTabContextMenu(event, surface)} className={cn( "group flex h-7 min-w-25 max-w-44 shrink-0 items-center gap-1.5 rounded-md px-2 text-sm", active ? "bg-accent text-foreground" : "text-muted-foreground hover:bg-accent/60 hover:text-foreground", )} > <Tooltip> <TooltipTrigger render={ <button type="button" className="flex min-w-0 flex-1 items-center gap-1.5" onClick={() => props.onActivate(surface)} > <SurfaceIcon surface={surface} sessions={props.previewSessions} theme={resolvedTheme} /> <span className="truncate">{title}</span> </button> } /> <TooltipPopup>{title}</TooltipPopup> </Tooltip> <button type="button" className={cn( "relative flex size-4 shrink-0 items-center justify-center rounded hover:bg-muted focus:opacity-100", pending ? "opacity-100" : "opacity-0 group-hover:opacity-100", )} aria-label={`Close ${title}`} onClick={() => props.onCloseSurface(surface)} > {pending ? ( <> <span className="size-2 rounded-full bg-current group-hover:hidden" aria-hidden /> <X className="hidden size-3 group-hover:block" /> </> ) : ( <X className="size-3" /> )} </button> </div> ); })} {props.surfaces.length > 0 ? ( <Menu> <MenuTrigger className="relative inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground" aria-label="Add panel surface" > <Plus className="size-4" /> </MenuTrigger> <MenuPopup align="start" side="bottom" sideOffset={6} className="min-w-44"> <SurfaceMenuItem available={props.browserAvailable} disabledReason={SURFACE_DISABLED_REASONS.browser} onClick={props.onAddBrowser} > <Globe2 /> Browser </SurfaceMenuItem> <SurfaceMenuItem available onClick={props.onAddTerminal}> <TerminalSquare /> Terminal </SurfaceMenuItem> <SurfaceMenuItem available={props.filesAvailable} disabledReason={SURFACE_DISABLED_REASONS.files} onClick={props.onAddFiles} > <Files /> Files </SurfaceMenuItem> <SurfaceMenuItem available={props.diffAvailable} disabledReason={SURFACE_DISABLED_REASONS.diff} onClick={props.onAddDiff} > <FileDiff /> Diff </SurfaceMenuItem> </MenuPopup> </Menu> ) : null} </div> </ScrollArea> {props.layoutControls} </div> <div className="flex min-h-0 flex-1 flex-col"> {props.activeSurfaceId === null ? ( <RightPanelEmptyState onAddBrowser={props.onAddBrowser} onAddTerminal={props.onAddTerminal} onAddDiff={props.onAddDiff} onAddFiles={props.onAddFiles} browserAvailable={props.browserAvailable} diffAvailable={props.diffAvailable} filesAvailable={props.filesAvailable} /> ) : ( props.children )} </div> </PreviewPanelShell> );}
import type { ContextMenuItem, PreviewSessionSnapshot } from "@t3tools/contracts";
import { getTerminalLabel } from "@t3tools/shared/terminalLabels";
import { ClipboardList, FileDiff, Files, Globe2, Plus, TerminalSquare, X } from "lucide-react";
import {
type MouseEvent as ReactMouseEvent,
type ReactElement,
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { isElectron } from "~/env";
import type { RightPanelSurface } from "~/rightPanelStore";
import { cn } from "~/lib/utils";
import { readLocalApi } from "~/localApi";
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu";
import { ScrollArea } from "~/components/ui/scroll-area";
import { faviconUrlForOrigin } from "~/lib/favicon";
import { useTheme } from "~/hooks/useTheme";
import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
import { PreviewPanelShell, type PreviewPanelMode } from "./preview/PreviewPanelShell";
import { PierreEntryIcon } from "./chat/PierreEntryIcon";
interface RightPanelTabsProps {
mode: PreviewPanelMode;
maximized?: boolean;
layoutControls?: ReactNode;
surfaces: readonly RightPanelSurface[];
activeSurfaceId: string | null;
pendingSurfaceIds: ReadonlySet<string>;
previewSessions: Readonly<Record<string, PreviewSessionSnapshot>>;
terminalLabelsById: ReadonlyMap<string, string>;
onActivate: (surface: RightPanelSurface) => void;
onCloseSurface: (surface: RightPanelSurface) => void;
onCloseOtherSurfaces: (surface: RightPanelSurface) => void;
onCloseSurfacesToRight: (surface: RightPanelSurface) => void;
onCloseAllSurfaces: () => void;
onCopyFilePath: (relativePath: string) => void;
onAddBrowser: () => void;
onAddTerminal: () => void;
onAddDiff: () => void;
onAddFiles: () => void;
browserAvailable: boolean;
diffAvailable: boolean;
filesAvailable: boolean;
children: ReactNode;
}
const SURFACE_DISABLED_REASONS = {
browser: "Browser previews are only available in the T3 Code desktop app.",
files: "Files are only available when a project is open.",
diff: "Diff is only available for server threads in Git repositories.",
} as const;
type TabContextMenuAction = "copy-path" | "close" | "close-others" | "close-to-right" | "close-all";
function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) {
return (
<Tooltip>
<TooltipTrigger render={props.trigger} />
<TooltipPopup side="top">{props.reason}</TooltipPopup>
</Tooltip>
);
}
function SurfaceMenuItem(props: {
available: boolean;
disabledReason?: string;
onClick: () => void;
children: ReactNode;
}) {
const item = (
<MenuItem
className={!props.available ? "data-disabled:pointer-events-auto" : undefined}
onClick={props.onClick}
disabled={!props.available}
>
{props.children}
</MenuItem>
);
if (props.available || !props.disabledReason) return item;
return <DisabledReasonTooltip reason={props.disabledReason} trigger={item} />;
}
function RightPanelEmptyState(props: {
onAddBrowser: () => void;
onAddTerminal: () => void;
onAddDiff: () => void;
onAddFiles: () => void;
browserAvailable: boolean;
diffAvailable: boolean;
filesAvailable: boolean;
}) {
const actions = [
{
label: "Browser",
description: "Open a local app or URL.",
icon: Globe2,
available: props.browserAvailable,
disabledReason: SURFACE_DISABLED_REASONS.browser,
onClick: props.onAddBrowser,
},
{
label: "Terminal",
description: "Start a shell in this workspace.",
icon: TerminalSquare,
available: true,
disabledReason: null,
onClick: props.onAddTerminal,
},
{
label: "Files",
description: "Browse and read workspace files.",
icon: Files,
available: props.filesAvailable,
disabledReason: SURFACE_DISABLED_REASONS.files,
onClick: props.onAddFiles,
},
{
label: "Diff",
description: "Review changes in this thread.",
icon: FileDiff,
available: props.diffAvailable,
disabledReason: SURFACE_DISABLED_REASONS.diff,
onClick: props.onAddDiff,
},
] as const;
return (
<div className="flex min-h-0 flex-1 items-center justify-center p-6">
<div className="w-full max-w-xl">
<div className="mb-5 text-center">
<h3 className="text-sm font-medium text-foreground">Open a surface</h3>
<p className="mt-1 text-xs text-muted-foreground">
Choose what to show in the right panel.
</p>
</div>
<div className="grid grid-cols-2 gap-2">
{actions.map((action) => {
const Icon = action.icon;
const content = (
<>
<Icon className="mb-3 size-5" />
<span className="text-sm font-medium">{action.label}</span>
<span className="mt-1 text-xs leading-relaxed text-muted-foreground">
{action.description}
</span>
</>
);
if (action.available) {
return (
<button
key={action.label}
type="button"
onClick={action.onClick}
className="flex min-h-28 w-full flex-col items-start rounded-lg border border-border/80 bg-card p-4 text-left transition hover:border-border hover:bg-accent/60 dark:border-transparent dark:shadow-none dark:inset-ring-1 dark:inset-ring-white/5"
>
{content}
</button>
);
}
const disabledCard = (
<button
type="button"
className="flex min-h-28 w-full cursor-not-allowed flex-col items-start rounded-lg border border-border/80 bg-card p-4 text-left opacity-40 dark:border-transparent dark:shadow-none dark:inset-ring-1 dark:inset-ring-white/5"
aria-disabled="true"
>
{content}
</button>
);
return (
<DisabledReasonTooltip
key={action.label}
reason={action.disabledReason}
trigger={disabledCard}
/>
);
})}
</div>
</div>
</div>
);
}
function surfaceTitle(
surface: RightPanelSurface,
sessions: Readonly<Record<string, PreviewSessionSnapshot>>,
terminalLabelsById: ReadonlyMap<string, string>,
): string {
switch (surface.kind) {
case "diff":
return "Diff";
case "files":
return "Files";
case "file":
return surface.relativePath.slice(surface.relativePath.lastIndexOf("/") + 1);
case "terminal":
return (
terminalLabelsById.get(surface.activeTerminalId) ??
getTerminalLabel(surface.activeTerminalId)
);
case "plan":
return "Plan";
case "preview": {
const snapshot = surface.resourceId ? sessions[surface.resourceId] : null;
if (!snapshot || snapshot.navStatus._tag === "Idle") return "Browser";
if (snapshot.navStatus.title.trim().length > 0) return snapshot.navStatus.title;
try {
return new URL(snapshot.navStatus.url).host || "Browser";
} catch {
return "Browser";
}
}
}
}
function PreviewFavicon({ url }: { url: string | null }) {
const faviconUrl = faviconUrlForOrigin(url, 32);
const [failedUrl, setFailedUrl] = useState<string | null>(null);
if (!faviconUrl || failedUrl === faviconUrl) return <Globe2 className="size-3.5 shrink-0" />;
return (
<img
src={faviconUrl}
alt=""
aria-hidden
draggable={false}
className="size-3.5 shrink-0 rounded-sm"
onError={() => setFailedUrl(faviconUrl)}
/>
);
}
function SurfaceIcon({
surface,
sessions,
theme,
}: {
surface: RightPanelSurface;
sessions: Readonly<Record<string, PreviewSessionSnapshot>>;
theme: "light" | "dark";
}) {
switch (surface.kind) {
case "preview": {
const snapshot = surface.resourceId ? sessions[surface.resourceId] : null;
const url = !snapshot || snapshot.navStatus._tag === "Idle" ? null : snapshot.navStatus.url;
return <PreviewFavicon url={url} />;
}
case "diff":
return <FileDiff className="size-3.5 shrink-0" />;
case "files":
return <Files className="size-3.5 shrink-0" />;
case "file":
return (
<PierreEntryIcon
pathValue={surface.relativePath}
kind="file"
theme={theme}
className="size-3.5"
/>
);
case "terminal":
return <TerminalSquare className="size-3.5 shrink-0" />;
case "plan":
return <ClipboardList className="size-3.5 shrink-0" />;
}
}
export function RightPanelTabs(props: RightPanelTabsProps) {
const ownsDesktopTitleBar = isElectron && props.mode === "inline";
const { resolvedTheme } = useTheme();
const tabListRef = useRef<HTMLDivElement>(null);
const handleTabContextMenu = useCallback(
async (event: ReactMouseEvent, surface: RightPanelSurface) => {
event.preventDefault();
event.stopPropagation();
const api = readLocalApi();
if (!api) return;
const surfaceIndex = props.surfaces.findIndex((entry) => entry.id === surface.id);
if (surfaceIndex < 0) return;
const items: ContextMenuItem<TabContextMenuAction>[] = [];
if (surface.kind === "file") {
items.push({ id: "copy-path", label: "Copy path" });
}
items.push(
{ id: "close", label: "Close" },
{
id: "close-others",
label: "Close others",
disabled: props.surfaces.length <= 1,
},
{
id: "close-to-right",
label: "Close to the right",
disabled: surfaceIndex >= props.surfaces.length - 1,
},
{
id: "close-all",
label: "Close all",
disabled: props.surfaces.length === 0,
},
);
const action = await api.contextMenu.show(items, { x: event.clientX, y: event.clientY });
switch (action) {
case "copy-path":
if (surface.kind === "file") props.onCopyFilePath(surface.relativePath);
break;
case "close":
props.onCloseSurface(surface);
break;
case "close-others":
props.onCloseOtherSurfaces(surface);
break;
case "close-to-right":
props.onCloseSurfacesToRight(surface);
break;
case "close-all":
props.onCloseAllSurfaces();
break;
case null:
break;
}
},
[props],
);
const handleTabMouseDown = useCallback((event: ReactMouseEvent) => {
if (event.button !== 1) return;
event.preventDefault();
}, []);
const handleTabAuxClick = useCallback(
(event: ReactMouseEvent, surface: RightPanelSurface) => {
if (event.button !== 1) return;
event.preventDefault();
event.stopPropagation();
props.onCloseSurface(surface);
},
[props],
);
useEffect(() => {
const activeTab = tabListRef.current?.querySelector<HTMLElement>("[data-active-tab='true']");
activeTab?.scrollIntoView({ block: "nearest", inline: "nearest" });
}, [props.activeSurfaceId]);
return (
<PreviewPanelShell
mode={props.mode}
{...(props.maximized !== undefined ? { maximized: props.maximized } : {})}
>
<div
className={cn(
"workspace-topbar gap-1 pl-2",
props.mode === "inline" ? "pr-28" : "pr-3",
ownsDesktopTitleBar && "wco:pr-[calc(var(--workspace-native-controls-inset)+6rem)]",
props.mode === "inline" && props.maximized && COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
)}
data-right-panel-tabbar
>
<ScrollArea
ref={tabListRef}
hideScrollbars
scrollFade
className={cn("min-w-0 flex-1 rounded-none", ownsDesktopTitleBar && "drag-region")}
data-right-panel-tab-list
>
<div className="flex h-full w-max min-w-full items-center gap-1">
{props.surfaces.map((surface) => {
const active = surface.id === props.activeSurfaceId;
const pending = props.pendingSurfaceIds.has(surface.id);
const title = surfaceTitle(surface, props.previewSessions, props.terminalLabelsById);
return (
<div
key={surface.id}
data-active-tab={active}
onMouseDown={handleTabMouseDown}
onAuxClick={(event) => handleTabAuxClick(event, surface)}
onContextMenu={(event) => void handleTabContextMenu(event, surface)}
className={cn(
"group flex h-7 min-w-25 max-w-44 shrink-0 items-center gap-1.5 rounded-md px-2 text-sm",
active
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
)}
>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className="flex min-w-0 flex-1 items-center gap-1.5"
onClick={() => props.onActivate(surface)}
>
<SurfaceIcon
surface={surface}
sessions={props.previewSessions}
theme={resolvedTheme}
/>
<span className="truncate">{title}</span>
</button>
}
/>
<TooltipPopup>{title}</TooltipPopup>
</Tooltip>
<button
type="button"
className={cn(
"relative flex size-4 shrink-0 items-center justify-center rounded hover:bg-muted focus:opacity-100",
pending ? "opacity-100" : "opacity-0 group-hover:opacity-100",
)}
aria-label={`Close ${title}`}
onClick={() => props.onCloseSurface(surface)}
>
{pending ? (
<>
<span
className="size-2 rounded-full bg-current group-hover:hidden"
aria-hidden
/>
<X className="hidden size-3 group-hover:block" />
</>
) : (
<X className="size-3" />
)}
</button>
</div>
);
})}
{props.surfaces.length > 0 ? (
<Menu>
<MenuTrigger
className="relative inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
aria-label="Add panel surface"
>
<Plus className="size-4" />
</MenuTrigger>
<MenuPopup align="start" side="bottom" sideOffset={6} className="min-w-44">
<SurfaceMenuItem
available={props.browserAvailable}
disabledReason={SURFACE_DISABLED_REASONS.browser}
onClick={props.onAddBrowser}
>
<Globe2 />
Browser
</SurfaceMenuItem>
<SurfaceMenuItem available onClick={props.onAddTerminal}>
<TerminalSquare />
Terminal
</SurfaceMenuItem>
<SurfaceMenuItem
available={props.filesAvailable}
disabledReason={SURFACE_DISABLED_REASONS.files}
onClick={props.onAddFiles}
>
<Files />
Files
</SurfaceMenuItem>
<SurfaceMenuItem
available={props.diffAvailable}
disabledReason={SURFACE_DISABLED_REASONS.diff}
onClick={props.onAddDiff}
>
<FileDiff />
Diff
</SurfaceMenuItem>
</MenuPopup>
</Menu>
) : null}
</div>
</ScrollArea>
{props.layoutControls}
</div>
<div className="flex min-h-0 flex-1 flex-col">
{props.activeSurfaceId === null ? (
<RightPanelEmptyState
onAddBrowser={props.onAddBrowser}
onAddTerminal={props.onAddTerminal}
onAddDiff={props.onAddDiff}
onAddFiles={props.onAddFiles}
browserAvailable={props.browserAvailable}
diffAvailable={props.diffAvailable}
filesAvailable={props.filesAvailable}
/>
) : (
props.children
)}
</div>
</PreviewPanelShell>
);
}