Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/background-subagent-reasoning-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Keep background subagent task cards collapsed and show their reasoning as a compact preview instead of expanding while the agent runs.
23 changes: 15 additions & 8 deletions packages/kilo-ui/src/components/message-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ export interface MessagePartProps {
* lets that one nested item open instead of every file in the patch. */
forceOpenFile?: string
reasoningAutoCollapse?: boolean
/** Show reasoning as a capped preview that starts open and never auto-expands
* while streaming. Used for background subagent transcripts. */
reasoningCapped?: boolean
/** True when the stream has moved past this reasoning part. Encrypted
* reasoning items hold every summary's `time.end` until the whole item
* finishes, so the caller settles finished summaries from the part order. */
Expand Down Expand Up @@ -1081,6 +1084,7 @@ export function Part(props: MessagePartProps) {
forceOpen={props.forceOpen}
forceOpenFile={props.forceOpenFile}
reasoningAutoCollapse={props.reasoningAutoCollapse}
reasoningCapped={props.reasoningCapped}
settled={props.settled}
showAssistantCopyPartID={props.showAssistantCopyPartID}
showTurnDiffSummary={props.showTurnDiffSummary}
Expand Down Expand Up @@ -1884,12 +1888,15 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp

// Auto-collapse mode: streaming or streamed this session -> open (capped),
// historical -> collapsed, unless the user toggled it. Expanded mode: open
// unless the user explicitly collapsed this reasoning part.
const initial = props.reasoningAutoCollapse
? !userCollapsed.has(id) && (streamed.has(id) || userOpened.has(id))
: !userCollapsed.has(id)
// unless the user explicitly collapsed this reasoning part. Background
// transcripts always start open in the capped preview so they stay compact.
const capped = () => props.reasoningAutoCollapse || props.reasoningCapped
const initial =
props.reasoningAutoCollapse && !props.reasoningCapped
? !userCollapsed.has(id) && (streamed.has(id) || userOpened.has(id))
: !userCollapsed.has(id)
const [open, setOpen] = createSignal(initial)
const [manual, setManual] = createSignal(props.reasoningAutoCollapse && userOpened.has(id))
const [manual, setManual] = createSignal(capped() && userOpened.has(id))
const title = createMemo(() => {
const value = view().title
if (value) return value
Expand All @@ -1908,7 +1915,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
const track = (value: boolean) => {
if (value) userCollapsed.delete(id)
else rememberReasoningState(userCollapsed, id)
if (props.reasoningAutoCollapse) {
if (capped()) {
if (value) rememberReasoningState(userOpened, id)
else userOpened.delete(id)
setManual(value)
Expand All @@ -1924,7 +1931,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
createEffect(() => {
if (!props.forceOpen || open()) return
userCollapsed.delete(id)
if (props.reasoningAutoCollapse) {
if (capped()) {
rememberReasoningState(userOpened, id)
setManual(true)
}
Expand Down Expand Up @@ -2007,7 +2014,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
<div
data-component="reasoning-part"
data-streaming={!done() ? "" : undefined}
data-auto-collapse={props.reasoningAutoCollapse ? "" : undefined}
data-auto-collapse={capped() ? "" : undefined}
data-manual={manual() ? "" : undefined}
>
<Show
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message.sessionID,
message.title,
this.getWorkspaceDirectory(message.parentSessionID),
message.background === true,
)
break
case "saveImage":
Expand Down
15 changes: 13 additions & 2 deletions packages/kilo-vscode/src/SubAgentViewerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@ import type { KiloConnectionService } from "./services/cli-backend"
export class SubAgentViewerProvider implements vscode.Disposable {
private panels = new Map<string, vscode.WebviewPanel>()
private providers = new Map<string, KiloProvider>()
private backgrounds = new Map<string, boolean>()

constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
private readonly context: vscode.ExtensionContext,
) {}

openPanel(sessionID: string, title?: string, directory?: string): void {
openPanel(sessionID: string, title?: string, directory?: string, background?: boolean): void {
Comment thread
marius-kilocode marked this conversation as resolved.
const existing = this.panels.get(sessionID)
if (existing) {
if (directory) this.providers.get(sessionID)?.setSessionDirectory(sessionID, directory)
// A reused panel keeps its original flag. Re-post when the caller reports
// a different one so a transcript that first opened before the task
// metadata landed still gets the reasoning cap.
if (background !== undefined && this.backgrounds.get(sessionID) !== background) {
this.backgrounds.set(sessionID, background)
this.providers.get(sessionID)?.postMessage({ type: "viewSubAgentSession", sessionID, background })
}
existing.reveal(vscode.ViewColumn.One)
return
}
Expand Down Expand Up @@ -61,7 +69,7 @@ export class SubAgentViewerProvider implements vscode.Disposable {
if (msg.type !== "webviewReady") return
readyDisposable.dispose()

provider.postMessage({ type: "viewSubAgentSession", sessionID })
provider.postMessage({ type: "viewSubAgentSession", sessionID, background })
void provider.loadMessages(sessionID)

try {
Expand All @@ -86,13 +94,15 @@ export class SubAgentViewerProvider implements vscode.Disposable {

this.panels.set(sessionID, panel)
this.providers.set(sessionID, provider)
this.backgrounds.set(sessionID, background === true)

panel.onDidDispose(() => {
console.log("[Kilo New] Sub-agent viewer panel disposed:", sessionID)
closeDisposable.dispose()
provider.dispose()
this.panels.delete(sessionID)
this.providers.delete(sessionID)
this.backgrounds.delete(sessionID)
})
}

Expand All @@ -102,5 +112,6 @@ export class SubAgentViewerProvider implements vscode.Disposable {
}
this.panels.clear()
this.providers.clear()
this.backgrounds.clear()
}
}
4 changes: 2 additions & 2 deletions packages/kilo-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,8 +598,8 @@ export async function activate(context: vscode.ExtensionContext) {
),
vscode.commands.registerCommand(
"kilo-code.new.openSubAgentViewer",
(sessionID: string, title?: string, directory?: string) => {
subAgentViewerProvider.openPanel(sessionID, title, directory)
(sessionID: string, title?: string, directory?: string, background?: boolean) => {
subAgentViewerProvider.openPanel(sessionID, title, directory, background)
},
),
vscode.commands.registerCommand("kilo-code.new.agentManager.previousSession", () => {
Expand Down
28 changes: 28 additions & 0 deletions packages/kilo-vscode/tests/unit/background-agents.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "bun:test"
import {
backgroundAgents,
backgroundChildren,
backgroundJobAgents,
fitBackgroundAgents,
showBackgroundAgent,
Expand Down Expand Up @@ -73,6 +74,33 @@ describe("fitBackgroundAgents", () => {
})
})

describe("backgroundChildren", () => {
it("collects only children spawned with the background flag", () => {
const tools = [
taskPart({ id: "part_1", child: "ses_a", background: true }),
taskPart({ id: "part_2", child: "ses_b", background: false }),
taskPart({ id: "part_3", child: "ses_c" }),
]

expect([...backgroundChildren(tools)]).toEqual(["ses_a"])
})

it("reads the flag from either the state or the part metadata", () => {
const tools = [
taskPart({ id: "part_1", child: "ses_a", background: true }),
taskPart({ id: "part_2", child: "ses_b", background: true, onPart: true }),
]

expect([...backgroundChildren(tools)].sort()).toEqual(["ses_a", "ses_b"])
})

it("ignores non-task tools and parts without a child session", () => {
const bash = { id: "part_3", type: "tool", tool: "bash", state: { status: "running", input: {} } } as ToolPart

expect([...backgroundChildren([bash, taskPart({ id: "part_4", background: true })])]).toEqual([])
})
})

describe("backgroundAgents", () => {
it("lists a running background agent from tool state metadata", () => {
const tools = [taskPart({ child: "ses_child", background: true, description: "Audit deps", agent: "explore" })]
Expand Down
27 changes: 21 additions & 6 deletions packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { createEffect, createMemo, on, type Accessor, type Component } from "solid-js"
import { DataBridge } from "../src/App"
import { ChatView } from "../src/components/chat"
import { children } from "../src/components/chat/background-agents"
import { children, backgroundChildren } from "../src/components/chat/background-agents"
import { useLanguage } from "../src/context/language"
import { SessionProvider, useSession, useSessionVisibility } from "../src/context/session"
import { description, label, type Activity } from "../src/utils/session-activity"
Expand All @@ -32,7 +32,7 @@ interface Props {
onClosePanel: () => void
}

const SubagentChat: Component<{ active: Accessor<string | undefined> }> = (props) => {
const SubagentChat: Component<{ active: Accessor<string | undefined>; capped: Accessor<boolean> }> = (props) => {
const session = useSession()

createEffect(
Expand All @@ -44,12 +44,22 @@ const SubagentChat: Component<{ active: Accessor<string | undefined> }> = (props

return (
<DataBridge>
<ChatView readonly interactivePrompts={false} promptBoxId="agent-manager:subagent" />
<ChatView
readonly
interactivePrompts={false}
reasoningCapped={props.capped()}
promptBoxId="agent-manager:subagent"
/>
</DataBridge>
)
}

const SubagentContent: Component<Props & { activity: (id: string) => Activity }> = (props) => {
interface ContentProps extends Props {
activity: (id: string) => Activity
background: Accessor<ReadonlySet<string>>
}

const SubagentContent: Component<ContentProps> = (props) => {
const session = useSession()
const language = useLanguage()
const ids = () => props.tabs().map((tab) => tab.id)
Expand Down Expand Up @@ -131,7 +141,7 @@ const SubagentContent: Component<Props & { activity: (id: string) => Activity }>
}}
/>
<div class="am-subagent-chat">
<SubagentChat active={props.active} />
<SubagentChat active={props.active} capped={() => props.background().has(props.active() ?? "")} />
</div>
</section>
)
Expand All @@ -145,10 +155,15 @@ export const SubagentPanel: Component<Props> = (props) => {
const id = session.currentSessionID()
return id ? children(session.getSessionToolParts(id)) : []
})
// Background subagent transcripts show reasoning as a compact capped preview.
const background = createMemo(() => {
const id = session.currentSessionID()
return id ? backgroundChildren(session.getSessionToolParts(id)) : new Set<string>()
})
return (
<AgentAvatarPalette ids={siblings()}>
<SessionProvider>
<SubagentContent {...props} activity={session.activityFor} />
<SubagentContent {...props} activity={session.activityFor} background={background} />
</SessionProvider>
</AgentAvatarPalette>
)
Expand Down
7 changes: 6 additions & 1 deletion packages/kilo-vscode/webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ChatView } from "./components/chat"
import { SidebarEmptyState } from "./components/chat/SidebarEmptyState"
import { SidebarTopBar } from "./components/chat/SidebarTopBar"
import { openSubagent } from "./components/chat/open-subagent"
import { backgroundChildren } from "./components/chat/background-agents"
import { registerExpandedTaskTool } from "./components/chat/TaskToolExpanded"
import { registerVscodeToolOverrides } from "./components/chat/VscodeToolOverrides"
import { useWorktreeMode } from "./context/worktree-mode"
Expand Down Expand Up @@ -132,10 +133,12 @@ export const DataBridge: Component<{ children: any }> = (props) => {

const openAgent = (id: string, title?: string) => {
const parent = session.sessions().find((item) => item.id === id)?.parentID ?? session.currentSessionID()
const background = parent ? backgroundChildren(session.getSessionToolParts(parent)).has(id) : false
openSubagent({
sessionID: id,
title,
parentSessionID: parent,
background,
worktree: !!worktree,
post: vscode.postMessage,
})
Expand Down Expand Up @@ -243,6 +246,7 @@ const AppContent: Component = () => {
const [currentView, setCurrentView] = createSignal<ViewType>("newTask")
const [settingsTab, setSettingsTab] = createSignal<string | undefined>()
const [agentManagerProjectId, setAgentManagerProjectId] = createSignal<string | undefined>()
const [subAgentCapped, setSubAgentCapped] = createSignal(false)
const [migration, setMigration] = createSignal(false)
const session = useSession()
const tabs = useLocalTabs()
Expand Down Expand Up @@ -345,6 +349,7 @@ const AppContent: Component = () => {
handleForked(message)
if (message?.type === "viewSubAgentSession" && message.sessionID) {
console.log("[Kilo New] App: 🔍 viewSubAgentSession:", message.sessionID)
setSubAgentCapped(message.background === true)
session.setCurrentSessionID(message.sessionID)
setCurrentView("subAgentViewer")
}
Expand Down Expand Up @@ -439,7 +444,7 @@ const AppContent: Component = () => {
/>
</Match>
<Match when={currentView() === "subAgentViewer"}>
<ChatView readonly />
<ChatView readonly reasoningCapped={subAgentCapped()} />
</Match>
</Switch>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ interface AssistantMessageProps {
highlight?: () => TimelineHighlight | undefined
readonly?: boolean
interactivePrompts?: boolean
/** Show reasoning as a compact capped preview (background subagent transcripts). */
reasoningCapped?: boolean
}

type ToolStateProps = {
Expand Down Expand Up @@ -364,6 +366,7 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
forceOpen={forceOpen()}
forceOpenFile={forceOpen() ? props.forceOpenFile : undefined}
reasoningAutoCollapse={display.reasoningAutoCollapse()}
reasoningCapped={props.reasoningCapped}
settled={settled()}
feedback={props.feedback}
throughput={throughputEl()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export const BackgroundAgents: Component<{ readonly?: boolean }> = (props) => {
sessionID: agent.id,
title: agent.description,
parentSessionID: session.currentSessionID(),
background: true,
worktree: !!worktree,
post: vscode.postMessage,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ interface ChatViewProps {
onForkMessage?: (sessionId: string, messageId: string) => void
onForkSession?: (sessionId: string) => void
readonly?: boolean
/** Show reasoning as a compact capped preview (background subagent transcripts). */
reasoningCapped?: boolean
/** Whether this chat owns actionable prompt controls. Defaults to true. */
interactivePrompts?: boolean
/** When true, show the "Continue in Worktree" button. Defaults to true in the sidebar. */
Expand Down Expand Up @@ -399,6 +401,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
questions={standaloneQuestions}
suggestions={standaloneSuggestions}
readonly={props.readonly}
reasoningCapped={props.reasoningCapped}
interactivePrompts={ownsPrompts()}
emptyState={props.emptyState}
introduction={props.introduction}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ interface MessageListProps {
suggestions?: () => SuggestionRequest[]
/** When true (subagent viewer), replace the welcome screen with an initializing indicator */
readonly?: boolean
/** Show reasoning as a compact capped preview (background subagent transcripts). */
reasoningCapped?: boolean
/** Whether inline questions and suggestions are actionable on this surface. */
interactivePrompts?: boolean
queuedDisabled?: boolean
Expand Down Expand Up @@ -1362,6 +1364,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
activeSearchPartFile={activeKey() === row.key ? activeMatch()?.partFile : undefined}
readonly={props.readonly}
interactivePrompts={props.interactivePrompts}
reasoningCapped={props.reasoningCapped}
/>
)}
</Virtualizer>
Expand All @@ -1382,6 +1385,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
activeSearchPartFile={activeKey() === key ? activeMatch()?.partFile : undefined}
readonly={props.readonly}
interactivePrompts={props.interactivePrompts}
reasoningCapped={props.reasoningCapped}
/>
)}
</For>
Expand All @@ -1404,6 +1408,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
activeSearchPartFile={activeKey() === row.key ? activeMatch()?.partFile : undefined}
readonly={props.readonly}
interactivePrompts={props.interactivePrompts}
reasoningCapped={props.reasoningCapped}
/>
)}
</For>
Expand Down
Loading
Loading