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}
);
}