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)}
/>
</>
)
}