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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user