import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor, type Component, } from "solid-js" import AppBar from "@suid/material/AppBar" import Box from "@suid/material/Box" import Drawer from "@suid/material/Drawer" import IconButton from "@suid/material/IconButton" import Toolbar from "@suid/material/Toolbar" import useMediaQuery from "@suid/material/useMediaQuery" import type { Instance } from "../../types/instance" import type { Command } from "../../lib/commands" import type { BackgroundProcess } from "../../../../server/src/api-types" import { keyboardRegistry, type KeyboardShortcut } from "../../lib/keyboard-registry" import { isOpen as isCommandPaletteOpen, hideCommandPalette, showCommandPalette } from "../../stores/command-palette" import Kbd from "../kbd" import InstanceWelcomeView from "../instance-welcome-view" import InfoView from "../info-view" import CommandPalette from "../command-palette" import PermissionNotificationBanner from "../permission-notification-banner" import PermissionApprovalModal from "../permission-approval-modal" import SessionView from "../session/session-view" import { formatTokenTotal } from "../../lib/formatters" import ContextMeter from "../context-meter" import { sseManager } from "../../lib/sse-manager" import { getLogger } from "../../lib/logger" import { serverApi } from "../../lib/api-client" import { loadBackgroundProcesses } from "../../stores/background-processes" import { BackgroundProcessOutputDialog } from "../background-process-output-dialog" import { useI18n } from "../../lib/i18n" import { getPermissionQueueLength, getQuestionQueueLength } from "../../stores/instances" import SessionSidebar from "./shell/SessionSidebar" import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests" import RightPanel from "./shell/right-panel/RightPanel" import { useDrawerChrome } from "./shell/useDrawerChrome" import { getSessionStatus } from "../../stores/session-status" import { Maximize2, ShieldAlert } from "lucide-solid" import type { LayoutMode } from "./shell/types" import { DEFAULT_SESSION_SIDEBAR_WIDTH, LEFT_DRAWER_STORAGE_KEY, RIGHT_DRAWER_STORAGE_KEY, RIGHT_DRAWER_WIDTH, clampRightWidth, clampWidth, } from "./shell/storage" import { useDrawerHostMeasure } from "./shell/useDrawerHostMeasure" import { useDrawerResize } from "./shell/useDrawerResize" import { useSessionCache } from "./shell/useSessionCache" import { useInstanceSessionContext } from "./shell/useInstanceSessionContext" 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 // In-memory only: mobile immersive/fullscreen mode. mobileFullscreenMode: boolean onEnterMobileFullscreen: () => void onExitMobileFullscreen: () => void } const InstanceShell2: Component = (props) => { const { t } = useI18n() const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH) const [rightDrawerWidth, setRightDrawerWidth] = createSignal( typeof window !== "undefined" ? clampRightWidth(window.innerWidth * 0.35) : RIGHT_DRAWER_WIDTH, ) const [rightDrawerWidthInitialized, setRightDrawerWidthInitialized] = createSignal(false) const [leftDrawerContentEl, setLeftDrawerContentEl] = createSignal(null) const [rightDrawerContentEl, setRightDrawerContentEl] = createSignal(null) const [leftToggleButtonEl, setLeftToggleButtonEl] = createSignal(null) const [rightToggleButtonEl, setRightToggleButtonEl] = createSignal(null) const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal(null) const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false) const [permissionModalOpen, setPermissionModalOpen] = createSignal(false) // Worktree selector manages its own dialogs. const [showSessionSearch, setShowSessionSearch] = createSignal(false) const { allInstanceSessions, sessionThreads, activeSessions, activeSessionIdForInstance, activeSessionForInstance, activeSessionDiffs, latestTodoState, tokenStats, backgroundProcessList, handleSessionSelect, } = useInstanceSessionContext({ instanceId: () => 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 mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout()) const compactPromptLayout = createMemo(() => layoutMode() !== "desktop") const leftPinningSupported = createMemo(() => layoutMode() !== "phone") const rightPinningSupported = createMemo(() => layoutMode() !== "phone") const { setDrawerHost, drawerContainer, measureDrawerHost, floatingTopPx, floatingHeight } = useDrawerHostMeasure( () => props.tabBarOffset, ) const drawerChrome = useDrawerChrome({ t, layoutMode, leftPinningSupported, rightPinningSupported, leftDrawerContentEl, rightDrawerContentEl, leftToggleButtonEl, rightToggleButtonEl, measureDrawerHost, }) const { leftPinned, leftOpen, rightPinned, rightOpen, setLeftOpen, setRightOpen, leftDrawerState, rightDrawerState, pinLeft: pinLeftDrawer, unpinLeft: unpinLeftDrawer, pinRight: pinRightDrawer, unpinRight: unpinRightDrawer, closeLeft: closeLeftDrawer, closeRight: closeRightDrawer, leftAppBarButtonLabel, rightAppBarButtonLabel, leftAppBarButtonIcon, rightAppBarButtonIcon, handleLeftAppBarButtonClick, handleRightAppBarButtonClick, } = drawerChrome createEffect(() => { const instanceId = props.instance.id loadBackgroundProcesses(instanceId).catch((error) => { log.warn("Failed to load background processes", error) }) }) 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)) } } let didLoadRightWidth = false const savedRight = window.localStorage.getItem(RIGHT_DRAWER_STORAGE_KEY) if (savedRight) { const parsed = Number.parseInt(savedRight, 10) if (Number.isFinite(parsed)) { setRightDrawerWidth(clampRightWidth(parsed)) didLoadRightWidth = true } } if (!didLoadRightWidth) { setRightDrawerWidth(clampRightWidth(window.innerWidth * 0.35)) } setRightDrawerWidthInitialized(true) const handleResize = () => { const width = clampWidth(window.innerWidth * 0.3) setSessionSidebarWidth((current) => clampWidth(current || width)) const fallbackRight = window.innerWidth * 0.35 setRightDrawerWidth((current) => clampRightWidth(current || fallbackRight)) measureDrawerHost() } handleResize() window.addEventListener("resize", handleResize) onCleanup(() => window.removeEventListener("resize", handleResize)) }) 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()) }) 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 hasPendingRequests = createMemo(() => { const permissions = getPermissionQueueLength(props.instance.id) const questions = getQuestionQueueLength(props.instance.id) return permissions + questions > 0 }) const activeSessionStatusPill = createMemo(() => { const activeSessionId = activeSessionIdForInstance() if (!activeSessionId || activeSessionId === "info") return null const activeSession = activeSessionForInstance() const needsPermission = Boolean(activeSession?.pendingPermission) const needsQuestion = Boolean(activeSession?.pendingQuestion) const needsInput = needsPermission || needsQuestion if (needsInput) { return { className: "session-permission", text: needsPermission ? t("sessionList.status.needsPermission") : t("sessionList.status.needsInput"), showAlertIcon: true, } } const status = getSessionStatus(props.instance.id, activeSessionId) const text = status === "working" ? t("sessionList.status.working") : status === "compacting" ? t("sessionList.status.compacting") : t("sessionList.status.idle") return { className: `session-${status}`, text, showAlertIcon: false, } }) const renderActiveSessionStatusPill = () => { const pill = activeSessionStatusPill() if (!pill) return null return ( {pill.showAlertIcon ?