Add skills catalog and sidebar tooling

This commit is contained in:
Gemini AI
2025-12-24 14:23:51 +04:00
Unverified
parent d153892bdf
commit f9748391a9
13 changed files with 1178 additions and 106 deletions

View File

@@ -33,6 +33,7 @@ import {
activeSessionId as activeSessionMap,
getSessionFamily,
getSessionInfo,
sessions,
setActiveSession,
executeCustomCommand,
runShellCommand,
@@ -60,10 +61,11 @@ 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 { Sparkles, Layout as LayoutIcon, Terminal as TerminalIcon, Search, Loader2, Zap, Shield, Settings } from "lucide-solid"
import { formatTokenTotal } from "../../lib/formatters"
import { sseManager } from "../../lib/sse-manager"
import { getLogger } from "../../lib/logger"
import AdvancedSettingsModal from "../advanced-settings-modal"
import {
getSoloState,
toggleAutonomous,
@@ -138,12 +140,19 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
const [rightDrawerContentEl, setRightDrawerContentEl] = createSignal<HTMLElement | null>(null)
const [leftToggleButtonEl, setLeftToggleButtonEl] = createSignal<HTMLElement | null>(null)
const [rightToggleButtonEl, setRightToggleButtonEl] = createSignal<HTMLElement | null>(null)
const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null)
const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | "chat" | "terminal" | null>(null)
const [resizeStartX, setResizeStartX] = createSignal(0)
const [resizeStartWidth, setResizeStartWidth] = createSignal(0)
const [resizeStartY, setResizeStartY] = createSignal(0)
const [resizeStartHeight, setResizeStartHeight] = createSignal(0)
const [chatPanelWidth, setChatPanelWidth] = createSignal(600)
const [terminalPanelHeight, setTerminalPanelHeight] = createSignal(200)
const [terminalOpen, setTerminalOpen] = createSignal(false)
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>(["lsp", "mcp", "plan"])
const [currentFile, setCurrentFile] = createSignal<FileNode | null>(null)
const [isSoloOpen, setIsSoloOpen] = createSignal(true)
const [showAdvancedSettings, setShowAdvancedSettings] = createSignal(false)
const [selectedBinary, setSelectedBinary] = createSignal("opencode")
// Handler to load file content when selected
const handleFileSelect = async (file: FileNode) => {
@@ -264,6 +273,17 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
onCleanup(() => window.removeEventListener(SESSION_SIDEBAR_EVENT, handler))
})
onMount(() => {
if (typeof window === "undefined") return
const handler = (event: Event) => {
const detail = (event as CustomEvent<{ instanceId: string; sessionId: string }>).detail
if (!detail || detail.instanceId !== props.instance.id) return
setShowAdvancedSettings(true)
}
window.addEventListener("open-advanced-settings", handler)
onCleanup(() => window.removeEventListener("open-advanced-settings", handler))
})
createEffect(() => {
if (typeof window === "undefined") return
window.localStorage.setItem(LEFT_DRAWER_STORAGE_KEY, sessionSidebarWidth().toString())
@@ -297,7 +317,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
const activeSessionForInstance = createMemo(() => {
const sessionId = activeSessionIdForInstance()
if (!sessionId || sessionId === "info") return null
return activeSessions().get(sessionId) ?? null
const instanceSessions = sessions().get(props.instance.id)
return instanceSessions?.get(sessionId) ?? null
})
const activeSessionUsage = createMemo(() => {
@@ -626,14 +647,35 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
scheduleDrawerMeasure()
}
const handleDrawerPointerMove = (clientX: number) => {
const applyPanelSize = (type: "chat" | "terminal", size: number) => {
if (type === "chat") {
setChatPanelWidth(size)
} else {
setTerminalPanelHeight(size)
}
}
const handlePointerMove = (clientX: number, clientY: number) => {
const side = activeResizeSide()
if (!side) return
const startWidth = resizeStartWidth()
const clamp = side === "left" ? clampWidth : clampRightWidth
const delta = side === "left" ? clientX - resizeStartX() : resizeStartX() - clientX
const nextWidth = clamp(startWidth + delta)
applyDrawerWidth(side, nextWidth)
if (side === "left" || side === "right") {
const startWidth = resizeStartWidth()
const clamp = side === "left" ? clampWidth : clampRightWidth
const delta = side === "left" ? clientX - resizeStartX() : resizeStartX() - clientX
const nextWidth = clamp(startWidth + delta)
applyDrawerWidth(side, nextWidth)
} else if (side === "chat") {
const startWidth = resizeStartWidth()
const delta = resizeStartX() - clientX // Dragging left increases width
const nextWidth = Math.max(300, Math.min(window.innerWidth - 300, startWidth + delta))
applyPanelSize("chat", nextWidth)
} else if (side === "terminal") {
const startHeight = resizeStartHeight()
const delta = resizeStartY() - clientY // Dragging up increases height
const nextHeight = Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta))
applyPanelSize("terminal", nextHeight)
}
}
function stopDrawerResize() {
@@ -646,7 +688,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
function drawerMouseMove(event: MouseEvent) {
event.preventDefault()
handleDrawerPointerMove(event.clientX)
handlePointerMove(event.clientX, event.clientY)
}
function drawerMouseUp() {
@@ -657,33 +699,39 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
const touch = event.touches[0]
if (!touch) return
event.preventDefault()
handleDrawerPointerMove(touch.clientX)
handlePointerMove(touch.clientX, touch.clientY)
}
function drawerTouchEnd() {
stopDrawerResize()
}
const startDrawerResize = (side: "left" | "right", clientX: number) => {
const startResize = (side: "left" | "right" | "chat" | "terminal", clientX: number, clientY: number) => {
setActiveResizeSide(side)
setResizeStartX(clientX)
setResizeStartWidth(side === "left" ? sessionSidebarWidth() : rightDrawerWidth())
setResizeStartY(clientY)
if (side === "left") setResizeStartWidth(sessionSidebarWidth())
else if (side === "right") setResizeStartWidth(rightDrawerWidth())
else if (side === "chat") setResizeStartWidth(chatPanelWidth())
else if (side === "terminal") setResizeStartHeight(terminalPanelHeight())
document.addEventListener("mousemove", drawerMouseMove)
document.addEventListener("mouseup", drawerMouseUp)
document.addEventListener("touchmove", drawerTouchMove, { passive: false })
document.addEventListener("touchend", drawerTouchEnd)
}
const handleDrawerResizeMouseDown = (side: "left" | "right") => (event: MouseEvent) => {
const handleResizeMouseDown = (side: "left" | "right" | "chat" | "terminal") => (event: MouseEvent) => {
event.preventDefault()
startDrawerResize(side, event.clientX)
startResize(side, event.clientX, event.clientY)
}
const handleDrawerResizeTouchStart = (side: "left" | "right") => (event: TouchEvent) => {
const touch = event.touches[0]
if (!touch) return
event.preventDefault()
startDrawerResize(side, touch.clientX)
startResize(side, touch.clientX, touch.clientY)
}
onCleanup(() => {
@@ -850,6 +898,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
sessions={Array.from(activeSessions().values())}
activeSessionId={activeSessionIdForInstance() || undefined}
onSessionSelect={handleSessionSelect}
onOpenCommandPalette={handleCommandPaletteClick}
onToggleTerminal={() => setTerminalOpen((current) => !current)}
isTerminalOpen={terminalOpen()}
onOpenAdvancedSettings={() => setShowAdvancedSettings(true)}
/>
)
@@ -858,7 +910,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
if (sessionId && sessionId !== "info") {
return (
<div class="flex flex-col h-full" ref={setRightDrawerContentEl}>
<div class="flex flex-col h-full relative" ref={setRightDrawerContentEl}>
<MultiTaskChat instanceId={props.instance.id} sessionId={sessionId} />
</div>
)
@@ -990,7 +1042,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
>
<div
class="session-resize-handle session-resize-handle--left"
onMouseDown={handleDrawerResizeMouseDown("left")}
onMouseDown={handleResizeMouseDown("left")}
onTouchStart={handleDrawerResizeTouchStart("left")}
role="presentation"
aria-hidden="true"
@@ -1052,7 +1104,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
>
<div
class="session-resize-handle session-resize-handle--right"
onMouseDown={handleDrawerResizeMouseDown("right")}
onMouseDown={handleResizeMouseDown("right")}
onTouchStart={handleDrawerResizeTouchStart("right")}
role="presentation"
aria-hidden="true"
@@ -1140,15 +1192,24 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<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)
}}
/>
<div class="flex items-center space-x-2">
<ModelStatusSelector
instanceId={props.instance.id}
sessionId={activeSessionIdForInstance()!}
currentModel={activeSessionForInstance()?.model || { providerId: "", modelId: "" }}
onModelChange={async (model) => {
const sid = activeSessionIdForInstance()
if (sid) await props.handleSidebarModelChange(sid, model)
}}
/>
<button
onClick={() => setShowAdvancedSettings(true)}
class="p-2 text-zinc-500 hover:text-blue-400 transition-all hover:bg-blue-500/10 rounded-full"
title="AI Settings: Manage model providers and API keys"
>
<Settings size={14} strokeWidth={2} />
</button>
</div>
</Show>
{/* SmartX Mode Buttons (Integrated HUD) */}
@@ -1156,7 +1217,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<button
onClick={handleSmartFix}
disabled={isFixing()}
title="Smart Fix"
title="Smart Fix: Automatically detect and fix issues in your code"
class={`transition-all flex items-center space-x-1.5 px-2 py-1 rounded-full hover:bg-white/10 ${isFixing() ? "text-blue-500" : "text-zinc-400 hover:text-white"}`}
>
<Show when={isFixing()} fallback={<Sparkles size={14} class="text-blue-400" />}>
@@ -1170,7 +1231,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<button
onClick={handleBuild}
disabled={isBuilding()}
title="Build"
title="Build: Build and deploy your application"
class={`transition-all flex items-center space-x-1.5 px-2 py-1 rounded-full hover:bg-white/10 ${isBuilding() ? "text-indigo-500" : "text-zinc-400 hover:text-white"}`}
>
<Show when={isBuilding()} fallback={<TerminalIcon size={14} />}>
@@ -1186,7 +1247,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<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)"
title="Autonomous Mode (SOLO): Enable autonomous AI agent operations"
class={`flex items-center space-x-1.5 px-2 py-0.5 rounded-full transition-all ${getSoloState(props.instance.id).isAutonomous
? "bg-blue-500/20 text-blue-400 border border-blue-500/30"
: "text-zinc-500 hover:text-zinc-300"
@@ -1197,7 +1258,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
</button>
<button
onClick={() => toggleAutoApproval(props.instance.id)}
title="Toggle Auto-Approval (SHIELD)"
title="Auto-Approval (SHIELD): Automatically approve AI agent actions"
class={`flex items-center space-x-1.5 px-2 py-0.5 rounded-full transition-all ${getSoloState(props.instance.id).autoApproval
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "text-zinc-500 hover:text-zinc-300"
@@ -1242,17 +1303,29 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
sx={{ flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column", overflowX: "hidden" }}
class="content-area relative"
>
<div class="flex-1 flex overflow-hidden">
<Editor file={currentFile()} />
<div class="flex-1 flex overflow-hidden min-h-0">
<Show when={!isPhoneLayout()}>
<Editor file={currentFile()} />
</Show>
<div class="flex-1 flex flex-col relative border-l border-white/5">
<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">
<div class="flex-1 flex flex-col min-h-0 relative">
<MultiTaskChat instanceId={props.instance.id} sessionId={activeSessionIdForInstance() || ""} />
</div>
</Show>
<div class="flex-1 flex flex-col relative"
<div class="flex-1 flex flex-col relative min-h-0"
style={{ display: isSoloOpen() ? "none" : "flex" }}>
<Show
when={showingInfoView()}
@@ -1305,28 +1378,57 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
</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>
{/* Bottom Toolbar/Terminal Area */}
<div
class="flex flex-col border-t border-white/5 relative bg-[#09090b] z-10 shrink-0 overflow-hidden"
style={{
height: terminalOpen() ? `${terminalPanelHeight()}px` : "32px",
transition: activeResizeSide() === 'terminal' ? 'none' : 'height 0.2s cubic-bezier(0.4, 0, 0.2, 1)'
}}
>
<Show when={terminalOpen()}>
<div
class="absolute -top-1 left-0 right-0 h-2 cursor-row-resize z-20 hover:bg-white/5 active:bg-white/10 transition-colors"
onMouseDown={handleResizeMouseDown("terminal")}
/>
<div class="flex-1 min-h-0 overflow-hidden p-4 bg-[#0d0d0d]">
<div class="font-mono text-xs text-zinc-400">
<div class="mb-2 text-zinc-600">// Terminal functionality coming soon</div>
<div class="text-emerald-500/80 flex items-center gap-2">
<span></span>
<span>~</span>
<span class="animate-pulse">_</span>
</div>
</div>
</div>
</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>
</Show>
<footer class="h-8 flex items-center justify-between px-3 text-[10px] text-zinc-500 tracking-wide shrink-0 border-t border-white/5 bg-[#09090b]">
<div class="flex items-center space-x-4">
<button
class={`flex items-center space-x-1.5 cursor-pointer hover:text-zinc-300 transition-colors outline-none ${terminalOpen() ? 'text-indigo-400 font-bold' : ''}`}
onClick={() => setTerminalOpen(!terminalOpen())}
>
<TerminalIcon size={12} />
<span>TERMINAL</span>
</button>
</div>
<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 class="flex items-center space-x-4 uppercase font-bold">
<div class="flex items-center space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_5px_rgba(34,197,94,0.5)]" />
<span>Sync Active</span>
</div>
<Show when={activeSessionForInstance()}>
{(session) => (
<>
<span class="hover:text-zinc-300 cursor-pointer">{session().model.modelId}</span>
<span class="hover:text-zinc-300 cursor-pointer">{session().agent}</span>
</>
)}
</Show>
</div>
</footer>
</div>
</Box>
{renderRightPanel()}
@@ -1350,6 +1452,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
commands={instancePaletteCommands()}
onExecute={props.onExecuteCommand}
/>
<AdvancedSettingsModal
open={showAdvancedSettings()}
onClose={() => setShowAdvancedSettings(false)}
selectedBinary={selectedBinary()}
onBinaryChange={(binary) => setSelectedBinary(binary)}
/>
</>
)
}

View File

@@ -1,10 +1,12 @@
import { Component, createSignal, For, Show, createEffect } from "solid-js"
import { Component, createSignal, For, Show, createEffect, createMemo } from "solid-js"
import {
Files,
Search,
GitBranch,
Play,
Settings,
Plug,
Sparkles,
ChevronRight,
ChevronDown,
Folder,
@@ -15,6 +17,9 @@ import {
Image as ImageIcon,
} from "lucide-solid"
import { serverApi } from "../../lib/api-client"
import InstanceServiceStatus from "../instance-service-status"
import { catalog, catalogLoading, catalogError, loadCatalog } from "../../stores/skills"
import { getSessionSkills, setSessionSkills } from "../../stores/session-state"
export interface FileNode {
name: string
@@ -32,6 +37,10 @@ interface SidebarProps {
sessions: any[] // Existing sessions to display in one of the tabs
activeSessionId?: string
onSessionSelect: (id: string) => void
onOpenCommandPalette?: () => void
onToggleTerminal?: () => void
isTerminalOpen?: boolean
onOpenAdvancedSettings?: () => void
}
const getFileIcon = (fileName: string) => {
@@ -119,6 +128,19 @@ const FileTree: Component<{
export const Sidebar: Component<SidebarProps> = (props) => {
const [activeTab, setActiveTab] = createSignal("files")
const [rootFiles, setRootFiles] = createSignal<FileNode[]>([])
const [searchQuery, setSearchQuery] = createSignal("")
const [searchResults, setSearchResults] = createSignal<FileNode[]>([])
const [searchLoading, setSearchLoading] = createSignal(false)
const [gitStatus, setGitStatus] = createSignal<{
isRepo: boolean
branch: string | null
ahead: number
behind: number
changes: Array<{ path: string; status: string }>
error?: string
} | null>(null)
const [gitLoading, setGitLoading] = createSignal(false)
const [skillsFilter, setSkillsFilter] = createSignal("")
createEffect(async () => {
if (props.instanceId) {
@@ -135,6 +157,89 @@ export const Sidebar: Component<SidebarProps> = (props) => {
}
})
createEffect(() => {
if (activeTab() === "skills") {
loadCatalog()
}
})
const filteredSkills = createMemo(() => {
const term = skillsFilter().trim().toLowerCase()
if (!term) return catalog()
return catalog().filter((skill) => {
const name = skill.name?.toLowerCase() ?? ""
const description = skill.description?.toLowerCase() ?? ""
return name.includes(term) || description.includes(term) || skill.id.toLowerCase().includes(term)
})
})
const selectedSkills = createMemo(() => {
if (!props.activeSessionId) return []
return getSessionSkills(props.instanceId, props.activeSessionId)
})
const toggleSkillSelection = (skillId: string) => {
if (!props.activeSessionId) return
const current = selectedSkills()
const exists = current.some((skill) => skill.id === skillId)
const next = exists
? current.filter((skill) => skill.id !== skillId)
: (() => {
const found = catalog().find((skill) => skill.id === skillId)
if (!found) return current
return [...current, { id: found.id, name: found.name, description: found.description }]
})()
setSessionSkills(props.instanceId, props.activeSessionId, next)
}
const handleSearch = async () => {
const query = searchQuery().trim()
if (!query) {
setSearchResults([])
return
}
setSearchLoading(true)
try {
const results = await serverApi.searchWorkspaceFiles(props.instanceId, query, { limit: 50, type: "all" })
setSearchResults(
results.map((entry) => ({
name: entry.name,
type: entry.type,
path: entry.path,
})),
)
} catch (error) {
console.error("Failed to search files", error)
} finally {
setSearchLoading(false)
}
}
const refreshGitStatus = async () => {
setGitLoading(true)
try {
const status = await serverApi.fetchWorkspaceGitStatus(props.instanceId)
setGitStatus(status)
} catch (error) {
setGitStatus({
isRepo: false,
branch: null,
ahead: 0,
behind: 0,
changes: [],
error: error instanceof Error ? error.message : "Unable to load git status",
})
} finally {
setGitLoading(false)
}
}
createEffect(() => {
if (activeTab() === "git") {
refreshGitStatus()
}
})
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"}`}
@@ -148,6 +253,9 @@ export const Sidebar: Component<SidebarProps> = (props) => {
{ id: "search", icon: Search },
{ id: "git", icon: GitBranch },
{ id: "debug", icon: Play },
{ id: "mcp", icon: Plug },
{ id: "skills", icon: Sparkles },
{ id: "settings", icon: Settings },
]}
>
{(item) => (
@@ -162,11 +270,6 @@ export const Sidebar: Component<SidebarProps> = (props) => {
</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 */}
@@ -196,6 +299,187 @@ export const Sidebar: Component<SidebarProps> = (props) => {
</For>
</div>
</Show>
<Show when={activeTab() === "search"}>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<input
value={searchQuery()}
onInput={(event) => setSearchQuery(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
handleSearch()
}
}}
placeholder="Search files..."
class="flex-1 rounded-md bg-white/5 border border-white/10 px-3 py-2 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/60"
/>
<button
onClick={handleSearch}
class="px-3 py-2 text-xs font-semibold uppercase tracking-wide rounded-md bg-blue-500/20 text-blue-300 border border-blue-500/30 hover:bg-blue-500/30"
>
Search
</button>
</div>
<Show when={searchLoading()}>
<div class="text-xs text-zinc-500">Searching...</div>
</Show>
<Show when={!searchLoading() && searchResults().length === 0 && searchQuery().trim().length > 0}>
<div class="text-xs text-zinc-500">No results found.</div>
</Show>
<div class="flex flex-col gap-1">
<For each={searchResults()}>
{(result) => (
<div
onClick={() => props.onFileSelect(result)}
class="flex items-center gap-2 px-3 py-2 text-xs text-zinc-300 rounded-md hover:bg-white/5 cursor-pointer"
>
<span class="text-zinc-500">{result.type === "directory" ? "DIR" : "FILE"}</span>
<span class="truncate">{result.path}</span>
</div>
)}
</For>
</div>
</div>
</Show>
<Show when={activeTab() === "git"}>
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between">
<span class="text-xs uppercase tracking-wide text-zinc-500">Repository Status</span>
<button
onClick={refreshGitStatus}
class="px-2 py-1 text-[10px] font-semibold uppercase tracking-wide rounded-md border border-white/10 text-zinc-300 hover:text-white"
>
Refresh
</button>
</div>
<Show when={gitLoading()}>
<div class="text-xs text-zinc-500">Loading git status...</div>
</Show>
<Show when={!gitLoading() && gitStatus()}>
{(status) => (
<div class="flex flex-col gap-3">
<Show when={!status().isRepo}>
<div class="text-xs text-zinc-500">
{status().error ? `Git unavailable: ${status().error}` : "No git repository detected."}
</div>
</Show>
<Show when={status().isRepo}>
<div class="rounded-md border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-200">
<div class="flex items-center justify-between">
<span class="font-semibold">{status().branch || "Detached"}</span>
<span class="text-zinc-500">
{status().ahead ? `${status().ahead}` : ""}
{status().behind ? `${status().behind}` : ""}
</span>
</div>
<div class="text-[11px] text-zinc-500 mt-1">
{status().changes.length} change{status().changes.length === 1 ? "" : "s"}
</div>
</div>
<div class="flex flex-col gap-1">
<For each={status().changes}>
{(change) => (
<div class="flex items-center gap-2 text-xs text-zinc-300 px-3 py-1 rounded-md hover:bg-white/5">
<span class="text-zinc-500 w-6">{change.status}</span>
<span class="truncate">{change.path}</span>
</div>
)}
</For>
</div>
</Show>
</div>
)}
</Show>
</div>
</Show>
<Show when={activeTab() === "debug"}>
<div class="flex flex-col gap-3">
<div class="text-xs uppercase tracking-wide text-zinc-500">Tools</div>
<button
onClick={() => props.onOpenCommandPalette?.()}
class="px-3 py-2 text-xs font-semibold uppercase tracking-wide rounded-md bg-white/5 border border-white/10 text-zinc-300 hover:text-white"
>
Open Command Palette
</button>
<button
onClick={() => props.onToggleTerminal?.()}
class="px-3 py-2 text-xs font-semibold uppercase tracking-wide rounded-md bg-white/5 border border-white/10 text-zinc-300 hover:text-white"
>
{props.isTerminalOpen ? "Close Terminal" : "Open Terminal"}
</button>
</div>
</Show>
<Show when={activeTab() === "mcp"}>
<div class="flex flex-col gap-3">
<div class="text-xs uppercase tracking-wide text-zinc-500">MCP Servers</div>
<InstanceServiceStatus sections={["mcp"]} />
</div>
</Show>
<Show when={activeTab() === "skills"}>
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between">
<span class="text-xs uppercase tracking-wide text-zinc-500">Skills</span>
<span class="text-[10px] text-zinc-500">
{selectedSkills().length} selected
</span>
</div>
<Show when={!props.activeSessionId}>
<div class="text-xs text-zinc-500">Select a session to assign skills.</div>
</Show>
<input
value={skillsFilter()}
onInput={(event) => setSkillsFilter(event.currentTarget.value)}
placeholder="Filter skills..."
class="rounded-md bg-white/5 border border-white/10 px-3 py-2 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/60"
/>
<Show when={catalogLoading()}>
<div class="text-xs text-zinc-500">Loading skills...</div>
</Show>
<Show when={catalogError()}>
{(error) => <div class="text-xs text-amber-400">{error()}</div>}
</Show>
<div class="flex flex-col gap-2">
<For each={filteredSkills()}>
{(skill) => {
const isSelected = () => selectedSkills().some((item) => item.id === skill.id)
return (
<button
type="button"
onClick={() => toggleSkillSelection(skill.id)}
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}>
<div class="text-[11px] text-zinc-500 mt-1">{skill.description}</div>
</Show>
</button>
)
}}
</For>
</div>
</div>
</Show>
<Show when={activeTab() === "settings"}>
<div class="flex flex-col gap-3">
<div class="text-xs uppercase tracking-wide text-zinc-500">Settings</div>
<button
onClick={() => props.onOpenAdvancedSettings?.()}
class="px-3 py-2 text-xs font-semibold uppercase tracking-wide rounded-md bg-white/5 border border-white/10 text-zinc-300 hover:text-white"
>
Open Advanced Settings
</button>
<button
onClick={() => props.onOpenCommandPalette?.()}
class="px-3 py-2 text-xs font-semibold uppercase tracking-wide rounded-md bg-white/5 border border-white/10 text-zinc-300 hover:text-white"
>
Open Command Palette
</button>
</div>
</Show>
</div>
</div>
</div>