import { useParams } from "@tanstack/react-router"; import { Rows2Icon, Columns2Icon, TerminalSquareIcon, XIcon } from "lucide-react"; import { Fragment, memo, useCallback, useEffect, useMemo, useRef } from "react"; import { createThreadSelectorByRef } from "../../storeSelectors"; import { useStore } from "../../store"; import { cn } from "../../lib/utils"; import { SidebarInset } from "../ui/sidebar"; import ChatView from "../ChatView"; import { useComposerDraftStore } from "../../composerDraftStore"; import { resolveThreadRouteTarget } from "../../threadRoutes"; import { ThreadTerminalSurface } from "./ThreadTerminalSurface"; import { useWorkspaceDocument, useWorkspaceStore } from "../../workspace/store"; import { normalizeWorkspaceSplitSizes, type WorkspaceNode, type WorkspaceSurfaceInstance, } from "../../workspace/types"; const WORKSPACE_MIN_PANE_SIZE_PX = 220; function WorkspaceEmptyState() { return (

Pick a thread to continue

Select an existing thread or create a new one to get started.

); } export function WorkspaceShell() { const document = useWorkspaceDocument(); return (
{document.rootNodeId ? : }
); } function WorkspaceRouteFallback() { const routeTarget = useParams({ strict: false, select: (params) => resolveThreadRouteTarget(params), }); const openThreadSurface = useWorkspaceStore((state) => state.openThreadSurface); const draftSession = useComposerDraftStore((store) => routeTarget?.kind === "draft" ? store.getDraftSession(routeTarget.draftId) : null, ); useEffect(() => { if (!routeTarget) { return; } if (routeTarget.kind === "server") { openThreadSurface( { scope: "server", threadRef: routeTarget.threadRef, }, "focus-or-tab", ); return; } if (!draftSession) { return; } openThreadSurface( { scope: "draft", draftId: routeTarget.draftId, environmentId: draftSession.environmentId, threadId: draftSession.threadId, }, "focus-or-tab", ); }, [draftSession, openThreadSurface, routeTarget]); if (!routeTarget) { return ; } if (routeTarget.kind === "server") { return ( ); } if (!draftSession) { return ; } return ( ); } function WorkspaceLayoutRoot() { const document = useWorkspaceDocument(); const focusedWindowId = useWorkspaceStore((state) => state.document.focusedWindowId); const mobileActiveWindowId = useWorkspaceStore((state) => state.document.mobileActiveWindowId); const setMobileActiveWindow = useWorkspaceStore((state) => state.setMobileActiveWindow); const windowIds = useMemo( () => Object.keys(document.windowsById).filter((windowId) => document.windowsById[windowId]), [document.windowsById], ); return (
{windowIds.length > 1 ? (
{windowIds.map((windowId, index) => { const isActive = (mobileActiveWindowId ?? focusedWindowId ?? windowIds[0]) === windowId; return ( ); })}
) : null}
); } function MobileWorkspaceWindow(props: { windowId: string | null }) { const document = useWorkspaceDocument(); if (!props.windowId) { return ; } const window = document.windowsById[props.windowId]; if (!window) { return ; } return ; } function WorkspaceNodeView(props: { nodeId: string | null }) { const document = useWorkspaceDocument(); if (!props.nodeId) { return null; } const node = document.nodesById[props.nodeId]; if (!node) { return null; } if (node.kind === "window") { return ; } return ; } function WorkspaceSplitNodeView(props: { node: Extract }) { const setSplitNodeSizes = useWorkspaceStore((state) => state.setSplitNodeSizes); const containerRef = useRef(null); const resizeStateRef = useRef<{ handle: HTMLButtonElement; handleIndex: number; pendingSizes: number[]; pointerId: number; rafId: number | null; startCoordinate: number; startSizes: number[]; totalPx: number; } | null>(null); const sizes = useMemo( () => normalizeWorkspaceSplitSizes(props.node.sizes, props.node.childIds.length), [props.node.childIds.length, props.node.sizes], ); const stopResize = useCallback( (pointerId: number) => { const resizeState = resizeStateRef.current; if (!resizeState) { return; } if (resizeState.rafId !== null) { window.cancelAnimationFrame(resizeState.rafId); setSplitNodeSizes(props.node.id, resizeState.pendingSizes); } resizeStateRef.current = null; if (resizeState.handle.hasPointerCapture(pointerId)) { resizeState.handle.releasePointerCapture(pointerId); } document.body.style.removeProperty("cursor"); document.body.style.removeProperty("user-select"); }, [props.node.id, setSplitNodeSizes], ); useEffect(() => { return () => { const resizeState = resizeStateRef.current; if (resizeState && resizeState.rafId !== null) { window.cancelAnimationFrame(resizeState.rafId); } document.body.style.removeProperty("cursor"); document.body.style.removeProperty("user-select"); }; }, []); const handleResizePointerDown = useCallback( (handleIndex: number, event: React.PointerEvent) => { if (event.button !== 0) { return; } const container = containerRef.current; if (!container) { return; } const rect = container.getBoundingClientRect(); const totalPx = props.node.axis === "x" ? rect.width : rect.height; if (totalPx <= 0) { return; } event.preventDefault(); event.stopPropagation(); resizeStateRef.current = { handle: event.currentTarget, handleIndex, pendingSizes: sizes, pointerId: event.pointerId, rafId: null, startCoordinate: props.node.axis === "x" ? event.clientX : event.clientY, startSizes: sizes, totalPx, }; event.currentTarget.setPointerCapture(event.pointerId); document.body.style.cursor = props.node.axis === "x" ? "col-resize" : "row-resize"; document.body.style.userSelect = "none"; }, [props.node.axis, sizes], ); const handleResizePointerMove = useCallback( (event: React.PointerEvent) => { const resizeState = resizeStateRef.current; if (!resizeState || resizeState.pointerId !== event.pointerId) { return; } event.preventDefault(); const deltaPx = (props.node.axis === "x" ? event.clientX : event.clientY) - resizeState.startCoordinate; const deltaFraction = deltaPx / resizeState.totalPx; const pairTotal = resizeState.startSizes[resizeState.handleIndex]! + resizeState.startSizes[resizeState.handleIndex + 1]!; const requestedMinFraction = WORKSPACE_MIN_PANE_SIZE_PX / resizeState.totalPx; const minFraction = Math.min(requestedMinFraction, Math.max(pairTotal / 2 - 0.001, 0)); const nextBefore = Math.min( pairTotal - minFraction, Math.max(minFraction, resizeState.startSizes[resizeState.handleIndex]! + deltaFraction), ); const nextAfter = pairTotal - nextBefore; const nextSizes = [...resizeState.startSizes]; nextSizes[resizeState.handleIndex] = nextBefore; nextSizes[resizeState.handleIndex + 1] = nextAfter; resizeState.pendingSizes = nextSizes; if (resizeState.rafId !== null) { return; } resizeState.rafId = window.requestAnimationFrame(() => { const activeResizeState = resizeStateRef.current; if (!activeResizeState) { return; } activeResizeState.rafId = null; setSplitNodeSizes(props.node.id, activeResizeState.pendingSizes); }); }, [props.node.axis, props.node.id, setSplitNodeSizes], ); const endResizeInteraction = useCallback( (event: React.PointerEvent) => { const resizeState = resizeStateRef.current; if (!resizeState || resizeState.pointerId !== event.pointerId) { return; } event.preventDefault(); stopResize(event.pointerId); }, [stopResize], ); return (
{props.node.childIds.map((childId, index) => (
{index < props.node.childIds.length - 1 ? ( ) : null}
))}
); } const WorkspaceWindowView = memo(function WorkspaceWindowView(props: { windowId: string }) { const document = useWorkspaceDocument(); const focusWindow = useWorkspaceStore((state) => state.focusWindow); const focusTab = useWorkspaceStore((state) => state.focusTab); const closeSurface = useWorkspaceStore((state) => state.closeSurface); const splitWindowSurface = useWorkspaceStore((state) => state.splitWindowSurface); const window = document.windowsById[props.windowId]; const focusedWindowId = document.focusedWindowId; if (!window) { return null; } const activeSurface = window.activeTabId ? document.surfacesById[window.activeTabId] : null; return (
focusWindow(props.windowId)} >
{window.tabIds.map((surfaceId) => { const surface = document.surfacesById[surfaceId]; if (!surface) { return null; } const isActive = window.activeTabId === surfaceId; return (
); })}
{activeSurface ? ( ) : null}
); }); function WorkspaceSurfaceView(props: { bindSharedComposerHandle?: boolean; surface: WorkspaceSurfaceInstance; }) { if (props.surface.kind === "thread") { if (props.surface.input.scope === "server") { return ( ); } return ( ); } return ; } function WorkspaceSurfaceTitle(props: { surface: WorkspaceSurfaceInstance }) { if (props.surface.kind === "terminal") { return ; } return ; } function ThreadSurfaceTitle(props: { surface: Extract; }) { const thread = useStore( useMemo( () => createThreadSelectorByRef( props.surface.input.scope === "server" ? props.surface.input.threadRef : null, ), [props.surface.input], ), ); if (props.surface.input.scope === "server") { return <>{thread?.title ?? props.surface.input.threadRef.threadId}; } return <>{thread?.title ?? props.surface.input.threadId ?? "Draft thread"}; } function TerminalSurfaceTitle(props: { threadRef: Extract["input"]["threadRef"]; }) { const thread = useStore( useMemo(() => createThreadSelectorByRef(props.threadRef), [props.threadRef]), ); const label = thread?.title ?? props.threadRef.threadId; return ( {label} ); }