Files
NomadArch/packages/ui/src/components/instance/instance-shell2.tsx

1552 lines
56 KiB
TypeScript

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<Command[]>
onCloseSession: (sessionId: string) => Promise<void> | void
onNewSession: () => Promise<void> | void
handleSidebarAgentChange: (sessionId: string, agent: string) => Promise<void>
handleSidebarModelChange: (sessionId: string, model: { providerId: string; modelId: string }) => Promise<void>
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<InstanceShellProps> = (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<string[]>([])
const [pendingEvictions, setPendingEvictions] = createSignal<string[]>([])
const [drawerHost, setDrawerHost] = createSignal<HTMLElement | null>(null)
const [floatingDrawerTop, setFloatingDrawerTop] = createSignal(0)
const [floatingDrawerHeight, setFloatingDrawerHeight] = createSignal(0)
const [leftDrawerContentEl, setLeftDrawerContentEl] = createSignal<HTMLElement | null>(null)
const [rightDrawerContentEl, setRightDrawerContentEl] = createSignal<HTMLElement | null>(null)
const [leftToggleButtonEl, setLeftToggleButtonEl] = createSignal<HTMLElement | null>(null)
const [rightToggleButtonEl, setRightToggleButtonEl] = createSignal<HTMLElement | null>(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<string[]>(["lsp", "mcp", "plan"])
const [currentFile, setCurrentFile] = createSignal<FileNode | null>(null)
const [centerTab, setCenterTab] = createSignal<"code" | "preview">("code")
const [previewUrl, setPreviewUrl] = createSignal<string | null>(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<LayoutMode>(() => {
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<HTMLElement>(".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<SessionSidebarRequestDetail>).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<string, ReturnType<typeof getSessionFamily>[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<ToolState | null>(() => {
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<PendingSidebarAction | null>(null)
const [sidebarRequestedTab, setSidebarRequestedTab] = createSignal<string | null>(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<HTMLInputElement>("[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<DrawerViewState>(() => {
if (leftPinned()) return "pinned"
return leftOpen() ? "floating-open" : "floating-closed"
})
const rightDrawerState = createMemo<DrawerViewState>(() => {
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 <MenuIcon fontSize="small" />
return <MenuOpenIcon fontSize="small" />
}
const rightAppBarButtonIcon = () => {
const state = rightDrawerState()
if (state === "floating-closed") return <MenuIcon fontSize="small" sx={{ transform: "scaleX(-1)" }} />
return <MenuOpenIcon fontSize="small" sx={{ transform: "scaleX(-1)" }} />
}
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 = () => (
<Sidebar
instanceId={props.instance.id}
isOpen={leftOpen()}
onFileSelect={handleFileSelect}
sessions={Array.from(activeSessions().values())}
activeSessionId={activeSessionIdForInstance() || undefined}
onSessionSelect={handleSessionSelect}
onOpenCommandPalette={handleCommandPaletteClick}
onToggleTerminal={() => setTerminalOpen((current) => !current)}
isTerminalOpen={terminalOpen()}
onOpenAdvancedSettings={() => setShowAdvancedSettings(true)}
requestedTab={sidebarRequestedTab()}
/>
)
const RightDrawerContent = () => {
const sessionId = activeSessionIdForInstance()
if (sessionId && sessionId !== "info") {
return (
<div class="flex flex-col h-full relative" ref={setRightDrawerContentEl}>
<MultiTaskChat instanceId={props.instance.id} sessionId={sessionId} />
</div>
)
}
const renderPlanSectionContent = () => {
const sessionId = activeSessionIdForInstance()
if (!sessionId || sessionId === "info") {
return <p class="text-xs text-secondary">Select a session to view plan.</p>
}
const todoState = latestTodoState()
if (!todoState) {
return <p class="text-xs text-secondary">Nothing planned yet.</p>
}
return <TodoListView state={todoState} emptyLabel="Nothing planned yet." showStatusLabel={false} />
}
const sections = [
{
id: "lsp",
label: "LSP Servers",
render: () => (
<InstanceServiceStatus
initialInstance={props.instance}
sections={["lsp"]}
showSectionHeadings={false}
class="space-y-2"
/>
),
},
{
id: "mcp",
label: "MCP Servers",
render: () => (
<InstanceServiceStatus
initialInstance={props.instance}
sections={["mcp"]}
showSectionHeadings={false}
class="space-y-2"
/>
),
},
{
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 (
<div class="flex flex-col h-full" ref={setRightDrawerContentEl}>
<div class="flex items-center justify-between px-4 py-2 border-b border-base">
<Typography variant="subtitle2" class="uppercase tracking-wide text-xs font-semibold">
Status Panel
</Typography>
<div class="flex items-center gap-2">
<Show when={!isPhoneLayout()}>
<IconButton
size="small"
color="inherit"
aria-label={rightPinned() ? "Unpin right drawer" : "Pin right drawer"}
onClick={() => (rightPinned() ? unpinRightDrawer() : pinRightDrawer())}
>
{rightPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
</IconButton>
</Show>
</div>
</div>
<div class="flex-1 overflow-y-auto">
<Accordion.Root
class="flex flex-col"
collapsible
multiple
value={rightPanelExpandedItems()}
onChange={handleAccordionChange}
>
<For each={sections}>
{(section) => (
<Accordion.Item
value={section.id}
class="w-full border border-base bg-surface-secondary text-primary"
>
<Accordion.Header>
<Accordion.Trigger class="w-full flex items-center justify-between gap-3 px-3 py-2 text-[11px] font-semibold uppercase tracking-wide">
<span>{section.label}</span>
<ChevronDown
class={`h-4 w-4 transition-transform duration-150 ${isSectionExpanded(section.id) ? "rotate-180" : ""}`}
/>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="w-full px-3 pb-3 text-sm text-primary">
{section.render()}
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion.Root>
</div>
</div>
)
}
const renderLeftPanel = () => {
if (leftPinned()) {
return (
<Box
class="session-sidebar-container"
sx={{
width: `${sessionSidebarWidth()}px`,
flexShrink: 0,
borderRight: "1px solid var(--border-base)",
backgroundColor: "var(--surface-secondary)",
height: "100%",
minHeight: 0,
position: "relative",
}}
>
<div
class="session-resize-handle session-resize-handle--left"
onMouseDown={handleResizeMouseDown("left")}
onTouchStart={handleDrawerResizeTouchStart("left")}
role="presentation"
aria-hidden="true"
/>
<LeftDrawerContent />
</Box>
)
}
const container = drawerContainer()
const modalProps = container ? { container: container as Element } : undefined
return (
<Drawer
anchor="left"
variant="temporary"
open={leftOpen()}
onClose={closeLeftDrawer}
ModalProps={modalProps}
sx={{
"& .MuiDrawer-paper": {
width: isPhoneLayout() ? "100vw" : `${sessionSidebarWidth()}px`,
boxSizing: "border-box",
borderRight: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
backgroundColor: "var(--surface-secondary)",
backgroundImage: "none",
color: "var(--text-primary)",
boxShadow: "none",
borderRadius: 0,
top: floatingTopPx(),
height: floatingHeight(),
},
"& .MuiBackdrop-root": {
backgroundColor: "transparent",
},
}}
>
<LeftDrawerContent />
</Drawer>
)
}
const renderRightPanel = () => {
if (isSoloOpen()) return null; // MultiX Mode uses the main stream area
if (rightPinned()) {
return (
<Box
class="session-right-panel"
sx={{
width: `${rightDrawerWidth()}px`,
flexShrink: 0,
borderLeft: "1px solid var(--border-base)",
backgroundColor: "var(--surface-secondary)",
height: "100%",
minHeight: 0,
position: "relative",
}}
>
<div
class="session-resize-handle session-resize-handle--right"
onMouseDown={handleResizeMouseDown("right")}
onTouchStart={handleDrawerResizeTouchStart("right")}
role="presentation"
aria-hidden="true"
/>
<RightDrawerContent />
</Box>
)
}
const container = drawerContainer()
const modalProps = container ? { container: container as Element } : undefined
return (
<Drawer
anchor="right"
variant="temporary"
open={rightOpen()}
onClose={closeRightDrawer}
ModalProps={modalProps}
sx={{
"& .MuiDrawer-paper": {
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
boxSizing: "border-box",
borderLeft: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
backgroundColor: "var(--surface-secondary)",
backgroundImage: "none",
color: "var(--text-primary)",
boxShadow: "none",
borderRadius: 0,
top: floatingTopPx(),
height: floatingHeight(),
},
"& .MuiBackdrop-root": {
backgroundColor: "transparent",
},
}}
>
<RightDrawerContent />
</Drawer>
)
}
const hasSessions = createMemo(() => activeSessions().size > 0)
const showingInfoView = createMemo(() => activeSessionIdForInstance() === "info")
const sessionLayout = (
<div
class="session-shell-panels flex flex-col flex-1 min-h-0 overflow-x-hidden relative bg-[#050505]"
ref={(element) => {
setDrawerHost(element)
measureDrawerHost()
}}
>
{/* Background Decorator - Antigravity Glows */}
<div class="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-600/10 blur-[120px] rounded-full pointer-events-none z-0" />
<div class="absolute bottom-[-10%] right-[-10%] w-[30%] h-[30%] bg-purple-600/5 blur-[100px] rounded-full pointer-events-none z-0" />
<AppBar position="sticky" color="default" elevation={0} class="border-b border-white/5 bg-[#050505]/80 backdrop-blur-md z-20">
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center justify-between gap-2 py-0 min-h-[40px]">
<div class="flex items-center space-x-4">
<IconButton
ref={setLeftToggleButtonEl}
color="inherit"
onClick={handleLeftAppBarButtonClick}
aria-label={leftAppBarButtonLabel()}
size="small"
class="text-zinc-500 hover:text-zinc-200"
>
<MenuIcon fontSize="small" />
</IconButton>
<div class="flex items-center space-x-2">
<div class="w-2.5 h-2.5 rounded-full bg-[#f87171] opacity-60" />
<div class="w-2.5 h-2.5 rounded-full bg-[#fbbf24] opacity-60" />
<div class="w-2.5 h-2.5 rounded-full bg-[#4ade80] opacity-60" />
</div>
</div>
<div class="hidden md:flex items-center bg-white/5 border border-white/5 rounded-full px-3 py-1 space-x-2 text-zinc-400 group hover:border-white/10 transition-all cursor-pointer" onClick={handleCommandPaletteClick}>
<Search size={14} />
<span class="text-[11px] min-w-[200px]">Search your project...</span>
<div class="flex items-center space-x-1 opacity-40">
<Kbd shortcut="cmd+shift+p" />
</div>
</div>
<div class="flex items-center space-x-4">
<Show when={activeSessionIdForInstance() && activeSessionIdForInstance() !== "info"}>
<div class="flex items-center space-x-2">
<ModelStatusSelector
instanceId={props.instance.id}
sessionId={activeSessionIdForInstance()!}
currentModel={activeSessionForInstance()?.model || { providerId: "", modelId: "" }}
onModelChange={async (model) => {
const sid = activeSessionIdForInstance()
if (sid) await props.handleSidebarModelChange(sid, model)
}}
/>
<button
onClick={() => setShowAdvancedSettings(true)}
class="p-2 text-zinc-500 hover:text-blue-400 transition-all hover:bg-blue-500/10 rounded-full"
title="AI Settings: Manage model providers and API keys"
>
<Settings size={14} strokeWidth={2} />
</button>
</div>
</Show>
{/* SmartX Mode Buttons (Integrated HUD) */}
<div class="flex items-center bg-white/5 border border-white/5 rounded-full px-2 py-1 space-x-1">
<button
onClick={handleSmartFix}
disabled={isFixing()}
title="Smart Fix: Automatically detect and fix issues in your code"
class={`transition-all flex items-center space-x-1.5 px-2 py-1 rounded-full hover:bg-white/10 ${isFixing() ? "text-blue-500" : "text-zinc-400 hover:text-white"}`}
>
<Show when={isFixing()} fallback={<Sparkles size={14} class="text-blue-400" />}>
<Loader2 size={14} class="animate-spin text-blue-400" />
</Show>
<span class="text-[10px] font-bold uppercase tracking-tight">
{isFixing() ? "FIXING..." : "SMART FIX"}
</span>
</button>
<div class="w-px h-3 bg-white/10" />
<button
onClick={handleBuild}
disabled={isBuilding()}
title="Build: Build and deploy your application"
class={`transition-all flex items-center space-x-1.5 px-2 py-1 rounded-full hover:bg-white/10 ${isBuilding() ? "text-indigo-500" : "text-zinc-400 hover:text-white"}`}
>
<Show when={isBuilding()} fallback={<TerminalIcon size={14} />}>
<Loader2 size={14} class="animate-spin text-indigo-400" />
</Show>
<span class="text-[10px] font-bold uppercase tracking-tight">
{isBuilding() ? "BUILDING..." : "BUILD"}
</span>
</button>
</div>
{/* APEX PRO Mode & Auto-Approval Toggles */}
<div class="flex items-center bg-white/5 border border-white/5 rounded-full px-1.5 py-1 space-x-1">
<button
onClick={() => toggleAutonomous(props.instance.id)}
title="Autonomous Mode (APEX PRO): Enable autonomous AI agent operations"
class={`flex items-center space-x-1.5 px-2 py-0.5 rounded-full transition-all ${getSoloState(props.instance.id).isAutonomous
? "bg-blue-500/20 text-blue-400 border border-blue-500/30"
: "text-zinc-500 hover:text-zinc-300"
}`}
>
<Zap size={12} class={getSoloState(props.instance.id).isAutonomous ? "animate-pulse" : ""} />
<span class="text-[9px] font-black uppercase tracking-tighter">APEX PRO</span>
</button>
<button
onClick={() => toggleAutoApproval(props.instance.id)}
title="Auto-Approval (SHIELD): Automatically approve AI agent actions"
class={`flex items-center space-x-1.5 px-2 py-0.5 rounded-full transition-all ${getSoloState(props.instance.id).autoApproval
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "text-zinc-500 hover:text-zinc-300"
}`}
>
<Shield size={12} />
<span class="text-[9px] font-black uppercase tracking-tighter">Shield</span>
</button>
</div>
<button
onClick={() => {
const newState = !(rightOpen() && isSoloOpen())
setRightOpen(newState)
setIsSoloOpen(newState)
}}
class={`flex items-center space-x-1.5 px-3 py-1 rounded-full text-[11px] font-bold transition-all ${(rightOpen() && isSoloOpen()) ? 'bg-blue-600/20 text-blue-400 border border-blue-500/30' : 'bg-white/5 text-zinc-400 border border-white/5'
}`}
>
<span class={`w-1.5 h-1.5 bg-current rounded-full ${(rightOpen() && isSoloOpen()) ? 'animate-pulse' : ''}`} />
<span>MULTIX MODE</span>
</button>
<IconButton
ref={setRightToggleButtonEl}
color="inherit"
onClick={handleRightAppBarButtonClick}
aria-label={rightAppBarButtonLabel()}
size="small"
class="text-zinc-500 hover:text-zinc-200"
>
{rightAppBarButtonIcon()}
</IconButton>
</div>
</Toolbar>
</AppBar>
<Box sx={{ display: "flex", flex: 1, minHeight: 0, overflowX: "hidden", position: "relative", zIndex: 10 }}>
{renderLeftPanel()}
<Box
component="main"
sx={{ flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column", overflowX: "hidden" }}
class="content-area relative"
>
<div class="flex-1 flex overflow-hidden min-h-0">
<Show when={!isPhoneLayout()}>
<div class="flex-1 flex flex-col min-h-0 bg-[#0d0d0d]">
<div class="h-10 glass border-b border-white/5 flex items-center justify-between px-4 shrink-0">
<div class="flex items-center gap-2">
<button
type="button"
class={`px-2.5 py-1 rounded-md text-[11px] font-semibold uppercase tracking-wide border ${
centerTab() === "code"
? "bg-white/10 border-white/20 text-white"
: "border-transparent text-zinc-400 hover:text-zinc-200 hover:bg-white/5"
}`}
onClick={() => setCenterTab("code")}
>
Code
</button>
<button
type="button"
class={`px-2.5 py-1 rounded-md text-[11px] font-semibold uppercase tracking-wide border ${
centerTab() === "preview"
? "bg-white/10 border-white/20 text-white"
: "border-transparent text-zinc-400 hover:text-zinc-200 hover:bg-white/5"
}`}
onClick={() => setCenterTab("preview")}
disabled={!previewUrl()}
title={previewUrl() ? previewUrl() : "Run build to enable preview"}
>
Preview
</button>
</div>
<Show when={previewUrl()}>
{(url) => (
<div class="text-[10px] text-zinc-500 truncate max-w-[50%]" title={url()}>
{url()}
</div>
)}
</Show>
</div>
<Show when={centerTab() === "preview"} fallback={<Editor file={currentFile()} />}>
<Show
when={previewUrl()}
fallback={
<div class="flex-1 flex items-center justify-center text-zinc-500">
<div class="text-center">
<p>No preview available yet.</p>
<p class="text-sm mt-2 opacity-60">Run build to detect a preview URL.</p>
</div>
</div>
}
>
{(url) => (
<iframe
class="flex-1 w-full h-full border-none bg-black"
src={url()}
title="App Preview"
sandbox="allow-scripts allow-same-origin allow-forms allow-pointer-lock allow-popups"
/>
)}
</Show>
</Show>
</div>
</Show>
<div
class="flex flex-col relative border-l border-white/5 min-h-0 overflow-hidden min-w-0"
style={{
width: isPhoneLayout() ? "100%" : `${chatPanelWidth()}px`,
"flex-shrink": isPhoneLayout() ? 1 : 0,
}}
>
<div
class="absolute -left-1 top-0 bottom-0 w-2 cursor-col-resize z-20 hover:bg-white/5 active:bg-white/10 transition-colors"
onMouseDown={handleResizeMouseDown("chat")}
/>
<Show when={isSoloOpen()}>
<div class="flex-1 flex flex-col min-h-0 relative">
<MultiTaskChat instanceId={props.instance.id} sessionId={activeSessionIdForInstance() || ""} />
</div>
</Show>
<div class="flex-1 flex flex-col relative min-h-0"
style={{ display: isSoloOpen() ? "none" : "flex" }}>
<Show
when={showingInfoView()}
fallback={
<Show
when={cachedSessionIds().length > 0 && activeSessionIdForInstance()}
fallback={
<div class="flex items-center justify-center h-full">
<div class="text-center text-zinc-500">
<p class="mb-2">No session selected</p>
<p class="text-sm">Select a session to view messages</p>
</div>
</div>
}
>
<For each={cachedSessionIds()}>
{(sessionId) => {
const isActive = () => activeSessionIdForInstance() === sessionId
return (
<div
class="session-cache-pane flex flex-col flex-1 min-h-0"
style={{ display: isActive() ? "flex" : "none" }}
data-session-id={sessionId}
aria-hidden={!isActive()}
>
<SessionView
sessionId={sessionId}
activeSessions={activeSessions()}
instanceId={props.instance.id}
instanceFolder={props.instance.folder}
escapeInDebounce={props.escapeInDebounce}
showSidebarToggle={showEmbeddedSidebarToggle()}
onSidebarToggle={() => setLeftOpen(true)}
forceCompactStatusLayout={showEmbeddedSidebarToggle()}
isActive={isActive()}
/>
</div>
)
}}
</For>
</Show>
}
>
<div class="info-view-pane flex flex-col flex-1 min-h-0 overflow-y-auto">
<InfoView instanceId={props.instance.id} />
</div>
</Show>
</div>
</div>
</div>
{/* Bottom Toolbar/Terminal Area */}
{/* Bottom Toolbar/Terminal Area */}
<div
class="flex flex-col border-t border-white/5 relative bg-[#09090b] z-10 shrink-0 overflow-hidden"
style={{
height: terminalOpen() ? `${terminalPanelHeight()}px` : "32px",
transition: activeResizeSide() === 'terminal' ? 'none' : 'height 0.2s cubic-bezier(0.4, 0, 0.2, 1)'
}}
>
<Show when={terminalOpen()}>
<div
class="absolute -top-1 left-0 right-0 h-2 cursor-row-resize z-20 hover:bg-white/5 active:bg-white/10 transition-colors"
onMouseDown={handleResizeMouseDown("terminal")}
/>
<div class="flex-1 min-h-0 overflow-hidden p-4 bg-[#0d0d0d]">
<div class="font-mono text-xs text-zinc-400">
<div class="mb-2 text-zinc-600">// Terminal functionality coming soon</div>
<div class="text-emerald-500/80 flex items-center gap-2">
<span></span>
<span>~</span>
<span class="animate-pulse">_</span>
</div>
</div>
</div>
</Show>
<footer class="h-8 flex items-center justify-between px-3 text-[10px] text-zinc-500 tracking-wide shrink-0 border-t border-white/5 bg-[#09090b]">
<div class="flex items-center space-x-4">
<button
class={`flex items-center space-x-1.5 cursor-pointer hover:text-zinc-300 transition-colors outline-none ${terminalOpen() ? 'text-indigo-400 font-bold' : ''}`}
onClick={() => setTerminalOpen(!terminalOpen())}
>
<TerminalIcon size={12} />
<span>TERMINAL</span>
</button>
</div>
<div class="flex items-center space-x-4 uppercase font-bold">
<div class="flex items-center space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_5px_rgba(34,197,94,0.5)]" />
<span>Sync Active</span>
</div>
<Show when={activeSessionForInstance()}>
{(session) => (
<>
<span class="hover:text-zinc-300 cursor-pointer">{session().model.modelId}</span>
<span class="hover:text-zinc-300 cursor-pointer">{session().agent}</span>
</>
)}
</Show>
</div>
</footer>
</div>
</Box>
{renderRightPanel()}
</Box>
{/* Floating Action Buttons removed - Integrated into Header */}
</div>
)
return (
<>
<div class="instance-shell2 flex flex-col flex-1 min-h-0">
<Show when={hasSessions()} fallback={<InstanceWelcomeView instance={props.instance} />}>
{sessionLayout}
</Show>
</div>
<CommandPalette
open={paletteOpen()}
onClose={() => hideCommandPalette(props.instance.id)}
commands={instancePaletteCommands()}
onExecute={props.onExecuteCommand}
/>
<AdvancedSettingsModal
open={showAdvancedSettings()}
onClose={() => setShowAdvancedSettings(false)}
selectedBinary={selectedBinary()}
onBinaryChange={(binary) => setSelectedBinary(binary)}
/>
</>
)
}
export default InstanceShell2