From 6bcaa198320e3e1ed1c1fba4122efc27560fdc0a Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Wed, 11 Feb 2026 11:54:36 +0100 Subject: [PATCH 1/5] feat: add Markdown rendering, BasicTool for tools, Collapsible for reasoning (Phase 3) - Add MarkedProvider to App.tsx provider hierarchy - TextPartView: render assistant messages as Markdown (headings, lists, code blocks, links, math via katex, syntax highlighting via shiki) - ToolPartView: replace custom tool-header/tool-details with BasicTool (collapsible with icon based on status + spinner for active) - ReasoningPartView: replace custom reasoning block with Collapsible - Remove ~150 lines of tool/reasoning CSS from chat.css - User messages still render as plain text (no markdown) - Code/Diff providers deferred (not needed for current features) - Update progress tracker --- .../docs/ui-implementation-plan.md | 2 +- packages/kilo-vscode/webview-ui/src/App.tsx | 21 +-- .../src/components/chat/Message.tsx | 124 ++++++++------ .../webview-ui/src/styles/chat.css | 152 ------------------ 4 files changed, 86 insertions(+), 213 deletions(-) diff --git a/packages/kilo-vscode/docs/ui-implementation-plan.md b/packages/kilo-vscode/docs/ui-implementation-plan.md index eb3a3edef0c..fb004987513 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/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..a0e7189f444 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/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index 254b3357099..8aee61b2c27 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -130,158 +130,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 ============================================ */ From 52fd3ed0146b00bfe22865f3391ed8fde79fe063 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Wed, 11 Feb 2026 12:45:18 +0100 Subject: [PATCH 2/5] feat(kilo-vscode): add mode switcher for agent/mode selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a mode switcher dropdown next to the model selector in the prompt input area. This allows users to switch between modes (Code, Plan, Debug, Orchestrator, and custom modes) when sending messages. Changes: - Add AgentInfo type and agent-related messages to the message protocol - Add listAgents() HTTP endpoint to fetch modes from CLI /agent API - Thread agent name through sendMessage → KiloProvider → HttpClient - Create ModeSwitcher component with dropdown UI - Wire up agent state in session context with agentsLoaded handler - Add CSS styles for mode switcher (mirrors model-selector pattern) Closes #162 --- .../docs/ui-implementation-plan.md | 2 +- packages/kilo-vscode/src/KiloProvider.ts | 59 +++++++++++- .../src/services/cli-backend/http-client.ts | 17 +++- .../src/services/cli-backend/index.ts | 1 + .../src/services/cli-backend/types.ts | 10 ++ .../src/components/chat/ModeSwitcher.tsx | 96 +++++++++++++++++++ .../src/components/chat/PromptInput.tsx | 2 + .../webview-ui/src/context/session.tsx | 30 ++++++ .../webview-ui/src/styles/chat.css | 89 +++++++++++++++++ .../webview-ui/src/types/messages.ts | 23 +++++ 10 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/src/components/chat/ModeSwitcher.tsx diff --git a/packages/kilo-vscode/docs/ui-implementation-plan.md b/packages/kilo-vscode/docs/ui-implementation-plan.md index 3308bb732c5..eb3a3edef0c 100644 --- a/packages/kilo-vscode/docs/ui-implementation-plan.md +++ b/packages/kilo-vscode/docs/ui-implementation-plan.md @@ -777,7 +777,7 @@ Each phase has specific acceptance criteria (listed above). The general verifica | Session history | List renders, selection works, navigation back to chat | | Login flow | Device auth card renders, QR code displays, success/error states | | Theme switching | Change VS Code theme → webview updates automatically | -| Sidebar ↔ tab | Both webview hosts render identically | +| Sidebar ↔ tab | Both webview hosts render identically | | Markdown content | Headers, lists, code blocks, links render correctly | | Long conversations | Auto-scroll works, performance is acceptable | diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 2fd828add35..bd8bb3a024c 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) @@ -182,6 +190,9 @@ export class KiloProvider implements vscode.WebviewViewProvider { case "requestProviders": await this.fetchAndSendProviders() break + case "requestAgents": + await this.fetchAndSendAgents() + break } }) } @@ -254,8 +265,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) { @@ -433,6 +445,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. */ @@ -441,6 +492,7 @@ export class KiloProvider implements vscode.WebviewViewProvider { sessionID?: string, providerID?: string, modelID?: string, + agent?: string, ): Promise { if (!this.httpClient) { this.postMessage({ @@ -469,10 +521,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) 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 59cc0096acf..3ebcc8a25ca 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 4fd38068f07..5810d6c8842 100644 --- a/packages/kilo-vscode/src/services/cli-backend/index.ts +++ b/packages/kilo-vscode/src/services/cli-backend/index.ts @@ -10,6 +10,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 d9636372603..3b7e60b4687 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -78,6 +78,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/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 ccf5e319cb8..9a7dc855101 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -29,6 +29,7 @@ import type { PermissionRequest, TodoItem, ModelSelection, + AgentInfo, ExtensionMessage, } from "../types/messages" @@ -69,6 +70,11 @@ interface SessionContextValue { selected: Accessor selectModel: (providerID: string, modelID: string) => void + // Agent/mode selection + agents: Accessor + selectedAgent: Accessor + selectAgent: (name: string) => void + // Actions sendMessage: (text: string, providerID?: string, modelID?: string) => void abort: () => void @@ -98,6 +104,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: {}, @@ -184,6 +195,15 @@ export const SessionProvider: ParentComponent = (props) => { case "sessionsLoaded": handleSessionsLoaded(message.sessions) break + + case "agentsLoaded": + 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) + } + break } }) @@ -307,18 +327,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, }) } @@ -417,6 +444,9 @@ export const SessionProvider: ParentComponent = (props) => { permissions, selected, selectModel, + agents, + selectedAgent: selectedAgentName, + selectAgent, sendMessage, abort, respondToPermission, diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index 254b3357099..50903bcb89d 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -594,3 +594,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 20b824a549a..21557e7b4b0 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -98,6 +98,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 @@ -259,6 +269,12 @@ export interface ProvidersLoadedMessage { defaultSelection: ModelSelection } +export interface AgentsLoadedMessage { + type: "agentsLoaded" + agents: AgentInfo[] + defaultAgent: string +} + export type ExtensionMessage = | ReadyMessage | ConnectionStateMessage @@ -278,6 +294,7 @@ export type ExtensionMessage = | DeviceAuthFailedMessage | DeviceAuthCancelledMessage | ProvidersLoadedMessage + | AgentsLoadedMessage // ============================================ // Messages FROM webview TO extension @@ -289,6 +306,7 @@ export interface SendMessageRequest { sessionID?: string providerID?: string modelID?: string + agent?: string } export interface AbortRequest { @@ -345,6 +363,10 @@ export interface RequestProvidersMessage { type: "requestProviders" } +export interface RequestAgentsMessage { + type: "requestAgents" +} + export type WebviewMessage = | SendMessageRequest | AbortRequest @@ -359,6 +381,7 @@ export type WebviewMessage = | CancelLoginRequest | WebviewReadyRequest | RequestProvidersMessage + | RequestAgentsMessage // ============================================ // VS Code API type From 9ac221ee61d101e86669e72be144edb5edb126fd Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Wed, 11 Feb 2026 12:47:22 +0100 Subject: [PATCH 3/5] fix: resolve Markdown rendering in webview Three issues prevented kilo-ui Markdown component from rendering: 1. Wrong prop name: Markdown expects 'text' not 'content' 2. Duplicate solid-js runtimes (pnpm 1.9.11 vs bun 1.9.10) caused MarkedProvider context to be invisible to Markdown's useMarked(). Added solidDedupePlugin to esbuild to force single instance. 3. Missing 'wasm-unsafe-eval' CSP for shiki WASM syntax highlighting. Also added ErrorBoundary safety net around Markdown that falls back to plain text rendering on errors. --- packages/kilo-vscode/esbuild.js | 31 ++++++++++++++++++- packages/kilo-vscode/src/KiloProvider.ts | 2 +- .../src/components/chat/Message.tsx | 4 +-- 3 files changed, 33 insertions(+), 4 deletions(-) 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 2fd828add35..c3c291e2b39 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -776,7 +776,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/webview-ui/src/components/chat/Message.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx index a0e7189f444..8cb54050453 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx @@ -25,7 +25,7 @@ interface MessageProps { const TextPartView: Component<{ part: TextPart; role: string }> = (props) => { return ( {props.part.text}
}> - + ) } @@ -155,7 +155,7 @@ export const Message: Component = (props) => { when={props.message.role === "assistant"} fallback={
{props.message.content}
} > - + } From 1100548aa29c0ed8961d849dc7dc0e685b9f2b6f Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Wed, 11 Feb 2026 13:38:41 +0100 Subject: [PATCH 4/5] fix(kilo-vscode): handle agentsLoaded outside onMount to prevent race The agentsLoaded message handler was inside onMount in session.tsx, which meant the initial push from the extension could arrive before the handler was registered. This happened reliably when opening a new tab (the server connection is already established, so fetchAndSendAgents fires instantly - before the webview DOM mounts). Fix: mirror the ProviderProvider pattern - register the agentsLoaded handler immediately at context creation time (not in onMount), and proactively request agents with retries. --- .../webview-ui/src/context/session.tsx | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 9a7dc855101..14ff8b4bc2c 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -160,6 +160,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) => { @@ -195,15 +233,6 @@ export const SessionProvider: ParentComponent = (props) => { case "sessionsLoaded": handleSessionsLoaded(message.sessions) break - - case "agentsLoaded": - 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) - } - break } }) From 659c6cd442fbb33a9eb67544ac094dcfeaa0ffce Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Wed, 11 Feb 2026 13:00:38 +0000 Subject: [PATCH 5/5] chore: generate --- packages/kilo-vscode/docs/ui-implementation-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/docs/ui-implementation-plan.md b/packages/kilo-vscode/docs/ui-implementation-plan.md index eb3a3edef0c..3308bb732c5 100644 --- a/packages/kilo-vscode/docs/ui-implementation-plan.md +++ b/packages/kilo-vscode/docs/ui-implementation-plan.md @@ -777,7 +777,7 @@ Each phase has specific acceptance criteria (listed above). The general verifica | Session history | List renders, selection works, navigation back to chat | | Login flow | Device auth card renders, QR code displays, success/error states | | Theme switching | Change VS Code theme → webview updates automatically | -| Sidebar ↔ tab | Both webview hosts render identically | +| Sidebar ↔ tab | Both webview hosts render identically | | Markdown content | Headers, lists, code blocks, links render correctly | | Long conversations | Auto-scroll works, performance is acceptable |