import type { PreviewSessionSnapshot } from "@t3tools/contracts"; import { getTerminalLabel } from "@t3tools/shared/terminalLabels"; import { ClipboardList, FileDiff, Files, Globe2, Plus, TerminalSquare, X } from "lucide-react"; import { type ReactElement, type ReactNode, useEffect, useRef, useState } from "react"; import { isElectron } from "~/env"; import type { RightPanelSurface } from "~/rightPanelStore"; import { cn } from "~/lib/utils"; 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 { PreviewPanelShell, type PreviewPanelMode } from "./preview/PreviewPanelShell"; import { PierreEntryIcon } from "./chat/PierreEntryIcon"; interface RightPanelTabsProps { mode: PreviewPanelMode; maximized?: boolean; surfaces: readonly RightPanelSurface[]; activeSurfaceId: string | null; pendingSurfaceIds: ReadonlySet; previewSessions: Readonly>; terminalLabelsById: ReadonlyMap; onActivate: (surface: RightPanelSurface) => void; onCloseSurface: (surface: RightPanelSurface) => 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; function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) { return ( {props.reason} ); } function SurfaceMenuItem(props: { available: boolean; disabledReason?: string; onClick: () => void; children: ReactNode; }) { const item = ( {props.children} ); if (props.available || !props.disabledReason) return item; return ; } 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 (

Open a surface

Choose what to show in the right panel.

{actions.map((action) => { const Icon = action.icon; const content = ( <> {action.label} {action.description} ); if (action.available) { return ( ); } const disabledCard = ( ); return ( ); })}
); } function surfaceTitle( surface: RightPanelSurface, sessions: Readonly>, terminalLabelsById: ReadonlyMap, ): 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(null); if (!faviconUrl || failedUrl === faviconUrl) return ; return ( setFailedUrl(faviconUrl)} /> ); } function SurfaceIcon({ surface, sessions, theme, }: { surface: RightPanelSurface; sessions: Readonly>; 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 ; } case "diff": return ; case "files": return ; case "file": return ( ); case "terminal": return ; case "plan": return ; } } export function RightPanelTabs(props: RightPanelTabsProps) { const ownsDesktopTitleBar = isElectron && props.mode === "inline"; const { resolvedTheme } = useTheme(); const tabListRef = useRef(null); useEffect(() => { const activeTab = tabListRef.current?.querySelector("[data-active-tab='true']"); activeTab?.scrollIntoView({ block: "nearest", inline: "nearest" }); }, [props.activeSurfaceId]); return (
{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 (
props.onActivate(surface)} > {title} } /> {title}
); })} {props.surfaces.length > 0 ? ( Browser Terminal Files Diff ) : null}
{props.activeSurfaceId === null ? ( ) : ( props.children )}
); }