diff --git a/packages/kilo-vscode/docs/ui-implementation-plan.md b/packages/kilo-vscode/docs/ui-implementation-plan.md index 3308bb732c5..a5c1a3a9351 100644 --- a/packages/kilo-vscode/docs/ui-implementation-plan.md +++ b/packages/kilo-vscode/docs/ui-implementation-plan.md @@ -794,7 +794,7 @@ Each phase has specific acceptance criteria (listed above). The general verifica | Phase 1.4: Cards | ✅ Complete | 2026-02-11 | Inline-styled card divs in ProfileView (profile + balance) and DeviceAuthCard (5 state cards) replaced with `Card` component. Removed `cardStyle` constant. | | Phase 1.5: Icons | ✅ Complete | 2026-02-11 | All 15 lucide-solid icons replaced with kilo-ui `Icon`. `lucide-solid` dep removed. Many icons use approximate matches (see deviation #7). | | Phase 2: Settings UI | ✅ Complete | 2026-02-11 | Settings shell uses kilo-ui `Tabs` with vertical orientation and `variant="settings"`. ~80 lines of inline styles removed. Tab content (stubs) unchanged. Back button in header kept with minimal CSS. | -| Phase 3: Chat UI | ⬚ Not started | — | | +| Phase 3: Chat UI | ✅ Complete | 2026-02-11 | MarkedProvider added. Assistant text renders as Markdown. Tool calls use BasicTool. Reasoning uses Collapsible. Removed ~150 lines of tool/reasoning CSS. Code/Diff providers deferred (not needed yet). | | Phase 4: Advanced | ⬚ Not started | — | | ### Deviations from plan diff --git a/packages/kilo-vscode/esbuild.js b/packages/kilo-vscode/esbuild.js index 54b637ab915..c6541f031c3 100644 --- a/packages/kilo-vscode/esbuild.js +++ b/packages/kilo-vscode/esbuild.js @@ -1,9 +1,38 @@ const esbuild = require("esbuild") +const path = require("path") const { solidPlugin } = require("esbuild-plugin-solid") const production = process.argv.includes("--production") const watch = process.argv.includes("--watch") +/** + * Force all solid-js imports (from kilo-ui and the webview) to resolve to + * the **same** copy so SolidJS contexts are shared across packages. + * Without this, the monorepo hoists separate copies (pnpm vs bun) and + * createContext / useContext can't see each other. + * + * @type {import('esbuild').Plugin} + */ +const solidDedupePlugin = { + name: "solid-dedupe", + setup(build) { + // Resolve these bare specifiers to the kilo-vscode-local copy + const solidRoot = path.dirname(require.resolve("solid-js/package.json")) + const aliases = { + "solid-js": path.join(solidRoot, "dist", "solid.js"), + "solid-js/web": path.join(solidRoot, "web", "dist", "web.js"), + "solid-js/store": path.join(solidRoot, "store", "dist", "store.js"), + } + + build.onResolve({ filter: /^solid-js(\/web|\/store)?$/ }, (args) => { + const key = args.path + if (aliases[key]) { + return { path: aliases[key] } + } + }) + }, +} + /** * @type {import('esbuild').Plugin} */ @@ -72,7 +101,7 @@ async function main() { ".woff2": "file", ".ttf": "file", }, - plugins: [cssPackageResolvePlugin, solidPlugin(), esbuildProblemMatcherPlugin], + plugins: [solidDedupePlugin, cssPackageResolvePlugin, solidPlugin(), esbuildProblemMatcherPlugin], }) if (watch) { diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 9fa0721c611..79e910478d3 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -11,6 +11,8 @@ export class KiloProvider implements vscode.WebviewViewProvider { private isWebviewReady = false /** Cached providersLoaded payload so requestProviders can be served before httpClient is ready */ private cachedProvidersMessage: unknown = null + /** Cached agentsLoaded payload so requestAgents can be served before httpClient is ready */ + private cachedAgentsMessage: unknown = null private trackedSessionIds: Set = new Set() private unsubscribeEvent: (() => void) | null = null @@ -144,7 +146,13 @@ export class KiloProvider implements vscode.WebviewViewProvider { await this.syncWebviewState("webviewReady") break case "sendMessage": - await this.handleSendMessage(message.text, message.sessionID, message.providerID, message.modelID) + await this.handleSendMessage( + message.text, + message.sessionID, + message.providerID, + message.modelID, + message.agent, + ) break case "abort": await this.handleAbort(message.sessionID) @@ -185,6 +193,9 @@ export class KiloProvider implements vscode.WebviewViewProvider { case "compact": await this.handleCompact(message.sessionID, message.providerID, message.modelID) break + case "requestAgents": + await this.fetchAndSendAgents() + break } }) } @@ -257,8 +268,9 @@ export class KiloProvider implements vscode.WebviewViewProvider { this.postMessage({ type: "connectionState", state: this.connectionState }) await this.syncWebviewState("initializeConnection") - // Fetch providers and send to webview + // Fetch providers and agents, then send to webview await this.fetchAndSendProviders() + await this.fetchAndSendAgents() console.log("[Kilo New] KiloProvider: ✅ initializeConnection completed successfully") } catch (error) { @@ -438,6 +450,45 @@ export class KiloProvider implements vscode.WebviewViewProvider { } } + /** + * Fetch agents (modes) from the backend and send to webview. + */ + private async fetchAndSendAgents(): Promise { + if (!this.httpClient) { + if (this.cachedAgentsMessage) { + this.postMessage(this.cachedAgentsMessage) + } + return + } + + try { + const workspaceDir = this.getWorkspaceDirectory() + const agents = await this.httpClient.listAgents(workspaceDir) + + // Filter to only visible primary/all modes (not subagents, not hidden) + const visible = agents.filter((a) => a.mode !== "subagent" && !a.hidden) + + // Find default agent: first one in list (CLI sorts default first) + const defaultAgent = visible.length > 0 ? visible[0].name : "code" + + const message = { + type: "agentsLoaded", + agents: visible.map((a) => ({ + name: a.name, + description: a.description, + mode: a.mode, + native: a.native, + color: a.color, + })), + defaultAgent, + } + this.cachedAgentsMessage = message + this.postMessage(message) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to fetch agents:", error) + } + } + /** * Handle sending a message from the webview. */ @@ -446,6 +497,7 @@ export class KiloProvider implements vscode.WebviewViewProvider { sessionID?: string, providerID?: string, modelID?: string, + agent?: string, ): Promise { if (!this.httpClient) { this.postMessage({ @@ -474,10 +526,11 @@ export class KiloProvider implements vscode.WebviewViewProvider { throw new Error("No session available") } - // Send message with text part and optional model selection + // Send message with text part, optional model selection, and optional agent/mode await this.httpClient.sendMessage(targetSessionID, [{ type: "text", text }], workspaceDir, { providerID, modelID, + agent, }) } catch (error) { console.error("[Kilo New] KiloProvider: Failed to send message:", error) @@ -818,7 +871,7 @@ export class KiloProvider implements vscode.WebviewViewProvider { const csp = [ "default-src 'none'", `style-src 'unsafe-inline' ${webview.cspSource}`, - `script-src 'nonce-${nonce}'`, + `script-src 'nonce-${nonce}' 'wasm-unsafe-eval'`, `font-src ${webview.cspSource}`, "connect-src http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*", `img-src ${webview.cspSource} data: https:`, diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts index 6d16a26a4e0..a2d525f6644 100644 --- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -3,6 +3,7 @@ import type { SessionInfo, MessageInfo, MessagePart, + AgentInfo, ProfileData, ProviderAuthAuthorization, ProviderListResponse, @@ -146,6 +147,17 @@ export class HttpClient { return this.request("GET", "/provider", undefined, { directory }) } + // ============================================ + // Agent/Mode Methods + // ============================================ + + /** + * List all available agents (modes) from the CLI backend. + */ + async listAgents(directory: string): Promise { + return this.request("GET", "/agent", undefined, { directory }) + } + // ============================================ // Messaging Methods // ============================================ @@ -157,13 +169,16 @@ export class HttpClient { sessionId: string, parts: Array<{ type: "text"; text: string } | { type: "file"; mime: string; url: string }>, directory: string, - options?: { providerID?: string; modelID?: string }, + options?: { providerID?: string; modelID?: string; agent?: string }, ): Promise { const body: Record = { parts } if (options?.providerID && options?.modelID) { // Backend expects model selection as a nested object: { model: { providerID, modelID } } body.model = { providerID: options.providerID, modelID: options.modelID } } + if (options?.agent) { + body.agent = options.agent + } await this.request("POST", `/session/${sessionId}/message`, body, { directory, allowEmpty: true }) } diff --git a/packages/kilo-vscode/src/services/cli-backend/index.ts b/packages/kilo-vscode/src/services/cli-backend/index.ts index db08cbf5e95..63575cbc1ac 100644 --- a/packages/kilo-vscode/src/services/cli-backend/index.ts +++ b/packages/kilo-vscode/src/services/cli-backend/index.ts @@ -11,6 +11,7 @@ export type { PermissionRequest, SSEEvent, TodoItem, + AgentInfo, ServerConfig, KilocodeOrganization, KilocodeProfile, diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts index ea1b9c965ea..49bbe71a728 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -89,6 +89,16 @@ export interface TodoItem { status: "pending" | "in_progress" | "completed" } +// Agent/mode info from the CLI /agent endpoint +export interface AgentInfo { + name: string + description?: string + mode: "subagent" | "primary" | "all" + native?: boolean + hidden?: boolean + color?: string +} + // Provider/model types from provider catalog // Model definition from provider catalog diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index b03e610b2c4..d079ffe6b8d 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -2,6 +2,7 @@ import { Component, createSignal, Switch, Match, onMount, onCleanup } from "soli import { ThemeProvider } from "@kilocode/kilo-ui/theme" import { I18nProvider } from "@kilocode/kilo-ui/context" import { DialogProvider } from "@kilocode/kilo-ui/context/dialog" +import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" import Settings from "./components/Settings" import ProfileView from "./components/ProfileView" import { VSCodeProvider } from "./context/vscode" @@ -116,15 +117,17 @@ const App: Component = () => { "en", t: (key) => key }}> - - - - - - - - - + + + + + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx index c6b30656c81..8cb54050453 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx @@ -1,10 +1,19 @@ /** * Message component * Displays a single chat message with its parts (text, tool calls, reasoning) + * + * Uses kilo-ui components: + * - Markdown for rendering assistant text (headings, lists, code blocks, links) + * - BasicTool for tool call display (collapsible with icon, title, subtitle) + * - Collapsible for reasoning blocks + * - Spinner for pending/running tool status */ -import { Component, For, Show, createSignal, JSX } from "solid-js" +import { Component, For, Show, JSX } from "solid-js" +import { Markdown } from "@kilocode/kilo-ui/markdown" import { Spinner } from "@kilocode/kilo-ui/spinner" +import { BasicTool } from "@kilocode/kilo-ui/basic-tool" +import { Collapsible } from "@kilocode/kilo-ui/collapsible" import { useSession } from "../../context/session" import type { Message as MessageType, Part, TextPart, ToolPart, ReasoningPart } from "../../types/messages" @@ -12,89 +21,97 @@ interface MessageProps { message: MessageType } -// Text part display -const TextPartView: Component<{ part: TextPart }> = (props) => { - return
{props.part.text}
-} - -// Reasoning part display -const ReasoningPartView: Component<{ part: ReasoningPart }> = (props) => { - const [expanded, setExpanded] = createSignal(false) - +// Text part display — renders markdown for assistant, plain text for user +const TextPartView: Component<{ part: TextPart; role: string }> = (props) => { return ( -
- - -
{props.part.text}
-
-
+ {props.part.text}}> + + ) } -// Tool part display -const ToolPartView: Component<{ part: ToolPart }> = (props) => { - const [expanded, setExpanded] = createSignal(false) +// Reasoning part display — collapsible "Thinking..." block +const ReasoningPartView: Component<{ part: ReasoningPart }> = (props) => { + return ( + + + 💭 Thinking... + + + +
+ {props.part.text} +
+
+
+ ) +} - const getStatusIcon = (): JSX.Element | string => { +// Tool part display — uses BasicTool for collapsible tool header +const ToolPartView: Component<{ part: ToolPart }> = (props) => { + const getStatusIcon = (): string => { switch (props.part.state.status) { case "pending": case "running": - return + return "settings-gear" case "completed": - return "✓" + return "check-small" case "error": - return "✕" + return "circle-x" } } - const getStatusClass = () => { - return `tool-status-${props.part.state.status}` - } - const getTitle = () => { if ("title" in props.part.state && props.part.state.title) { - return props.part.state.title + return props.part.state.title as string } return props.part.tool } + const isActive = () => props.part.state.status === "pending" || props.part.state.status === "running" + return ( -
- - -
-
+
+ ) as JSX.Element) : undefined, + }} + > +
+
Input: -
{JSON.stringify(props.part.state.input, null, 2)}
+
+              {JSON.stringify(props.part.state.input, null, 2)}
+            
-
+
Output: -
{(props.part.state as { output: string }).output}
+
+                {(props.part.state as { output: string }).output}
+              
-
+
Error: -
{(props.part.state as { error: string }).error}
+
+                {(props.part.state as { error: string }).error}
+              
-
+
) } // Part renderer -const PartView: Component<{ part: Part }> = (props) => { +const PartView: Component<{ part: Part; role: string }> = (props) => { // Skip step-start and step-finish parts - they're metadata, not displayable content if (props.part.type === "step-start" || props.part.type === "step-finish") { return null @@ -103,7 +120,7 @@ const PartView: Component<{ part: Part }> = (props) => { return ( <> - + @@ -134,11 +151,16 @@ export const Message: Component = (props) => { when={hasParts()} fallback={ -
{props.message.content}
+ {props.message.content}
} + > + + } > - {(part) => } + {(part) => }
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ModeSwitcher.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ModeSwitcher.tsx new file mode 100644 index 00000000000..aedb9ef1166 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ModeSwitcher.tsx @@ -0,0 +1,96 @@ +/** + * ModeSwitcher component + * Dropdown for selecting an agent/mode in the chat prompt area. + * Mirrors the pattern used in ModelSelector. + */ + +import { Component, createSignal, createEffect, onCleanup, For, Show } from "solid-js" +import { useSession } from "../../context/session" + +export const ModeSwitcher: Component = () => { + const session = useSession() + + const [open, setOpen] = createSignal(false) + let containerRef: HTMLDivElement | undefined + + const available = () => session.agents() + const hasAgents = () => available().length > 1 + + // Click-outside handler + createEffect(() => { + if (!open()) { + return + } + + const handler = (e: MouseEvent) => { + if (containerRef && !containerRef.contains(e.target as Node)) { + setOpen(false) + } + } + + document.addEventListener("mousedown", handler) + onCleanup(() => document.removeEventListener("mousedown", handler)) + }) + + function toggle() { + if (!hasAgents()) { + return + } + setOpen((prev) => !prev) + } + + function pick(name: string) { + session.selectAgent(name) + setOpen(false) + } + + const triggerLabel = () => { + const name = session.selectedAgent() + const agent = available().find((a) => a.name === name) + if (agent) { + // Capitalize first letter for display + return agent.name.charAt(0).toUpperCase() + agent.name.slice(1) + } + return name || "Code" + } + + return ( +
+ + + +
+ + {(agent) => ( +
pick(agent.name)} + > + {agent.name.charAt(0).toUpperCase() + agent.name.slice(1)} + + {agent.description} + +
+ )} +
+
+
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index a59e1993b44..c07d5a2ab09 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -8,6 +8,7 @@ import { Button } from "@kilocode/kilo-ui/button" import { useSession } from "../../context/session" import { useServer } from "../../context/server" import { ModelSelector } from "./ModelSelector" +import { ModeSwitcher } from "./ModeSwitcher" export const PromptInput: Component = () => { const session = useSession() @@ -98,6 +99,7 @@ export const PromptInput: Component = () => {
+ Press Enter to send, Shift+Enter for new line diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 565a1d782e6..97b3419498e 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -30,6 +30,7 @@ import type { TodoItem, ModelSelection, ContextUsage, + AgentInfo, ExtensionMessage, } from "../types/messages" @@ -74,6 +75,11 @@ interface SessionContextValue { totalCost: Accessor contextUsage: Accessor + // Agent/mode selection + agents: Accessor + selectedAgent: Accessor + selectAgent: (name: string) => void + // Actions sendMessage: (text: string, providerID?: string, modelID?: string) => void abort: () => void @@ -104,6 +110,11 @@ export const SessionProvider: ParentComponent = (props) => { const [pendingModelSelection, setPendingModelSelection] = createSignal(null) const [pendingWasUserSet, setPendingWasUserSet] = createSignal(false) + // Agents (modes) loaded from the CLI backend + const [agents, setAgents] = createSignal([]) + const [defaultAgent, setDefaultAgent] = createSignal("code") + const [selectedAgentName, setSelectedAgentName] = createSignal("code") + // Store for sessions, messages, parts, todos, modelSelections const [store, setStore] = createStore({ sessions: {}, @@ -155,6 +166,44 @@ export const SessionProvider: ParentComponent = (props) => { } } + // Handle agentsLoaded immediately (not in onMount) so we never miss + // the initial push that arrives before the DOM mounts. This mirrors the + // pattern used by ProviderProvider for providersLoaded. + const unsubAgents = vscode.onMessage((message: ExtensionMessage) => { + if (message.type !== "agentsLoaded") { + return + } + setAgents(message.agents) + setDefaultAgent(message.defaultAgent) + // Only override if the user hasn't explicitly selected an agent + if (selectedAgentName() === "code" || !message.agents.some((a) => a.name === selectedAgentName())) { + setSelectedAgentName(message.defaultAgent) + } + }) + + // Request agents in case the initial push was missed. + // Retry a few times because the extension's httpClient may + // not be ready yet when the first request arrives. + let agentRetries = 0 + const agentMaxRetries = 5 + const agentRetryMs = 500 + + vscode.postMessage({ type: "requestAgents" }) + + const agentRetryTimer = setInterval(() => { + agentRetries++ + if (agents().length > 0 || agentRetries >= agentMaxRetries) { + clearInterval(agentRetryTimer) + return + } + vscode.postMessage({ type: "requestAgents" }) + }, agentRetryMs) + + onCleanup(() => { + unsubAgents() + clearInterval(agentRetryTimer) + }) + // Handle messages from extension onMount(() => { const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { @@ -313,18 +362,25 @@ export const SessionProvider: ParentComponent = (props) => { } // Actions + function selectAgent(name: string) { + setSelectedAgentName(name) + } + function sendMessage(text: string, providerID?: string, modelID?: string) { if (!server.isConnected()) { console.warn("[Kilo New] Cannot send message: not connected") return } + const agent = selectedAgentName() !== defaultAgent() ? selectedAgentName() : undefined + vscode.postMessage({ type: "sendMessage", text, sessionID: currentSessionID(), providerID, modelID, + agent, }) } @@ -463,6 +519,9 @@ export const SessionProvider: ParentComponent = (props) => { selectModel, totalCost, contextUsage, + agents, + selectedAgent: selectedAgentName, + selectAgent, sendMessage, abort, compact, diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index bf756181127..6af3b2cc116 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -168,158 +168,6 @@ line-height: 1.5; } -/* ============================================ - Tool Parts - ============================================ */ - -.message-tool { - border: 1px solid var(--vscode-panel-border); - border-radius: 4px; - overflow: hidden; -} - -.tool-header { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - padding: 6px 10px; - background: var(--vscode-sideBar-background); - border: none; - cursor: pointer; - text-align: left; - color: var(--vscode-foreground); - font-size: 12px; -} - -.tool-header:hover { - background: var(--vscode-list-hoverBackground); -} - -.tool-icon { - font-size: 12px; -} - -.tool-name { - flex: 1; - font-family: var(--vscode-editor-font-family); -} - -.tool-toggle { - font-size: 10px; - color: var(--vscode-descriptionForeground); -} - -.tool-details { - padding: 8px 10px; - background: var(--vscode-editor-background); - font-size: 11px; -} - -.tool-input, -.tool-output, -.tool-error { - margin-bottom: 8px; -} - -.tool-input:last-child, -.tool-output:last-child, -.tool-error:last-child { - margin-bottom: 0; -} - -.tool-details pre { - margin: 4px 0 0 0; - font-family: var(--vscode-editor-font-family); - font-size: 11px; - white-space: pre-wrap; - word-break: break-word; - overflow-x: auto; -} - -/* Tool status styles */ -.tool-status-pending .tool-icon { - color: var(--vscode-charts-yellow); -} - -.tool-status-running .tool-icon { - color: var(--vscode-charts-blue); - animation: spin 1s linear infinite; -} - -.tool-status-completed .tool-icon { - color: var(--vscode-charts-green); -} - -.tool-status-error .tool-icon { - color: var(--vscode-errorForeground); -} - -.tool-error { - color: var(--vscode-errorForeground); -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -/* ============================================ - Reasoning Parts - ============================================ */ - -.message-reasoning { - border: 1px solid var(--vscode-panel-border); - border-radius: 4px; - overflow: hidden; - opacity: 0.8; -} - -.reasoning-header { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - padding: 6px 10px; - background: var(--vscode-sideBar-background); - border: none; - cursor: pointer; - text-align: left; - color: var(--vscode-descriptionForeground); - font-size: 12px; - font-style: italic; -} - -.reasoning-header:hover { - background: var(--vscode-list-hoverBackground); -} - -.reasoning-icon { - font-size: 12px; -} - -.reasoning-label { - flex: 1; -} - -.reasoning-toggle { - font-size: 10px; -} - -.reasoning-content { - padding: 8px 10px; - background: var(--vscode-editor-background); - font-size: 12px; - font-style: italic; - color: var(--vscode-descriptionForeground); - white-space: pre-wrap; - word-break: break-word; -} - /* ============================================ Prompt Input ============================================ */ @@ -632,3 +480,92 @@ text-transform: uppercase; letter-spacing: 0.03em; } + +/* ============================================ + Mode Switcher + ============================================ */ + +.mode-switcher { + position: relative; +} + +.mode-switcher-trigger { + display: inline-flex; + align-items: center; + gap: 4px; + background: none; + border: none; + color: var(--vscode-descriptionForeground); + font-size: 11px; + cursor: pointer; + padding: 2px 6px; + border-radius: 3px; + white-space: nowrap; + max-width: 120px; +} + +.mode-switcher-trigger:hover:not(:disabled) { + color: var(--vscode-foreground); + background: var(--vscode-toolbar-hoverBackground); +} + +.mode-switcher-trigger:disabled { + opacity: 0.5; + cursor: default; +} + +.mode-switcher-trigger-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mode-switcher-trigger-chevron { + flex-shrink: 0; +} + +.mode-switcher-dropdown { + position: absolute; + bottom: 100%; + left: 0; + margin-bottom: 4px; + min-width: 200px; + max-width: 300px; + max-height: 280px; + overflow-y: auto; + background: var(--vscode-dropdown-background); + border: 1px solid var(--vscode-dropdown-border); + border-radius: 4px; + z-index: 100; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.mode-switcher-item { + display: flex; + flex-direction: column; + gap: 2px; + padding: 8px 12px; + cursor: pointer; + font-size: 12px; +} + +.mode-switcher-item:hover { + background: var(--vscode-list-hoverBackground); +} + +.mode-switcher-item.selected { + color: var(--vscode-list-activeSelectionForeground, var(--vscode-foreground)); + background: var(--vscode-list-activeSelectionBackground, var(--vscode-list-hoverBackground)); +} + +.mode-switcher-item-name { + font-weight: 500; +} + +.mode-switcher-item-desc { + font-size: 11px; + color: var(--vscode-descriptionForeground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 195380e3f67..3abc7663712 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -114,6 +114,16 @@ export interface TodoItem { status: "pending" | "in_progress" | "completed" } +// Agent/mode info from CLI backend +export interface AgentInfo { + name: string + description?: string + mode: "subagent" | "primary" | "all" + native?: boolean + hidden?: boolean + color?: string +} + // Server info export interface ServerInfo { port: number @@ -277,6 +287,12 @@ export interface ProvidersLoadedMessage { defaultSelection: ModelSelection } +export interface AgentsLoadedMessage { + type: "agentsLoaded" + agents: AgentInfo[] + defaultAgent: string +} + export type ExtensionMessage = | ReadyMessage | ConnectionStateMessage @@ -296,6 +312,7 @@ export type ExtensionMessage = | DeviceAuthFailedMessage | DeviceAuthCancelledMessage | ProvidersLoadedMessage + | AgentsLoadedMessage // ============================================ // Messages FROM webview TO extension @@ -307,6 +324,7 @@ export interface SendMessageRequest { sessionID?: string providerID?: string modelID?: string + agent?: string } export interface AbortRequest { @@ -370,6 +388,10 @@ export interface CompactRequest { modelID?: string } +export interface RequestAgentsMessage { + type: "requestAgents" +} + export type WebviewMessage = | SendMessageRequest | AbortRequest @@ -385,6 +407,7 @@ export type WebviewMessage = | WebviewReadyRequest | RequestProvidersMessage | CompactRequest + | RequestAgentsMessage // ============================================ // VS Code API type