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:
Gemini AI
2025-12-23 13:18:37 +04:00
Unverified
parent 157449a9ad
commit c4ac079660
47 changed files with 4550 additions and 527 deletions

View File

@@ -0,0 +1,52 @@
import { Component, For, Show } from "solid-js"
import { FileNode } from "./sidebar"
interface EditorProps {
file: FileNode | null
}
export const Editor: Component<EditorProps> = (props) => {
return (
<Show
when={props.file}
fallback={
<div class="flex-1 flex items-center justify-center text-zinc-500 bg-[#0d0d0d]">
<div class="text-center">
<div class="mb-4 opacity-20 flex justify-center">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
</svg>
</div>
<p>Select a file to start editing</p>
<p class="text-sm mt-2 opacity-60">Press Ctrl+P to search</p>
</div>
</div>
}
>
<div class="flex-1 overflow-hidden flex flex-col bg-[#0d0d0d]">
<div class="h-10 glass border-b border-white/5 flex items-center px-4 space-x-2 shrink-0">
<span class="text-xs text-zinc-400 font-medium">{props.file?.name}</span>
<span class="text-[10px] text-zinc-600 uppercase">{props.file?.language || "text"}</span>
</div>
<div class="flex-1 p-6 overflow-auto mono text-sm leading-relaxed">
<pre class="text-zinc-300">
<Show
when={props.file?.content}
fallback={<span class="italic text-zinc-600">// Empty file</span>}
>
<For each={props.file?.content?.split("\n")}>
{(line, i) => (
<div class="flex group">
<span class="w-12 text-zinc-600 select-none text-right pr-4">{i() + 1}</span>
<span class="whitespace-pre">{line}</span>
</div>
)}
</For>
</Show>
</pre>
</div>
</div>
</Show>
)
}

View File

@@ -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>
)

View File

