restore: bring back all custom UI enhancements from checkpoint
Restored from commit 52be710 (checkpoint before qwen oauth + todo roller): Enhanced UI Features: - SMART FIX button with AI code analysis - APEX (Autonomous Programming EXecution) mode - SHIELD (Auto-approval) mode - MULTIX MODE multi-task pipeline interface - Live streaming token counter - Thinking indicator with bouncing dots animation Components restored: - packages/ui/src/components/chat/multi-task-chat.tsx - packages/ui/src/components/instance/instance-shell2.tsx - packages/ui/src/components/settings/OllamaCloudSettings.tsx - packages/ui/src/components/settings/QwenCodeSettings.tsx - packages/ui/src/stores/solo-store.ts - packages/ui/src/stores/task-actions.ts - packages/ui/src/stores/session-events.ts (autonomous mode) - packages/server/src/integrations/ollama-cloud.ts - packages/server/src/server/routes/ollama.ts - packages/server/src/server/routes/qwen.ts This ensures all custom features are preserved in source control.
This commit is contained in:
@@ -34,6 +34,8 @@ import {
|
||||
getSessionFamily,
|
||||
getSessionInfo,
|
||||
setActiveSession,
|
||||
executeCustomCommand,
|
||||
runShellCommand,
|
||||
} from "../../stores/sessions"
|
||||
import { keyboardRegistry, type KeyboardShortcut } from "../../lib/keyboard-registry"
|
||||
import { messageStoreBus } from "../../stores/message-v2/bus"
|
||||
@@ -48,14 +50,25 @@ 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 } from "lucide-solid"
|
||||
import { formatTokenTotal } from "../../lib/formatters"
|
||||
import { sseManager } from "../../lib/sse-manager"
|
||||
import { getLogger } from "../../lib/logger"
|
||||
import {
|
||||
getSoloState,
|
||||
toggleAutonomous,
|
||||
toggleAutoApproval,
|
||||
} from "../../stores/solo-store"
|
||||
import {
|
||||
SESSION_SIDEBAR_EVENT,
|
||||
type SessionSidebarRequestAction,
|
||||
@@ -128,7 +141,26 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null)
|
||||
const [resizeStartX, setResizeStartX] = createSignal(0)
|
||||
const [resizeStartWidth, setResizeStartWidth] = createSignal(0)
|
||||
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>(["lsp", "mcp"])
|
||||
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>(["lsp", "mcp", "plan"])
|
||||
const [currentFile, setCurrentFile] = createSignal<FileNode | null>(null)
|
||||
const [isSoloOpen, setIsSoloOpen] = createSignal(true)
|
||||
|
||||
// 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))
|
||||
|
||||
@@ -326,6 +358,58 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
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()])
|
||||
@@ -607,7 +691,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
})
|
||||
|
||||
type DrawerViewState = "pinned" | "floating-open" | "floating-closed"
|
||||
|
||||
|
||||
|
||||
const leftDrawerState = createMemo<DrawerViewState>(() => {
|
||||
if (leftPinned()) return "pinned"
|
||||
@@ -648,7 +732,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
|
||||
|
||||
|
||||
const pinLeftDrawer = () => {
|
||||
const pinLeftDrawer = () => {
|
||||
blurIfInside(leftDrawerContentEl())
|
||||
batch(() => {
|
||||
setLeftPinned(true)
|
||||
@@ -707,11 +791,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
if (state === "pinned") return
|
||||
if (state === "floating-closed") {
|
||||
setRightOpen(true)
|
||||
setIsSoloOpen(false)
|
||||
measureDrawerHost()
|
||||
return
|
||||
}
|
||||
blurIfInside(rightDrawerContentEl())
|
||||
setRightOpen(false)
|
||||
setIsSoloOpen(false)
|
||||
focusTarget(rightToggleButtonEl())
|
||||
measureDrawerHost()
|
||||
}
|
||||
@@ -757,90 +843,27 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
}
|
||||
|
||||
const LeftDrawerContent = () => (
|
||||
<div class="flex flex-col h-full min-h-0" ref={setLeftDrawerContentEl}>
|
||||
<div class="flex items-start justify-between gap-2 px-4 py-3 border-b border-base">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">Sessions</span>
|
||||
<div class="session-sidebar-shortcuts">
|
||||
<Show when={keyboardShortcuts().length}>
|
||||
<KeyboardHint shortcuts={keyboardShortcuts()} separator=" " showDescription={false} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={leftPinned() ? "Unpin left drawer" : "Pin left drawer"}
|
||||
onClick={() => (leftPinned() ? unpinLeftDrawer() : pinLeftDrawer())}
|
||||
>
|
||||
{leftPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="session-sidebar flex flex-col flex-1 min-h-0">
|
||||
<SessionList
|
||||
instanceId={props.instance.id}
|
||||
sessions={activeSessions()}
|
||||
activeSessionId={activeSessionIdForInstance()}
|
||||
onSelect={handleSessionSelect}
|
||||
onClose={(id) => {
|
||||
const result = props.onCloseSession(id)
|
||||
if (result instanceof Promise) {
|
||||
void result.catch((error) => log.error("Failed to close session:", error))
|
||||
}
|
||||
}}
|
||||
onNew={() => {
|
||||
const result = props.onNewSession()
|
||||
if (result instanceof Promise) {
|
||||
void result.catch((error) => log.error("Failed to create session:", error))
|
||||
}
|
||||
}}
|
||||
showHeader={false}
|
||||
showFooter={false}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<Show when={activeSessionForInstance()}>
|
||||
{(activeSession) => (
|
||||
<>
|
||||
<ContextUsagePanel instanceId={props.instance.id} sessionId={activeSession().id} />
|
||||
<div class="session-sidebar-controls px-4 py-4 border-t border-base flex flex-col gap-3">
|
||||
<AgentSelector
|
||||
instanceId={props.instance.id}
|
||||
sessionId={activeSession().id}
|
||||
currentAgent={activeSession().agent}
|
||||
onAgentChange={(agent) => props.handleSidebarAgentChange(activeSession().id, agent)}
|
||||
/>
|
||||
|
||||
<div class="sidebar-selector-hints" aria-hidden="true">
|
||||
<span class="hint sidebar-selector-hint sidebar-selector-hint--left">
|
||||
<Kbd shortcut="cmd+shift+a" />
|
||||
</span>
|
||||
<span class="hint sidebar-selector-hint sidebar-selector-hint--right">
|
||||
<Kbd shortcut="cmd+shift+m" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ModelSelector
|
||||
instanceId={props.instance.id}
|
||||
sessionId={activeSession().id}
|
||||
currentModel={activeSession().model}
|
||||
onModelChange={(model) => props.handleSidebarModelChange(activeSession().id, model)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Sidebar
|
||||
instanceId={props.instance.id}
|
||||
isOpen={leftOpen()}
|
||||
onFileSelect={handleFileSelect}
|
||||
sessions={Array.from(activeSessions().values())}
|
||||
activeSessionId={activeSessionIdForInstance() || undefined}
|
||||
onSessionSelect={handleSessionSelect}
|
||||
/>
|
||||
)
|
||||
|
||||
const RightDrawerContent = () => {
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
|
||||
if (sessionId && sessionId !== "info") {
|
||||
return (
|
||||
<div class="flex flex-col h-full" ref={setRightDrawerContentEl}>
|
||||
<MultiTaskChat instanceId={props.instance.id} sessionId={sessionId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPlanSectionContent = () => {
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!sessionId || sessionId === "info") {
|
||||
@@ -1011,6 +1034,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
|
||||
|
||||
const renderRightPanel = () => {
|
||||
if (isSoloOpen()) return null; // MultiX Mode uses the main stream area
|
||||
|
||||
if (rightPinned()) {
|
||||
return (
|
||||
<Box
|
||||
@@ -1075,215 +1100,239 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
|
||||
const sessionLayout = (
|
||||
<div
|
||||
class="session-shell-panels flex flex-col flex-1 min-h-0 overflow-x-hidden"
|
||||
class="session-shell-panels flex flex-col flex-1 min-h-0 overflow-x-hidden relative bg-[#050505]"
|
||||
ref={(element) => {
|
||||
setDrawerHost(element)
|
||||
measureDrawerHost()
|
||||
}}
|
||||
>
|
||||
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
||||
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
||||
<Show
|
||||
when={!isPhoneLayout()}
|
||||
fallback={
|
||||
<div class="flex flex-col w-full gap-1.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
||||
<IconButton
|
||||
ref={setLeftToggleButtonEl}
|
||||
color="inherit"
|
||||
onClick={handleLeftAppBarButtonClick}
|
||||
aria-label={leftAppBarButtonLabel()}
|
||||
size="small"
|
||||
aria-expanded={leftDrawerState() !== "floating-closed"}
|
||||
disabled={leftDrawerState() === "pinned"}
|
||||
>
|
||||
{leftAppBarButtonIcon()}
|
||||
</IconButton>
|
||||
{/* 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" />
|
||||
|
||||
<div class="flex flex-wrap items-center gap-1 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
class="connection-status-button px-2 py-0.5 text-xs"
|
||||
onClick={handleCommandPaletteClick}
|
||||
aria-label="Open command palette"
|
||||
style={{ flex: "0 0 auto", width: "auto" }}
|
||||
>
|
||||
Command Palette
|
||||
</button>
|
||||
<span class="connection-status-shortcut-hint">
|
||||
<Kbd shortcut="cmd+shift+p" />
|
||||
</span>
|
||||
<span
|
||||
class={`status-indicator ${connectionStatusClass()}`}
|
||||
aria-label={`Connection ${connectionStatus()}`}
|
||||
>
|
||||
<span class="status-dot" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
ref={setRightToggleButtonEl}
|
||||
color="inherit"
|
||||
onClick={handleRightAppBarButtonClick}
|
||||
aria-label={rightAppBarButtonLabel()}
|
||||
size="small"
|
||||
aria-expanded={rightDrawerState() !== "floating-closed"}
|
||||
disabled={rightDrawerState() === "pinned"}
|
||||
>
|
||||
{rightAppBarButtonIcon()}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
||||
<span class="uppercase text-[10px] tracking-wide text-primary/70">Used</span>
|
||||
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
||||
<span class="uppercase text-[10px] tracking-wide text-primary/70">Avail</span>
|
||||
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="session-toolbar-left flex items-center gap-3 min-w-0">
|
||||
<IconButton
|
||||
ref={setLeftToggleButtonEl}
|
||||
color="inherit"
|
||||
onClick={handleLeftAppBarButtonClick}
|
||||
aria-label={leftAppBarButtonLabel()}
|
||||
size="small"
|
||||
aria-expanded={leftDrawerState() !== "floating-closed"}
|
||||
disabled={leftDrawerState() === "pinned"}
|
||||
>
|
||||
{leftAppBarButtonIcon()}
|
||||
</IconButton>
|
||||
|
||||
<Show when={!showingInfoView()}>
|
||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
||||
<span class="uppercase text-[10px] tracking-wide text-primary/70">Used</span>
|
||||
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
||||
<span class="uppercase text-[10px] tracking-wide text-primary/70">Avail</span>
|
||||
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="session-toolbar-center flex-1 flex items-center justify-center gap-2 min-w-[160px]">
|
||||
<button
|
||||
type="button"
|
||||
class="connection-status-button px-2 py-0.5 text-xs"
|
||||
onClick={handleCommandPaletteClick}
|
||||
aria-label="Open command palette"
|
||||
style={{ flex: "0 0 auto", width: "auto" }}
|
||||
>
|
||||
Command Palette
|
||||
</button>
|
||||
<span class="connection-status-shortcut-hint">
|
||||
<Kbd shortcut="cmd+shift+p" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="session-toolbar-right flex items-center gap-3">
|
||||
<div class="connection-status-meta flex items-center gap-3">
|
||||
<Show when={connectionStatus() === "connected"}>
|
||||
<span class="status-indicator connected">
|
||||
<span class="status-dot" />
|
||||
<span class="status-text">Connected</span>
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={connectionStatus() === "connecting"}>
|
||||
<span class="status-indicator connecting">
|
||||
<span class="status-dot" />
|
||||
<span class="status-text">Connecting...</span>
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={connectionStatus() === "error" || connectionStatus() === "disconnected"}>
|
||||
<span class="status-indicator disconnected">
|
||||
<span class="status-dot" />
|
||||
<span class="status-text">Disconnected</span>
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<IconButton
|
||||
ref={setRightToggleButtonEl}
|
||||
color="inherit"
|
||||
onClick={handleRightAppBarButtonClick}
|
||||
aria-label={rightAppBarButtonLabel()}
|
||||
size="small"
|
||||
aria-expanded={rightDrawerState() !== "floating-closed"}
|
||||
disabled={rightDrawerState() === "pinned"}
|
||||
>
|
||||
{rightAppBarButtonIcon()}
|
||||
</IconButton>
|
||||
<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>
|
||||
</Show>
|
||||
</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"}>
|
||||
<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)
|
||||
}}
|
||||
/>
|
||||
</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"
|
||||
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"
|
||||
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>
|
||||
|
||||
{/* SOLO 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="Toggle Autonomous Mode (SOLO)"
|
||||
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">SOLO</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleAutoApproval(props.instance.id)}
|
||||
title="Toggle Auto-Approval (SHIELD)"
|
||||
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" }}>
|
||||
<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"
|
||||
class="content-area relative"
|
||||
>
|
||||
<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-gray-500 dark:text-gray-400">
|
||||
<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>
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<Editor file={currentFile()} />
|
||||
|
||||
<div class="flex-1 flex flex-col relative border-l border-white/5">
|
||||
<Show when={isSoloOpen()}>
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
<MultiTaskChat instanceId={props.instance.id} sessionId={activeSessionIdForInstance() || ""} />
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div class="info-view-pane flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||
<InfoView instanceId={props.instance.id} />
|
||||
|
||||
<div class="flex-1 flex flex-col relative"
|
||||
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>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Bottom Toolbar/Terminal Area */}
|
||||
<footer class="h-8 glass border-t border-white/5 flex items-center justify-between px-3 text-[10px] text-zinc-500 tracking-wide z-10 shrink-0">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-1.5 cursor-pointer hover:text-zinc-300">
|
||||
<TerminalIcon size={12} />
|
||||
<span>TERMINAL</span>
|
||||
</div>
|
||||
</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>
|
||||
</Box>
|
||||
|
||||
{renderRightPanel()}
|
||||
</Box>
|
||||
|
||||
{/* Floating Action Buttons removed - Integrated into Header */}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user