mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 10:02:04 +08:00
Merge pull request #6911 from Kilo-Org/mark/sub-agent-viewer
feat(vscode): sub-agent viewer panel
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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" })
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user