import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type PointerEvent as ReactPointerEvent, } from "react"; import { useNavigate } from "@tanstack/react-router"; import { useStyletron } from "baseui"; import { DiffContent } from "./mock-layout/diff-content"; import { MessageList } from "./mock-layout/message-list"; import { PromptComposer } from "./mock-layout/prompt-composer"; import { RightSidebar } from "./mock-layout/right-sidebar"; import { Sidebar } from "./mock-layout/sidebar"; import { TabStrip } from "./mock-layout/tab-strip"; import { TerminalPane } from "./mock-layout/terminal-pane"; import { TranscriptHeader } from "./mock-layout/transcript-header"; import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT, SPanel, ScrollBody, Shell } from "./mock-layout/ui"; import { buildDisplayMessages, buildHistoryEvents, diffPath, diffTabId, formatThinkingDuration, isDiffTab, type Handoff, type HistoryEvent, type LineAttachment, type Message, type ModelId, } from "./mock-layout/view-model"; import { backendClient } from "../lib/backend"; import { handoffWorkbenchClient } from "../lib/workbench"; const STAR_SANDBOX_AGENT_REPO_STORAGE_KEY = "hf.onboarding.starSandboxAgentRepo"; function firstAgentTabId(handoff: Handoff): string | null { return handoff.tabs[0]?.id ?? null; } function sanitizeOpenDiffs(handoff: Handoff, paths: string[] | undefined): string[] { if (!paths) { return []; } return paths.filter((path) => handoff.diffs[path] != null); } function sanitizeLastAgentTabId(handoff: Handoff, tabId: string | null | undefined): string | null { if (tabId && handoff.tabs.some((tab) => tab.id === tabId)) { return tabId; } return firstAgentTabId(handoff); } function sanitizeActiveTabId(handoff: Handoff, tabId: string | null | undefined, openDiffs: string[], lastAgentTabId: string | null): string | null { if (tabId) { if (handoff.tabs.some((tab) => tab.id === tabId)) { return tabId; } if (isDiffTab(tabId) && openDiffs.includes(diffPath(tabId))) { return tabId; } } return openDiffs.length > 0 ? diffTabId(openDiffs[openDiffs.length - 1]!) : lastAgentTabId; } const TranscriptPanel = memo(function TranscriptPanel({ handoff, activeTabId, lastAgentTabId, openDiffs, onSyncRouteSession, onSetActiveTabId, onSetLastAgentTabId, onSetOpenDiffs, }: { handoff: Handoff; activeTabId: string | null; lastAgentTabId: string | null; openDiffs: string[]; onSyncRouteSession: (handoffId: string, sessionId: string | null, replace?: boolean) => void; onSetActiveTabId: (tabId: string | null) => void; onSetLastAgentTabId: (tabId: string | null) => void; onSetOpenDiffs: (paths: string[]) => void; }) { const [defaultModel, setDefaultModel] = useState("claude-sonnet-4"); const [editingField, setEditingField] = useState<"title" | "branch" | null>(null); const [editValue, setEditValue] = useState(""); const [editingSessionTabId, setEditingSessionTabId] = useState(null); const [editingSessionName, setEditingSessionName] = useState(""); const [pendingHistoryTarget, setPendingHistoryTarget] = useState<{ messageId: string; tabId: string } | null>(null); const [copiedMessageId, setCopiedMessageId] = useState(null); const [timerNowMs, setTimerNowMs] = useState(() => Date.now()); const scrollRef = useRef(null); const textareaRef = useRef(null); const messageRefs = useRef(new Map()); const activeDiff = activeTabId && isDiffTab(activeTabId) ? diffPath(activeTabId) : null; const activeAgentTab = activeDiff ? null : (handoff.tabs.find((candidate) => candidate.id === activeTabId) ?? handoff.tabs[0] ?? null); const promptTab = handoff.tabs.find((candidate) => candidate.id === lastAgentTabId) ?? handoff.tabs[0] ?? null; const isTerminal = handoff.status === "archived"; const historyEvents = useMemo(() => buildHistoryEvents(handoff.tabs), [handoff.tabs]); const activeMessages = useMemo(() => buildDisplayMessages(activeAgentTab), [activeAgentTab]); const draft = promptTab?.draft.text ?? ""; const attachments = promptTab?.draft.attachments ?? []; useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [activeMessages.length]); useEffect(() => { textareaRef.current?.focus(); }, [activeTabId, handoff.id]); useEffect(() => { setEditingSessionTabId(null); setEditingSessionName(""); }, [handoff.id]); useLayoutEffect(() => { const textarea = textareaRef.current; if (!textarea) { return; } textarea.style.height = `${PROMPT_TEXTAREA_MIN_HEIGHT}px`; const nextHeight = Math.min(textarea.scrollHeight, PROMPT_TEXTAREA_MAX_HEIGHT); textarea.style.height = `${Math.max(PROMPT_TEXTAREA_MIN_HEIGHT, nextHeight)}px`; textarea.style.overflowY = textarea.scrollHeight > PROMPT_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden"; }, [draft, activeTabId, handoff.id]); useEffect(() => { if (!pendingHistoryTarget || activeTabId !== pendingHistoryTarget.tabId) { return; } const targetNode = messageRefs.current.get(pendingHistoryTarget.messageId); if (!targetNode) { return; } targetNode.scrollIntoView({ behavior: "smooth", block: "center" }); setPendingHistoryTarget(null); }, [activeMessages.length, activeTabId, pendingHistoryTarget]); useEffect(() => { if (!copiedMessageId) { return; } const timer = setTimeout(() => { setCopiedMessageId(null); }, 1_200); return () => clearTimeout(timer); }, [copiedMessageId]); useEffect(() => { if (!activeAgentTab || activeAgentTab.status !== "running" || activeAgentTab.thinkingSinceMs === null) { return; } setTimerNowMs(Date.now()); const timer = window.setInterval(() => { setTimerNowMs(Date.now()); }, 1_000); return () => window.clearInterval(timer); }, [activeAgentTab?.id, activeAgentTab?.status, activeAgentTab?.thinkingSinceMs]); useEffect(() => { if (!activeAgentTab?.unread) { return; } void handoffWorkbenchClient.setSessionUnread({ handoffId: handoff.id, tabId: activeAgentTab.id, unread: false, }); }, [activeAgentTab?.id, activeAgentTab?.unread, handoff.id]); const startEditingField = useCallback((field: "title" | "branch", value: string) => { setEditingField(field); setEditValue(value); }, []); const cancelEditingField = useCallback(() => { setEditingField(null); }, []); const commitEditingField = useCallback( (field: "title" | "branch") => { const value = editValue.trim(); if (!value) { setEditingField(null); return; } if (field === "title") { void handoffWorkbenchClient.renameHandoff({ handoffId: handoff.id, value }); } else { void handoffWorkbenchClient.renameBranch({ handoffId: handoff.id, value }); } setEditingField(null); }, [editValue, handoff.id], ); const updateDraft = useCallback( (nextText: string, nextAttachments: LineAttachment[]) => { if (!promptTab) { return; } void handoffWorkbenchClient.updateDraft({ handoffId: handoff.id, tabId: promptTab.id, text: nextText, attachments: nextAttachments, }); }, [handoff.id, promptTab], ); const sendMessage = useCallback(() => { const text = draft.trim(); if (!text || !promptTab) { return; } onSetActiveTabId(promptTab.id); onSetLastAgentTabId(promptTab.id); void handoffWorkbenchClient.sendMessage({ handoffId: handoff.id, tabId: promptTab.id, text, attachments, }); }, [attachments, draft, handoff.id, onSetActiveTabId, onSetLastAgentTabId, promptTab]); const stopAgent = useCallback(() => { if (!promptTab) { return; } void handoffWorkbenchClient.stopAgent({ handoffId: handoff.id, tabId: promptTab.id, }); }, [handoff.id, promptTab]); const switchTab = useCallback( (tabId: string) => { onSetActiveTabId(tabId); if (!isDiffTab(tabId)) { onSetLastAgentTabId(tabId); const tab = handoff.tabs.find((candidate) => candidate.id === tabId); if (tab?.unread) { void handoffWorkbenchClient.setSessionUnread({ handoffId: handoff.id, tabId, unread: false, }); } onSyncRouteSession(handoff.id, tabId); } }, [handoff.id, handoff.tabs, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], ); const setTabUnread = useCallback( (tabId: string, unread: boolean) => { void handoffWorkbenchClient.setSessionUnread({ handoffId: handoff.id, tabId, unread }); }, [handoff.id], ); const startRenamingTab = useCallback( (tabId: string) => { const targetTab = handoff.tabs.find((candidate) => candidate.id === tabId); if (!targetTab) { throw new Error(`Unable to rename missing session tab ${tabId}`); } setEditingSessionTabId(tabId); setEditingSessionName(targetTab.sessionName); }, [handoff.tabs], ); const cancelTabRename = useCallback(() => { setEditingSessionTabId(null); setEditingSessionName(""); }, []); const commitTabRename = useCallback(() => { if (!editingSessionTabId) { return; } const trimmedName = editingSessionName.trim(); if (!trimmedName) { cancelTabRename(); return; } void handoffWorkbenchClient.renameSession({ handoffId: handoff.id, tabId: editingSessionTabId, title: trimmedName, }); cancelTabRename(); }, [cancelTabRename, editingSessionName, editingSessionTabId, handoff.id]); const closeTab = useCallback( (tabId: string) => { const remainingTabs = handoff.tabs.filter((candidate) => candidate.id !== tabId); const nextTabId = remainingTabs[0]?.id ?? null; if (activeTabId === tabId) { onSetActiveTabId(nextTabId); } if (lastAgentTabId === tabId) { onSetLastAgentTabId(nextTabId); } onSyncRouteSession(handoff.id, nextTabId); void handoffWorkbenchClient.closeTab({ handoffId: handoff.id, tabId }); }, [activeTabId, handoff.id, handoff.tabs, lastAgentTabId, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], ); const closeDiffTab = useCallback( (path: string) => { const nextOpenDiffs = openDiffs.filter((candidate) => candidate !== path); onSetOpenDiffs(nextOpenDiffs); if (activeTabId === diffTabId(path)) { onSetActiveTabId(nextOpenDiffs.length > 0 ? diffTabId(nextOpenDiffs[nextOpenDiffs.length - 1]!) : (lastAgentTabId ?? firstAgentTabId(handoff))); } }, [activeTabId, handoff, lastAgentTabId, onSetActiveTabId, onSetOpenDiffs, openDiffs], ); const addTab = useCallback(() => { void (async () => { const { tabId } = await handoffWorkbenchClient.addTab({ handoffId: handoff.id }); onSetLastAgentTabId(tabId); onSetActiveTabId(tabId); onSyncRouteSession(handoff.id, tabId); })(); }, [handoff.id, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession]); const changeModel = useCallback( (model: ModelId) => { if (!promptTab) { throw new Error(`Unable to change model for task ${handoff.id} without an active prompt tab`); } void handoffWorkbenchClient.changeModel({ handoffId: handoff.id, tabId: promptTab.id, model, }); }, [handoff.id, promptTab], ); const addAttachment = useCallback( (filePath: string, lineNumber: number, lineContent: string) => { if (!promptTab) { return; } const nextAttachment = { id: `${filePath}:${lineNumber}`, filePath, lineNumber, lineContent }; if (attachments.some((attachment) => attachment.filePath === filePath && attachment.lineNumber === lineNumber)) { return; } updateDraft(draft, [...attachments, nextAttachment]); }, [attachments, draft, promptTab, updateDraft], ); const removeAttachment = useCallback( (id: string) => { updateDraft( draft, attachments.filter((attachment) => attachment.id !== id), ); }, [attachments, draft, updateDraft], ); const jumpToHistoryEvent = useCallback( (event: HistoryEvent) => { setPendingHistoryTarget({ messageId: event.messageId, tabId: event.tabId }); if (activeTabId !== event.tabId) { switchTab(event.tabId); return; } const targetNode = messageRefs.current.get(event.messageId); if (targetNode) { targetNode.scrollIntoView({ behavior: "smooth", block: "center" }); setPendingHistoryTarget(null); } }, [activeTabId, switchTab], ); const copyMessage = useCallback(async (message: Message) => { try { if (!window.navigator.clipboard) { throw new Error("Clipboard API unavailable in mock layout"); } await window.navigator.clipboard.writeText(message.text); setCopiedMessageId(message.id); } catch (error) { console.error("Failed to copy transcript message", error); } }, []); const thinkingTimerLabel = activeAgentTab?.status === "running" && activeAgentTab.thinkingSinceMs !== null ? formatThinkingDuration(timerNowMs - activeAgentTab.thinkingSinceMs) : null; return ( { if (activeAgentTab) { setTabUnread(activeAgentTab.id, unread); } }} /> {activeDiff ? ( file.path === activeDiff)} diff={handoff.diffs[activeDiff]} onAddAttachment={addAttachment} /> ) : handoff.tabs.length === 0 ? (

Create the first session

Sessions are where you chat with the agent. Start one now to send the first prompt on this task.

) : ( { void copyMessage(message); }} thinkingTimerLabel={thinkingTimerLabel} /> )} {!isTerminal && promptTab ? ( updateDraft(value, attachments)} onSend={sendMessage} onStop={stopAgent} onRemoveAttachment={removeAttachment} onChangeModel={changeModel} onSetDefaultModel={setDefaultModel} /> ) : null}
); }); const RIGHT_RAIL_MIN_SECTION_HEIGHT = 180; const RIGHT_RAIL_SPLITTER_HEIGHT = 10; const DEFAULT_TERMINAL_HEIGHT = 320; const TERMINAL_HEIGHT_STORAGE_KEY = "openhandoff:foundry-terminal-height"; const RightRail = memo(function RightRail({ workspaceId, handoff, activeTabId, onOpenDiff, onArchive, onRevertFile, onPublishPr, }: { workspaceId: string; handoff: Handoff; activeTabId: string | null; onOpenDiff: (path: string) => void; onArchive: () => void; onRevertFile: (path: string) => void; onPublishPr: () => void; }) { const [css] = useStyletron(); const railRef = useRef(null); const [terminalHeight, setTerminalHeight] = useState(() => { if (typeof window === "undefined") { return DEFAULT_TERMINAL_HEIGHT; } const stored = window.localStorage.getItem(TERMINAL_HEIGHT_STORAGE_KEY); const parsed = stored ? Number.parseInt(stored, 10) : Number.NaN; return Number.isFinite(parsed) ? parsed : DEFAULT_TERMINAL_HEIGHT; }); const clampTerminalHeight = useCallback((nextHeight: number) => { const railHeight = railRef.current?.getBoundingClientRect().height ?? 0; const maxHeight = Math.max( RIGHT_RAIL_MIN_SECTION_HEIGHT, railHeight - RIGHT_RAIL_MIN_SECTION_HEIGHT - RIGHT_RAIL_SPLITTER_HEIGHT, ); return Math.min(Math.max(nextHeight, RIGHT_RAIL_MIN_SECTION_HEIGHT), maxHeight); }, []); useEffect(() => { if (typeof window === "undefined") { return; } window.localStorage.setItem(TERMINAL_HEIGHT_STORAGE_KEY, String(terminalHeight)); }, [terminalHeight]); useEffect(() => { const handleResize = () => { setTerminalHeight((current) => clampTerminalHeight(current)); }; window.addEventListener("resize", handleResize); handleResize(); return () => window.removeEventListener("resize", handleResize); }, [clampTerminalHeight]); const startResize = useCallback( (event: ReactPointerEvent) => { event.preventDefault(); const startY = event.clientY; const startHeight = terminalHeight; document.body.style.cursor = "ns-resize"; const handlePointerMove = (moveEvent: PointerEvent) => { const deltaY = moveEvent.clientY - startY; setTerminalHeight(clampTerminalHeight(startHeight - deltaY)); }; const stopResize = () => { document.body.style.cursor = ""; window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", stopResize); }; window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", stopResize, { once: true }); }, [clampTerminalHeight, terminalHeight], ); return (
); }); interface MockLayoutProps { workspaceId: string; selectedHandoffId?: string | null; selectedSessionId?: string | null; } export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }: MockLayoutProps) { const navigate = useNavigate(); const viewModel = useSyncExternalStore( handoffWorkbenchClient.subscribe.bind(handoffWorkbenchClient), handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient), handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient), ); const handoffs = viewModel.handoffs ?? []; const projects = viewModel.projects ?? []; const [activeTabIdByHandoff, setActiveTabIdByHandoff] = useState>({}); const [lastAgentTabIdByHandoff, setLastAgentTabIdByHandoff] = useState>({}); const [openDiffsByHandoff, setOpenDiffsByHandoff] = useState>({}); const [starRepoPromptOpen, setStarRepoPromptOpen] = useState(false); const [starRepoPending, setStarRepoPending] = useState(false); const [starRepoError, setStarRepoError] = useState(null); const activeHandoff = useMemo(() => handoffs.find((handoff) => handoff.id === selectedHandoffId) ?? handoffs[0] ?? null, [handoffs, selectedHandoffId]); useEffect(() => { try { const status = globalThis.localStorage?.getItem(STAR_SANDBOX_AGENT_REPO_STORAGE_KEY); if (status !== "completed" && status !== "dismissed") { setStarRepoPromptOpen(true); } } catch { setStarRepoPromptOpen(true); } }, []); useEffect(() => { if (activeHandoff) { return; } const fallbackHandoffId = handoffs[0]?.id; if (!fallbackHandoffId) { return; } const fallbackHandoff = handoffs.find((handoff) => handoff.id === fallbackHandoffId) ?? null; void navigate({ to: "/workspaces/$workspaceId/handoffs/$handoffId", params: { workspaceId, handoffId: fallbackHandoffId, }, search: { sessionId: fallbackHandoff?.tabs[0]?.id ?? undefined }, replace: true, }); }, [activeHandoff, handoffs, navigate, workspaceId]); const openDiffs = activeHandoff ? sanitizeOpenDiffs(activeHandoff, openDiffsByHandoff[activeHandoff.id]) : []; const lastAgentTabId = activeHandoff ? sanitizeLastAgentTabId(activeHandoff, lastAgentTabIdByHandoff[activeHandoff.id]) : null; const activeTabId = activeHandoff ? sanitizeActiveTabId(activeHandoff, activeTabIdByHandoff[activeHandoff.id], openDiffs, lastAgentTabId) : null; const syncRouteSession = useCallback( (handoffId: string, sessionId: string | null, replace = false) => { void navigate({ to: "/workspaces/$workspaceId/handoffs/$handoffId", params: { workspaceId, handoffId, }, search: { sessionId: sessionId ?? undefined }, ...(replace ? { replace: true } : {}), }); }, [navigate, workspaceId], ); useEffect(() => { if (!activeHandoff) { return; } const resolvedRouteSessionId = sanitizeLastAgentTabId(activeHandoff, selectedSessionId); if (!resolvedRouteSessionId) { return; } if (selectedSessionId !== resolvedRouteSessionId) { syncRouteSession(activeHandoff.id, resolvedRouteSessionId, true); return; } if (lastAgentTabIdByHandoff[activeHandoff.id] === resolvedRouteSessionId) { return; } setLastAgentTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: resolvedRouteSessionId, })); setActiveTabIdByHandoff((current) => { const currentActive = current[activeHandoff.id]; if (currentActive && isDiffTab(currentActive)) { return current; } return { ...current, [activeHandoff.id]: resolvedRouteSessionId, }; }); }, [activeHandoff, lastAgentTabIdByHandoff, selectedSessionId, syncRouteSession]); const createHandoff = useCallback(() => { void (async () => { const repoId = activeHandoff?.repoId ?? viewModel.repos[0]?.id ?? ""; if (!repoId) { throw new Error("Cannot create a task without an available repo"); } const task = window.prompt("Describe the task", "Investigate and implement the requested change"); if (!task) { return; } const title = window.prompt("Optional task title", "")?.trim() || undefined; const branch = window.prompt("Optional branch name", "")?.trim() || undefined; const { handoffId, tabId } = await handoffWorkbenchClient.createHandoff({ repoId, task, model: "gpt-4o", ...(title ? { title } : {}), ...(branch ? { branch } : {}), }); await navigate({ to: "/workspaces/$workspaceId/handoffs/$handoffId", params: { workspaceId, handoffId, }, search: { sessionId: tabId ?? undefined }, }); })(); }, [activeHandoff?.repoId, navigate, viewModel.repos, workspaceId]); const openDiffTab = useCallback( (path: string) => { if (!activeHandoff) { throw new Error("Cannot open a diff tab without an active task"); } setOpenDiffsByHandoff((current) => { const existing = sanitizeOpenDiffs(activeHandoff, current[activeHandoff.id]); if (existing.includes(path)) { return current; } return { ...current, [activeHandoff.id]: [...existing, path], }; }); setActiveTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: diffTabId(path), })); }, [activeHandoff], ); const selectHandoff = useCallback( (id: string) => { const handoff = handoffs.find((candidate) => candidate.id === id) ?? null; void navigate({ to: "/workspaces/$workspaceId/handoffs/$handoffId", params: { workspaceId, handoffId: id, }, search: { sessionId: handoff?.tabs[0]?.id ?? undefined }, }); }, [handoffs, navigate, workspaceId], ); const markHandoffUnread = useCallback((id: string) => { void handoffWorkbenchClient.markHandoffUnread({ handoffId: id }); }, []); const renameHandoff = useCallback( (id: string) => { const currentHandoff = handoffs.find((handoff) => handoff.id === id); if (!currentHandoff) { throw new Error(`Unable to rename missing task ${id}`); } const nextTitle = window.prompt("Rename task", currentHandoff.title); if (nextTitle === null) { return; } const trimmedTitle = nextTitle.trim(); if (!trimmedTitle) { return; } void handoffWorkbenchClient.renameHandoff({ handoffId: id, value: trimmedTitle }); }, [handoffs], ); const renameBranch = useCallback( (id: string) => { const currentHandoff = handoffs.find((handoff) => handoff.id === id); if (!currentHandoff) { throw new Error(`Unable to rename missing task ${id}`); } const nextBranch = window.prompt("Rename branch", currentHandoff.branch ?? ""); if (nextBranch === null) { return; } const trimmedBranch = nextBranch.trim(); if (!trimmedBranch) { return; } void handoffWorkbenchClient.renameBranch({ handoffId: id, value: trimmedBranch }); }, [handoffs], ); const archiveHandoff = useCallback(() => { if (!activeHandoff) { throw new Error("Cannot archive without an active task"); } void handoffWorkbenchClient.archiveHandoff({ handoffId: activeHandoff.id }); }, [activeHandoff]); const publishPr = useCallback(() => { if (!activeHandoff) { throw new Error("Cannot publish PR without an active task"); } void handoffWorkbenchClient.publishPr({ handoffId: activeHandoff.id }); }, [activeHandoff]); const revertFile = useCallback( (path: string) => { if (!activeHandoff) { throw new Error("Cannot revert a file without an active task"); } setOpenDiffsByHandoff((current) => ({ ...current, [activeHandoff.id]: sanitizeOpenDiffs(activeHandoff, current[activeHandoff.id]).filter((candidate) => candidate !== path), })); setActiveTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: current[activeHandoff.id] === diffTabId(path) ? sanitizeLastAgentTabId(activeHandoff, lastAgentTabIdByHandoff[activeHandoff.id]) : (current[activeHandoff.id] ?? null), })); void handoffWorkbenchClient.revertFile({ handoffId: activeHandoff.id, path, }); }, [activeHandoff, lastAgentTabIdByHandoff], ); const dismissStarRepoPrompt = useCallback(() => { setStarRepoError(null); try { globalThis.localStorage?.setItem(STAR_SANDBOX_AGENT_REPO_STORAGE_KEY, "dismissed"); } catch { // ignore storage failures } setStarRepoPromptOpen(false); }, []); const starSandboxAgentRepo = useCallback(() => { setStarRepoPending(true); setStarRepoError(null); void backendClient .starSandboxAgentRepo(workspaceId) .then(() => { try { globalThis.localStorage?.setItem(STAR_SANDBOX_AGENT_REPO_STORAGE_KEY, "completed"); } catch { // ignore storage failures } setStarRepoPromptOpen(false); }) .catch((error) => { setStarRepoError(error instanceof Error ? error.message : String(error)); }) .finally(() => { setStarRepoPending(false); }); }, [workspaceId]); const starRepoPrompt = starRepoPromptOpen ? (
Onboarding

Give us support for sandbox agent

Before you keep going, give us support for sandbox agent and star the repo right here in the app.

{starRepoError ? (
{starRepoError}
) : null}
) : null; if (!activeHandoff) { return ( <>

Create your first task

{viewModel.repos.length > 0 ? "Start from the sidebar to create a task on the first available repo." : "No repos are available in this workspace yet."}

{starRepoPrompt} ); } return ( <> { setActiveTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId })); }} onSetLastAgentTabId={(tabId) => { setLastAgentTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId })); }} onSetOpenDiffs={(paths) => { setOpenDiffsByHandoff((current) => ({ ...current, [activeHandoff.id]: paths })); }} /> {starRepoPrompt} ); }