mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge branch 'dev' into add-task-header
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user