Add skills catalog and sidebar tooling
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user