import { useEffect, useMemo, useState, type ReactNode } from "react"; import type { AgentType, RepoBranchRecord, RepoOverview, RepoStackAction, TaskWorkbenchSnapshot, WorkbenchTaskStatus } from "@sandbox-agent/foundry-shared"; import { useInterest } from "@sandbox-agent/foundry-client"; import { useMutation, useQuery } from "@tanstack/react-query"; import { Link, useNavigate } from "@tanstack/react-router"; import { Button } from "baseui/button"; import { Input } from "baseui/input"; import { Modal, ModalBody, ModalFooter, ModalHeader } from "baseui/modal"; import { Select, type OnChangeParams, type Option, type Value } from "baseui/select"; import { Skeleton } from "baseui/skeleton"; import { Tag } from "baseui/tag"; import { Textarea } from "baseui/textarea"; import { StyledDivider } from "baseui/divider"; import { styled, useStyletron } from "baseui"; import { HeadingSmall, HeadingXSmall, LabelSmall, LabelXSmall, MonoLabelSmall, ParagraphSmall } from "baseui/typography"; import { Bot, CircleAlert, FolderGit2, GitBranch, MessageSquareText, SendHorizontal, Shuffle } from "lucide-react"; import { formatDiffStat } from "../features/tasks/model"; import { deriveHeaderStatus, describeTaskState } from "../features/tasks/status"; import { HeaderStatusPill } from "./mock-layout/ui"; import { buildTranscript, resolveSessionSelection } from "../features/sessions/model"; import { backendClient } from "../lib/backend"; import { interestManager } from "../lib/interest"; import { DevPanel, useDevPanel } from "./dev-panel"; interface WorkspaceDashboardProps { workspaceId: string; selectedTaskId?: string; selectedRepoId?: string; } type RepoOverviewFilter = "active" | "archived" | "unmapped" | "all"; type StatusTagKind = "neutral" | "positive" | "warning" | "negative"; type SelectItem = Readonly<{ id: string; label: string; disabled?: boolean }>; const AppShell = styled("main", ({ $theme }) => ({ minHeight: "100dvh", backgroundColor: $theme.colors.backgroundPrimary, })); const DashboardGrid = styled("div", ({ $theme }) => ({ display: "grid", gap: "1px", minHeight: "100dvh", backgroundColor: $theme.colors.borderOpaque, gridTemplateColumns: "minmax(0, 1fr)", "@media screen and (min-width: 960px)": { gridTemplateColumns: "260px minmax(0, 1fr)", }, "@media screen and (min-width: 1480px)": { gridTemplateColumns: "260px minmax(0, 1fr) 280px", }, })); const Panel = styled("section", ({ $theme }) => ({ minHeight: 0, display: "flex", flexDirection: "column", backgroundColor: $theme.colors.backgroundSecondary, overflow: "hidden", })); const PanelHeader = styled("div", ({ $theme }) => ({ padding: "10px 12px", borderBottom: `1px solid ${$theme.colors.borderOpaque}`, display: "flex", flexDirection: "column", gap: "8px", })); const ScrollBody = styled("div", ({ $theme }) => ({ minHeight: 0, flex: 1, overflowY: "auto", padding: "10px 12px", display: "flex", flexDirection: "column", gap: "8px", })); const DetailRail = styled("aside", ({ $theme }) => ({ minHeight: 0, display: "none", backgroundColor: $theme.colors.backgroundSecondary, overflow: "hidden", "@media screen and (min-width: 1480px)": { display: "flex", flexDirection: "column", }, })); const FILTER_OPTIONS: SelectItem[] = [ { id: "active", label: "Active + Unmapped" }, { id: "archived", label: "Archived Tasks" }, { id: "unmapped", label: "Unmapped Only" }, { id: "all", label: "All Branches" }, ]; const AGENT_OPTIONS: SelectItem[] = [ { id: "codex", label: "codex" }, { id: "claude", label: "claude" }, ]; function statusKind(status: WorkbenchTaskStatus): StatusTagKind { if (status === "running") return "positive"; if (status === "error") return "negative"; if (status === "new" || String(status).startsWith("init_")) return "warning"; return "neutral"; } function normalizeAgent(agent: string | null): AgentType | undefined { if (agent === "claude" || agent === "codex") { return agent; } return undefined; } function formatTime(value: number): string { return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function formatRelativeAge(value: number): string { const deltaSeconds = Math.max(0, Math.floor((Date.now() - value) / 1000)); if (deltaSeconds < 60) return `${deltaSeconds}s`; const minutes = Math.floor(deltaSeconds / 60); if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h`; const days = Math.floor(hours / 24); return `${days}d`; } function branchTestIdToken(value: string): string { const token = value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return token || "branch"; } function repoSummary(overview: RepoOverview | undefined): { total: number; mapped: number; unmapped: number; conflicts: number; needsRestack: number; openPrs: number; } { if (!overview) { return { total: 0, mapped: 0, unmapped: 0, conflicts: 0, needsRestack: 0, openPrs: 0, }; } let mapped = 0; let conflicts = 0; let needsRestack = 0; let openPrs = 0; for (const row of overview.branches) { if (row.taskId) { mapped += 1; } if (row.conflictsWithMain) { conflicts += 1; } if (row.trackedInStack && row.parentBranch && row.hasUnpushed) { needsRestack += 1; } if (row.prNumber && row.prState !== "MERGED" && row.prState !== "CLOSED") { openPrs += 1; } } return { total: overview.branches.length, mapped, unmapped: Math.max(0, overview.branches.length - mapped), conflicts, needsRestack, openPrs, }; } function branchKind(row: RepoBranchRecord): StatusTagKind { if (row.conflictsWithMain) { return "negative"; } if (row.prState === "OPEN" || row.prState === "DRAFT") { return "warning"; } if (row.prState === "MERGED") { return "positive"; } return "neutral"; } function matchesOverviewFilter(branch: RepoBranchRecord, filter: RepoOverviewFilter): boolean { if (filter === "archived") { return branch.taskStatus === "archived"; } if (filter === "unmapped") { return branch.taskId === null; } if (filter === "active") { return branch.taskStatus !== "archived"; } return true; } function selectValue(option: Option | null | undefined): Value { return option ? [option] : []; } function optionId(value: Value): string | null { const id = value[0]?.id; if (typeof id === "string") return id; if (typeof id === "number") return String(id); return null; } function createOption(item: SelectItem): Option { return { id: item.id, label: item.label, disabled: item.disabled, }; } function inputTestIdOverrides(testId?: string) { return testId ? { Input: { props: { "data-testid": testId, }, }, } : undefined; } function textareaTestIdOverrides(testId?: string) { return testId ? { Input: { props: { "data-testid": testId, }, }, } : undefined; } function selectTestIdOverrides(testId?: string) { return testId ? { ControlContainer: { props: { "data-testid": testId, }, }, } : undefined; } function EmptyState({ children, testId }: { children: string; testId?: string }) { return (
{children}
); } function StatusPill({ children, kind }: { children: ReactNode; kind: StatusTagKind }) { return ( {children} ); } function MetaRow({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) { return (
{label} {mono ? ( {value} ) : ( {value} )}
); } export function WorkspaceDashboard({ workspaceId, selectedTaskId, selectedRepoId }: WorkspaceDashboardProps) { const [css, theme] = useStyletron(); const navigate = useNavigate(); const showDevPanel = useDevPanel(); const repoOverviewMode = typeof selectedRepoId === "string" && selectedRepoId.length > 0; const [draft, setDraft] = useState(""); const [activeSessionId, setActiveSessionId] = useState(null); const [createRepoId, setCreateRepoId] = useState(""); const [newTask, setNewTask] = useState(""); const [newTitle, setNewTitle] = useState(""); const [newBranchName, setNewBranchName] = useState(""); const [createOnBranch, setCreateOnBranch] = useState(null); const [addRepoOpen, setAddRepoOpen] = useState(false); const [createTaskOpen, setCreateTaskOpen] = useState(false); const [addRepoRemote, setAddRepoRemote] = useState(""); const [addRepoError, setAddRepoError] = useState(null); const [stackActionError, setStackActionError] = useState(null); const [stackActionMessage, setStackActionMessage] = useState(null); const [selectedOverviewBranch, setSelectedOverviewBranch] = useState(null); const [overviewFilter, setOverviewFilter] = useState("active"); const [reparentBranchName, setReparentBranchName] = useState(null); const [reparentParentBranch, setReparentParentBranch] = useState(""); const [newAgentType, setNewAgentType] = useState(() => { try { const raw = globalThis.localStorage?.getItem("hf.settings.agentType"); return raw === "claude" || raw === "codex" ? raw : "codex"; } catch { return "codex"; } }); const [createError, setCreateError] = useState(null); const workspaceState = useInterest(interestManager, "workspace", { workspaceId }); const repos = workspaceState.data?.repos ?? []; const rows = workspaceState.data?.taskSummaries ?? []; const selectedSummary = useMemo(() => rows.find((row) => row.id === selectedTaskId) ?? rows[0] ?? null, [rows, selectedTaskId]); const taskState = useInterest( interestManager, "task", !repoOverviewMode && selectedSummary ? { workspaceId, repoId: selectedSummary.repoId, taskId: selectedSummary.id, } : null, ); const activeRepoId = selectedRepoId ?? createRepoId; const repoOverviewQuery = useQuery({ queryKey: ["workspace", workspaceId, "repo-overview", activeRepoId], enabled: Boolean(repoOverviewMode && activeRepoId), queryFn: async () => { if (!activeRepoId) { throw new Error("No repo selected"); } return backendClient.getRepoOverview(workspaceId, activeRepoId); }, }); useEffect(() => { if (repoOverviewMode && selectedRepoId) { setCreateRepoId(selectedRepoId); return; } if (!createRepoId && repos.length > 0) { setCreateRepoId(repos[0]!.id); } }, [createRepoId, repoOverviewMode, repos, selectedRepoId]); useEffect(() => { try { globalThis.localStorage?.setItem("hf.settings.agentType", newAgentType); } catch { // ignore storage failures } }, [newAgentType]); const repoGroups = useMemo(() => { const byRepo = new Map(); for (const row of rows) { const bucket = byRepo.get(row.repoId); if (bucket) { bucket.push(row); } else { byRepo.set(row.repoId, [row]); } } return repos .map((repo) => { const tasks = [...(byRepo.get(repo.id) ?? [])].sort((a, b) => b.updatedAtMs - a.updatedAtMs); const latestTaskAt = tasks[0]?.updatedAtMs ?? 0; return { repoId: repo.id, repoLabel: repo.label, latestActivityAt: Math.max(repo.latestActivityMs, latestTaskAt), tasks, }; }) .sort((a, b) => { if (a.latestActivityAt !== b.latestActivityAt) { return b.latestActivityAt - a.latestActivityAt; } return a.repoLabel.localeCompare(b.repoLabel); }); }, [repos, rows]); const selectedForSession = repoOverviewMode ? null : (taskState.data ?? null); const activeSandbox = useMemo(() => { if (!selectedForSession) return null; const byActive = selectedForSession.activeSandboxId ? (selectedForSession.sandboxes.find((sandbox) => sandbox.sandboxId === selectedForSession.activeSandboxId) ?? null) : null; return byActive ?? selectedForSession.sandboxes[0] ?? null; }, [selectedForSession]); useEffect(() => { if (!repoOverviewMode && !selectedTaskId && rows.length > 0) { void navigate({ to: "/workspaces/$workspaceId/tasks/$taskId", params: { workspaceId, taskId: rows[0]!.id, }, search: { sessionId: undefined }, replace: true, }); } }, [navigate, repoOverviewMode, rows, selectedTaskId, workspaceId]); useEffect(() => { setActiveSessionId(null); setDraft(""); }, [selectedForSession?.id]); const sessionRows = selectedForSession?.sessionsSummary ?? []; const taskRuntimeStatus = selectedForSession?.runtimeStatus ?? selectedForSession?.status ?? null; const taskStatusState = describeTaskState(taskRuntimeStatus, selectedForSession?.statusMessage ?? null); const taskStateSummary = `${taskStatusState.title}. ${taskStatusState.detail}`; const shouldUseTaskStateEmptyState = Boolean(selectedForSession && taskRuntimeStatus && taskRuntimeStatus !== "running" && taskRuntimeStatus !== "idle"); const sessionSelection = useMemo( () => resolveSessionSelection({ explicitSessionId: activeSessionId, taskSessionId: selectedForSession?.activeSessionId ?? null, sessions: sessionRows.map((session) => ({ id: session.id, agent: session.agent, agentSessionId: session.sessionId ?? "", lastConnectionId: "", createdAt: 0, status: session.status, })), }), [activeSessionId, selectedForSession?.activeSessionId, sessionRows], ); const resolvedSessionId = sessionSelection.sessionId; const staleSessionId = sessionSelection.staleSessionId; const sessionState = useInterest( interestManager, "session", selectedForSession && resolvedSessionId ? { workspaceId, repoId: selectedForSession.repoId, taskId: selectedForSession.id, sessionId: resolvedSessionId, } : null, ); const selectedSessionSummary = useMemo(() => sessionRows.find((session) => session.id === resolvedSessionId) ?? null, [resolvedSessionId, sessionRows]); const isPendingProvision = selectedSessionSummary?.status === "pending_provision"; const isPendingSessionCreate = selectedSessionSummary?.status === "pending_session_create"; const isSessionError = selectedSessionSummary?.status === "error"; const canStartSession = Boolean(selectedForSession && activeSandbox?.sandboxId); const devPanelFocusedTask = useMemo(() => { if (repoOverviewMode) { return null; } const task = selectedForSession ?? selectedSummary; if (!task) { return null; } return { id: task.id, repoId: task.repoId, title: task.title, status: task.status, runtimeStatus: selectedForSession?.runtimeStatus ?? null, statusMessage: selectedForSession?.statusMessage ?? null, branch: task.branch ?? null, activeSandboxId: selectedForSession?.activeSandboxId ?? null, activeSessionId: selectedForSession?.activeSessionId ?? null, sandboxes: selectedForSession?.sandboxes ?? [], sessions: selectedForSession?.sessionsSummary ?? [], }; }, [repoOverviewMode, selectedForSession, selectedSummary]); const devPanelSnapshot = useMemo( (): TaskWorkbenchSnapshot => ({ workspaceId, repos: repos.map((repo) => ({ id: repo.id, label: repo.label })), projects: [], tasks: rows.map((task) => ({ id: task.id, repoId: task.repoId, title: task.title, status: task.status, runtimeStatus: selectedForSession?.id === task.id ? selectedForSession.runtimeStatus : undefined, statusMessage: selectedForSession?.id === task.id ? selectedForSession.statusMessage : null, repoName: task.repoName, updatedAtMs: task.updatedAtMs, branch: task.branch ?? null, pullRequest: task.pullRequest, tabs: task.sessionsSummary.map((session) => ({ ...session, draft: { text: "", attachments: [], updatedAtMs: null, }, transcript: [], })), fileChanges: [], diffs: {}, fileTree: [], minutesUsed: 0, activeSandboxId: selectedForSession?.id === task.id ? selectedForSession.activeSandboxId : null, })), }), [repos, rows, selectedForSession, workspaceId], ); const startSessionFromTask = async (): Promise<{ id: string; status: "running" | "idle" | "error" }> => { if (!selectedForSession || !activeSandbox?.sandboxId) { throw new Error("No sandbox is available for this task"); } return backendClient.createSandboxSession({ workspaceId, providerId: activeSandbox.providerId, sandboxId: activeSandbox.sandboxId, prompt: selectedForSession.task, cwd: activeSandbox.cwd ?? undefined, agent: normalizeAgent(selectedForSession.agentType), }); }; const createSession = useMutation({ mutationFn: async () => startSessionFromTask(), onSuccess: (session) => { setActiveSessionId(session.id); }, }); const ensureSessionForPrompt = async (): Promise => { if (resolvedSessionId) { return resolvedSessionId; } const created = await startSessionFromTask(); setActiveSessionId(created.id); return created.id; }; const sendPrompt = useMutation({ mutationFn: async (prompt: string) => { if (!selectedForSession || !activeSandbox?.sandboxId) { throw new Error("No sandbox is available for this task"); } const sessionId = await ensureSessionForPrompt(); await backendClient.sendSandboxPrompt({ workspaceId, providerId: activeSandbox.providerId, sandboxId: activeSandbox.sandboxId, sessionId, prompt, }); }, onSuccess: () => { setDraft(""); }, }); const transcript = buildTranscript(sessionState.data?.transcript ?? []); const canCreateTask = createRepoId.trim().length > 0 && newTask.trim().length > 0; const createTask = useMutation({ mutationFn: async () => { const repoId = createRepoId.trim(); const task = newTask.trim(); if (!repoId || !task) { throw new Error("Repository and task are required"); } const draftTitle = newTitle.trim(); const draftBranchName = newBranchName.trim(); return backendClient.createTask({ workspaceId, repoId, task, agentType: newAgentType, explicitTitle: draftTitle || undefined, explicitBranchName: createOnBranch ? undefined : draftBranchName || undefined, onBranch: createOnBranch ?? undefined, }); }, onSuccess: async (task) => { setCreateError(null); setNewTask(""); setNewTitle(""); setNewBranchName(""); setCreateOnBranch(null); setCreateTaskOpen(false); await navigate({ to: "/workspaces/$workspaceId/tasks/$taskId", params: { workspaceId, taskId: task.taskId, }, search: { sessionId: undefined }, }); }, onError: (error) => { setCreateError(error instanceof Error ? error.message : String(error)); }, }); const addRepo = useMutation({ mutationFn: async (remoteUrl: string) => { const trimmed = remoteUrl.trim(); if (!trimmed) { throw new Error("Remote URL is required"); } return backendClient.addRepo(workspaceId, trimmed); }, onSuccess: async (created) => { setAddRepoError(null); setAddRepoRemote(""); setAddRepoOpen(false); setCreateRepoId(created.repoId); if (repoOverviewMode) { await navigate({ to: "/workspaces/$workspaceId/repos/$repoId", params: { workspaceId, repoId: created.repoId, }, }); } }, onError: (error) => { setAddRepoError(error instanceof Error ? error.message : String(error)); }, }); const runStackAction = useMutation({ mutationFn: async (input: { action: RepoStackAction; branchName?: string; parentBranch?: string }) => { if (!activeRepoId) { throw new Error("No repository selected"); } return backendClient.runRepoStackAction({ workspaceId, repoId: activeRepoId, action: input.action, branchName: input.branchName, parentBranch: input.parentBranch, }); }, onSuccess: async (result) => { if (result.executed) { setStackActionError(null); setStackActionMessage(result.message); } else { setStackActionMessage(null); setStackActionError(result.message); } await repoOverviewQuery.refetch(); }, onError: (error) => { setStackActionMessage(null); setStackActionError(error instanceof Error ? error.message : String(error)); }, }); const openCreateFromBranch = (repoId: string, branchName: string): void => { setCreateRepoId(repoId); setCreateOnBranch(branchName); setNewBranchName(""); setCreateError(null); if (!newTask.trim()) { setNewTask(`Continue work on ${branchName}`); } setCreateTaskOpen(true); }; const repoOptions = useMemo(() => repos.map((repo) => createOption({ id: repo.id, label: repo.label })), [repos]); const selectedRepoOption = repoOptions.find((option) => option.id === createRepoId) ?? null; const selectedAgentOption = useMemo(() => createOption(AGENT_OPTIONS.find((option) => option.id === newAgentType) ?? AGENT_OPTIONS[0]!), [newAgentType]); const selectedFilterOption = useMemo( () => createOption(FILTER_OPTIONS.find((option) => option.id === overviewFilter) ?? FILTER_OPTIONS[0]!), [overviewFilter], ); const sessionOptions = useMemo( () => sessionRows.map((session) => createOption({ id: session.id, label: `${session.sessionName} (${session.status})` })), [sessionRows], ); const selectedSessionOption = sessionOptions.find((option) => option.id === resolvedSessionId) ?? null; const overview = repoOverviewQuery.data; const overviewStats = repoSummary(overview); const stackActionsEnabled = Boolean(overview?.stackAvailable) && !runStackAction.isPending; const filteredOverviewBranches = useMemo(() => { if (!overview?.branches?.length) { return []; } return overview.branches.filter((branch) => matchesOverviewFilter(branch, overviewFilter)); }, [overview, overviewFilter]); const selectedBranchOverview = useMemo(() => { if (!filteredOverviewBranches.length) { return null; } if (!selectedOverviewBranch) { return filteredOverviewBranches[0] ?? null; } return filteredOverviewBranches.find((row) => row.branchName === selectedOverviewBranch) ?? filteredOverviewBranches[0] ?? null; }, [filteredOverviewBranches, selectedOverviewBranch]); useEffect(() => { if (!filteredOverviewBranches.length) { setSelectedOverviewBranch(null); return; } if (!selectedOverviewBranch || !filteredOverviewBranches.some((row) => row.branchName === selectedOverviewBranch)) { setSelectedOverviewBranch(filteredOverviewBranches[0]?.branchName ?? null); } }, [filteredOverviewBranches, selectedOverviewBranch]); const handleReparentSubmit = (): void => { if (!reparentBranchName || !reparentParentBranch.trim()) { return; } setStackActionError(null); void runStackAction .mutateAsync({ action: "reparent_branch", branchName: reparentBranchName, parentBranch: reparentParentBranch.trim(), }) .then(() => { setReparentBranchName(null); setReparentParentBranch(""); }) .catch(() => { // mutation state is surfaced above }); }; const modalOverrides = useMemo( () => ({ Dialog: { style: { borderRadius: "0", backgroundColor: theme.colors.backgroundSecondary, border: `1px solid ${theme.colors.borderOpaque}`, boxShadow: "0 18px 40px rgba(0, 0, 0, 0.45)", }, }, Close: { style: { borderRadius: "0", }, }, }), [theme.colors.backgroundSecondary, theme.colors.borderOpaque], ); return (
Workspace
{workspaceId}
Tasks
{workspaceState.status === "loading" ? ( <> ) : null} {workspaceState.status !== "loading" && repoGroups.length === 0 ? ( No repos or tasks yet. Add a repo to start a workspace. ) : null} {repoGroups.map((group) => (
{group.repoLabel}
{group.tasks .filter((task) => task.status !== "archived" || task.id === selectedSummary?.id) .map((task) => { const isActive = !repoOverviewMode && task.id === selectedSummary?.id; return ( {task.title ?? "Determining title..."}
{task.branch ?? "Determining branch..."} {task.status}
); })}
))}
{repoOverviewMode ? ( <>
Repo Overview
{ const next = optionId(params.value); if (next) { setActiveSessionId(next); } }} overrides={selectTestIdOverrides("task-session-select")} />
) : null}
{resolvedSessionId && sessionState.status === "loading" ? : null} {selectedSessionSummary && (isPendingProvision || isPendingSessionCreate) ? (
{shouldUseTaskStateEmptyState ? taskStatusState.title : isPendingProvision ? "Provisioning sandbox..." : "Creating session..."} {shouldUseTaskStateEmptyState ? taskStateSummary : (selectedForSession?.statusMessage ?? (isPendingProvision ? "The task is still provisioning." : "The session is being created."))}
) : null} {transcript.length === 0 && !(resolvedSessionId && sessionState.status === "loading") ? ( {shouldUseTaskStateEmptyState ? taskStateSummary : isPendingProvision ? (selectedForSession.statusMessage ?? "Provisioning sandbox...") : isPendingSessionCreate ? "Creating session..." : isSessionError ? (selectedSessionSummary?.errorMessage ?? "Session failed to start.") : !activeSandbox?.sandboxId ? selectedForSession.statusMessage ? `Sandbox unavailable: ${selectedForSession.statusMessage}` : "This task is still provisioning its sandbox." : staleSessionId ? `Session ${staleSessionId} is unavailable. Start a new session to continue.` : resolvedSessionId ? "No transcript events yet. Send a prompt to start this session." : "No active session for this task."} ) : null}
{transcript.map((entry) => (
{entry.sender} {formatTime(entry.createdAt)}
                                {entry.text}
                              
))}