@@ -0,0 +1,203 @@
import { Component, createSignal, For, Show, createEffect } from "solid-js"
import {
Files,
Search,
GitBranch,
Play,
Settings,
ChevronRight,
ChevronDown,
Folder,
User,
FileCode,
FileJson,
FileText,
Image as ImageIcon,
} from "lucide-solid"
import { serverApi } from "../../lib/api-client"
export interface FileNode {
name: string
type: "file" | "directory"
path: string
language?: string
content?: string
children?: FileNode[]
}
interface SidebarProps {
instanceId: string
onFileSelect: (file: FileNode) => void
isOpen: boolean
sessions: any[] // Existing sessions to display in one of the tabs
activeSessionId?: string
onSessionSelect: (id: string) => void
}
const getFileIcon = (fileName: string) => {
if (fileName.endsWith(".tsx") || fileName.endsWith(".ts"))
return <FileCode size={16} class="text-blue-400" />
if (fileName.endsWith(".json")) return <FileJson size={16} class="text-yellow-400" />
if (fileName.endsWith(".md")) return <FileText size={16} class="text-gray-400" />
if (fileName.endsWith(".png") || fileName.endsWith(".jpg"))
return <ImageIcon size={16} class="text-purple-400" />
return <FileCode size={16} class="text-blue-300" />
}
const FileTree: Component<{
node: FileNode;
depth: number;
onSelect: (f: FileNode) => void;
instanceId: string;
}> = (props) => {
const [isOpen, setIsOpen] = createSignal(props.depth === 0)
const [children, setChildren] = createSignal<FileNode[]>([])
const [isLoading, setIsLoading] = createSignal(false)
const handleClick = async () => {
if (props.node.type === "directory") {
const nextOpen = !isOpen()
setIsOpen(nextOpen)
if (nextOpen && children().length === 0) {
setIsLoading(true)
try {
const entries = await serverApi.listWorkspaceFiles(props.instanceId, props.node.path)
setChildren(entries.map(e => ({
name: e.name,
type: e.type,
path: e.path
})))
} catch (e) {
console.error("Failed to list files", e)
} finally {
setIsLoading(false)
}
}
} else {
props.onSelect(props.node)
}
}
return (
<div>
<div
onClick={handleClick}
class={`flex items-center py-1 px-2 cursor-pointer hover:bg-white/5 text-zinc-400 text-sm transition-colors rounded ${props.depth > 0 ? "ml-2" : ""}`}
>
<span class="mr-1 w-4 flex justify-center">
<Show when={props.node.type === "directory"}>
<Show when={isOpen()} fallback={<ChevronRight size={14} />}>
<ChevronDown size={14} />
</Show>
</Show>
</span>
<span class="mr-2">
<Show
when={props.node.type === "directory"}
fallback={getFileIcon(props.node.name)}
>
<Folder size={14} class="text-blue-500/80" />
</Show>
</span>
<span class={props.node.type === "directory" ? "font-medium" : ""}>{props.node.name}</span>
<Show when={isLoading()}>
<span class="ml-2 w-3 h-3 border border-blue-500/30 border-t-blue-500 rounded-full animate-spin" />
</Show>
</div>
<Show when={props.node.type === "directory" && isOpen()}>
<div class="border-l border-white/5 ml-3">
<For each={children()}>
{(child) => <FileTree node={child} depth={props.depth + 1} onSelect={props.onSelect} instanceId={props.instanceId} />}
</For>
</div>
</Show>
</div>
)
}
export const Sidebar: Component<SidebarProps> = (props) => {
const [activeTab, setActiveTab] = createSignal("files")
const [rootFiles, setRootFiles] = createSignal<FileNode[]>([])
createEffect(async () => {
if (props.instanceId) {
try {
const entries = await serverApi.listWorkspaceFiles(props.instanceId, ".")
setRootFiles(entries.map(e => ({
name: e.name,
type: e.type,
path: e.path
})))
} catch (e) {
console.error("Failed to load root files", e)
}
}
})
return (
<div
class={`flex bg-[#111111] border-r border-white/5 transition-all duration-300 ease-in-out h-full ${props.isOpen ? "w-72" : "w-0 overflow-hidden"}`}
>
{/* Activity Bar */}
<div class="w-14 border-r border-white/5 flex flex-col items-center py-4 space-y-6 shrink-0">
<For
each={[
{ id: "files", icon: Files },
{ id: "sessions", icon: User },
{ id: "search", icon: Search },
{ id: "git", icon: GitBranch },
{ id: "debug", icon: Play },
]}
>
{(item) => (
<button
onClick={() => setActiveTab(item.id)}
class={`p-2 transition-all duration-200 relative ${activeTab() === item.id ? "text-white" : "text-zinc-500 hover:text-zinc-300"}`}
>
<item.icon size={22} strokeWidth={1.5} />
<Show when={activeTab() === item.id}>
<div class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-6 bg-blue-500 rounded-r-full shadow-[0_0_10px_rgba(59,130,246,0.5)]" />
</Show>
</button>
)}
</For>
<div class="mt-auto pb-2">
<button class="text-zinc-500 hover:text-white transition-colors">
<Settings size={22} strokeWidth={1.5} />
</button>
</div>
</div>
{/* Side Pane */}
<div class="flex-1 flex flex-col py-3 min-w-0">
<div class="px-4 mb-4 flex items-center justify-between">
<h2 class="text-[10px] uppercase font-bold text-zinc-500 tracking-wider">
{activeTab() === "files" ? "Explorer" : activeTab() === "sessions" ? "Sessions" : activeTab()}
</h2>
</div>
<div class="flex-1 overflow-auto px-2">
<Show when={activeTab() === "files"}>
<For each={rootFiles()}>
{(node) => <FileTree node={node} depth={0} onSelect={props.onFileSelect} instanceId={props.instanceId} />}
</For>
</Show>
<Show when={activeTab() === "sessions"}>
<div class="flex flex-col gap-1">
<For each={props.sessions}>
{(session) => (
<div
onClick={() => props.onSessionSelect(session.id)}
class={`px-3 py-1.5 rounded cursor-pointer text-sm transition-colors ${props.activeSessionId === session.id ? 'bg-blue-600/20 text-blue-400 border border-blue-500/20' : 'text-zinc-400 hover:bg-white/5'}`}
>
{session.title || session.id.slice(0, 8)}
</div>
)}
</For>
</div>
</Show>
</div>
</div>
</div>
)
}