mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
Merge pull request #13261 from Kilo-Org/fix-worktree-sidebar-agent-switching
fix(agent-manager): scope subagent inspector by session
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep Agent Manager subagent inspector tabs aligned with the selected session and worktree.
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { createRoot, createSignal } from "solid-js"
|
||||
import { createSubagentTabs } from "../../webview-ui/agent-manager/subagent-tabs"
|
||||
import { availableSubagents, createSubagentTabs } from "../../webview-ui/agent-manager/subagent-tabs"
|
||||
|
||||
function scene() {
|
||||
const [current] = createSignal<string | undefined>("parent")
|
||||
@@ -89,4 +89,68 @@ describe("Agent Manager subagent tabs", () => {
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps tabs and active children separate for each context", () => {
|
||||
createRoot((dispose) => {
|
||||
const [context, setContext] = createSignal("worktree-a")
|
||||
const calls = {
|
||||
synced: [] as Array<[string, string | undefined]>,
|
||||
unsynced: [] as string[],
|
||||
shown: 0,
|
||||
hidden: 0,
|
||||
}
|
||||
const item = createSubagentTabs({
|
||||
current: () => "parent",
|
||||
context: () => context(),
|
||||
sync: (id, parent) => calls.synced.push([id, parent]),
|
||||
unsync: (id) => calls.unsynced.push(id),
|
||||
show: () => calls.shown++,
|
||||
hide: () => calls.hidden++,
|
||||
})
|
||||
|
||||
item.open("child-a", "A", "parent-a")
|
||||
setContext("worktree-b")
|
||||
item.open("child-b", "B", "parent-b")
|
||||
|
||||
expect(item.tabs().map((tab) => tab.id)).toEqual(["child-b"])
|
||||
expect(item.active()).toBe("child-b")
|
||||
setContext("worktree-a")
|
||||
expect(item.tabs().map((tab) => tab.id)).toEqual(["child-a"])
|
||||
expect(item.active()).toBe("child-a")
|
||||
expect(calls.synced).toEqual([
|
||||
["child-a", "parent-a"],
|
||||
["child-b", "parent-b"],
|
||||
])
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it("finds direct subagent sessions in task tool parts", () => {
|
||||
const tabs = availableSubagents([
|
||||
{
|
||||
id: "task-1",
|
||||
type: "tool",
|
||||
tool: "task",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { description: "Inspect files", subagent_type: "explore" },
|
||||
output: "",
|
||||
title: "",
|
||||
},
|
||||
metadata: { sessionId: "child-1" },
|
||||
},
|
||||
{
|
||||
id: "task-2",
|
||||
type: "tool",
|
||||
tool: "task",
|
||||
state: { status: "running", input: { subagent_type: "general" } },
|
||||
metadata: { sessionId: "child-2" },
|
||||
},
|
||||
])
|
||||
|
||||
expect(tabs).toEqual([
|
||||
{ id: "child-1", title: "Inspect files" },
|
||||
{ id: "child-2", title: "general" },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -179,7 +179,7 @@ import { SidebarToggleButton } from "./SidebarToggleButton"
|
||||
import { setTabWidths } from "./tab-widths"
|
||||
import { clampPanelWidth, createPanelResize, maxPanelWidth, minPanelWidth, SidePanel } from "./side-panel-layout"
|
||||
import { SubagentPanel } from "./SubagentPanel"
|
||||
import { createSubagentTabs } from "./subagent-tabs"
|
||||
import { createSubagentController } from "./subagent-tabs"
|
||||
import { buildShortcutCategories } from "./shortcuts"
|
||||
import { tracker } from "./telemetry"
|
||||
import { createChatFocus, createFocusBridge, createPromptFocus, forgetTerminalFocus, hasQuestionOption } from "./focus"
|
||||
@@ -308,8 +308,6 @@ const AgentManagerContent: Component = () => {
|
||||
const diffLoading = diffs.diffLoading
|
||||
const setDiffLoading = diffs.setDiffLoading
|
||||
const diffNotices = diffs.diffNotices
|
||||
// Diff, PR, terminal, and subagent views share one inspector width, restored
|
||||
// from webview state so the user's divider position survives panel reloads.
|
||||
const [panelWidth, setPanelWidth] = createSignal(clampPanelWidth(persisted?.sidePanelWidth, window.innerWidth))
|
||||
const resizeSide = createPanelResize(setPanelWidth, () => window.innerWidth)
|
||||
const showSideTerminal = () => {
|
||||
@@ -323,8 +321,12 @@ const AgentManagerContent: Component = () => {
|
||||
const reviewComposer = createReviewComposer()
|
||||
const [reviewActive, setReviewActive] = createSignal(false)
|
||||
const [reviewDiffStyle, setReviewDiffStyle] = createSignal<"unified" | "split">("unified")
|
||||
const subagents = createSubagentTabs({
|
||||
const subagentCtl = createSubagentController({
|
||||
project: currentProjectId,
|
||||
current: session.currentSessionID,
|
||||
selection,
|
||||
parts: session.getSessionToolParts,
|
||||
visible: () => sidePanel() === SidePanel.Subagents,
|
||||
sync: (id, parentID) => session.syncSession(id, parentID, "inspector"),
|
||||
unsync: (id) => session.unsyncSession(id, "inspector"),
|
||||
show: () => {
|
||||
@@ -334,8 +336,8 @@ const AgentManagerContent: Component = () => {
|
||||
},
|
||||
hide: () => setSidePanel(null),
|
||||
})
|
||||
const subagents = subagentCtl.tabs
|
||||
const markdown = createMarkdownRender(vscode)
|
||||
// Per-worktree git stats (diff additions/deletions, commits missing from origin)
|
||||
const worktreeStats = () => registry.active().worktreeStats()
|
||||
|
||||
const prStatuses = () => registry.active().prStatuses()
|
||||
@@ -344,7 +346,6 @@ const AgentManagerContent: Component = () => {
|
||||
const runScriptConfigured = () => registry.active().runScriptConfigured()
|
||||
const setRunScriptConfigured = (v: Parameters<Setter<boolean>>[0]) => registry.active().setRunScriptConfigured(v)
|
||||
|
||||
// Local repo git stats (branch name, diff additions/deletions, commits)
|
||||
const localStats = () => registry.active().localStats()
|
||||
const projectLive = createProjectLive({
|
||||
ensure: (pid) => (pid ? registry.ensure(pid) : registry.active()),
|
||||
@@ -459,7 +460,6 @@ const AgentManagerContent: Component = () => {
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
// Ambient setup reveal restores the panel after success unless the user engaged.
|
||||
const ambientSetup = createAmbientSetup({
|
||||
terms,
|
||||
selection: () => {
|
||||
@@ -471,7 +471,6 @@ const AgentManagerContent: Component = () => {
|
||||
})
|
||||
const cancelAmbientSetup = ambientSetup.cancel
|
||||
|
||||
// Inline delete confirmation: tracks which worktree is awaiting a second click/press
|
||||
const [pendingDelete, setPendingDelete] = createSignal<string | null>(null)
|
||||
let pendingDeleteTimer: ReturnType<typeof setTimeout> | undefined
|
||||
const cancelPendingDelete = () => {
|
||||
@@ -2455,6 +2454,9 @@ const AgentManagerContent: Component = () => {
|
||||
prStatus={() => activePR()?.pr}
|
||||
prOpen={prOpen}
|
||||
onTogglePR={togglePRPanel}
|
||||
subagentsAvailable={() => subagentCtl.tabs.tabs().length > 0 || subagentCtl.toolbar.available().length > 0}
|
||||
subagentsOpen={() => sidePanel() === SidePanel.Subagents}
|
||||
onToggleSubagents={subagentCtl.toolbar.toggle}
|
||||
terminalDestination={sideCtl.destination}
|
||||
terminalDestinationActive={() => sidePanel() === SidePanel.Terminal}
|
||||
terminalKeybind={() => kb().showTerminal ?? ""}
|
||||
|
||||
@@ -58,6 +58,9 @@ export interface TabBarProps {
|
||||
prStatus: () => PRStatus | undefined
|
||||
prOpen: () => boolean
|
||||
onTogglePR: () => void
|
||||
subagentsAvailable: () => boolean
|
||||
subagentsOpen: () => boolean
|
||||
onToggleSubagents: () => void
|
||||
terminalDestination: () => TerminalDestination
|
||||
terminalDestinationActive: () => boolean
|
||||
terminalKeybind: () => string
|
||||
@@ -232,6 +235,18 @@ export const TabBar: Component<TabBarProps> = (props) => (
|
||||
</Tooltip>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.subagentsAvailable()}>
|
||||
<Tooltip value="Subagents" placement="bottom">
|
||||
<IconButton
|
||||
icon="task"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label="Subagents"
|
||||
class={props.subagentsOpen() ? "am-tab-diff-btn-active" : ""}
|
||||
onClick={props.onToggleSubagents}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
title={props.t("agentManager.diff.toggle")}
|
||||
keybind={props.bindings().toggleDiff ?? ""}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { batch, createSignal, type Accessor } from "solid-js"
|
||||
import { batch, createEffect, createMemo, createSignal, on, type Accessor } from "solid-js"
|
||||
import { reorderTabs } from "../src/utils/tab-order"
|
||||
import { childID } from "../src/context/session-utils"
|
||||
import type { ToolPart } from "../src/types/messages"
|
||||
|
||||
export interface SubagentTab {
|
||||
id: string
|
||||
@@ -8,83 +10,193 @@ export interface SubagentTab {
|
||||
|
||||
interface Options {
|
||||
current: Accessor<string | undefined>
|
||||
context?: (parentID?: string) => string
|
||||
sync: (id: string, parentID?: string) => void
|
||||
unsync: (id: string) => void
|
||||
show: () => void
|
||||
hide: () => void
|
||||
}
|
||||
|
||||
export function createSubagentContext(opts: {
|
||||
project: Accessor<string | undefined>
|
||||
current: Accessor<string | undefined>
|
||||
selection: Accessor<string | null>
|
||||
}) {
|
||||
return (parentID?: string) => {
|
||||
const project = opts.project() ?? "single"
|
||||
return `${project}:${parentID ?? opts.current() ?? opts.selection() ?? "unassigned"}`
|
||||
}
|
||||
}
|
||||
|
||||
export function createSubagentTabs(opts: Options) {
|
||||
const [tabs, setTabs] = createSignal<SubagentTab[]>([])
|
||||
const [active, setActive] = createSignal<string>()
|
||||
const [tabs, setTabs] = createSignal<Record<string, SubagentTab[]>>({})
|
||||
const [active, setActive] = createSignal<Record<string, string | undefined>>({})
|
||||
const key = (parentID?: string) => opts.context?.(parentID) ?? "default"
|
||||
const list = () => tabs()[key()] ?? []
|
||||
const selected = () => active()[key()]
|
||||
|
||||
const open = (id: string, title?: string, parentID?: string) => {
|
||||
if (!id) return
|
||||
const label = title?.trim() || "Sub-agent"
|
||||
const existing = tabs().some((tab) => tab.id === id)
|
||||
const scope = key(parentID)
|
||||
const existing = (tabs()[scope] ?? []).some((tab) => tab.id === id)
|
||||
batch(() => {
|
||||
setTabs((prev) => {
|
||||
const existing = prev.find((tab) => tab.id === id)
|
||||
if (!existing) return [...prev, { id, title: label }]
|
||||
const current = prev[scope] ?? []
|
||||
const existing = current.find((tab) => tab.id === id)
|
||||
if (!existing) return { ...prev, [scope]: [...current, { id, title: label }] }
|
||||
if (title?.trim() && existing.title !== label) {
|
||||
return prev.map((tab) => (tab.id === id ? { ...tab, title: label } : tab))
|
||||
return { ...prev, [scope]: current.map((tab) => (tab.id === id ? { ...tab, title: label } : tab)) }
|
||||
}
|
||||
return prev
|
||||
})
|
||||
setActive(id)
|
||||
setActive((prev) => ({ ...prev, [scope]: id }))
|
||||
opts.show()
|
||||
})
|
||||
if (!existing) opts.sync(id, parentID ?? opts.current())
|
||||
}
|
||||
|
||||
const select = (id: string) => {
|
||||
if (!tabs().some((tab) => tab.id === id)) return
|
||||
setActive(id)
|
||||
if (!list().some((tab) => tab.id === id)) return
|
||||
setActive((prev) => ({ ...prev, [key()]: id }))
|
||||
opts.show()
|
||||
}
|
||||
|
||||
const close = (id: string) => {
|
||||
const current = tabs()
|
||||
const scope = key()
|
||||
const current = tabs()[scope] ?? []
|
||||
const index = current.findIndex((tab) => tab.id === id)
|
||||
if (index < 0) return
|
||||
const next = current.filter((tab) => tab.id !== id)
|
||||
opts.unsync(id)
|
||||
setTabs(next)
|
||||
if (active() !== id) return
|
||||
setTabs((prev) => ({ ...prev, [scope]: next }))
|
||||
if (selected() !== id) return
|
||||
const replacement = next[Math.min(index, next.length - 1)]
|
||||
if (replacement) {
|
||||
setActive(replacement.id)
|
||||
setActive((prev) => ({ ...prev, [scope]: replacement.id }))
|
||||
return
|
||||
}
|
||||
setActive(undefined)
|
||||
setActive((prev) => ({ ...prev, [scope]: undefined }))
|
||||
opts.hide()
|
||||
}
|
||||
|
||||
const closeOthers = (id: string) => {
|
||||
if (!tabs().some((tab) => tab.id === id)) return
|
||||
for (const tab of tabs()) {
|
||||
const scope = key()
|
||||
const current = tabs()[scope] ?? []
|
||||
if (!current.some((tab) => tab.id === id)) return
|
||||
for (const tab of current) {
|
||||
if (tab.id !== id) opts.unsync(tab.id)
|
||||
}
|
||||
setTabs((prev) => prev.filter((tab) => tab.id === id))
|
||||
setActive(id)
|
||||
setTabs((prev) => ({ ...prev, [scope]: current.filter((tab) => tab.id === id) }))
|
||||
setActive((prev) => ({ ...prev, [scope]: id }))
|
||||
opts.show()
|
||||
}
|
||||
|
||||
const reorder = (from: string, to: string) => {
|
||||
const order = reorderTabs(
|
||||
tabs().map((tab) => tab.id),
|
||||
list().map((tab) => tab.id),
|
||||
from,
|
||||
to,
|
||||
)
|
||||
if (!order) return
|
||||
const scope = key()
|
||||
setTabs((prev) => {
|
||||
const lookup = new Map(prev.map((tab) => [tab.id, tab]))
|
||||
return order.flatMap((id) => {
|
||||
const tab = lookup.get(id)
|
||||
return tab ? [tab] : []
|
||||
})
|
||||
const lookup = new Map((prev[scope] ?? []).map((tab) => [tab.id, tab]))
|
||||
return {
|
||||
...prev,
|
||||
[scope]: order.flatMap((id) => {
|
||||
const tab = lookup.get(id)
|
||||
return tab ? [tab] : []
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { tabs, active, open, select, close, closeOthers, reorder }
|
||||
return { tabs: list, active: selected, open, select, close, closeOthers, reorder }
|
||||
}
|
||||
|
||||
export function availableSubagents(parts: ToolPart[]): SubagentTab[] {
|
||||
const seen = new Set<string>()
|
||||
return parts.flatMap((part) => {
|
||||
const child = childID(part as Parameters<typeof childID>[0])
|
||||
if (!child || seen.has(child)) return []
|
||||
seen.add(child)
|
||||
const input = part.state.input
|
||||
const description = input.description
|
||||
const type = input.subagent_type
|
||||
const title = typeof description === "string" ? description : typeof type === "string" ? type : "Sub-agent"
|
||||
return [{ id: child, title }]
|
||||
})
|
||||
}
|
||||
|
||||
export function createSubagentToolbar(opts: {
|
||||
context: Accessor<string>
|
||||
current: Accessor<string | undefined>
|
||||
parts: (id: string) => ToolPart[]
|
||||
tabs: Accessor<SubagentTab[]>
|
||||
open: (id: string, title: string, parentID: string) => void
|
||||
visible: Accessor<boolean>
|
||||
show: () => void
|
||||
hide: () => void
|
||||
}) {
|
||||
const available = createMemo(() => {
|
||||
const id = opts.current()
|
||||
return id ? availableSubagents(opts.parts(id)) : []
|
||||
})
|
||||
const toggle = () => {
|
||||
if (opts.visible()) {
|
||||
opts.hide()
|
||||
return
|
||||
}
|
||||
if (opts.tabs().length > 0) {
|
||||
opts.show()
|
||||
return
|
||||
}
|
||||
const id = opts.current()
|
||||
if (!id) return
|
||||
for (const tab of available()) opts.open(tab.id, tab.title, id)
|
||||
}
|
||||
createEffect(
|
||||
on(
|
||||
opts.context,
|
||||
() => {
|
||||
if (opts.visible() && opts.tabs().length === 0) opts.hide()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
return { available, toggle }
|
||||
}
|
||||
|
||||
export function createSubagentController(opts: {
|
||||
project: Accessor<string | undefined>
|
||||
current: Accessor<string | undefined>
|
||||
selection: Accessor<string | null>
|
||||
parts: (id: string) => ToolPart[]
|
||||
visible: Accessor<boolean>
|
||||
show: () => void
|
||||
sync: (id: string, parentID?: string) => void
|
||||
unsync: (id: string) => void
|
||||
hide: () => void
|
||||
}) {
|
||||
const context = createSubagentContext(opts)
|
||||
const tabs = createSubagentTabs({
|
||||
current: opts.current,
|
||||
context,
|
||||
sync: opts.sync,
|
||||
unsync: opts.unsync,
|
||||
show: opts.show,
|
||||
hide: opts.hide,
|
||||
})
|
||||
const toolbar = createSubagentToolbar({
|
||||
context: createMemo(() => context()),
|
||||
current: opts.current,
|
||||
parts: opts.parts,
|
||||
tabs: tabs.tabs,
|
||||
open: tabs.open,
|
||||
visible: opts.visible,
|
||||
show: opts.show,
|
||||
hide: opts.hide,
|
||||
})
|
||||
return { tabs, toolbar }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user