Merge branch 'dev' into add-task-header

This commit is contained in:
Igor Šćekić
2026-02-11 14:15:56 +01:00
13 changed files with 469 additions and 219 deletions
@@ -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
+30 -1
View File
@@ -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) {
+57 -4
View File
@@ -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<string> = 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<void> {
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<void> {
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:`,
@@ -3,6 +3,7 @@ import type {
SessionInfo,
MessageInfo,
MessagePart,
AgentInfo,
ProfileData,
ProviderAuthAuthorization,
ProviderListResponse,
@@ -146,6 +147,17 @@ export class HttpClient {
return this.request<ProviderListResponse>("GET", "/provider", undefined, { directory })
}
// ============================================
// Agent/Mode Methods
// ============================================
/**
* List all available agents (modes) from the CLI backend.
*/
async listAgents(directory: string): Promise<AgentInfo[]> {
return this.request<AgentInfo[]>("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<void> {
const body: Record<string, unknown> = { 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<void>("POST", `/session/${sessionId}/message`, body, { directory, allowEmpty: true })
}
@@ -11,6 +11,7 @@ export type {
PermissionRequest,
SSEEvent,
TodoItem,
AgentInfo,
ServerConfig,
KilocodeOrganization,
KilocodeProfile,
@@ -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
+12 -9
View File
@@ -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 = () => {
<ThemeProvider defaultTheme="kilo-vscode">
<I18nProvider value={{ locale: () => "en", t: (key) => key }}>
<DialogProvider>
<VSCodeProvider>
<ServerProvider>
<ProviderProvider>
<SessionProvider>
<AppContent />
</SessionProvider>
</ProviderProvider>
</ServerProvider>
</VSCodeProvider>
<MarkedProvider>
<VSCodeProvider>
<ServerProvider>
<ProviderProvider>
<SessionProvider>
<AppContent />
</SessionProvider>
</ProviderProvider>
</ServerProvider>
</VSCodeProvider>
</MarkedProvider>
</DialogProvider>
</I18nProvider>
</ThemeProvider>
@@ -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 <div class="message-text">{props.part.text}</div>
}
// 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 (
<div class="message-reasoning">
<button class="reasoning-header" onClick={() => setExpanded(!expanded())} aria-expanded={expanded()}>
<span class="reasoning-icon">💭</span>
<span class="reasoning-label">Thinking...</span>
<span class="reasoning-toggle">{expanded() ? "▼" : "▶"}</span>
</button>
<Show when={expanded()}>
<div class="reasoning-content">{props.part.text}</div>
</Show>
</div>
<Show when={props.role === "assistant"} fallback={<div class="message-text">{props.part.text}</div>}>
<Markdown text={props.part.text} />
</Show>
)
}
// 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 (
<Collapsible variant="ghost">
<Collapsible.Trigger>
<span>💭 Thinking...</span>
<Collapsible.Arrow />
</Collapsible.Trigger>
<Collapsible.Content>
<div class="message-text" style={{ "font-style": "italic", opacity: 0.8 }}>
{props.part.text}
</div>
</Collapsible.Content>
</Collapsible>
)
}
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 <Spinner style={{ width: "14px", height: "14px" }} />
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 (
<div class={`message-tool ${getStatusClass()}`}>
<button class="tool-header" onClick={() => setExpanded(!expanded())} aria-expanded={expanded()}>
<span class="tool-icon">{getStatusIcon()}</span>
<span class="tool-name">{getTitle()}</span>
<span class="tool-toggle">{expanded() ? "▼" : "▶"}</span>
</button>
<Show when={expanded()}>
<div class="tool-details">
<div class="tool-input">
<div data-component="tool-part-wrapper">
<BasicTool
icon={getStatusIcon()}
defaultOpen={false}
trigger={{
title: getTitle(),
subtitle: props.part.tool !== getTitle() ? props.part.tool : undefined,
action: isActive() ? ((<Spinner style={{ width: "14px", height: "14px" }} />) as JSX.Element) : undefined,
}}
>
<div style={{ padding: "8px", "font-size": "12px" }}>
<div>
<strong>Input:</strong>
<pre>{JSON.stringify(props.part.state.input, null, 2)}</pre>
<pre style={{ margin: "4px 0 8px", "white-space": "pre-wrap", "word-break": "break-word" }}>
{JSON.stringify(props.part.state.input, null, 2)}
</pre>
</div>
<Show when={props.part.state.status === "completed" && "output" in props.part.state}>
<div class="tool-output">
<div>
<strong>Output:</strong>
<pre>{(props.part.state as { output: string }).output}</pre>
<pre style={{ margin: "4px 0 0", "white-space": "pre-wrap", "word-break": "break-word" }}>
{(props.part.state as { output: string }).output}
</pre>
</div>
</Show>
<Show when={props.part.state.status === "error" && "error" in props.part.state}>
<div class="tool-error">
<div style={{ color: "var(--vscode-errorForeground)" }}>
<strong>Error:</strong>
<pre>{(props.part.state as { error: string }).error}</pre>
<pre style={{ margin: "4px 0 0", "white-space": "pre-wrap", "word-break": "break-word" }}>
{(props.part.state as { error: string }).error}
</pre>
</div>
</Show>
</div>
</Show>
</BasicTool>
</div>
)
}
// 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 (
<>
<Show when={props.part.type === "text"}>
<TextPartView part={props.part as TextPart} />
<TextPartView part={props.part as TextPart} role={props.role} />
</Show>
<Show when={props.part.type === "tool"}>
<ToolPartView part={props.part as ToolPart} />
@@ -134,11 +151,16 @@ export const Message: Component<MessageProps> = (props) => {
when={hasParts()}
fallback={
<Show when={props.message.content}>
<div class="message-text">{props.message.content}</div>
<Show
when={props.message.role === "assistant"}
fallback={<div class="message-text">{props.message.content}</div>}
>
<Markdown text={props.message.content!} />
</Show>
</Show>
}
>
<For each={parts()}>{(part) => <PartView part={part} />}</For>
<For each={parts()}>{(part) => <PartView part={part} role={props.message.role} />}</For>
</Show>
</div>
</div>
@@ -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 (
<div class="mode-switcher" ref={containerRef}>
<button
class="mode-switcher-trigger"
onClick={toggle}
disabled={!hasAgents()}
aria-haspopup="listbox"
aria-expanded={open()}
title={`Mode: ${session.selectedAgent()}`}
>
<span class="mode-switcher-trigger-label">{triggerLabel()}</span>
<Show when={hasAgents()}>
<svg class="mode-switcher-trigger-chevron" width="10" height="10" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4l4 5H4l4-5z" />
</svg>
</Show>
</button>
<Show when={open()}>
<div class="mode-switcher-dropdown" role="listbox">
<For each={available()}>
{(agent) => (
<div
class={`mode-switcher-item${agent.name === session.selectedAgent() ? " selected" : ""}`}
role="option"
aria-selected={agent.name === session.selectedAgent()}
onClick={() => pick(agent.name)}
>
<span class="mode-switcher-item-name">{agent.name.charAt(0).toUpperCase() + agent.name.slice(1)}</span>
<Show when={agent.description}>
<span class="mode-switcher-item-desc">{agent.description}</span>
</Show>
</div>
)}
</For>
</div>
</Show>
</div>
)
}
@@ -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 = () => {
</div>
</div>
<div class="prompt-input-hint">
<ModeSwitcher />
<ModelSelector />
<Show when={!isDisabled()}>
<span>Press Enter to send, Shift+Enter for new line</span>
@@ -30,6 +30,7 @@ import type {
TodoItem,
ModelSelection,
ContextUsage,
AgentInfo,
ExtensionMessage,
} from "../types/messages"
@@ -74,6 +75,11 @@ interface SessionContextValue {
totalCost: Accessor<number>
contextUsage: Accessor<ContextUsage | undefined>
// Agent/mode selection
agents: Accessor<AgentInfo[]>
selectedAgent: Accessor<string>
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<ModelSelection | null>(null)
const [pendingWasUserSet, setPendingWasUserSet] = createSignal(false)
// Agents (modes) loaded from the CLI backend
const [agents, setAgents] = createSignal<AgentInfo[]>([])
const [defaultAgent, setDefaultAgent] = createSignal("code")
const [selectedAgentName, setSelectedAgentName] = createSignal("code")
// Store for sessions, messages, parts, todos, modelSelections
const [store, setStore] = createStore<SessionStore>({
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,
@@ -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;
}
@@ -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