v0.5.0: Binary-Free Mode - No OpenCode binary required
✨ Major Features: - Native session management without OpenCode binary - Provider routing: OpenCode Zen (free), Qwen OAuth, Z.AI - Streaming chat with tool execution loop - Mode detection API (/api/meta/mode) - MCP integration fix (resolved infinite loading) - NomadArch Native option in UI with comparison info 🆓 Free Models (No API Key): - GPT-5 Nano (400K context) - Grok Code Fast 1 (256K context) - GLM-4.7 (205K context) - Doubao Seed Code (256K context) - Big Pickle (200K context) 📦 New Files: - session-store.ts: Native session persistence - native-sessions.ts: REST API for sessions - lite-mode.ts: UI mode detection client - native-sessions.ts (UI): SolidJS store 🔧 Updated: - All installers: Optional binary download - All launchers: Mode detection display - Binary selector: Added NomadArch Native option - README: Binary-Free Mode documentation
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
type Accessor,
|
||||
type Component,
|
||||
} from "solid-js"
|
||||
import toast from "solid-toast"
|
||||
import type { ToolState } from "@opencode-ai/sdk"
|
||||
import { Accordion } from "@kobalte/core"
|
||||
import { ChevronDown } from "lucide-solid"
|
||||
@@ -36,8 +37,11 @@ import {
|
||||
sessions,
|
||||
setActiveSession,
|
||||
executeCustomCommand,
|
||||
sendMessage,
|
||||
runShellCommand,
|
||||
} from "../../stores/sessions"
|
||||
import { compactSession } from "../../stores/session-actions";
|
||||
import { addTask, setActiveTask } from "../../stores/task-actions"
|
||||
import { keyboardRegistry, type KeyboardShortcut } from "../../lib/keyboard-registry"
|
||||
import { messageStoreBus } from "../../stores/message-v2/bus"
|
||||
import { clearSessionRenderCache } from "../message-block"
|
||||
@@ -54,14 +58,15 @@ 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"
|
||||
// Using rebuilt MultiX v2 with polling architecture (no freeze)
|
||||
import MultiTaskChat from "../chat/multix-v2"
|
||||
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 { Sparkles, Layout as LayoutIcon, Terminal as TerminalIcon, Search, Loader2, Zap, Shield, Settings, FileArchive } from "lucide-solid"
|
||||
import { formatTokenTotal } from "../../lib/formatters"
|
||||
import { sseManager } from "../../lib/sse-manager"
|
||||
import { getLogger } from "../../lib/logger"
|
||||
@@ -159,18 +164,32 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const [selectedBinary, setSelectedBinary] = createSignal("opencode")
|
||||
|
||||
// Handler to load file content when selected
|
||||
createEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).ACTIVE_INSTANCE_ID = props.instance.id;
|
||||
}
|
||||
});
|
||||
|
||||
const handleFileSelect = async (file: FileNode) => {
|
||||
try {
|
||||
const response = await serverApi.readWorkspaceFile(props.instance.id, file.path)
|
||||
const language = file.name.split('.').pop() || 'text'
|
||||
setCurrentFile({
|
||||
const updatedFile = {
|
||||
...file,
|
||||
content: response.contents,
|
||||
language,
|
||||
})
|
||||
}
|
||||
setCurrentFile(updatedFile)
|
||||
|
||||
// If it's a previewable file, update the preview URL
|
||||
if (file.name.endsWith('.html') || file.name.endsWith('.htm')) {
|
||||
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"
|
||||
const apiOrigin = origin.replace(":3000", ":9898")
|
||||
const url = `${apiOrigin}/api/workspaces/${props.instance.id}/serve/${file.path}`
|
||||
setPreviewUrl(url)
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Failed to read file content', error)
|
||||
// Still show the file but without content
|
||||
setCurrentFile(file)
|
||||
}
|
||||
}
|
||||
@@ -292,21 +311,55 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
if (typeof window === "undefined") return
|
||||
const handler = async (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ url?: string; instanceId?: string }>).detail
|
||||
console.log(`[InstanceShell2] Received BUILD_PREVIEW_EVENT`, {
|
||||
detail,
|
||||
currentInstanceId: props.instance.id,
|
||||
match: detail?.instanceId === props.instance.id
|
||||
});
|
||||
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",
|
||||
|
||||
// Auto-switch to preview mode for new AI content
|
||||
setCenterTab("preview")
|
||||
toast.success("Preview updated", {
|
||||
icon: '🚀',
|
||||
duration: 3000,
|
||||
position: 'bottom-center'
|
||||
})
|
||||
if (confirmed) {
|
||||
setCenterTab("preview")
|
||||
}
|
||||
}
|
||||
window.addEventListener(BUILD_PREVIEW_EVENT, handler)
|
||||
onCleanup(() => window.removeEventListener(BUILD_PREVIEW_EVENT, handler))
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const handler = async (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ code: string; fileName: string | null; instanceId: string }>).detail
|
||||
if (!detail || detail.instanceId !== props.instance.id) return
|
||||
|
||||
if (detail.fileName) {
|
||||
const origin = window.location.origin
|
||||
const apiOrigin = origin.includes(":3000") ? origin.replace(":3000", ":9898") : origin
|
||||
const url = `${apiOrigin}/api/workspaces/${props.instance.id}/serve/${detail.fileName}`
|
||||
setPreviewUrl(url)
|
||||
} else {
|
||||
const blob = new Blob([detail.code], { type: 'text/html' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
setPreviewUrl(url)
|
||||
}
|
||||
|
||||
setCenterTab("preview")
|
||||
toast.success("Previewing code block", {
|
||||
icon: '🔍',
|
||||
duration: 2000,
|
||||
position: 'bottom-center'
|
||||
})
|
||||
}
|
||||
window.addEventListener("MANUAL_PREVIEW_EVENT", handler)
|
||||
onCleanup(() => window.removeEventListener("MANUAL_PREVIEW_EVENT", handler))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
window.localStorage.setItem(LEFT_DRAWER_STORAGE_KEY, sessionSidebarWidth().toString())
|
||||
@@ -402,23 +455,90 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
showCommandPalette(props.instance.id)
|
||||
}
|
||||
|
||||
/* Compact Logic */
|
||||
const [isCompacting, setIsCompacting] = createSignal(false);
|
||||
const handleCompact = async () => {
|
||||
const sessionId = activeSessionIdForInstance();
|
||||
if (!sessionId || sessionId === "info" || isCompacting()) return;
|
||||
|
||||
setIsCompacting(true);
|
||||
const toastId = toast.loading("Compacting...", { icon: <FileArchive class="animate-pulse text-indigo-400" /> });
|
||||
try {
|
||||
await compactSession(props.instance.id, sessionId);
|
||||
toast.success("Session compacted!", { id: toastId });
|
||||
} catch (e) {
|
||||
toast.error("Failed to compact", { id: toastId });
|
||||
} finally {
|
||||
setIsCompacting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const [isFixing, setIsFixing] = createSignal(false)
|
||||
const [isBuilding, setIsBuilding] = createSignal(false)
|
||||
|
||||
const handleSmartFix = async () => {
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!sessionId || sessionId === "info" || isFixing()) {
|
||||
const parentSessionId = activeSessionIdForInstance()
|
||||
if (!parentSessionId || parentSessionId === "info" || isFixing()) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsFixing(true)
|
||||
const toastId = toast.loading("Smart Fix: Creating analysis task...", {
|
||||
icon: <Sparkles class="text-indigo-400 animate-spin" />
|
||||
});
|
||||
try {
|
||||
// Smart Fix targets the active task if available, otherwise general fix
|
||||
const session = activeSessionForInstance()
|
||||
const activeTaskId = session?.activeTaskId
|
||||
const args = activeTaskId ? `task:${activeTaskId}` : ""
|
||||
// ALWAYS create a dedicated "Smart Fix" task in the MultiX pipeline
|
||||
// This ensures the analysis and fixes appear in their own tab
|
||||
const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
||||
const taskResult = await addTask(
|
||||
props.instance.id,
|
||||
parentSessionId,
|
||||
`🔧 Smart Fix ${timestamp}`
|
||||
)
|
||||
|
||||
await executeCustomCommand(props.instance.id, sessionId, "fix", args)
|
||||
const targetSessionId = taskResult.taskSessionId || parentSessionId
|
||||
const taskId = taskResult.id
|
||||
|
||||
// Set this as the active task so the user sees it immediately
|
||||
setActiveTask(props.instance.id, parentSessionId, taskId)
|
||||
|
||||
toast.loading("Analyzing project...", { id: toastId });
|
||||
|
||||
// Use sendMessage to force visible feedback in the chat stream
|
||||
// Prompt enforces: Report → Plan → Approval → Execute workflow
|
||||
const smartFixPrompt = `**Smart Fix Analysis Request**
|
||||
|
||||
Please analyze this project for errors, bugs, warnings, or potential improvements.
|
||||
|
||||
**Your response MUST follow this exact format:**
|
||||
|
||||
1. **ANALYSIS RESULTS:**
|
||||
- If NO errors/issues found: Clearly state "✅ No errors or issues detected in the project."
|
||||
- If errors/issues ARE found: List each issue with file path and line number if applicable.
|
||||
|
||||
2. **FIX PLAN (only if issues found):**
|
||||
For each issue, outline:
|
||||
- What the problem is
|
||||
- How you will fix it
|
||||
- Which files will be modified
|
||||
|
||||
3. **AWAIT APPROVAL:**
|
||||
After presenting the plan, explicitly ask: "Do you approve this fix plan? Reply 'yes' to proceed, or provide feedback for adjustments."
|
||||
|
||||
4. **EXECUTION (only after I say 'yes'):**
|
||||
Only apply fixes after receiving explicit approval. Use write_file tool to make changes.
|
||||
|
||||
Now analyze the project and report your findings.`
|
||||
|
||||
await sendMessage(
|
||||
props.instance.id,
|
||||
targetSessionId,
|
||||
smartFixPrompt,
|
||||
[],
|
||||
taskId
|
||||
)
|
||||
|
||||
toast.success("Smart Fix task created. Check the pipeline.", { id: toastId, duration: 3000 });
|
||||
|
||||
// Auto-open right panel to show agent progress if it's not open
|
||||
if (!rightOpen()) {
|
||||
@@ -427,6 +547,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to run Smart Fix command", error)
|
||||
toast.error("Smart Fix failed to start", { id: toastId });
|
||||
} finally {
|
||||
setTimeout(() => setIsFixing(false), 2000) // Reset after delay
|
||||
}
|
||||
@@ -1180,7 +1301,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
|
||||
const sessionLayout = (
|
||||
<div
|
||||
class="session-shell-panels flex flex-col flex-1 min-h-0 overflow-x-hidden relative bg-[#050505]"
|
||||
class="session-shell-panels flex flex-col flex-1 min-h-0 w-full overflow-hidden relative bg-[#050505]"
|
||||
ref={(element) => {
|
||||
setDrawerHost(element)
|
||||
measureDrawerHost()
|
||||
@@ -1190,8 +1311,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
<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]">
|
||||
<AppBar position="sticky" color="default" elevation={0} class="border-b border-white/5 bg-[#050505]/80 backdrop-blur-md z-20 shrink-0">
|
||||
<Toolbar variant="dense" class="session-toolbar flex items-center justify-between gap-2 py-0 min-h-[48px]">
|
||||
<div class="flex items-center space-x-4">
|
||||
<IconButton
|
||||
ref={setLeftToggleButtonEl}
|
||||
@@ -1221,6 +1342,19 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
<div class="flex items-center space-x-4">
|
||||
<Show when={activeSessionIdForInstance() && activeSessionIdForInstance() !== "info"}>
|
||||
<div class="flex items-center space-x-2">
|
||||
{/* Compact Button */}
|
||||
<button
|
||||
onClick={handleCompact}
|
||||
disabled={isCompacting()}
|
||||
class="flex items-center gap-1.5 px-2.5 py-1 text-[11px] font-semibold text-cyan-400 bg-cyan-500/10 border border-cyan-500/20 hover:bg-cyan-500/20 hover:border-cyan-500/40 transition-all rounded-full"
|
||||
title="Compact Context: Summarize conversation to save tokens"
|
||||
>
|
||||
<Show when={isCompacting()} fallback={<FileArchive size={14} strokeWidth={2} />}>
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
</Show>
|
||||
<span>Compact</span>
|
||||
</button>
|
||||
|
||||
<ModelStatusSelector
|
||||
instanceId={props.instance.id}
|
||||
sessionId={activeSessionIdForInstance()!}
|
||||
@@ -1246,14 +1380,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
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"}`}
|
||||
class={`transition-all flex items-center space-x-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-tight ${isFixing() ? "text-blue-500 smart-fix-highlight bg-blue-500/10" : "text-zinc-400 hover:text-white hover:bg-white/5"}`}
|
||||
>
|
||||
<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>
|
||||
<Zap size={12} class={isFixing() ? "animate-bounce" : ""} />
|
||||
<span>Fix</span>
|
||||
</button>
|
||||
<div class="w-px h-3 bg-white/10" />
|
||||
<button
|
||||
@@ -1303,11 +1433,11 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
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'
|
||||
class={`flex items-center space-x-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-tight 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>
|
||||
<LayoutIcon size={12} />
|
||||
<span>MultiX</span>
|
||||
</button>
|
||||
<IconButton
|
||||
ref={setRightToggleButtonEl}
|
||||
@@ -1323,146 +1453,67 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Box sx={{ display: "flex", flex: 1, minHeight: 0, overflowX: "hidden", position: "relative", zIndex: 10 }}>
|
||||
<Box sx={{ display: "flex", flex: 1, minHeight: 0, width: "100%", overflow: "hidden", position: "relative", zIndex: 10 }}>
|
||||
{renderLeftPanel()}
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
sx={{ flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column", overflowX: "hidden" }}
|
||||
class="content-area relative"
|
||||
component="div"
|
||||
sx={{ flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}
|
||||
class="content-area relative bg-[#050505]"
|
||||
>
|
||||
<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() || "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>
|
||||
}
|
||||
{/* Main workspace area */}
|
||||
<div class="flex-1 flex flex-row min-h-0 w-full overflow-hidden">
|
||||
{/* Center Area (Editor/Preview) */}
|
||||
<div class="flex-1 flex flex-col min-h-0 bg-[#0d0d0d] overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-white/5 bg-[#111112]">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => setCenterTab("code")}
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
{(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>
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCenterTab("preview")}
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
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>
|
||||
|
||||
<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() || ""} />
|
||||
<Show when={centerTab() === "preview"} fallback={<Editor file={currentFile()} />}>
|
||||
<div class="flex-1 min-h-0 bg-white">
|
||||
<iframe
|
||||
src={previewUrl() || "about:blank"}
|
||||
class="w-full h-full border-none"
|
||||
title="Preview"
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Right Panel (MultiX Chat) */}
|
||||
<Show when={rightOpen() && isSoloOpen()}>
|
||||
<div class="flex flex-col relative border-l border-white/5 min-h-0 overflow-hidden" style={{ width: `${chatPanelWidth()}px`, "flex-shrink": 0 }}>
|
||||
<MultiTaskChat instanceId={props.instance.id} sessionId={activeSessionIdForInstance()!} />
|
||||
</div>
|
||||
</Show>
|
||||
</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={{
|
||||
@@ -1502,23 +1553,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
<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>
|
||||
)
|
||||
|
||||
|
||||
@@ -55,9 +55,9 @@ const getFileIcon = (fileName: string) => {
|
||||
return <FileCode size={16} class="text-blue-300" />
|
||||
}
|
||||
|
||||
const FileTree: Component<{
|
||||
node: FileNode;
|
||||
depth: number;
|
||||
const FileTree: Component<{
|
||||
node: FileNode;
|
||||
depth: number;
|
||||
onSelect: (f: FileNode) => void;
|
||||
instanceId: string;
|
||||
}> = (props) => {
|
||||
@@ -69,7 +69,7 @@ const FileTree: Component<{
|
||||
if (props.node.type === "directory") {
|
||||
const nextOpen = !isOpen()
|
||||
setIsOpen(nextOpen)
|
||||
|
||||
|
||||
if (nextOpen && children().length === 0) {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
@@ -173,6 +173,11 @@ export const Sidebar: Component<SidebarProps> = (props) => {
|
||||
if (typeof window === "undefined") return
|
||||
const handler = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ instanceId?: string }>).detail
|
||||
console.log(`[Sidebar] Received FILE_CHANGE_EVENT`, {
|
||||
detail,
|
||||
currentInstanceId: props.instanceId,
|
||||
match: detail?.instanceId === props.instanceId
|
||||
});
|
||||
if (!detail || detail.instanceId !== props.instanceId) return
|
||||
void refreshRootFiles()
|
||||
}
|
||||
@@ -316,18 +321,18 @@ export const Sidebar: Component<SidebarProps> = (props) => {
|
||||
</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>
|
||||
<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>
|
||||
<Show when={activeTab() === "search"}>
|
||||
<div class="flex flex-col gap-3">
|
||||
@@ -473,11 +478,10 @@ export const Sidebar: Component<SidebarProps> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSkillSelection(skill.id)}
|
||||
class={`w-full text-left px-3 py-2 rounded-md border transition-colors ${
|
||||
isSelected()
|
||||
class={`w-full text-left px-3 py-2 rounded-md border transition-colors ${isSelected()
|
||||
? "border-blue-500/60 bg-blue-500/10 text-blue-200"
|
||||
: "border-white/10 bg-white/5 text-zinc-300 hover:text-white"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div class="text-xs font-semibold">{skill.name}</div>
|
||||
<Show when={skill.description}>
|
||||
|
||||
Reference in New Issue
Block a user