import { For, Show, batch, createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor, type Component, } from "solid-js" import type { ToolState } from "@opencode-ai/sdk" import { Accordion } from "@kobalte/core" import { ChevronDown } from "lucide-solid" import AppBar from "@suid/material/AppBar" import Box from "@suid/material/Box" import Divider from "@suid/material/Divider" import Drawer from "@suid/material/Drawer" import IconButton from "@suid/material/IconButton" import Toolbar from "@suid/material/Toolbar" import Typography from "@suid/material/Typography" import useMediaQuery from "@suid/material/useMediaQuery" import CloseIcon from "@suid/icons-material/Close" import MenuIcon from "@suid/icons-material/Menu" import MenuOpenIcon from "@suid/icons-material/MenuOpen" import PushPinIcon from "@suid/icons-material/PushPin" import PushPinOutlinedIcon from "@suid/icons-material/PushPinOutlined" import type { Instance } from "../../types/instance" import type { Command } from "../../lib/commands" import { activeParentSessionId, activeSessionId as activeSessionMap, getSessionFamily, getSessionInfo, sessions, setActiveSession, executeCustomCommand, runShellCommand, } from "../../stores/sessions" import { keyboardRegistry, type KeyboardShortcut } from "../../lib/keyboard-registry" import { messageStoreBus } from "../../stores/message-v2/bus" import { clearSessionRenderCache } from "../message-block" import { buildCustomCommandEntries } from "../../lib/command-utils" import { getCommands as getInstanceCommands } from "../../stores/commands" import { isOpen as isCommandPaletteOpen, hideCommandPalette, showCommandPalette } from "../../stores/command-palette" import SessionList from "../session-list" import KeyboardHint from "../keyboard-hint" import InstanceWelcomeView from "../instance-welcome-view" import InfoView from "../info-view" import InstanceServiceStatus from "../instance-service-status" import AgentSelector from "../agent-selector" import ModelSelector from "../model-selector" import ModelStatusSelector from "../model-status-selector" import CommandPalette from "../command-palette" import Kbd from "../kbd" import MultiTaskChat from "../chat/multi-task-chat" import { TodoListView } from "../tool-call/renderers/todo" import ContextUsagePanel from "../session/context-usage-panel" import SessionView from "../session/session-view" import { Sidebar, type FileNode } from "./sidebar" import { Editor } from "./editor" import { serverApi } from "../../lib/api-client" import { Sparkles, Layout as LayoutIcon, Terminal as TerminalIcon, Search, Loader2, Zap, Shield, Settings } from "lucide-solid" import { formatTokenTotal } from "../../lib/formatters" import { sseManager } from "../../lib/sse-manager" import { getLogger } from "../../lib/logger" import AdvancedSettingsModal from "../advanced-settings-modal" import { showConfirmDialog } from "../../stores/alerts" import { getSoloState, toggleAutonomous, toggleAutoApproval, } from "../../stores/solo-store" import { SESSION_SIDEBAR_EVENT, type SessionSidebarRequestAction, type SessionSidebarRequestDetail, } from "../../lib/session-sidebar-events" const log = getLogger("session") interface InstanceShellProps { instance: Instance escapeInDebounce: boolean paletteCommands: Accessor onCloseSession: (sessionId: string) => Promise | void onNewSession: () => Promise | void handleSidebarAgentChange: (sessionId: string, agent: string) => Promise handleSidebarModelChange: (sessionId: string, model: { providerId: string; modelId: string }) => Promise onExecuteCommand: (command: Command) => void tabBarOffset: number } const DEFAULT_SESSION_SIDEBAR_WIDTH = 280 const MIN_SESSION_SIDEBAR_WIDTH = 220 const MAX_SESSION_SIDEBAR_WIDTH = 360 const RIGHT_DRAWER_WIDTH = 260 const MIN_RIGHT_DRAWER_WIDTH = 200 const MAX_RIGHT_DRAWER_WIDTH = 380 const SESSION_CACHE_LIMIT = 2 const APP_BAR_HEIGHT = 56 const LEFT_DRAWER_STORAGE_KEY = "opencode-session-sidebar-width-v8" const RIGHT_DRAWER_STORAGE_KEY = "opencode-session-right-drawer-width-v1" const LEFT_PIN_STORAGE_KEY = "opencode-session-left-drawer-pinned-v1" const RIGHT_PIN_STORAGE_KEY = "opencode-session-right-drawer-pinned-v1" const BUILD_PREVIEW_EVENT = "opencode:build-preview" type LayoutMode = "desktop" | "tablet" | "phone" const clampWidth = (value: number) => Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value)) const clampRightWidth = (value: number) => Math.min(MAX_RIGHT_DRAWER_WIDTH, Math.max(MIN_RIGHT_DRAWER_WIDTH, value)) const getPinStorageKey = (side: "left" | "right") => (side === "left" ? LEFT_PIN_STORAGE_KEY : RIGHT_PIN_STORAGE_KEY) function readStoredPinState(side: "left" | "right", defaultValue: boolean) { if (typeof window === "undefined") return defaultValue const stored = window.localStorage.getItem(getPinStorageKey(side)) if (stored === "true") return true if (stored === "false") return false return defaultValue } function persistPinState(side: "left" | "right", value: boolean) { if (typeof window === "undefined") return window.localStorage.setItem(getPinStorageKey(side), value ? "true" : "false") } const InstanceShell2: Component = (props) => { const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH) const [rightDrawerWidth, setRightDrawerWidth] = createSignal(RIGHT_DRAWER_WIDTH) const [leftPinned, setLeftPinned] = createSignal(true) const [leftOpen, setLeftOpen] = createSignal(true) const [rightPinned, setRightPinned] = createSignal(true) const [rightOpen, setRightOpen] = createSignal(true) const [cachedSessionIds, setCachedSessionIds] = createSignal([]) const [pendingEvictions, setPendingEvictions] = createSignal([]) const [drawerHost, setDrawerHost] = createSignal(null) const [floatingDrawerTop, setFloatingDrawerTop] = createSignal(0) const [floatingDrawerHeight, setFloatingDrawerHeight] = createSignal(0) const [leftDrawerContentEl, setLeftDrawerContentEl] = createSignal(null) const [rightDrawerContentEl, setRightDrawerContentEl] = createSignal(null) const [leftToggleButtonEl, setLeftToggleButtonEl] = createSignal(null) const [rightToggleButtonEl, setRightToggleButtonEl] = createSignal(null) const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | "chat" | "terminal" | null>(null) const [resizeStartX, setResizeStartX] = createSignal(0) const [resizeStartWidth, setResizeStartWidth] = createSignal(0) const [resizeStartY, setResizeStartY] = createSignal(0) const [resizeStartHeight, setResizeStartHeight] = createSignal(0) const [chatPanelWidth, setChatPanelWidth] = createSignal(600) const [terminalPanelHeight, setTerminalPanelHeight] = createSignal(200) const [terminalOpen, setTerminalOpen] = createSignal(false) const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal(["lsp", "mcp", "plan"]) const [currentFile, setCurrentFile] = createSignal(null) const [centerTab, setCenterTab] = createSignal<"code" | "preview">("code") const [previewUrl, setPreviewUrl] = createSignal(null) const [isSoloOpen, setIsSoloOpen] = createSignal(true) const [showAdvancedSettings, setShowAdvancedSettings] = createSignal(false) const [selectedBinary, setSelectedBinary] = createSignal("opencode") // Handler to load file content when selected const handleFileSelect = async (file: FileNode) => { try { const response = await serverApi.readWorkspaceFile(props.instance.id, file.path) const language = file.name.split('.').pop() || 'text' setCurrentFile({ ...file, content: response.contents, language, }) } catch (error) { log.error('Failed to read file content', error) // Still show the file but without content setCurrentFile(file) } } const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instance.id)) const desktopQuery = useMediaQuery("(min-width: 1280px)") const tabletQuery = useMediaQuery("(min-width: 768px)") const layoutMode = createMemo(() => { if (desktopQuery()) return "desktop" if (tabletQuery()) return "tablet" return "phone" }) const isPhoneLayout = createMemo(() => layoutMode() === "phone") const leftPinningSupported = createMemo(() => layoutMode() === "desktop") const rightPinningSupported = createMemo(() => layoutMode() !== "phone") const persistPinIfSupported = (side: "left" | "right", value: boolean) => { if (side === "left" && !leftPinningSupported()) return if (side === "right" && !rightPinningSupported()) return persistPinState(side, value) } createEffect(() => { switch (layoutMode()) { case "desktop": { const leftSaved = readStoredPinState("left", true) const rightSaved = readStoredPinState("right", true) setLeftPinned(leftSaved) setLeftOpen(leftSaved) setRightPinned(rightSaved) setRightOpen(rightSaved) break } case "tablet": { const rightSaved = readStoredPinState("right", true) setLeftPinned(false) setLeftOpen(false) setRightPinned(rightSaved) setRightOpen(rightSaved) break } default: setLeftPinned(false) setLeftOpen(false) setRightPinned(false) setRightOpen(false) break } }) const measureDrawerHost = () => { if (typeof window === "undefined") return const host = drawerHost() if (!host) return const rect = host.getBoundingClientRect() const toolbar = host.querySelector(".session-toolbar") const toolbarHeight = toolbar?.offsetHeight ?? APP_BAR_HEIGHT setFloatingDrawerTop(rect.top + toolbarHeight) setFloatingDrawerHeight(Math.max(0, rect.height - toolbarHeight)) } onMount(() => { if (typeof window === "undefined") return const savedLeft = window.localStorage.getItem(LEFT_DRAWER_STORAGE_KEY) if (savedLeft) { const parsed = Number.parseInt(savedLeft, 10) if (Number.isFinite(parsed)) { setSessionSidebarWidth(clampWidth(parsed)) } } const savedRight = window.localStorage.getItem(RIGHT_DRAWER_STORAGE_KEY) if (savedRight) { const parsed = Number.parseInt(savedRight, 10) if (Number.isFinite(parsed)) { setRightDrawerWidth(clampRightWidth(parsed)) } } const handleResize = () => { const width = clampWidth(window.innerWidth * 0.3) setSessionSidebarWidth((current) => clampWidth(current || width)) measureDrawerHost() } handleResize() window.addEventListener("resize", handleResize) onCleanup(() => window.removeEventListener("resize", handleResize)) }) onMount(() => { if (typeof window === "undefined") return const handler = (event: Event) => { const detail = (event as CustomEvent).detail if (!detail || detail.instanceId !== props.instance.id) return handleSidebarRequest(detail.action) } window.addEventListener(SESSION_SIDEBAR_EVENT, handler) onCleanup(() => window.removeEventListener(SESSION_SIDEBAR_EVENT, handler)) }) onMount(() => { if (typeof window === "undefined") return const handler = (event: Event) => { const detail = (event as CustomEvent<{ instanceId: string; sessionId: string }>).detail if (!detail || detail.instanceId !== props.instance.id) return setShowAdvancedSettings(true) } window.addEventListener("open-advanced-settings", handler) onCleanup(() => window.removeEventListener("open-advanced-settings", handler)) }) onMount(() => { if (typeof window === "undefined") return const handler = async (event: Event) => { const detail = (event as CustomEvent<{ url?: string; instanceId?: string }>).detail if (!detail || detail.instanceId !== props.instance.id || !detail.url) return setPreviewUrl(detail.url) const confirmed = await showConfirmDialog(`Preview available at ${detail.url}. Open now?`, { title: "Preview ready", confirmLabel: "Open preview", cancelLabel: "Later", }) if (confirmed) { setCenterTab("preview") } } window.addEventListener(BUILD_PREVIEW_EVENT, handler) onCleanup(() => window.removeEventListener(BUILD_PREVIEW_EVENT, handler)) }) createEffect(() => { if (typeof window === "undefined") return window.localStorage.setItem(LEFT_DRAWER_STORAGE_KEY, sessionSidebarWidth().toString()) }) createEffect(() => { if (typeof window === "undefined") return window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString()) }) createEffect(() => { props.tabBarOffset requestAnimationFrame(() => measureDrawerHost()) }) const activeSessions = createMemo(() => { const parentId = activeParentSessionId().get(props.instance.id) if (!parentId) return new Map[number]>() const sessionFamily = getSessionFamily(props.instance.id, parentId) return new Map(sessionFamily.map((s) => [s.id, s])) }) const activeSessionIdForInstance = createMemo(() => { return activeSessionMap().get(props.instance.id) || null }) const parentSessionIdForInstance = createMemo(() => { return activeParentSessionId().get(props.instance.id) || null }) const activeSessionForInstance = createMemo(() => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") return null const instanceSessions = sessions().get(props.instance.id) return instanceSessions?.get(sessionId) ?? null }) const activeSessionUsage = createMemo(() => { const sessionId = activeSessionIdForInstance() if (!sessionId) return null const store = messageStore() return store?.getSessionUsage(sessionId) ?? null }) const activeSessionInfoDetails = createMemo(() => { const sessionId = activeSessionIdForInstance() if (!sessionId) return null return getSessionInfo(props.instance.id, sessionId) ?? null }) const tokenStats = createMemo(() => { const usage = activeSessionUsage() const info = activeSessionInfoDetails() return { used: usage?.actualUsageTokens ?? info?.actualUsageTokens ?? 0, avail: info?.contextAvailableTokens ?? null, } }) const latestTodoSnapshot = createMemo(() => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") return null const store = messageStore() if (!store) return null const snapshot = store.state.latestTodos[sessionId] return snapshot ?? null }) const latestTodoState = createMemo(() => { const snapshot = latestTodoSnapshot() if (!snapshot) return null const store = messageStore() if (!store) return null const message = store.getMessage(snapshot.messageId) if (!message) return null const partRecord = message.parts?.[snapshot.partId] const part = partRecord?.data as { type?: string; tool?: string; state?: ToolState } if (!part || part.type !== "tool" || part.tool !== "todowrite") return null const state = part.state if (!state || state.status !== "completed") return null return state }) const connectionStatus = () => sseManager.getStatus(props.instance.id) const connectionStatusClass = () => { const status = connectionStatus() if (status === "connecting") return "connecting" if (status === "connected") return "connected" return "disconnected" } const handleCommandPaletteClick = () => { showCommandPalette(props.instance.id) } const [isFixing, setIsFixing] = createSignal(false) const [isBuilding, setIsBuilding] = createSignal(false) const handleSmartFix = async () => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info" || isFixing()) { return } setIsFixing(true) try { // Smart Fix targets the active task if available, otherwise general fix const session = activeSessionForInstance() const activeTaskId = session?.activeTaskId const args = activeTaskId ? `task:${activeTaskId}` : "" await executeCustomCommand(props.instance.id, sessionId, "fix", args) // Auto-open right panel to show agent progress if it's not open if (!rightOpen()) { setRightOpen(true) measureDrawerHost() } } catch (error) { log.error("Failed to run Smart Fix command", error) } finally { setTimeout(() => setIsFixing(false), 2000) // Reset after delay } } const handleBuild = async () => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info" || isBuilding()) { return } setIsBuilding(true) try { await runShellCommand(props.instance.id, sessionId, "build") // Auto-open right panel to show build logs if it's not open if (!rightOpen()) { setRightOpen(true) measureDrawerHost() } } catch (error) { log.error("Failed to run Build command", error) } finally { setTimeout(() => setIsBuilding(false), 2000) // Reset after delay } } const customCommands = createMemo(() => buildCustomCommandEntries(props.instance.id, getInstanceCommands(props.instance.id))) const instancePaletteCommands = createMemo(() => [...props.paletteCommands(), ...customCommands()]) const paletteOpen = createMemo(() => isCommandPaletteOpen(props.instance.id)) const keyboardShortcuts = createMemo(() => [keyboardRegistry.get("session-prev"), keyboardRegistry.get("session-next")].filter( (shortcut): shortcut is KeyboardShortcut => Boolean(shortcut), ), ) interface PendingSidebarAction { action: SessionSidebarRequestAction id: number } let sidebarActionId = 0 const [pendingSidebarAction, setPendingSidebarAction] = createSignal(null) const [sidebarRequestedTab, setSidebarRequestedTab] = createSignal(null) const triggerKeyboardEvent = (target: HTMLElement, options: { key: string; code: string; keyCode: number }) => { target.dispatchEvent( new KeyboardEvent("keydown", { key: options.key, code: options.code, keyCode: options.keyCode, which: options.keyCode, bubbles: true, cancelable: true, }), ) } const focusAgentSelectorControl = () => { const agentTrigger = leftDrawerContentEl()?.querySelector("[data-agent-selector]") as HTMLElement | null if (!agentTrigger) return false agentTrigger.focus() setTimeout(() => triggerKeyboardEvent(agentTrigger, { key: "Enter", code: "Enter", keyCode: 13 }), 10) return true } const focusModelSelectorControl = () => { const input = leftDrawerContentEl()?.querySelector("[data-model-selector]") if (!input) return false input.focus() setTimeout(() => triggerKeyboardEvent(input, { key: "ArrowDown", code: "ArrowDown", keyCode: 40 }), 10) return true } createEffect(() => { const pending = pendingSidebarAction() if (!pending) return const action = pending.action const contentReady = Boolean(leftDrawerContentEl()) if (!contentReady) { return } if (action === "show-session-list") { setPendingSidebarAction(null) return } const handled = action === "focus-agent-selector" ? focusAgentSelectorControl() : focusModelSelectorControl() if (handled) { setPendingSidebarAction(null) } }) const handleSidebarRequest = (action: SessionSidebarRequestAction) => { setPendingSidebarAction({ action, id: sidebarActionId++ }) if (action === "show-skills") { setSidebarRequestedTab("skills") } if (!leftPinned() && !leftOpen()) { setLeftOpen(true) measureDrawerHost() } } const closeFloatingDrawersIfAny = () => { let handled = false if (!leftPinned() && leftOpen()) { setLeftOpen(false) blurIfInside(leftDrawerContentEl()) focusTarget(leftToggleButtonEl()) handled = true } if (!rightPinned() && rightOpen()) { setRightOpen(false) blurIfInside(rightDrawerContentEl()) focusTarget(rightToggleButtonEl()) handled = true } return handled } onMount(() => { if (typeof window === "undefined") return const handleEscape = (event: KeyboardEvent) => { if (event.key !== "Escape") return if (!closeFloatingDrawersIfAny()) return event.preventDefault() event.stopPropagation() } window.addEventListener("keydown", handleEscape, true) onCleanup(() => window.removeEventListener("keydown", handleEscape, true)) }) const handleSessionSelect = (sessionId: string) => { setActiveSession(props.instance.id, sessionId) } const evictSession = (sessionId: string) => { if (!sessionId) return log.info("Evicting cached session", { instanceId: props.instance.id, sessionId }) const store = messageStoreBus.getInstance(props.instance.id) store?.clearSession(sessionId) clearSessionRenderCache(props.instance.id, sessionId) } const scheduleEvictions = (ids: string[]) => { if (!ids.length) return setPendingEvictions((current) => { const existing = new Set(current) const next = [...current] ids.forEach((id) => { if (!existing.has(id)) { next.push(id) existing.add(id) } }) return next }) } createEffect(() => { const pending = pendingEvictions() if (!pending.length) return const cached = new Set(cachedSessionIds()) const remaining: string[] = [] pending.forEach((id) => { if (cached.has(id)) { remaining.push(id) } else { evictSession(id) } }) if (remaining.length !== pending.length) { setPendingEvictions(remaining) } }) createEffect(() => { const sessionsMap = activeSessions() const parentId = parentSessionIdForInstance() const activeId = activeSessionIdForInstance() setCachedSessionIds((current) => { const next: string[] = [] const append = (id: string | null) => { if (!id || id === "info") return if (!sessionsMap.has(id)) return if (next.includes(id)) return next.push(id) } append(parentId) append(activeId) const limit = parentId ? SESSION_CACHE_LIMIT + 1 : SESSION_CACHE_LIMIT const trimmed = next.length > limit ? next.slice(0, limit) : next const trimmedSet = new Set(trimmed) const removed = current.filter((id) => !trimmedSet.has(id)) if (removed.length) { scheduleEvictions(removed) } return trimmed }) }) const showEmbeddedSidebarToggle = createMemo(() => !leftPinned() && !leftOpen()) const drawerContainer = () => { const host = drawerHost() if (host) return host if (typeof document !== "undefined") { return document.body } return undefined } const fallbackDrawerTop = () => APP_BAR_HEIGHT + props.tabBarOffset const floatingTop = () => { const measured = floatingDrawerTop() if (measured > 0) return measured return fallbackDrawerTop() } const floatingTopPx = () => `${floatingTop()}px` const floatingHeight = () => { const measured = floatingDrawerHeight() if (measured > 0) return `${measured}px` return `calc(100% - ${floatingTop()}px)` } const scheduleDrawerMeasure = () => { if (typeof window === "undefined") { measureDrawerHost() return } requestAnimationFrame(() => measureDrawerHost()) } const applyDrawerWidth = (side: "left" | "right", width: number) => { if (side === "left") { setSessionSidebarWidth(width) } else { setRightDrawerWidth(width) } scheduleDrawerMeasure() } const applyPanelSize = (type: "chat" | "terminal", size: number) => { if (type === "chat") { setChatPanelWidth(size) } else { setTerminalPanelHeight(size) } } const handlePointerMove = (clientX: number, clientY: number) => { const side = activeResizeSide() if (!side) return if (side === "left" || side === "right") { const startWidth = resizeStartWidth() const clamp = side === "left" ? clampWidth : clampRightWidth const delta = side === "left" ? clientX - resizeStartX() : resizeStartX() - clientX const nextWidth = clamp(startWidth + delta) applyDrawerWidth(side, nextWidth) } else if (side === "chat") { const startWidth = resizeStartWidth() const delta = resizeStartX() - clientX // Dragging left increases width const nextWidth = Math.max(300, Math.min(window.innerWidth - 300, startWidth + delta)) applyPanelSize("chat", nextWidth) } else if (side === "terminal") { const startHeight = resizeStartHeight() const delta = resizeStartY() - clientY // Dragging up increases height const nextHeight = Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta)) applyPanelSize("terminal", nextHeight) } } function stopDrawerResize() { setActiveResizeSide(null) document.removeEventListener("mousemove", drawerMouseMove) document.removeEventListener("mouseup", drawerMouseUp) document.removeEventListener("touchmove", drawerTouchMove) document.removeEventListener("touchend", drawerTouchEnd) } function drawerMouseMove(event: MouseEvent) { event.preventDefault() handlePointerMove(event.clientX, event.clientY) } function drawerMouseUp() { stopDrawerResize() } function drawerTouchMove(event: TouchEvent) { const touch = event.touches[0] if (!touch) return event.preventDefault() handlePointerMove(touch.clientX, touch.clientY) } function drawerTouchEnd() { stopDrawerResize() } const startResize = (side: "left" | "right" | "chat" | "terminal", clientX: number, clientY: number) => { setActiveResizeSide(side) setResizeStartX(clientX) setResizeStartY(clientY) if (side === "left") setResizeStartWidth(sessionSidebarWidth()) else if (side === "right") setResizeStartWidth(rightDrawerWidth()) else if (side === "chat") setResizeStartWidth(chatPanelWidth()) else if (side === "terminal") setResizeStartHeight(terminalPanelHeight()) document.addEventListener("mousemove", drawerMouseMove) document.addEventListener("mouseup", drawerMouseUp) document.addEventListener("touchmove", drawerTouchMove, { passive: false }) document.addEventListener("touchend", drawerTouchEnd) } const handleResizeMouseDown = (side: "left" | "right" | "chat" | "terminal") => (event: MouseEvent) => { event.preventDefault() startResize(side, event.clientX, event.clientY) } const handleDrawerResizeTouchStart = (side: "left" | "right") => (event: TouchEvent) => { const touch = event.touches[0] if (!touch) return event.preventDefault() startResize(side, touch.clientX, touch.clientY) } onCleanup(() => { stopDrawerResize() }) type DrawerViewState = "pinned" | "floating-open" | "floating-closed" const leftDrawerState = createMemo(() => { if (leftPinned()) return "pinned" return leftOpen() ? "floating-open" : "floating-closed" }) const rightDrawerState = createMemo(() => { if (rightPinned()) return "pinned" return rightOpen() ? "floating-open" : "floating-closed" }) const leftAppBarButtonLabel = () => { const state = leftDrawerState() if (state === "pinned") return "Left drawer pinned" if (state === "floating-closed") return "Open left drawer" return "Close left drawer" } const rightAppBarButtonLabel = () => { const state = rightDrawerState() if (state === "pinned") return "Right drawer pinned" if (state === "floating-closed") return "Open right drawer" return "Close right drawer" } const leftAppBarButtonIcon = () => { const state = leftDrawerState() if (state === "floating-closed") return return } const rightAppBarButtonIcon = () => { const state = rightDrawerState() if (state === "floating-closed") return return } const pinLeftDrawer = () => { blurIfInside(leftDrawerContentEl()) batch(() => { setLeftPinned(true) setLeftOpen(true) }) persistPinIfSupported("left", true) measureDrawerHost() } const unpinLeftDrawer = () => { blurIfInside(leftDrawerContentEl()) batch(() => { setLeftPinned(false) setLeftOpen(true) }) persistPinIfSupported("left", false) measureDrawerHost() } const pinRightDrawer = () => { blurIfInside(rightDrawerContentEl()) batch(() => { setRightPinned(true) setRightOpen(true) }) persistPinIfSupported("right", true) measureDrawerHost() } const unpinRightDrawer = () => { blurIfInside(rightDrawerContentEl()) batch(() => { setRightPinned(false) setRightOpen(true) }) persistPinIfSupported("right", false) measureDrawerHost() } const handleLeftAppBarButtonClick = () => { const state = leftDrawerState() if (state === "pinned") return if (state === "floating-closed") { setLeftOpen(true) measureDrawerHost() return } blurIfInside(leftDrawerContentEl()) setLeftOpen(false) focusTarget(leftToggleButtonEl()) measureDrawerHost() } const handleRightAppBarButtonClick = () => { const state = rightDrawerState() if (state === "pinned") return if (state === "floating-closed") { setRightOpen(true) setIsSoloOpen(false) measureDrawerHost() return } blurIfInside(rightDrawerContentEl()) setRightOpen(false) setIsSoloOpen(false) focusTarget(rightToggleButtonEl()) measureDrawerHost() } const focusTarget = (element: HTMLElement | null) => { if (!element) return requestAnimationFrame(() => { element.focus() }) } const blurIfInside = (element: HTMLElement | null) => { if (typeof document === "undefined" || !element) return const active = document.activeElement as HTMLElement | null if (active && element.contains(active)) { active.blur() } } const closeLeftDrawer = () => { if (leftDrawerState() === "pinned") return blurIfInside(leftDrawerContentEl()) setLeftOpen(false) focusTarget(leftToggleButtonEl()) } const closeRightDrawer = () => { if (rightDrawerState() === "pinned") return blurIfInside(rightDrawerContentEl()) setRightOpen(false) focusTarget(rightToggleButtonEl()) } const formattedUsedTokens = () => formatTokenTotal(tokenStats().used) const formattedAvailableTokens = () => { const avail = tokenStats().avail if (typeof avail === "number") { return formatTokenTotal(avail) } return "--" } const LeftDrawerContent = () => ( setTerminalOpen((current) => !current)} isTerminalOpen={terminalOpen()} onOpenAdvancedSettings={() => setShowAdvancedSettings(true)} requestedTab={sidebarRequestedTab()} /> ) const RightDrawerContent = () => { const sessionId = activeSessionIdForInstance() if (sessionId && sessionId !== "info") { return (
) } const renderPlanSectionContent = () => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") { return

Select a session to view plan.

} const todoState = latestTodoState() if (!todoState) { return

Nothing planned yet.

} return } const sections = [ { id: "lsp", label: "LSP Servers", render: () => ( ), }, { id: "mcp", label: "MCP Servers", render: () => ( ), }, { id: "plan", label: "Plan", render: renderPlanSectionContent, }, ] createEffect(() => { const currentExpanded = new Set(rightPanelExpandedItems()) if (sections.every((section) => currentExpanded.has(section.id))) return setRightPanelExpandedItems(sections.map((section) => section.id)) }) const handleAccordionChange = (values: string[]) => { setRightPanelExpandedItems(values) } const isSectionExpanded = (id: string) => rightPanelExpandedItems().includes(id) return (
Status Panel
(rightPinned() ? unpinRightDrawer() : pinRightDrawer())} > {rightPinned() ? : }
{(section) => ( {section.label} {section.render()} )}
) } const renderLeftPanel = () => { if (leftPinned()) { return (