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:
Gemini AI
2025-12-26 02:08:13 +04:00
Unverified
parent 8dddf4d0cf
commit 4bd2893864
83 changed files with 10678 additions and 1290 deletions

View File

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

View File

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