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, TerminalSquare, Trash2, XOctagon } 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 InfoOutlinedIcon from "@suid/icons-material/InfoOutlined" import type { Instance } from "../../types/instance" import type { Command } from "../../lib/commands" import type { BackgroundProcess } from "../../../../server/src/api-types" import type { Session } from "../../types/session" import { activeParentSessionId, activeSessionId as activeSessionMap, getSessionFamily, getSessionInfo, getSessionThreads, sessions, setActiveParentSession, setActiveSession, } from "../../stores/sessions" import { keyboardRegistry, type KeyboardShortcut } from "../../lib/keyboard-registry" import { messageStoreBus } from "../../stores/message-v2/bus" import { clearSessionRenderCache } from "../message-block" import { isOpen as isCommandPaletteOpen, hideCommandPalette, showCommandPalette } from "../../stores/command-palette" import SessionList from "../session-list" import KeyboardHint from "../keyboard-hint" import Kbd from "../kbd" 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 ThinkingSelector from "../thinking-selector" import CommandPalette from "../command-palette" import PermissionNotificationBanner from "../permission-notification-banner" import PermissionApprovalModal from "../permission-approval-modal" import { TodoListView } from "../tool-call/renderers/todo" import ContextUsagePanel from "../session/context-usage-panel" import SessionView from "../session/session-view" import { formatTokenTotal } from "../../lib/formatters" import { sseManager } from "../../lib/sse-manager" import { getLogger } from "../../lib/logger" import { serverApi } from "../../lib/api-client" import { getBackgroundProcesses, loadBackgroundProcesses } from "../../stores/background-processes" import { BackgroundProcessOutputDialog } from "../background-process-output-dialog" import { useI18n } from "../../lib/i18n" 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 = 340 const MIN_SESSION_SIDEBAR_WIDTH = 220 const MAX_SESSION_SIDEBAR_WIDTH = 400 const RIGHT_DRAWER_WIDTH = 260 const MIN_RIGHT_DRAWER_WIDTH = 200 const MAX_RIGHT_DRAWER_WIDTH = 380 const SESSION_CACHE_LIMIT = 5 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" 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 { t } = useI18n() 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" | null>(null) const [resizeStartX, setResizeStartX] = createSignal(0) const [resizeStartWidth, setResizeStartWidth] = createSignal(0) const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal([ "plan", "background-processes", "mcp", "lsp", "plugins", ]) const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal(null) const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false) const [permissionModalOpen, setPermissionModalOpen] = createSignal(false) 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(() => { const instanceId = props.instance.id loadBackgroundProcesses(instanceId).catch((error) => { log.warn("Failed to load background processes", error) }) }) 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)) }) 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 allInstanceSessions = createMemo>(() => { return sessions().get(props.instance.id) ?? new Map() }) const sessionThreads = createMemo(() => getSessionThreads(props.instance.id)) 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 return activeSessions().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 backgroundProcessList = createMemo(() => getBackgroundProcesses(props.instance.id)) 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 connectionStatusLabel = () => { const status = connectionStatus() if (status === "connected") return t("instanceShell.connection.connected") if (status === "connecting") return t("instanceShell.connection.connecting") if (status === "error" || status === "disconnected") return t("instanceShell.connection.disconnected") return t("instanceShell.connection.unknown") } const handleCommandPaletteClick = () => { showCommandPalette(props.instance.id) } const openBackgroundOutput = (process: BackgroundProcess) => { setSelectedBackgroundProcess(process) setShowBackgroundOutput(true) } const closeBackgroundOutput = () => { setShowBackgroundOutput(false) setSelectedBackgroundProcess(null) } const stopBackgroundProcess = async (processId: string) => { try { await serverApi.stopBackgroundProcess(props.instance.id, processId) } catch (error) { log.warn("Failed to stop background process", error) } } const terminateBackgroundProcess = async (processId: string) => { try { await serverApi.terminateBackgroundProcess(props.instance.id, processId) } catch (error) { log.warn("Failed to terminate background process", error) } } const instancePaletteCommands = createMemo(() => props.paletteCommands()) 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 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 } const focusVariantSelectorControl = () => { const input = leftDrawerContentEl()?.querySelector("[data-thinking-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() : action === "focus-model-selector" ? focusModelSelectorControl() : focusVariantSelectorControl() if (handled) { setPendingSidebarAction(null) } }) const handleSidebarRequest = (action: SessionSidebarRequestAction) => { setPendingSidebarAction({ action, id: sidebarActionId++ }) 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) => { if (sessionId === "info") { setActiveSession(props.instance.id, sessionId) return } const session = allInstanceSessions().get(sessionId) if (!session) return if (session.parentId === null) { setActiveParentSession(props.instance.id, sessionId) return } const parentId = session.parentId if (!parentId) return batch(() => { setActiveParentSession(props.instance.id, parentId) 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 instanceSessions = allInstanceSessions() const activeId = activeSessionIdForInstance() setCachedSessionIds((current) => { const next = current.filter((id) => id !== "info" && instanceSessions.has(id)) const touch = (id: string | null) => { if (!id || id === "info") return if (!instanceSessions.has(id)) return const index = next.indexOf(id) if (index !== -1) { next.splice(index, 1) } next.unshift(id) } touch(activeId) const trimmed = next.length > SESSION_CACHE_LIMIT ? next.slice(0, SESSION_CACHE_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 handleDrawerPointerMove = (clientX: number) => { const side = activeResizeSide() if (!side) return 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) } 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() handleDrawerPointerMove(event.clientX) } function drawerMouseUp() { stopDrawerResize() } function drawerTouchMove(event: TouchEvent) { const touch = event.touches[0] if (!touch) return event.preventDefault() handleDrawerPointerMove(touch.clientX) } function drawerTouchEnd() { stopDrawerResize() } const startDrawerResize = (side: "left" | "right", clientX: number) => { setActiveResizeSide(side) setResizeStartX(clientX) setResizeStartWidth(side === "left" ? sessionSidebarWidth() : rightDrawerWidth()) document.addEventListener("mousemove", drawerMouseMove) document.addEventListener("mouseup", drawerMouseUp) document.addEventListener("touchmove", drawerTouchMove, { passive: false }) document.addEventListener("touchend", drawerTouchEnd) } const handleDrawerResizeMouseDown = (side: "left" | "right") => (event: MouseEvent) => { event.preventDefault() startDrawerResize(side, event.clientX) } const handleDrawerResizeTouchStart = (side: "left" | "right") => (event: TouchEvent) => { const touch = event.touches[0] if (!touch) return event.preventDefault() startDrawerResize(side, touch.clientX) } 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 t("instanceShell.leftDrawer.toggle.pinned") if (state === "floating-closed") return t("instanceShell.leftDrawer.toggle.open") return t("instanceShell.leftDrawer.toggle.close") } const rightAppBarButtonLabel = () => { const state = rightDrawerState() if (state === "pinned") return t("instanceShell.rightDrawer.toggle.pinned") if (state === "floating-closed") return t("instanceShell.rightDrawer.toggle.open") return t("instanceShell.rightDrawer.toggle.close") } 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) measureDrawerHost() return } blurIfInside(rightDrawerContentEl()) setRightOpen(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 = () => (
{t("instanceShell.leftPanel.sessionsTitle")}
handleSessionSelect("info")} > (leftPinned() ? unpinLeftDrawer() : pinLeftDrawer())} > {leftPinned() ? : }
{ const result = props.onNewSession() if (result instanceof Promise) { void result.catch((error) => log.error("Failed to create session:", error)) } }} showHeader={false} showFooter={false} /> {(activeSession) => ( <>
props.handleSidebarAgentChange(activeSession().id, agent)} /> props.handleSidebarModelChange(activeSession().id, model)} />
)}
) const RightDrawerContent = () => { const renderPlanSectionContent = () => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") { return

{t("instanceShell.plan.noSessionSelected")}

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

{t("instanceShell.plan.empty")}

} return } const renderBackgroundProcesses = () => { const processes = backgroundProcessList() if (processes.length === 0) { return

{t("instanceShell.backgroundProcesses.empty")}

} return (
{(process) => (
{process.title}
{t("instanceShell.backgroundProcesses.status", { status: process.status })} {t("instanceShell.backgroundProcesses.output", { sizeKb: Math.round((process.outputSizeBytes ?? 0) / 1024), })}
)}
) } const sections = [ { id: "plan", labelKey: "instanceShell.rightPanel.sections.plan", render: renderPlanSectionContent, }, { id: "background-processes", labelKey: "instanceShell.rightPanel.sections.backgroundProcesses", render: renderBackgroundProcesses, }, { id: "mcp", labelKey: "instanceShell.rightPanel.sections.mcp", render: () => ( ), }, { id: "lsp", labelKey: "instanceShell.rightPanel.sections.lsp", render: () => ( ), }, { id: "plugins", labelKey: "instanceShell.rightPanel.sections.plugins", render: () => ( ), }, ] 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 (
{t("instanceShell.rightPanel.title")}
(rightPinned() ? unpinRightDrawer() : pinRightDrawer())} > {rightPinned() ? : }
{(section) => ( {t(section.labelKey)} {section.render()} )}
) } const renderLeftPanel = () => { if (leftPinned()) { return (