import { TextAttachmentContext } from "@/context/TextAttachmentContext"; import { TaskAttachmentPanel } from "./TaskAttachmentPanel"; import { useTaskBrowsers } from "@/hooks/useTaskBrowsers"; import { TaskBrowserPanel } from "./TaskBrowserPanel"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { createPortal } from "react-dom"; import { useQuery } from "@tanstack/react-query"; import { isArtifactReviewDocumentKey, type Issue, type IssueDocument, } from "@paperclipai/shared"; import { Globe, Box, FileCode2, FileText, FolderOpen, Lightbulb, ListChecks, ListTree, Plus, SlidersHorizontal, X, } from "lucide-react"; import { fileResourcesApi } from "@/api/file-resources"; import { IssueProperties } from "@/components/IssueProperties"; import { PROPERTIES_PANE_HEADER_SLOT_ID } from "@/components/PropertiesPanel"; import { WorkspaceFileBrowser } from "@/components/WorkspaceFileBrowser"; import { AgentArtifactsPanel, AgentTasksPanel } from "@/components/chat/AgentWorkPanels"; import { useAgentChatEnabled } from "@/hooks/useAgentChatEnabled"; import { IssuePropertiesArtifactsTab } from "@/components/issue-properties/IssuePropertiesArtifactsTab"; import { IssuePropertiesPlansTab } from "@/components/issue-properties/IssuePropertiesPlansTab"; import { TaskDetailSubtasksPanel } from "@/components/task-detail/TaskDetailRelationsPanel"; import { SidePanelLauncher, SidePanelToggleButton, SidePanelTabs, SidePanelMobileTabs, useScrollbarWhileScrolling, useSidePanelTabs, type SidePanelLauncherItem, type SidePanelLauncherSection, type SidePanelTabItem, type SidePanelTabRecord, } from "@/components/side-panel"; import { Button } from "@/components/ui/button"; import { FILE_VIEWER_NAVIGATE_OPTIONS, getCurrentFileViewerSearch, readBrowseStateFromSearch, readFileViewerStateFromSearch, shouldNavigateFileViewerSearch, writeBrowseStateToSearch, writeFileViewerStateToSearch, type FileViewerBrowseState, type FileViewerUrlState, } from "@/context/FileViewerContext"; import { useIssueDocuments } from "@/hooks/useIssueDocuments"; import type { IssueExternalObjectGroup } from "@/hooks/useIssueExternalObjects"; import { useIssuePlanDocument } from "@/hooks/useIssuePlanDocument"; import { documentDisplayTitle } from "@/lib/issue-artifacts"; import { queryKeys } from "@/lib/queryKeys"; import { useLocation, useNavigate } from "@/lib/router"; import { readTaskSidePanelState, taskPanelBrowserTab, taskPanelAgentTasksTab, taskPanelArtifactsTab, taskPanelAttachmentTab, taskPanelDocumentTab, taskPanelFilesTab, taskPanelPropertiesTab, taskPanelSkillTab, taskPanelSubtasksTab, taskPanelWorkspaceFileTab, writeTaskSidePanelState, type TaskSidePanelTabPayload, } from "@/lib/task-side-panel-state"; import { cn } from "@/lib/utils"; import { TaskDocumentPanel } from "./TaskDocumentPanel"; import { TaskWorkspaceFilePanel } from "./TaskWorkspaceFilePanel"; import { TaskSkillPanel } from "./TaskSkillPanel"; export interface TaskSidePanelProps { issue: Issue; openBrowserId?: string | null; onBrowserOpened?: () => void; accountScope: string; childIssues?: Issue[]; issueLinkState?: unknown; onAddSubIssue?: () => void; onUpdate: (data: Record) => void; inline?: boolean; mobile?: boolean; hasActiveRun?: boolean; externalObjects?: IssueExternalObjectGroup[]; externalObjectsLoading?: boolean; externalObjectsError?: boolean; onRetryExternalObjects?: () => void; onCheckMonitorNow?: () => void; checkingMonitorNow?: boolean; fileTabsEnabled: boolean; documentDeepLink?: { requestId: number; documentKey: string } | null; /** A new durable output asks the host to reveal this tab once. */ artifactsOpenRequestId?: number; onArtifactsOpened?: (requestId: number) => void; onRequestClose?: () => void; streamlinedTabs?: boolean; showSubtasksTab?: boolean; /** Optional related-work projection; the host still owns tab layout and state. */ tasksTab?: { count: number; content: ReactNode; hasError?: boolean }; onAttachmentOpened?: () => void; openAttachment?: { id: string; title: string; requestId: number } | null; openSkillId?: string | null; openSkillName?: string | null; onSkillOpened?: (skillId: string) => void; } const EMPTY_ISSUE_DOCUMENTS: IssueDocument[] = []; function tabIcon(tab: SidePanelTabRecord): ReactNode { switch (tab.payload.kind) { case "browser": return ; case "properties": return ; case "subtasks": return ; case "artifacts": return ; case "agent-tasks": return ; case "attachment": return ; case "files-browser": return ; case "workspace-file": return ; case "issue-document": return tab.payload.documentKey === "plan" ? : ; } } function insertSubtasksAfterProperties( tabs: SidePanelTabRecord[], ) { if (tabs.some((tab) => tab.id === "subtasks")) return tabs; const propertiesIndex = tabs.findIndex((tab) => tab.id === "properties"); const next = [...tabs]; next.splice(propertiesIndex < 0 ? 0 : propertiesIndex + 1, 0, taskPanelSubtasksTab()); return next; } function selectorForWorkspaceKind(kind: "execution_workspace" | "project_workspace") { return kind === "execution_workspace" ? "execution" as const : "project" as const; } /** * The desktop panel is rendered beside the route outlet, so task file routing * deliberately lives in this adapter instead of depending on provider ancestry. */ function useTaskSidePanelFileRouting() { const location = useLocation(); const navigate = useNavigate(); const state = useMemo(() => readFileViewerStateFromSearch(location.search), [location.search]); const browseState = useMemo(() => readBrowseStateFromSearch(location.search), [location.search]); const navigateSearch = useCallback((nextSearch: string, replace = false) => { if (!shouldNavigateFileViewerSearch(nextSearch, location.search)) return; navigate( { pathname: location.pathname, hash: location.hash, search: nextSearch }, { ...FILE_VIEWER_NAVIGATE_OPTIONS, replace, state: location.state }, ); }, [location.hash, location.pathname, location.search, location.state, navigate]); const open = useCallback((ref: FileViewerUrlState, options?: { fromBrowse?: boolean; browseState?: Partial; }) => { let nextSearch = writeFileViewerStateToSearch(location.search, ref); if (options?.fromBrowse) { const params = new URLSearchParams(nextSearch); const previous = new URLSearchParams(location.search); params.set("browse", "1"); const query = Object.prototype.hasOwnProperty.call(options.browseState ?? {}, "q") ? options.browseState?.q : previous.get("q"); const folder = Object.prototype.hasOwnProperty.call(options.browseState ?? {}, "folderPath") ? options.browseState?.folderPath : previous.get("folder"); if (query) params.set("q", query); else params.delete("q"); if (folder) params.set("folder", folder); else params.delete("folder"); nextSearch = params.toString() ? `?${params.toString()}` : ""; } navigateSearch(nextSearch); }, [location.search, navigateSearch]); const openBrowse = useCallback((options?: { q?: string }) => { const params = new URLSearchParams(location.search); params.delete("file"); params.delete("line"); params.delete("column"); params.delete("folder"); params.set("browse", "1"); if (options?.q) params.set("q", options.q); else params.delete("q"); navigateSearch(params.toString() ? `?${params.toString()}` : ""); }, [location.search, navigateSearch]); const updateBrowseState = useCallback((next: Partial) => { navigateSearch(writeBrowseStateToSearch(location.search, next), true); }, [location.search, navigateSearch]); const close = useCallback(() => { const currentSearch = getCurrentFileViewerSearch(location.search); const params = new URLSearchParams(writeFileViewerStateToSearch(currentSearch, null).replace(/^\?/, "")); params.delete("browse"); params.delete("q"); params.delete("folder"); navigateSearch(params.toString() ? `?${params.toString()}` : ""); }, [location.search, navigateSearch]); return { state, browse: browseState !== null, query: browseState?.q ?? null, folderPath: browseState?.folderPath ?? null, browseProjectId: browseState?.projectId ?? null, browseWorkspaceId: browseState?.workspaceId ?? null, open, openBrowse, updateBrowseState, close, }; } export function TaskSidePanel({ openBrowserId, onBrowserOpened, issue, accountScope, childIssues = [], issueLinkState, onAddSubIssue, onUpdate, inline = false, mobile = false, hasActiveRun = false, externalObjects, externalObjectsLoading, externalObjectsError, onRetryExternalObjects, onCheckMonitorNow, checkingMonitorNow = false, fileTabsEnabled, documentDeepLink, artifactsOpenRequestId, onArtifactsOpened, onRequestClose, streamlinedTabs = false, showSubtasksTab = false, tasksTab, openAttachment, onAttachmentOpened, openSkillId, openSkillName, onSkillOpened, }: TaskSidePanelProps) { const handleScroll = useScrollbarWhileScrolling(); // Agent Chat: agent chats swap the conversation issue's own panels for // agent-scoped ones — the agent's tasks lead, and Artifacts lists the // agent's output. With the flag off a chat keeps the issue's own panels. const { enabled: agentChatEnabled } = useAgentChatEnabled(); const conversationAgentId = agentChatEnabled ? issue.conversationAgentId ?? null : null; const showRelatedTasks = showSubtasksTab && !conversationAgentId; const viewer = useTaskSidePanelFileRouting(); const { data: documentsData } = useIssueDocuments(issue.id); const documents = documentsData ?? EMPTY_ISSUE_DOCUMENTS; const { data: planDocument } = useIssuePlanDocument(issue.id); const restoredRef = useRef( readTaskSidePanelState(accountScope, issue.companyId, issue.id, fileTabsEnabled), ); const taskCount = tasksTab?.count ?? childIssues.length; const taskLabel = tasksTab ? "Tasks" : "Subtasks"; const initialSubtasksAvailableRef = useRef(showRelatedTasks && (taskCount > 0 || tasksTab?.hasError === true)); const subtasksDismissedRef = useRef( restoredRef.current?.userInteracted === true && restoredRef.current.state.tabs.length === 0, ); const [launcherOpen, setLauncherOpen] = useState(restoredRef.current?.launcherOpen ?? false); const [paneHeaderSlot, setPaneHeaderSlot] = useState(null); const bodyRef = useRef(null); const scrollPositionsRef = useRef(new Map()); const userInteractedRef = useRef(restoredRef.current?.userInteracted ?? false); const autoPlanHandledRef = useRef(restoredRef.current?.autoPlanHandled ?? false); const handledArtifactsRequestRef = useRef(undefined); const handledDocumentRequestRef = useRef(undefined); const initialState = useMemo(() => { const restored = restoredRef.current?.state; let tabs = restored?.tabs ?? (conversationAgentId ? [taskPanelAgentTasksTab()] : issue.conversationAgentId ? [taskPanelArtifactsTab()] : [taskPanelPropertiesTab()]); // A tab saved while Agent Chat was on has nothing to render without it; // a chat left with no tabs falls back to its Agent-Chat-off default. if (!conversationAgentId) { const kept = tabs.filter((tab) => tab.payload.kind !== "agent-tasks"); if (kept.length === 0 && tabs.length > 0 && issue.conversationAgentId) tabs = [taskPanelArtifactsTab()]; else tabs = kept; } // Chats used to open on Artifacts by default; move an untouched default // onto the agent's tasks. An Artifacts tab the user chose to keep stays. if (conversationAgentId && !restoredRef.current?.userInteracted && tabs.length === 1 && tabs[0]!.id === "artifacts") { tabs = [taskPanelAgentTasksTab()]; } if (!initialSubtasksAvailableRef.current) { tabs = tabs.filter((tab) => tab.payload.kind !== "subtasks"); } else if (!subtasksDismissedRef.current) { tabs = insertSubtasksAfterProperties(tabs); } const requestedActive = restored?.activeTabId ?? "properties"; return { tabs, activeTabId: tabs.some((tab) => tab.id === requestedActive) ? requestedActive : tabs[0]?.id ?? null, }; }, []); const persist = useCallback((state: { tabs: SidePanelTabRecord[]; activeTabId: string | null }) => { writeTaskSidePanelState(accountScope, issue.companyId, issue.id, { state, launcherOpen, userInteracted: userInteractedRef.current, autoPlanHandled: autoPlanHandledRef.current, updatedAt: Date.now(), }); }, [accountScope, issue.companyId, issue.id, launcherOpen]); const controller = useSidePanelTabs({ initialState, onStateChange: persist }); const browsersQuery = useTaskBrowsers(issue.id); const newestLiveBrowser = browsersQuery.data?.findLast((browser) => browser.status === "running" || browser.status === "idle"); useEffect(() => { if (!openBrowserId) return; setLauncherOpen(false); controller.openTab(taskPanelBrowserTab(openBrowserId)); }, [openBrowserId, controller.openTab]); useEffect(() => { // Acknowledge only after selection and persistence commit. Clearing the // parent request earlier can replace the panel before its tab state lands. if (openBrowserId && controller.activeTabId === `browser:${openBrowserId}` && readTaskSidePanelState(accountScope, issue.companyId, issue.id, fileTabsEnabled)?.state.activeTabId === controller.activeTabId) { onBrowserOpened?.(); } }, [openBrowserId, controller.activeTabId, onBrowserOpened, accountScope, issue.companyId, issue.id, fileTabsEnabled]); const activeTab = controller.tabs.find((tab) => tab.id === controller.activeTabId) ?? null; const subtasksAvailable = showRelatedTasks && (taskCount > 0 || tasksTab?.hasError === true); const hasSubtasksTab = controller.tabs.some((tab) => tab.id === "subtasks"); useEffect(() => { if (!openSkillId) return; setLauncherOpen(false); controller.openTab(taskPanelSkillTab(openSkillId, openSkillName ?? "Skill")); onSkillOpened?.(openSkillId); }, [controller.openTab, onSkillOpened, openSkillId, openSkillName]); useEffect(() => { if (!subtasksAvailable) { subtasksDismissedRef.current = false; if (hasSubtasksTab) controller.closeTab("subtasks"); return; } if (!hasSubtasksTab && !subtasksDismissedRef.current) { controller.resetTabs({ tabs: insertSubtasksAfterProperties(controller.tabs), activeTabId: controller.activeTabId, }); } }, [ controller.activeTabId, controller.closeTab, controller.resetTabs, controller.tabs, hasSubtasksTab, subtasksAvailable, ]); useEffect(() => { if (inline) { setPaneHeaderSlot(null); return; } setPaneHeaderSlot(document.getElementById(PROPERTIES_PANE_HEADER_SLOT_ID)); }, [inline]); // Older clients persisted an empty Plan tab merely because the task was in // planning mode. Remove that stale tab once the plan lookup confirms there // is no document; a real plan will be opened by the materialization effect // below when it arrives. useEffect(() => { if (planDocument !== null || !controller.tabs.some((tab) => tab.id === "document:plan")) return; autoPlanHandledRef.current = false; controller.closeTab("document:plan"); }, [controller.closeTab, controller.tabs, planDocument]); // Surface a newly materialized plan exactly once until the user takes manual // control of the tab set. Persisted empty/custom states therefore stay put. useEffect(() => { if (!planDocument || autoPlanHandledRef.current || userInteractedRef.current) return; autoPlanHandledRef.current = true; controller.openTab(taskPanelDocumentTab("plan", documentDisplayTitle(planDocument))); }, [controller.openTab, planDocument]); useEffect(() => { if (!documentDeepLink || handledDocumentRequestRef.current === documentDeepLink.requestId) return; if ( documentDeepLink.documentKey === "plan" && planDocument === null ) return; const document = documents.find((candidate) => candidate.key === documentDeepLink.documentKey); const label = document ? documentDisplayTitle(document) : documentDeepLink.documentKey === "plan" ? "Plan" : documentDeepLink.documentKey; // A refresh must not replay a link after the user selects another tab. handledDocumentRequestRef.current = documentDeepLink.requestId; controller.openTab(taskPanelDocumentTab(documentDeepLink.documentKey, label)); }, [controller.openTab, documentDeepLink, documents, planDocument]); // Existing URL-backed workspace links remain the external integration API. useEffect(() => { if (!fileTabsEnabled) return; if (viewer.state) { controller.openTab(taskPanelWorkspaceFileTab(viewer.state)); } else if (viewer.browse) { controller.openTab(taskPanelFilesTab()); controller.updateTab("files", { payload: { kind: "files-browser", query: viewer.query, folderPath: viewer.folderPath, projectId: viewer.browseProjectId, workspaceId: viewer.browseWorkspaceId, }, }); } }, [ controller.openTab, controller.updateTab, fileTabsEnabled, viewer.browse, viewer.browseProjectId, viewer.browseWorkspaceId, viewer.folderPath, viewer.query, viewer.state, ]); useEffect(() => { if (artifactsOpenRequestId === undefined || handledArtifactsRequestRef.current === artifactsOpenRequestId) return; handledArtifactsRequestRef.current = artifactsOpenRequestId; // Background outputs add a discoverable tab without interrupting the // current document, file, or launcher. Opening the pane is a user action. controller.openTab(taskPanelArtifactsTab(), false); onArtifactsOpened?.(artifactsOpenRequestId); }, [artifactsOpenRequestId, controller.openTab, onArtifactsOpened]); useEffect(() => { if (!openAttachment) return; userInteractedRef.current = true; setLauncherOpen(false); controller.openTab(taskPanelAttachmentTab(openAttachment.id, openAttachment.title)); if (viewer.state || viewer.browse) viewer.close(); onAttachmentOpened?.(); }, [controller.openTab, openAttachment, onAttachmentOpened]); const recentFilesQuery = useQuery({ queryKey: queryKeys.issues.fileResources(issue.id, { workspace: "auto", mode: "recent", limit: 5, offset: 0, }), queryFn: () => fileResourcesApi.list(issue.id, { workspace: "auto", mode: "recent", limit: 5, offset: 0 }), enabled: fileTabsEnabled && (launcherOpen || !activeTab), retry: false, staleTime: 15_000, }); function markInteracted() { userInteractedRef.current = true; } function captureScroll() { if (controller.activeTabId && bodyRef.current) { scrollPositionsRef.current.set(controller.activeTabId, bodyRef.current.scrollTop); } } function selectTab(tabId: string) { captureScroll(); markInteracted(); controller.selectTab(tabId); const tab = controller.tabs.find((candidate) => candidate.id === tabId); if (!tab) return; if (tab.payload.kind === "workspace-file") { viewer.open(tab.payload); } else if (tab.payload.kind === "files-browser") { viewer.openBrowse({ q: tab.payload.query ?? undefined }); } else if (viewer.state || viewer.browse) { viewer.close(); } } useEffect(() => { const top = controller.activeTabId ? scrollPositionsRef.current.get(controller.activeTabId) ?? 0 : 0; window.requestAnimationFrame(() => bodyRef.current?.scrollTo({ top, behavior: "auto" })); }, [controller.activeTabId]); useEffect(() => { if (activeTab) return; window.requestAnimationFrame(() => { bodyRef.current?.querySelector('input[aria-label="Search tabs and resources…"]')?.focus(); }); }, [activeTab]); function closeTab(tabId: string) { markInteracted(); if (tabId === "subtasks") subtasksDismissedRef.current = true; const tab = controller.tabs.find((candidate) => candidate.id === tabId); controller.closeTab(tabId); if (tabId === controller.activeTabId && (tab?.payload.kind === "workspace-file" || tab?.payload.kind === "files-browser")) { viewer.close(); } } function openDocument(document: Pick) { markInteracted(); controller.openTab(taskPanelDocumentTab(document.key, documentDisplayTitle(document))); } function openWorkspaceFile(ref: { path: string; workspace: "auto" | "execution" | "project"; line?: number | null; column?: number | null; projectId?: string | null; workspaceId?: string | null; }) { markInteracted(); const tab = taskPanelWorkspaceFileTab(ref); controller.openTab(tab); viewer?.open({ ...ref, projectId: ref.projectId ?? null, workspaceId: ref.workspaceId ?? null, line: ref.line ?? null, column: ref.column ?? null, }, { fromBrowse: true }); } const documentByKey = useMemo(() => new Map(documents.map((document) => [document.key, document])), [documents]); const visualTabs = useMemo(() => controller.tabs.map((tab) => { const document = tab.payload.kind === "issue-document" ? documentByKey.get(tab.payload.documentKey) : null; const browserIndex = browsersQuery.data?.findIndex((browser) => tab.payload.kind === "browser" && (browser.id === tab.payload.browserId || browser.sessionId === tab.payload.browserId)) ?? -1; const browserLabel = browserIndex >= 0 && (browsersQuery.data?.length ?? 0) > 1 ? `Browser ${browserIndex + 1}` : null; return { id: tab.id, type: tab.type, label: browserLabel ?? (tab.payload.kind === "subtasks" && tasksTab ? "Tasks" : document ? documentDisplayTitle(document) : tab.label), ariaLabel: tab.payload.kind === "subtasks" ? taskLabel : tab.ariaLabel, closable: true, contentMode: tab.contentMode, icon: tabIcon(tab), }; }), [controller.tabs, documentByKey, taskCount, taskLabel, tasksTab, browsersQuery.data]); const launcherSections = useMemo(() => { const primary: SidePanelLauncherItem[] = conversationAgentId ? [ { id: "agent-tasks", label: "Tasks", description: "Tasks this agent worked on", icon: , alreadyOpen: controller.tabs.some((tab) => tab.id === "agent-tasks") }, { id: "artifacts", label: "Artifacts", description: "Files this agent produced", icon: , alreadyOpen: controller.tabs.some((tab) => tab.id === "artifacts") }, { id: "properties", label: "Properties", icon: , alreadyOpen: controller.tabs.some((tab) => tab.id === "properties") }, ] : [ { id: "properties", label: "Properties", icon: , alreadyOpen: controller.tabs.some((tab) => tab.id === "properties") }, ...(subtasksAvailable ? [{ id: "subtasks", label: taskLabel, description: tasksTab?.hasError ? "Could not load all tasks" : `${taskCount} total`, icon: , alreadyOpen: controller.tabs.some((tab) => tab.id === "subtasks") }] : []), { id: "artifacts", label: "Artifacts", icon: , alreadyOpen: controller.tabs.some((tab) => tab.id === "artifacts") }, ]; for (const [index, browser] of (browsersQuery.data ?? []).entries()) { primary.push({ id: `browser:${browser.id}`, label: (browsersQuery.data?.length ?? 0) > 1 ? `Browser ${index + 1}` : "Browser", description: browser.status, icon: , alreadyOpen: controller.tabs.some(tab => tab.id === `browser:${browser.id}`) }); } if (fileTabsEnabled) { primary.push({ id: "files", label: "Files", icon: , shortcut: "G F", alreadyOpen: controller.tabs.some((tab) => tab.id === "files") }); } const documentItems: SidePanelLauncherItem[] = [ ...(planDocument ? [{ id: "document:plan", label: documentDisplayTitle(planDocument), description: `Revision ${planDocument.latestRevisionNumber ?? 1}`, icon: , alreadyOpen: controller.tabs.some((tab) => tab.id === "document:plan"), }] : []), ...documents .filter((document) => document.key !== "plan" && !isArtifactReviewDocumentKey(document.key)) .sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()) .map((document) => ({ id: `document:${document.key}`, label: documentDisplayTitle(document), description: `Revision ${document.latestRevisionNumber ?? 1}`, icon: , alreadyOpen: controller.tabs.some((tab) => tab.id === `document:${document.key}`), })), ]; const sections: SidePanelLauncherSection[] = [ { id: "open", label: "Open", items: primary }, ]; if (documentItems.length > 0) { sections.push({ id: "documents", label: "Task documents", items: documentItems }); } if (fileTabsEnabled) { const recentItems = recentFilesQuery.data?.state === "available" ? recentFilesQuery.data.items.filter((item) => item.kind === "file").slice(0, 5).map((item) => ({ id: `recent-file:${item.workspaceId}:${item.relativePath}`, label: item.title, description: item.displayPath, searchText: item.relativePath, icon: , })) : []; sections.push({ id: "recent-files", label: "Recent workspace files", items: recentItems, loading: recentFilesQuery.isLoading, error: recentFilesQuery.isError ? "Recent files are temporarily unavailable." : null, }); } return sections; }, [browsersQuery.data, conversationAgentId, taskCount, taskLabel, tasksTab?.hasError, controller.tabs, documents, fileTabsEnabled, planDocument, recentFilesQuery.data, recentFilesQuery.isError, recentFilesQuery.isLoading, subtasksAvailable]); function selectLauncherItem(item: SidePanelLauncherItem) { markInteracted(); if (item.id.startsWith("browser:")) controller.openTab(taskPanelBrowserTab(item.id.slice(8))); else if (item.id === "properties") controller.openTab(taskPanelPropertiesTab()); else if (item.id === "subtasks") { subtasksDismissedRef.current = false; controller.resetTabs({ tabs: insertSubtasksAfterProperties(controller.tabs), activeTabId: "subtasks", }); } else if (item.id === "artifacts") controller.openTab(taskPanelArtifactsTab()); else if (item.id === "agent-tasks") controller.openTab(taskPanelAgentTasksTab()); else if (item.id === "files") { controller.openTab(taskPanelFilesTab()); viewer.openBrowse(); } else if (item.id.startsWith("document:")) { const key = item.id.slice("document:".length); controller.openTab(taskPanelDocumentTab(key, item.label)); } else if (item.id.startsWith("recent-file:") && recentFilesQuery.data?.state === "available") { const recent = recentFilesQuery.data.items.find((candidate) => item.id === `recent-file:${candidate.workspaceId}:${candidate.relativePath}`, ); if (recent?.kind === "file") { openWorkspaceFile({ path: recent.relativePath, workspace: selectorForWorkspaceKind(recent.workspaceKind), projectId: recent.projectId ?? null, workspaceId: recent.workspaceId, }); } } } const launcherControl = ( )} /> ); const tabStrip = mobile ? ( ) : ( { markInteracted(); controller.reorderTabs(ordered); }} addControl={launcherControl} appearance={streamlinedTabs ? "streamlined-task" : "default"} /> ); let content: ReactNode; if (!activeTab) { content = ; } else if (activeTab.payload.kind === "browser") { content = null; // Mounted below independently of the selected tab. } else if (activeTab.payload.kind === "properties") { content = ( ); } else if (activeTab.payload.kind === "subtasks") { content = tasksTab?.content ?? ( ); } else if (activeTab.payload.kind === "agent-tasks") { content = conversationAgentId ? ( ) : null; } else if (activeTab.payload.kind === "artifacts" && conversationAgentId) { content = ; } else if (activeTab.payload.kind === "artifacts") { content = { markInteracted(); controller.openTab(taskPanelAttachmentTab(id, title)); if (viewer.state || viewer.browse) viewer.close(); }}>; } else if (activeTab.payload.kind === "issue-document") { content = activeTab.payload.documentKey === "plan" ? ( ) : ( ); } else if (activeTab.payload.kind === "attachment") { content = ; } else if (activeTab.payload.kind === "skill") { content = ; } else if (activeTab.payload.kind === "files-browser") { content = ( { controller.updateTab(activeTab.id, { payload: { kind: "files-browser", query: next.q, folderPath: next.folderPath, projectId: next.projectId, workspaceId: next.workspaceId, }, }); viewer.updateBrowseState(next); }} initialQuery={activeTab.payload.query} initialFolderPath={activeTab.payload.folderPath} initialProjectId={activeTab.payload.projectId} initialWorkspaceId={activeTab.payload.workspaceId} active className="h-full min-h-0 p-3" /> ); } else { const filePayload = activeTab.payload; content = ( openWorkspaceFile({ ...filePayload, workspace: "project" }) : undefined} /> ); } const contentMode = activeTab?.contentMode ?? "full-bleed"; return (
{paneHeaderSlot ? createPortal(tabStrip, paneHeaderSlot) : (
{tabStrip} {onRequestClose ? ( mobile ? ( ) : ) : null}
)}
{controller.tabs.map(tab => tab.payload.kind === "browser" ? ( ) : null)} {contentMode === "prose" ? (
{content}
) : content}
); }