import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react"; import { useNavigate } from "@tanstack/react-router"; import { useStyletron } from "baseui"; import { createErrorContext, type FoundryOrganization, type TaskWorkbenchSnapshot, type WorkbenchOpenPrSummary, type WorkbenchSessionSummary, type WorkbenchTaskDetail, type WorkbenchTaskSummary, } from "@sandbox-agent/foundry-shared"; import { useSubscription } from "@sandbox-agent/foundry-client"; import { CircleAlert, PanelLeft, PanelRight } from "lucide-react"; import { useFoundryTokens } from "../app/theme"; import { logger } from "../logging.js"; 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 { SessionStrip } from "./mock-layout/session-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, SpinnerDot } from "./mock-layout/ui"; import { DevPanel, useDevPanel } from "./dev-panel"; import { buildDisplayMessages, diffPath, diffTabId, formatThinkingDuration, isDiffTab, buildHistoryEvents, type Task, type HistoryEvent, type LineAttachment, type Message, type ModelId, } from "./mock-layout/view-model"; import { activeMockOrganization, useMockAppSnapshot } from "../lib/mock-app"; import { backendClient } from "../lib/backend"; import { subscriptionManager } from "../lib/subscription"; import { describeTaskState, isProvisioningTaskStatus } from "../features/tasks/status"; function firstAgentSessionId(task: Task): string | null { return task.sessions[0]?.id ?? null; } function sanitizeOpenDiffs(task: Task, paths: string[] | undefined): string[] { if (!paths) { return []; } return paths.filter((path) => task.diffs[path] != null); } function sanitizeLastAgentSessionId(task: Task, sessionId: string | null | undefined): string | null { if (sessionId && task.sessions.some((tab) => tab.id === sessionId)) { return sessionId; } return firstAgentSessionId(task); } function sanitizeActiveSessionId(task: Task, sessionId: string | null | undefined, openDiffs: string[], lastAgentSessionId: string | null): string | null { if (sessionId) { if (task.sessions.some((tab) => tab.id === sessionId)) { return sessionId; } if (isDiffTab(sessionId) && openDiffs.includes(diffPath(sessionId))) { return sessionId; } } return openDiffs.length > 0 ? diffTabId(openDiffs[openDiffs.length - 1]!) : lastAgentSessionId; } function githubInstallationWarningTitle(organization: FoundryOrganization): string { return organization.github.installationStatus === "install_required" ? "GitHub App not installed" : "GitHub App needs reconnection"; } function githubInstallationWarningDetail(organization: FoundryOrganization): string { const statusDetail = organization.github.lastSyncLabel.trim(); const requirementDetail = organization.github.installationStatus === "install_required" ? "Webhooks are required for Foundry to function. Repo sync and PR updates will not work until the GitHub App is installed for this organization." : "Webhook delivery is unavailable. Repo sync and PR updates will not work until the GitHub App is reconnected."; return statusDetail ? `${requirementDetail} ${statusDetail}.` : requirementDetail; } function GithubInstallationWarning({ organization, css, t, }: { organization: FoundryOrganization; css: ReturnType[0]; t: ReturnType; }) { if (organization.github.installationStatus === "connected") { return null; } return (
{githubInstallationWarningTitle(organization)}
{githubInstallationWarningDetail(organization)}
); } function toSessionModel( summary: WorkbenchSessionSummary, sessionDetail?: { draft: Task["sessions"][number]["draft"]; transcript: Task["sessions"][number]["transcript"] }, ): Task["sessions"][number] { return { id: summary.id, sessionId: summary.sessionId, sessionName: summary.sessionName, agent: summary.agent, model: summary.model, status: summary.status, thinkingSinceMs: summary.thinkingSinceMs, unread: summary.unread, created: summary.created, errorMessage: summary.errorMessage ?? null, draft: sessionDetail?.draft ?? { text: "", attachments: [], updatedAtMs: null, }, transcript: sessionDetail?.transcript ?? [], }; } function toTaskModel( summary: WorkbenchTaskSummary, detail?: WorkbenchTaskDetail, sessionCache?: Map, ): Task { const sessions = detail?.sessionsSummary ?? summary.sessionsSummary; return { id: summary.id, repoId: summary.repoId, title: detail?.title ?? summary.title, status: detail?.runtimeStatus ?? detail?.status ?? summary.status, runtimeStatus: detail?.runtimeStatus, statusMessage: detail?.statusMessage ?? null, repoName: detail?.repoName ?? summary.repoName, updatedAtMs: detail?.updatedAtMs ?? summary.updatedAtMs, branch: detail?.branch ?? summary.branch, pullRequest: detail?.pullRequest ?? summary.pullRequest, sessions: sessions.map((session) => toSessionModel(session, sessionCache?.get(session.id))), fileChanges: detail?.fileChanges ?? [], diffs: detail?.diffs ?? {}, fileTree: detail?.fileTree ?? [], minutesUsed: detail?.minutesUsed ?? 0, activeSandboxId: detail?.activeSandboxId ?? null, }; } const OPEN_PR_TASK_PREFIX = "pr:"; function openPrTaskId(prId: string): string { return `${OPEN_PR_TASK_PREFIX}${prId}`; } function isOpenPrTaskId(taskId: string): boolean { return taskId.startsWith(OPEN_PR_TASK_PREFIX); } function toOpenPrTaskModel(pullRequest: WorkbenchOpenPrSummary): Task { return { id: openPrTaskId(pullRequest.prId), repoId: pullRequest.repoId, title: pullRequest.title, status: "new", runtimeStatus: undefined, statusMessage: pullRequest.authorLogin ? `@${pullRequest.authorLogin}` : null, repoName: pullRequest.repoFullName, updatedAtMs: pullRequest.updatedAtMs, branch: pullRequest.headRefName, pullRequest: { number: pullRequest.number, status: pullRequest.isDraft ? "draft" : "ready", }, sessions: [], fileChanges: [], diffs: {}, fileTree: [], minutesUsed: 0, activeSandboxId: null, }; } function sessionStateMessage(tab: Task["sessions"][number] | null | undefined): string | null { if (!tab) { return null; } if (tab.status === "pending_provision") { return "Provisioning sandbox..."; } if (tab.status === "pending_session_create") { return "Creating session..."; } if (tab.status === "error") { return tab.errorMessage ?? "Session failed to start."; } return null; } function groupRepositories(repos: Array<{ id: string; label: string }>, tasks: Task[]) { return repos .map((repo) => ({ id: repo.id, label: repo.label, updatedAtMs: tasks.filter((task) => task.repoId === repo.id).reduce((latest, task) => Math.max(latest, task.updatedAtMs), 0), tasks: tasks.filter((task) => task.repoId === repo.id).sort((left, right) => right.updatedAtMs - left.updatedAtMs), })) .filter((repo) => repo.tasks.length > 0); } interface WorkbenchActions { createTask(input: { repoId: string; task: string; title?: string; branch?: string; onBranch?: string; model?: ModelId; }): Promise<{ taskId: string; sessionId?: string }>; markTaskUnread(input: { taskId: string }): Promise; renameTask(input: { taskId: string; value: string }): Promise; renameBranch(input: { taskId: string; value: string }): Promise; archiveTask(input: { taskId: string }): Promise; publishPr(input: { taskId: string }): Promise; revertFile(input: { taskId: string; path: string }): Promise; updateDraft(input: { taskId: string; sessionId: string; text: string; attachments: LineAttachment[] }): Promise; sendMessage(input: { taskId: string; sessionId: string; text: string; attachments: LineAttachment[] }): Promise; stopAgent(input: { taskId: string; sessionId: string }): Promise; setSessionUnread(input: { taskId: string; sessionId: string; unread: boolean }): Promise; renameSession(input: { taskId: string; sessionId: string; title: string }): Promise; closeSession(input: { taskId: string; sessionId: string }): Promise; addSession(input: { taskId: string; model?: string }): Promise<{ sessionId: string }>; changeModel(input: { taskId: string; sessionId: string; model: ModelId }): Promise; reloadGithubOrganization(): Promise; reloadGithubPullRequests(): Promise; reloadGithubRepository(repoId: string): Promise; reloadGithubPullRequest(repoId: string, prNumber: number): Promise; } const TranscriptPanel = memo(function TranscriptPanel({ taskWorkbenchClient, task, hasSandbox, activeSessionId, lastAgentSessionId, openDiffs, onSyncRouteSession, onSetActiveSessionId, onSetLastAgentSessionId, onSetOpenDiffs, sidebarCollapsed, onToggleSidebar, onSidebarPeekStart, onSidebarPeekEnd, rightSidebarCollapsed, onToggleRightSidebar, selectedSessionHydrating = false, onNavigateToUsage, }: { taskWorkbenchClient: WorkbenchActions; task: Task; hasSandbox: boolean; activeSessionId: string | null; lastAgentSessionId: string | null; openDiffs: string[]; onSyncRouteSession: (taskId: string, sessionId: string | null, replace?: boolean) => void; onSetActiveSessionId: (sessionId: string | null) => void; onSetLastAgentSessionId: (sessionId: string | null) => void; onSetOpenDiffs: (paths: string[]) => void; sidebarCollapsed?: boolean; onToggleSidebar?: () => void; onSidebarPeekStart?: () => void; onSidebarPeekEnd?: () => void; rightSidebarCollapsed?: boolean; onToggleRightSidebar?: () => void; selectedSessionHydrating?: boolean; onNavigateToUsage?: () => void; }) { const t = useFoundryTokens(); const [defaultModel, setDefaultModel] = useState("claude-sonnet-4"); const [editingField, setEditingField] = useState<"title" | "branch" | null>(null); const [editValue, setEditValue] = useState(""); const [editingSessionId, setEditingSessionId] = useState(null); const [editingSessionName, setEditingSessionName] = useState(""); const [pendingHistoryTarget, setPendingHistoryTarget] = useState<{ messageId: string; sessionId: string } | null>(null); const [copiedMessageId, setCopiedMessageId] = useState(null); const [timerNowMs, setTimerNowMs] = useState(() => Date.now()); const [localDraft, setLocalDraft] = useState(""); const [localAttachments, setLocalAttachments] = useState([]); const [pendingMessage, setPendingMessage] = useState<{ text: string; sessionId: string; sentAt: number } | null>(null); const lastEditTimeRef = useRef(0); const throttleTimerRef = useRef | null>(null); const pendingDraftRef = useRef<{ text: string; attachments: LineAttachment[] } | null>(null); const scrollRef = useRef(null); const textareaRef = useRef(null); const messageRefs = useRef(new Map()); const activeDiff = activeSessionId && isDiffTab(activeSessionId) ? diffPath(activeSessionId) : null; const activeAgentSession = activeDiff ? null : (task.sessions.find((candidate) => candidate.id === activeSessionId) ?? task.sessions[0] ?? null); const promptSession = task.sessions.find((candidate) => candidate.id === lastAgentSessionId) ?? task.sessions[0] ?? null; const isTerminal = task.status === "archived"; const historyEvents = useMemo(() => buildHistoryEvents(task.sessions), [task.sessions]); const activeMessages = useMemo(() => buildDisplayMessages(activeAgentSession), [activeAgentSession]); const taskRuntimeStatus = task.runtimeStatus ?? task.status; const taskState = describeTaskState(taskRuntimeStatus, task.statusMessage ?? null); const taskProvisioning = isProvisioningTaskStatus(taskRuntimeStatus); const taskProvisioningMessage = taskState.detail; const activeSessionMessage = sessionStateMessage(activeAgentSession); const showPendingSessionState = !activeDiff && !!activeAgentSession && (activeAgentSession.status === "pending_provision" || activeAgentSession.status === "pending_session_create" || activeAgentSession.status === "error") && activeMessages.length === 0; const serverDraft = promptSession?.draft.text ?? ""; const serverAttachments = promptSession?.draft.attachments ?? []; // Sync server → local only when user hasn't typed recently (3s cooldown) const DRAFT_SYNC_COOLDOWN_MS = 3_000; useEffect(() => { if (Date.now() - lastEditTimeRef.current > DRAFT_SYNC_COOLDOWN_MS) { setLocalDraft(serverDraft); setLocalAttachments(serverAttachments); } }, [serverDraft, serverAttachments]); // Reset local draft immediately on session/task switch useEffect(() => { lastEditTimeRef.current = 0; setLocalDraft(promptSession?.draft.text ?? ""); setLocalAttachments(promptSession?.draft.attachments ?? []); }, [promptSession?.id, task.id]); // Clear pending message once the real transcript contains a client message newer than when we sent const pendingMessageClientCount = useRef(0); useEffect(() => { if (!pendingMessage) return; const targetSession = task.sessions.find((s) => s.id === pendingMessage.sessionId); if (!targetSession) return; const clientEventCount = targetSession.transcript.filter((event) => event.sender === "client").length; if (clientEventCount > pendingMessageClientCount.current) { setPendingMessage(null); } }, [task.sessions, pendingMessage]); const draft = localDraft; const attachments = localAttachments; useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [activeMessages.length]); useEffect(() => { textareaRef.current?.focus(); }, [activeSessionId, task.id]); useEffect(() => { setEditingSessionId(null); setEditingSessionName(""); }, [task.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, activeSessionId, task.id]); useEffect(() => { if (!copiedMessageId) { return; } const timer = setTimeout(() => { setCopiedMessageId(null); }, 1_200); return () => clearTimeout(timer); }, [copiedMessageId]); useEffect(() => { if (!activeAgentSession || activeAgentSession.status !== "running" || activeAgentSession.thinkingSinceMs === null) { return; } setTimerNowMs(Date.now()); const timer = window.setInterval(() => { setTimerNowMs(Date.now()); }, 1_000); return () => window.clearInterval(timer); }, [activeAgentSession?.id, activeAgentSession?.status, activeAgentSession?.thinkingSinceMs]); useEffect(() => { if (!activeAgentSession?.unread) { return; } void taskWorkbenchClient.setSessionUnread({ taskId: task.id, sessionId: activeAgentSession.id, unread: false, }); }, [activeAgentSession?.id, activeAgentSession?.unread, task.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 taskWorkbenchClient.renameTask({ taskId: task.id, value }); } else { void taskWorkbenchClient.renameBranch({ taskId: task.id, value }); } setEditingField(null); }, [editValue, task.id], ); const DRAFT_THROTTLE_MS = 500; const flushDraft = useCallback( (text: string, nextAttachments: LineAttachment[], sessionId: string) => { void taskWorkbenchClient.updateDraft({ taskId: task.id, sessionId, text, attachments: nextAttachments, }); }, [task.id], ); // Clean up throttle timer on unmount useEffect(() => { return () => { if (throttleTimerRef.current) { clearTimeout(throttleTimerRef.current); } }; }, []); const updateDraft = useCallback( (nextText: string, nextAttachments: LineAttachment[]) => { if (!promptSession) { return; } // Update local state immediately for responsive typing lastEditTimeRef.current = Date.now(); setLocalDraft(nextText); setLocalAttachments(nextAttachments); // Throttle the network call pendingDraftRef.current = { text: nextText, attachments: nextAttachments }; if (!throttleTimerRef.current) { throttleTimerRef.current = setTimeout(() => { throttleTimerRef.current = null; if (pendingDraftRef.current) { flushDraft(pendingDraftRef.current.text, pendingDraftRef.current.attachments, promptSession.id); pendingDraftRef.current = null; } }, DRAFT_THROTTLE_MS); } }, [promptSession, flushDraft], ); const sendMessage = useCallback(() => { const text = draft.trim(); if (!text || !promptSession) { return; } // Clear draft and show optimistic message immediately (don't wait for server round-trip) setLocalDraft(""); setLocalAttachments([]); lastEditTimeRef.current = Date.now(); // Snapshot current client message count so we can detect when the server adds ours pendingMessageClientCount.current = promptSession.transcript.filter((event) => event.sender === "client").length; setPendingMessage({ text, sessionId: promptSession.id, sentAt: Date.now() }); onSetActiveSessionId(promptSession.id); onSetLastAgentSessionId(promptSession.id); void taskWorkbenchClient.sendMessage({ taskId: task.id, sessionId: promptSession.id, text, attachments, }); }, [attachments, draft, task.id, onSetActiveSessionId, onSetLastAgentSessionId, promptSession]); const stopAgent = useCallback(() => { if (!promptSession) { return; } void taskWorkbenchClient.stopAgent({ taskId: task.id, sessionId: promptSession.id, }); }, [task.id, promptSession]); const switchSession = useCallback( (sessionId: string) => { onSetActiveSessionId(sessionId); if (!isDiffTab(sessionId)) { onSetLastAgentSessionId(sessionId); const session = task.sessions.find((candidate) => candidate.id === sessionId); if (session?.unread) { void taskWorkbenchClient.setSessionUnread({ taskId: task.id, sessionId, unread: false, }); } onSyncRouteSession(task.id, sessionId); } }, [task.id, task.sessions, onSetActiveSessionId, onSetLastAgentSessionId, onSyncRouteSession], ); const setSessionUnread = useCallback( (sessionId: string, unread: boolean) => { void taskWorkbenchClient.setSessionUnread({ taskId: task.id, sessionId, unread }); }, [task.id], ); const startRenamingSession = useCallback( (sessionId: string) => { const targetSession = task.sessions.find((candidate) => candidate.id === sessionId); if (!targetSession) { throw new Error(`Unable to rename missing session ${sessionId}`); } setEditingSessionId(sessionId); setEditingSessionName(targetSession.sessionName); }, [task.sessions], ); const cancelSessionRename = useCallback(() => { setEditingSessionId(null); setEditingSessionName(""); }, []); const commitSessionRename = useCallback(() => { if (!editingSessionId) { return; } const trimmedName = editingSessionName.trim(); if (!trimmedName) { cancelSessionRename(); return; } void taskWorkbenchClient.renameSession({ taskId: task.id, sessionId: editingSessionId, title: trimmedName, }); cancelSessionRename(); }, [cancelSessionRename, editingSessionName, editingSessionId, task.id]); const closeSession = useCallback( (sessionId: string) => { const remainingSessions = task.sessions.filter((candidate) => candidate.id !== sessionId); const nextSessionId = remainingSessions[0]?.id ?? null; if (activeSessionId === sessionId) { onSetActiveSessionId(nextSessionId); } if (lastAgentSessionId === sessionId) { onSetLastAgentSessionId(nextSessionId); } onSyncRouteSession(task.id, nextSessionId); void taskWorkbenchClient.closeSession({ taskId: task.id, sessionId }); }, [activeSessionId, task.id, task.sessions, lastAgentSessionId, onSetActiveSessionId, onSetLastAgentSessionId, onSyncRouteSession], ); const closeDiffTab = useCallback( (path: string) => { const nextOpenDiffs = openDiffs.filter((candidate) => candidate !== path); onSetOpenDiffs(nextOpenDiffs); if (activeSessionId === diffTabId(path)) { onSetActiveSessionId( nextOpenDiffs.length > 0 ? diffTabId(nextOpenDiffs[nextOpenDiffs.length - 1]!) : (lastAgentSessionId ?? firstAgentSessionId(task)), ); } }, [activeSessionId, task, lastAgentSessionId, onSetActiveSessionId, onSetOpenDiffs, openDiffs], ); const addSession = useCallback(() => { void (async () => { const { sessionId } = await taskWorkbenchClient.addSession({ taskId: task.id }); onSetLastAgentSessionId(sessionId); onSetActiveSessionId(sessionId); onSyncRouteSession(task.id, sessionId); })(); }, [task.id, onSetActiveSessionId, onSetLastAgentSessionId, onSyncRouteSession]); const changeModel = useCallback( (model: ModelId) => { if (!promptSession) { throw new Error(`Unable to change model for task ${task.id} without an active prompt session`); } void taskWorkbenchClient.changeModel({ taskId: task.id, sessionId: promptSession.id, model, }); }, [task.id, promptSession], ); const addAttachment = useCallback( (filePath: string, lineNumber: number, lineContent: string) => { if (!promptSession) { 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, promptSession, 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, sessionId: event.sessionId }); if (activeSessionId !== event.sessionId) { switchSession(event.sessionId); } }, [activeSessionId, switchSession], ); 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) { logger.error( { messageId: message.id, ...createErrorContext(error), }, "failed_to_copy_transcript_message", ); } }, []); const isOptimisticThinking = pendingMessage !== null && activeAgentSession?.id === pendingMessage.sessionId; const thinkingTimerLabel = activeAgentSession?.status === "running" && activeAgentSession.thinkingSinceMs !== null ? formatThinkingDuration(timerNowMs - activeAgentSession.thinkingSinceMs) : isOptimisticThinking ? formatThinkingDuration(timerNowMs - pendingMessage.sentAt) : null; return ( { if (activeAgentSession) { setSessionUnread(activeAgentSession.id, unread); } }} sidebarCollapsed={sidebarCollapsed} onToggleSidebar={onToggleSidebar} onSidebarPeekStart={onSidebarPeekStart} onSidebarPeekEnd={onSidebarPeekEnd} rightSidebarCollapsed={rightSidebarCollapsed} onToggleRightSidebar={onToggleRightSidebar} onNavigateToUsage={onNavigateToUsage} />
{activeDiff ? ( file.path === activeDiff)} diff={task.diffs[activeDiff]} onAddAttachment={addAttachment} /> ) : task.sessions.length === 0 ? (
{taskProvisioning ? ( <>

{taskState.title}

{taskProvisioningMessage}

) : ( <>

Create the first session

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

)}
) : selectedSessionHydrating ? (

Loading session

Fetching the latest transcript for this session.

) : showPendingSessionState ? (
{activeAgentSession?.status === "error" ? null : }

{activeAgentSession?.status === "pending_provision" ? "Provisioning sandbox" : activeAgentSession?.status === "pending_session_create" ? "Creating session" : "Session unavailable"}

{activeSessionMessage}

{activeAgentSession?.status === "error" ? ( ) : null}
) : ( setPendingHistoryTarget(null)} copiedMessageId={copiedMessageId} onCopyMessage={(message) => { void copyMessage(message); }} thinkingTimerLabel={thinkingTimerLabel} pendingMessage={ pendingMessage && activeAgentSession?.id === pendingMessage.sessionId ? { text: pendingMessage.text, sentAt: pendingMessage.sentAt } : null } /> )} {!isTerminal && promptSession && (promptSession.status === "ready" || promptSession.status === "running" || promptSession.status === "idle") ? ( updateDraft(value, attachments)} onSend={sendMessage} onStop={stopAgent} onRemoveAttachment={removeAttachment} onChangeModel={changeModel} onSetDefaultModel={setDefaultModel} /> ) : null}
); }); const LEFT_SIDEBAR_DEFAULT_WIDTH = 340; const RIGHT_SIDEBAR_DEFAULT_WIDTH = 380; const SIDEBAR_MIN_WIDTH = 220; const SIDEBAR_MAX_WIDTH = 600; const RESIZE_HANDLE_WIDTH = 1; const LEFT_WIDTH_STORAGE_KEY = "foundry:left-sidebar-width"; const RIGHT_WIDTH_STORAGE_KEY = "foundry:right-sidebar-width"; function readStoredWidth(key: string, fallback: number): number { if (typeof window === "undefined") return fallback; const stored = window.localStorage.getItem(key); const parsed = stored ? Number.parseInt(stored, 10) : Number.NaN; return Number.isFinite(parsed) ? Math.min(Math.max(parsed, SIDEBAR_MIN_WIDTH), SIDEBAR_MAX_WIDTH) : fallback; } const PanelResizeHandle = memo(function PanelResizeHandle({ onResizeStart, onResize }: { onResizeStart: () => void; onResize: (deltaX: number) => void }) { const handlePointerDown = useCallback( (event: ReactPointerEvent) => { event.preventDefault(); const startX = event.clientX; onResizeStart(); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; const handlePointerMove = (moveEvent: PointerEvent) => { onResize(moveEvent.clientX - startX); }; const stopResize = () => { document.body.style.cursor = ""; document.body.style.userSelect = ""; window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", stopResize); }; window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", stopResize, { once: true }); }, [onResize, onResizeStart], ); return (
); }); const RIGHT_RAIL_MIN_SECTION_HEIGHT = 180; const RIGHT_RAIL_SPLITTER_HEIGHT = 10; const DEFAULT_TERMINAL_HEIGHT = 320; const TERMINAL_HEIGHT_STORAGE_KEY = "foundry:terminal-height"; const RightRail = memo(function RightRail({ organizationId, task, activeSessionId, onOpenDiff, onArchive, onRevertFile, onPublishPr, onToggleSidebar, }: { organizationId: string; task: Task; activeSessionId: string | null; onOpenDiff: (path: string) => void; onArchive: () => void; onRevertFile: (path: string) => void; onPublishPr: () => void; onToggleSidebar?: () => void; }) { const [css] = useStyletron(); const t = useFoundryTokens(); 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, 43), 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 (
{ const railHeight = railRef.current?.getBoundingClientRect().height ?? 0; return railHeight > 0 && terminalHeight >= railHeight * 0.7; })()} onExpand={() => { const railHeight = railRef.current?.getBoundingClientRect().height ?? 0; const maxHeight = Math.max(RIGHT_RAIL_MIN_SECTION_HEIGHT, railHeight - RIGHT_RAIL_SPLITTER_HEIGHT - 42); setTerminalHeight(maxHeight); }} onCollapse={() => { setTerminalHeight(43); }} />
); }); interface MockLayoutProps { organizationId: string; selectedTaskId?: string | null; selectedSessionId?: string | null; } function MockOrganizationOrgBar() { const navigate = useNavigate(); const snapshot = useMockAppSnapshot(); const organization = activeMockOrganization(snapshot); const t = useFoundryTokens(); if (!organization) { return null; } const buttonStyle = { border: `1px solid ${t.borderMedium}`, borderRadius: "999px", padding: "8px 12px", background: t.interactiveSubtle, color: t.textPrimary, cursor: "pointer", fontSize: "13px", fontWeight: 600, } satisfies React.CSSProperties; return (
{organization.settings.displayName} {organization.settings.primaryDomain}
); } export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }: MockLayoutProps) { const [css] = useStyletron(); const t = useFoundryTokens(); const navigate = useNavigate(); const taskWorkbenchClient = useMemo( () => ({ createTask: (input) => backendClient.createWorkbenchTask(organizationId, input), markTaskUnread: (input) => backendClient.markWorkbenchUnread(organizationId, input), renameTask: (input) => backendClient.renameWorkbenchTask(organizationId, input), renameBranch: (input) => backendClient.renameWorkbenchBranch(organizationId, input), archiveTask: async (input) => backendClient.runAction(organizationId, input.taskId, "archive"), publishPr: (input) => backendClient.publishWorkbenchPr(organizationId, input), revertFile: (input) => backendClient.revertWorkbenchFile(organizationId, input), updateDraft: (input) => backendClient.updateWorkbenchDraft(organizationId, input), sendMessage: (input) => backendClient.sendWorkbenchMessage(organizationId, input), stopAgent: (input) => backendClient.stopWorkbenchSession(organizationId, input), setSessionUnread: (input) => backendClient.setWorkbenchSessionUnread(organizationId, input), renameSession: (input) => backendClient.renameWorkbenchSession(organizationId, input), closeSession: (input) => backendClient.closeWorkbenchSession(organizationId, input), addSession: (input) => backendClient.createWorkbenchSession(organizationId, input), changeModel: (input) => backendClient.changeWorkbenchModel(organizationId, input), reloadGithubOrganization: () => backendClient.reloadGithubOrganization(organizationId), reloadGithubPullRequests: () => backendClient.reloadGithubPullRequests(organizationId), reloadGithubRepository: (repoId) => backendClient.reloadGithubRepository(organizationId, repoId), reloadGithubPullRequest: (repoId, prNumber) => backendClient.reloadGithubPullRequest(organizationId, repoId, prNumber), }), [organizationId], ); const organizationState = useSubscription(subscriptionManager, "organization", { organizationId }); const organizationRepos = organizationState.data?.repos ?? []; const taskSummaries = organizationState.data?.taskSummaries ?? []; const openPullRequests = organizationState.data?.openPullRequests ?? []; const openPullRequestsByTaskId = useMemo( () => new Map(openPullRequests.map((pullRequest) => [openPrTaskId(pullRequest.prId), pullRequest])), [openPullRequests], ); const selectedOpenPullRequest = useMemo( () => (selectedTaskId ? (openPullRequestsByTaskId.get(selectedTaskId) ?? null) : null), [openPullRequestsByTaskId, selectedTaskId], ); const selectedTaskSummary = useMemo( () => taskSummaries.find((task) => task.id === selectedTaskId) ?? taskSummaries[0] ?? null, [selectedTaskId, taskSummaries], ); const taskState = useSubscription( subscriptionManager, "task", selectedTaskSummary ? { organizationId, repoId: selectedTaskSummary.repoId, taskId: selectedTaskSummary.id, } : null, ); const sessionState = useSubscription( subscriptionManager, "session", selectedTaskSummary && selectedSessionId ? { organizationId, repoId: selectedTaskSummary.repoId, taskId: selectedTaskSummary.id, sessionId: selectedSessionId, } : null, ); const activeSandbox = useMemo(() => { if (!taskState.data?.activeSandboxId) return null; return taskState.data.sandboxes?.find((s) => s.sandboxId === taskState.data!.activeSandboxId) ?? null; }, [taskState.data?.activeSandboxId, taskState.data?.sandboxes]); const sandboxState = useSubscription( subscriptionManager, "sandboxProcesses", activeSandbox ? { organizationId, sandboxProviderId: activeSandbox.sandboxProviderId, sandboxId: activeSandbox.sandboxId, } : null, ); const hasSandbox = Boolean(activeSandbox) && sandboxState.status !== "error"; const tasks = useMemo(() => { const sessionCache = new Map(); if (selectedTaskSummary && taskState.data) { for (const session of taskState.data.sessionsSummary) { const cached = (selectedSessionId && session.id === selectedSessionId ? sessionState.data : undefined) ?? subscriptionManager.getSnapshot("session", { organizationId, repoId: selectedTaskSummary.repoId, taskId: selectedTaskSummary.id, sessionId: session.id, }); if (cached) { sessionCache.set(session.id, { draft: cached.draft, transcript: cached.transcript, }); } } } const hydratedTasks = taskSummaries.map((summary) => summary.id === selectedTaskSummary?.id ? toTaskModel(summary, taskState.data, sessionCache) : toTaskModel(summary), ); const openPrTasks = openPullRequests.map((pullRequest) => toOpenPrTaskModel(pullRequest)); return [...hydratedTasks, ...openPrTasks].sort((left, right) => right.updatedAtMs - left.updatedAtMs); }, [openPullRequests, selectedTaskSummary, selectedSessionId, sessionState.data, taskState.data, taskSummaries, organizationId]); const rawRepositories = useMemo(() => groupRepositories(organizationRepos, tasks), [tasks, organizationRepos]); const appSnapshot = useMockAppSnapshot(); const activeOrg = activeMockOrganization(appSnapshot); const navigateToUsage = useCallback(() => { if (activeOrg) { void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: activeOrg.id } as never }); } }, [activeOrg, navigate]); const [repositoryOrder, setRepositoryOrder] = useState(null); const repositories = useMemo(() => { if (!repositoryOrder) return rawRepositories; const byId = new Map(rawRepositories.map((p) => [p.id, p])); const ordered = repositoryOrder.map((id) => byId.get(id)).filter(Boolean) as typeof rawRepositories; for (const p of rawRepositories) { if (!repositoryOrder.includes(p.id)) ordered.push(p); } return ordered; }, [rawRepositories, repositoryOrder]); const [activeSessionIdByTask, setActiveSessionIdByTask] = useState>({}); const [lastAgentSessionIdByTask, setLastAgentSessionIdByTask] = useState>({}); const [openDiffsByTask, setOpenDiffsByTask] = useState>({}); const [selectedNewTaskRepoId, setSelectedNewTaskRepoId] = useState(""); const [leftWidth, setLeftWidth] = useState(() => readStoredWidth(LEFT_WIDTH_STORAGE_KEY, LEFT_SIDEBAR_DEFAULT_WIDTH)); const [rightWidth, setRightWidth] = useState(() => readStoredWidth(RIGHT_WIDTH_STORAGE_KEY, RIGHT_SIDEBAR_DEFAULT_WIDTH)); const leftWidthRef = useRef(leftWidth); const rightWidthRef = useRef(rightWidth); const autoCreatingSessionForTaskRef = useRef>(new Set()); const resolvingOpenPullRequestsRef = useRef>(new Set()); const [leftSidebarOpen, setLeftSidebarOpen] = useState(true); const [rightSidebarOpen, setRightSidebarOpen] = useState(true); const [leftSidebarPeeking, setLeftSidebarPeeking] = useState(false); const [materializingOpenPrId, setMaterializingOpenPrId] = useState(null); const showDevPanel = useDevPanel(); const peekTimeoutRef = useRef | null>(null); const startPeek = useCallback(() => { if (peekTimeoutRef.current) clearTimeout(peekTimeoutRef.current); setLeftSidebarPeeking(true); }, []); const endPeek = useCallback(() => { peekTimeoutRef.current = setTimeout(() => setLeftSidebarPeeking(false), 200); }, []); const reorderRepositories = useCallback( (fromIndex: number, toIndex: number) => { const ids = repositories.map((p) => p.id); const [moved] = ids.splice(fromIndex, 1); ids.splice(toIndex, 0, moved!); setRepositoryOrder(ids); }, [repositories], ); const [taskOrderByRepository, setTaskOrderByRepository] = useState>({}); const reorderTasks = useCallback( (repositoryId: string, fromIndex: number, toIndex: number) => { const repository = repositories.find((p) => p.id === repositoryId); if (!repository) return; const currentOrder = taskOrderByRepository[repositoryId] ?? repository.tasks.map((t) => t.id); const ids = [...currentOrder]; const [moved] = ids.splice(fromIndex, 1); ids.splice(toIndex, 0, moved!); setTaskOrderByRepository((prev) => ({ ...prev, [repositoryId]: ids })); }, [repositories, taskOrderByRepository], ); useEffect(() => { leftWidthRef.current = leftWidth; window.localStorage.setItem(LEFT_WIDTH_STORAGE_KEY, String(leftWidth)); }, [leftWidth]); useEffect(() => { rightWidthRef.current = rightWidth; window.localStorage.setItem(RIGHT_WIDTH_STORAGE_KEY, String(rightWidth)); }, [rightWidth]); const startLeftRef = useRef(leftWidth); const startRightRef = useRef(rightWidth); const onLeftResize = useCallback((deltaX: number) => { setLeftWidth(Math.min(Math.max(startLeftRef.current + deltaX, SIDEBAR_MIN_WIDTH), SIDEBAR_MAX_WIDTH)); }, []); const onLeftResizeStart = useCallback(() => { startLeftRef.current = leftWidthRef.current; }, []); const onRightResize = useCallback((deltaX: number) => { setRightWidth(Math.min(Math.max(startRightRef.current - deltaX, SIDEBAR_MIN_WIDTH), SIDEBAR_MAX_WIDTH)); }, []); const onRightResizeStart = useCallback(() => { startRightRef.current = rightWidthRef.current; }, []); const activeTask = useMemo(() => { const realTasks = tasks.filter((task) => !isOpenPrTaskId(task.id)); if (selectedOpenPullRequest) { return null; } if (selectedTaskId) { return realTasks.find((task) => task.id === selectedTaskId) ?? realTasks[0] ?? null; } return realTasks[0] ?? null; }, [selectedOpenPullRequest, selectedTaskId, tasks]); const materializeOpenPullRequest = useCallback( async (pullRequest: WorkbenchOpenPrSummary) => { if (resolvingOpenPullRequestsRef.current.has(pullRequest.prId)) { return; } resolvingOpenPullRequestsRef.current.add(pullRequest.prId); setMaterializingOpenPrId(pullRequest.prId); try { const { taskId, sessionId } = await taskWorkbenchClient.createTask({ repoId: pullRequest.repoId, task: `Continue work on GitHub PR #${pullRequest.number}: ${pullRequest.title}`, model: "gpt-5.3-codex", title: pullRequest.title, onBranch: pullRequest.headRefName, }); await navigate({ to: "/organizations/$organizationId/tasks/$taskId", params: { organizationId, taskId, }, search: { sessionId: sessionId ?? undefined }, replace: true, }); } catch (error) { setMaterializingOpenPrId((current) => (current === pullRequest.prId ? null : current)); resolvingOpenPullRequestsRef.current.delete(pullRequest.prId); logger.error( { prId: pullRequest.prId, repoId: pullRequest.repoId, branchName: pullRequest.headRefName, ...createErrorContext(error), }, "failed_to_materialize_open_pull_request_task", ); } }, [navigate, taskWorkbenchClient, organizationId], ); useEffect(() => { if (!selectedOpenPullRequest) { if (materializingOpenPrId) { resolvingOpenPullRequestsRef.current.delete(materializingOpenPrId); } setMaterializingOpenPrId(null); return; } void materializeOpenPullRequest(selectedOpenPullRequest); }, [materializeOpenPullRequest, materializingOpenPrId, selectedOpenPullRequest]); useEffect(() => { if (activeTask) { return; } if (selectedOpenPullRequest || materializingOpenPrId) { return; } const fallbackTaskId = tasks[0]?.id; if (!fallbackTaskId) { return; } const fallbackTask = tasks.find((task) => task.id === fallbackTaskId) ?? null; void navigate({ to: "/organizations/$organizationId/tasks/$taskId", params: { organizationId, taskId: fallbackTaskId, }, search: { sessionId: fallbackTask?.sessions[0]?.id ?? undefined }, replace: true, }); }, [activeTask, materializingOpenPrId, navigate, selectedOpenPullRequest, tasks, organizationId]); const openDiffs = activeTask ? sanitizeOpenDiffs(activeTask, openDiffsByTask[activeTask.id]) : []; const lastAgentSessionId = activeTask ? sanitizeLastAgentSessionId(activeTask, lastAgentSessionIdByTask[activeTask.id]) : null; const activeSessionId = activeTask ? sanitizeActiveSessionId(activeTask, activeSessionIdByTask[activeTask.id], openDiffs, lastAgentSessionId) : null; const selectedSessionHydrating = Boolean( selectedSessionId && activeSessionId === selectedSessionId && sessionState.status === "loading" && !sessionState.data, ); const syncRouteSession = useCallback( (taskId: string, sessionId: string | null, replace = false) => { void navigate({ to: "/organizations/$organizationId/tasks/$taskId", params: { organizationId, taskId, }, search: { sessionId: sessionId ?? undefined }, ...(replace ? { replace: true } : {}), }); }, [navigate, organizationId], ); useEffect(() => { if (!activeTask) { return; } const resolvedRouteSessionId = sanitizeLastAgentSessionId(activeTask, selectedSessionId); if (!resolvedRouteSessionId) { return; } if (selectedSessionId !== resolvedRouteSessionId) { syncRouteSession(activeTask.id, resolvedRouteSessionId, true); return; } if (lastAgentSessionIdByTask[activeTask.id] === resolvedRouteSessionId) { return; } setLastAgentSessionIdByTask((current) => ({ ...current, [activeTask.id]: resolvedRouteSessionId, })); setActiveSessionIdByTask((current) => { const currentActive = current[activeTask.id]; if (currentActive && isDiffTab(currentActive)) { return current; } return { ...current, [activeTask.id]: resolvedRouteSessionId, }; }); }, [activeTask, lastAgentSessionIdByTask, selectedSessionId, syncRouteSession]); useEffect(() => { if (selectedNewTaskRepoId && organizationRepos.some((repo) => repo.id === selectedNewTaskRepoId)) { return; } const fallbackRepoId = activeTask?.repoId && organizationRepos.some((repo) => repo.id === activeTask.repoId) ? activeTask.repoId : (organizationRepos[0]?.id ?? ""); if (fallbackRepoId !== selectedNewTaskRepoId) { setSelectedNewTaskRepoId(fallbackRepoId); } }, [activeTask?.repoId, selectedNewTaskRepoId, organizationRepos]); useEffect(() => { if (!activeTask) { return; } if (activeTask.sessions.length > 0) { autoCreatingSessionForTaskRef.current.delete(activeTask.id); return; } if (selectedSessionId) { return; } if (autoCreatingSessionForTaskRef.current.has(activeTask.id)) { return; } autoCreatingSessionForTaskRef.current.add(activeTask.id); void (async () => { try { const { sessionId } = await taskWorkbenchClient.addSession({ taskId: activeTask.id }); syncRouteSession(activeTask.id, sessionId, true); } catch (error) { logger.error( { taskId: activeTask.id, ...createErrorContext(error), }, "failed_to_auto_create_workbench_session", ); // Keep the guard in the set on error to prevent retry storms. // The guard is cleared when sessions appear (line above) or the task changes. } })(); }, [activeTask, selectedSessionId, syncRouteSession, taskWorkbenchClient]); const createTask = useCallback( (overrideRepoId?: string, options?: { title?: string; task?: string; branch?: string; onBranch?: string }) => { void (async () => { const repoId = overrideRepoId || selectedNewTaskRepoId; if (!repoId) { throw new Error("Cannot create a task without an available repo"); } const { taskId, sessionId } = await taskWorkbenchClient.createTask({ repoId, task: options?.task ?? "New task", model: "gpt-5.3-codex", title: options?.title ?? "New task", ...(options?.branch ? { branch: options.branch } : {}), ...(options?.onBranch ? { onBranch: options.onBranch } : {}), }); await navigate({ to: "/organizations/$organizationId/tasks/$taskId", params: { organizationId, taskId, }, search: { sessionId: sessionId ?? undefined }, }); })(); }, [navigate, selectedNewTaskRepoId, taskWorkbenchClient, organizationId], ); const openDiffTab = useCallback( (path: string) => { if (!activeTask) { throw new Error("Cannot open a diff tab without an active task"); } setOpenDiffsByTask((current) => { const existing = sanitizeOpenDiffs(activeTask, current[activeTask.id]); if (existing.includes(path)) { return current; } return { ...current, [activeTask.id]: [...existing, path], }; }); setActiveSessionIdByTask((current) => ({ ...current, [activeTask.id]: diffTabId(path), })); }, [activeTask], ); const selectTask = useCallback( (id: string) => { if (isOpenPrTaskId(id)) { const pullRequest = openPullRequestsByTaskId.get(id); if (!pullRequest) { return; } void materializeOpenPullRequest(pullRequest); return; } const task = tasks.find((candidate) => candidate.id === id) ?? null; void navigate({ to: "/organizations/$organizationId/tasks/$taskId", params: { organizationId, taskId: id, }, search: { sessionId: task?.sessions[0]?.id ?? undefined }, }); }, [materializeOpenPullRequest, navigate, openPullRequestsByTaskId, tasks, organizationId], ); const markTaskUnread = useCallback((id: string) => { void taskWorkbenchClient.markTaskUnread({ taskId: id }); }, []); const renameTask = useCallback( (id: string) => { const currentTask = tasks.find((task) => task.id === id); if (!currentTask) { throw new Error(`Unable to rename missing task ${id}`); } const nextTitle = window.prompt("Rename task", currentTask.title); if (nextTitle === null) { return; } const trimmedTitle = nextTitle.trim(); if (!trimmedTitle) { return; } void taskWorkbenchClient.renameTask({ taskId: id, value: trimmedTitle }); }, [tasks], ); const renameBranch = useCallback( (id: string) => { const currentTask = tasks.find((task) => task.id === id); if (!currentTask) { throw new Error(`Unable to rename missing task ${id}`); } const nextBranch = window.prompt("Rename branch", currentTask.branch ?? ""); if (nextBranch === null) { return; } const trimmedBranch = nextBranch.trim(); if (!trimmedBranch) { return; } void taskWorkbenchClient.renameBranch({ taskId: id, value: trimmedBranch }); }, [tasks], ); const archiveTask = useCallback(() => { if (!activeTask) { throw new Error("Cannot archive without an active task"); } void taskWorkbenchClient.archiveTask({ taskId: activeTask.id }); }, [activeTask]); const publishPr = useCallback(() => { if (!activeTask) { throw new Error("Cannot publish PR without an active task"); } void taskWorkbenchClient.publishPr({ taskId: activeTask.id }); }, [activeTask]); const revertFile = useCallback( (path: string) => { if (!activeTask) { throw new Error("Cannot revert a file without an active task"); } setOpenDiffsByTask((current) => ({ ...current, [activeTask.id]: sanitizeOpenDiffs(activeTask, current[activeTask.id]).filter((candidate) => candidate !== path), })); setActiveSessionIdByTask((current) => ({ ...current, [activeTask.id]: current[activeTask.id] === diffTabId(path) ? sanitizeLastAgentSessionId(activeTask, lastAgentSessionIdByTask[activeTask.id]) : (current[activeTask.id] ?? null), })); void taskWorkbenchClient.revertFile({ taskId: activeTask.id, path, }); }, [activeTask, lastAgentSessionIdByTask], ); const isDesktop = !!import.meta.env.VITE_DESKTOP; const onDragMouseDown = useCallback((event: ReactPointerEvent) => { if (event.button !== 0) return; // Tauri v2 IPC: invoke start_dragging on the webview window const ipc = (window as unknown as Record).__TAURI_INTERNALS__ as | { invoke: (cmd: string, args?: unknown) => Promise; } | undefined; if (ipc?.invoke) { ipc.invoke("plugin:window|start_dragging").catch(() => {}); } }, []); const dragRegion = isDesktop ? (
{/* Background drag target – sits behind interactive elements */}
) : null; const collapsedToggleClass = css({ width: "26px", height: "26px", borderRadius: "6px", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", color: t.textTertiary, position: "relative", zIndex: 9999, flexShrink: 0, ":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover }, }); const sidebarTransition = "width 200ms ease"; const contentFrameStyle: React.CSSProperties = { flex: 1, minWidth: 0, display: "flex", flexDirection: "row", overflow: "hidden", marginBottom: "8px", marginRight: "8px", marginLeft: leftSidebarOpen ? 0 : "8px", }; if (!activeTask) { const isMaterializingSelectedOpenPr = Boolean(selectedOpenPullRequest) || materializingOpenPrId != null; return ( <> {dragRegion}
void taskWorkbenchClient.reloadGithubOrganization()} onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()} onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)} onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)} onToggleSidebar={() => setLeftSidebarOpen(false)} />
{leftSidebarOpen ? : null} {!leftSidebarOpen || !rightSidebarOpen ? (
{leftSidebarOpen ? null : (
setLeftSidebarOpen(true)}>
)}
{rightSidebarOpen ? null : (
setRightSidebarOpen(true)}>
)}
) : null}
{activeOrg?.github.syncStatus === "syncing" || activeOrg?.github.syncStatus === "pending" ? ( <>

Syncing with GitHub

Importing repos from @{activeOrg.github.connectedAccount || "GitHub"}... {activeOrg.github.importedRepoCount > 0 && <> {activeOrg.github.importedRepoCount} repos imported so far.}

) : isMaterializingSelectedOpenPr && selectedOpenPullRequest ? ( <>

Creating task from pull request

Preparing a task for {selectedOpenPullRequest.title} on {selectedOpenPullRequest.headRefName}.

) : activeOrg?.github.syncStatus === "error" ? ( <>

GitHub sync failed

There was a problem syncing repos from GitHub. Check the dev panel for details.

) : ( <>

Create your first task

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

)}
{rightSidebarOpen ? : null}
{activeOrg && } {showDevPanel && ( )} ); } return ( <> {dragRegion}
void taskWorkbenchClient.reloadGithubOrganization()} onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()} onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)} onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)} onToggleSidebar={() => setLeftSidebarOpen(false)} />
{!leftSidebarOpen && leftSidebarPeeking ? ( <>
setLeftSidebarPeeking(false)} onMouseEnter={endPeek} />
{ selectTask(id); setLeftSidebarPeeking(false); }} onCreate={createTask} onSelectNewTaskRepo={setSelectedNewTaskRepoId} onMarkUnread={markTaskUnread} onRenameTask={renameTask} onRenameBranch={renameBranch} onReorderRepositories={reorderRepositories} taskOrderByRepository={taskOrderByRepository} onReorderTasks={reorderTasks} onReloadOrganization={() => void taskWorkbenchClient.reloadGithubOrganization()} onReloadPullRequests={() => void taskWorkbenchClient.reloadGithubPullRequests()} onReloadRepository={(repoId) => void taskWorkbenchClient.reloadGithubRepository(repoId)} onReloadPullRequest={(repoId, prNumber) => void taskWorkbenchClient.reloadGithubPullRequest(repoId, prNumber)} onToggleSidebar={() => { setLeftSidebarPeeking(false); setLeftSidebarOpen(true); }} />
) : null}
{leftSidebarOpen ? : null}
{ setActiveSessionIdByTask((current) => ({ ...current, [activeTask.id]: sessionId })); }} onSetLastAgentSessionId={(sessionId) => { setLastAgentSessionIdByTask((current) => ({ ...current, [activeTask.id]: sessionId })); }} onSetOpenDiffs={(paths) => { setOpenDiffsByTask((current) => ({ ...current, [activeTask.id]: paths })); }} sidebarCollapsed={!leftSidebarOpen} onToggleSidebar={() => { setLeftSidebarPeeking(false); setLeftSidebarOpen(true); }} onSidebarPeekStart={startPeek} onSidebarPeekEnd={endPeek} rightSidebarCollapsed={!rightSidebarOpen} onToggleRightSidebar={() => setRightSidebarOpen(true)} selectedSessionHydrating={selectedSessionHydrating} onNavigateToUsage={navigateToUsage} />
{rightSidebarOpen ? : null}
setRightSidebarOpen(false)} />
{activeOrg && } {showDevPanel && ( ({ id: tab.id, sessionId: tab.sessionId ?? null, sessionName: tab.sessionName ?? tab.id, agent: tab.agent, model: tab.model, status: tab.status, thinkingSinceMs: tab.thinkingSinceMs ?? null, unread: tab.unread ?? false, created: tab.created ?? false, })) ?? [], }} /> )} ); }