Merge pull request #6911 from Kilo-Org/mark/sub-agent-viewer

feat(vscode): sub-agent viewer panel
This commit is contained in:
Mark IJbema
2026-03-11 11:35:06 +01:00
committed by GitHub
8 changed files with 199 additions and 16 deletions
+3
View File
@@ -394,6 +394,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "openChanges":
vscode.commands.executeCommand("kilo-code.new.showChanges")
break
case "openSubAgentViewer":
vscode.commands.executeCommand("kilo-code.new.openSubAgentViewer", message.sessionID, message.title)
break
case "openFile":
if (message.filePath) {
this.handleOpenFile(message.filePath, message.line, message.column)
@@ -0,0 +1,110 @@
import * as vscode from "vscode"
import { KiloProvider } from "./KiloProvider"
import type { KiloConnectionService } from "./services/cli-backend"
/**
* Opens a read-only editor panel to view a sub-agent session.
*
* Each child session ID maps to at most one panel — calling openPanel()
* again with the same ID reveals the existing panel.
*
* Uses a full KiloProvider so the viewer has backend connectivity
* (messages, parts, SSE events) identical to the sidebar.
*/
export class SubAgentViewerProvider implements vscode.Disposable {
private panels = new Map<string, vscode.WebviewPanel>()
private providers = new Map<string, KiloProvider>()
constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
private readonly context: vscode.ExtensionContext,
) {}
openPanel(sessionID: string, title?: string): void {
const existing = this.panels.get(sessionID)
if (existing) {
existing.reveal(vscode.ViewColumn.One)
return
}
const label = title ? `Sub-agent: ${title}` : "Sub-agent Viewer"
const panel = vscode.window.createWebviewPanel("kilo-code.new.SubAgentViewerPanel", label, vscode.ViewColumn.One, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [this.extensionUri],
})
panel.iconPath = {
light: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-light.svg"),
dark: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-dark.svg"),
}
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context)
provider.resolveWebviewPanel(panel)
// Once the webview is ready, fetch the session and display it in read-only mode.
const readyDisposable = panel.webview.onDidReceiveMessage(async (msg) => {
if (msg.type !== "webviewReady") return
readyDisposable.dispose()
// Small delay to let KiloProvider's own webviewReady handler finish first
await new Promise((resolve) => setTimeout(resolve, 50))
try {
const client = this.connectionService.getClient()
const { data: session } = await client.session.get({ sessionID }, { throwOnError: true })
// Register the session on the provider — this adds it to
// trackedSessionIds for live SSE updates and sends
// sessionCreated to the webview.
provider.registerSession(session)
// Fetch and send existing messages
const { data: messagesData } = await client.session.messages({ sessionID }, { throwOnError: true })
const messages = messagesData.map((m) => ({
...m.info,
parts: m.parts,
createdAt: new Date(m.info.time.created).toISOString(),
}))
provider.postMessage({
type: "messagesLoaded",
sessionID,
messages,
})
// Navigate to the sub-agent viewer
provider.postMessage({ type: "viewSubAgentSession", sessionID })
} catch (err) {
console.error("[Kilo New] SubAgentViewerProvider: Failed to load session:", err)
}
})
// Listen for closePanel from the webview (back button)
const closeDisposable = panel.webview.onDidReceiveMessage((msg) => {
if (msg.type === "closePanel") {
panel.dispose()
}
})
this.panels.set(sessionID, panel)
this.providers.set(sessionID, provider)
panel.onDidDispose(() => {
console.log("[Kilo New] Sub-agent viewer panel disposed:", sessionID)
closeDisposable.dispose()
provider.dispose()
this.panels.delete(sessionID)
this.providers.delete(sessionID)
})
}
dispose(): void {
for (const [, panel] of this.panels) {
panel.dispose()
}
this.panels.clear()
this.providers.clear()
}
}
+8
View File
@@ -3,6 +3,7 @@ import { KiloProvider } from "./KiloProvider"
import { AgentManagerProvider } from "./agent-manager/AgentManagerProvider"
import { DiffViewerProvider } from "./DiffViewerProvider"
import { SettingsEditorProvider } from "./SettingsEditorProvider"
import { SubAgentViewerProvider } from "./SubAgentViewerProvider"
import { EXTENSION_DISPLAY_NAME } from "./constants"
import { KiloConnectionService } from "./services/cli-backend"
import { registerAutocompleteProvider } from "./services/autocomplete"
@@ -60,6 +61,10 @@ export function activate(context: vscode.ExtensionContext) {
const settingsEditorProvider = new SettingsEditorProvider(context.extensionUri, connectionService, context)
context.subscriptions.push(settingsEditorProvider)
// Create sub-agent viewer provider (read-only editor panel for sub-agent sessions)
const subAgentViewerProvider = new SubAgentViewerProvider(context.extensionUri, connectionService, context)
context.subscriptions.push(subAgentViewerProvider)
// Register toolbar button command handlers
context.subscriptions.push(
vscode.commands.registerCommand("kilo-code.new.plusButtonClicked", () => {
@@ -94,6 +99,9 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.showChanges", () => {
diffViewerProvider.openPanel()
}),
vscode.commands.registerCommand("kilo-code.new.openSubAgentViewer", (sessionID: string, title?: string) => {
subAgentViewerProvider.openPanel(sessionID, title)
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.previousSession", () => {
agentManagerProvider.postMessage({ type: "action", action: "sessionPrevious" })
}),
+20 -3
View File
@@ -33,7 +33,15 @@ import { NotificationsProvider } from "./context/notifications"
import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2"
import "./styles/chat.css"
type ViewType = "newTask" | "marketplace" | "history" | "cloudHistory" | "profile" | "settings" | "migration" // legacy-migration
type ViewType =
| "newTask"
| "marketplace"
| "history"
| "cloudHistory"
| "profile"
| "settings"
| "migration" // legacy-migration
| "subAgentViewer"
const VALID_VIEWS = new Set<string>([
"newTask",
"marketplace",
@@ -41,8 +49,9 @@ const VALID_VIEWS = new Set<string>([
"cloudHistory",
"profile",
"settings",
"migration",
]) // legacy-migration
"migration", // legacy-migration
"subAgentViewer",
])
const DummyView: Component<{ title: string }> = (props) => {
return (
@@ -209,6 +218,11 @@ const AppContent: Component = () => {
session.selectCloudSession(message.sessionId)
setCurrentView("newTask")
}
if (message?.type === "viewSubAgentSession" && message.sessionID) {
console.log("[Kilo New] App: 🔍 viewSubAgentSession:", message.sessionID)
session.setCurrentSessionID(message.sessionID)
setCurrentView("subAgentViewer")
}
}
window.addEventListener("message", handler)
onCleanup(() => window.removeEventListener("message", handler))
@@ -266,6 +280,9 @@ const AppContent: Component = () => {
/>
</Match>
{/* legacy-migration end */}
<Match when={currentView() === "subAgentViewer"}>
<ChatView readonly />
</Match>
</Switch>
</div>
)
@@ -89,6 +89,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
)
onMount(() => {
if (props.readonly) return
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape" && session.status() === "busy") {
e.preventDefault()
@@ -107,7 +108,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
return (
<div class="chat-view">
<TaskHeader />
<TaskHeader readonly={props.readonly} />
<div class="chat-messages-wrapper">
<div class="chat-messages">
<MessageList onSelectSession={props.onSelectSession} />
@@ -14,7 +14,11 @@ import { useSession } from "../../context/session"
import { useLanguage } from "../../context/language"
import type { TodoItem } from "../../types/messages"
export const TaskHeader: Component = () => {
interface TaskHeaderProps {
readonly?: boolean
}
export const TaskHeader: Component<TaskHeaderProps> = (props) => {
const session = useSession()
const language = useLanguage()
@@ -80,16 +84,18 @@ export const TaskHeader: Component = () => {
</Tooltip>
)}
</Show>
<Tooltip value={language.t("command.session.compact")} placement="bottom">
<IconButton
icon="collapse"
size="small"
variant="ghost"
disabled={!canCompact()}
onClick={() => session.compact()}
aria-label={language.t("command.session.compact")}
/>
</Tooltip>
<Show when={!props.readonly}>
<Tooltip value={language.t("command.session.compact")} placement="bottom">
<IconButton
icon="collapse"
size="small"
variant="ghost"
disabled={!canCompact()}
onClick={() => session.compact()}
aria-label={language.t("command.session.compact")}
/>
</Tooltip>
</Show>
</div>
</div>
<Show when={hasTodos()}>
@@ -11,10 +11,13 @@ import { Component, createEffect, createMemo, For, Show } from "solid-js"
import { ToolRegistry, ToolProps, getToolInfo } from "@kilocode/kilo-ui/message-part"
import { BasicTool } from "@kilocode/kilo-ui/basic-tool"
import { Icon } from "@kilocode/kilo-ui/icon"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { useData } from "@kilocode/kilo-ui/context/data"
import { useI18n } from "@kilocode/kilo-ui/context/i18n"
import { createAutoScroll } from "@kilocode/kilo-ui/hooks"
import { useSession } from "../../context/session"
import { useVSCode } from "../../context/vscode"
import { useWorktreeMode } from "../../context/worktree-mode"
import type { ToolPart, Message as SDKMessage } from "@kilocode/sdk/v2"
/** Collect all tool parts from all assistant messages in a given session. */
@@ -37,6 +40,10 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
const data = useData()
const i18n = useI18n()
const session = useSession()
const vscode = useVSCode()
const worktreeMode = useWorktreeMode()
// Hide the open-in-tab button inside the Agent Manager
const inAgentManager = worktreeMode !== undefined
const childSessionId = () => props.metadata.sessionId as string | undefined
@@ -68,6 +75,13 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
overflowAnchor: "auto",
})
const openInTab = (e: MouseEvent) => {
e.stopPropagation()
const id = childSessionId()
if (!id) return
vscode.postMessage({ type: "openSubAgentViewer", sessionID: id, title: description() })
}
const trigger = () => (
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
@@ -78,6 +92,15 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
<span data-slot="basic-tool-tool-subtitle">{description()}</span>
</Show>
</div>
<Show when={!inAgentManager && childSessionId()}>
<IconButton
icon="square-arrow-top-right"
size="small"
variant="ghost"
aria-label="Open sub-agent in tab"
onClick={openInTab}
/>
</Show>
</div>
)
@@ -531,7 +531,7 @@ export interface DeviceAuthCancelledMessage {
export interface NavigateMessage {
type: "navigate"
view: "newTask" | "marketplace" | "history" | "cloudHistory" | "profile" | "settings" | "migration" // legacy-migration
view: "newTask" | "marketplace" | "history" | "cloudHistory" | "profile" | "settings" | "migration" | "subAgentViewer" // legacy-migration: "migration"
}
export interface ProvidersLoadedMessage {
@@ -961,6 +961,12 @@ export interface EnhancePromptErrorMessage {
requestId: string
}
// Sub-agent viewer: open a child session in read-only mode (extension → webview)
export interface ViewSubAgentSessionMessage {
type: "viewSubAgentSession"
sessionID: string
}
export interface DiffViewerDiffsMessage {
type: "diffViewer.diffs"
diffs: WorktreeFileDiff[]
@@ -1043,6 +1049,7 @@ export type ExtensionMessage =
// legacy-migration end
| EnhancePromptResultMessage
| EnhancePromptErrorMessage
| ViewSubAgentSessionMessage
| DiffViewerDiffsMessage
| DiffViewerLoadingMessage
@@ -1497,6 +1504,13 @@ export interface OpenChangesRequest {
type: "openChanges"
}
// Open a sub-agent session in a read-only editor panel
export interface OpenSubAgentViewerRequest {
type: "openSubAgentViewer"
sessionID: string
title?: string
}
// Set default base branch (webview → extension)
export interface SetDefaultBaseBranchRequest {
type: "agentManager.setDefaultBaseBranch"
@@ -1586,6 +1600,7 @@ export type WebviewMessage =
| ApplyWorktreeDiffMessage
| EnhancePromptRequest
| OpenChangesRequest
| OpenSubAgentViewerRequest
| SetDefaultBaseBranchRequest
// ============================================