mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #12860 from Kilo-Org/agent-manager-12806-isolation
fix(vscode): isolate project worktree row state
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep Agent Manager worktree rows isolated when projects contain identical raw worktree IDs.
|
||||
@@ -1050,6 +1050,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
if (!req) return
|
||||
if (directory) {
|
||||
req.directory = directory
|
||||
req.projectId ??= this.contexts.byDirectory(directory)?.id
|
||||
}
|
||||
void this.startToolRequest(req)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ToolTask {
|
||||
|
||||
export interface ToolRequest {
|
||||
requestID: string
|
||||
projectId?: string
|
||||
sessionID?: string
|
||||
directory?: string
|
||||
sandboxInheritanceToken?: string
|
||||
@@ -234,7 +235,14 @@ export async function startFromTool(deps: ToolDeps, req: ToolRequest): Promise<v
|
||||
const state = { ok: 0 }
|
||||
const source = { sandboxInheritanceToken: req.sandboxInheritanceToken }
|
||||
|
||||
deps.post({ type: "agentManager.multiVersionProgress", status: "creating", total, completed: 0, groupId })
|
||||
deps.post({
|
||||
type: "agentManager.multiVersionProgress",
|
||||
projectId: req.projectId,
|
||||
status: "creating",
|
||||
total,
|
||||
completed: 0,
|
||||
groupId,
|
||||
})
|
||||
for (let i = 0; i < req.tasks.length; i++) {
|
||||
const task = req.tasks[i]!
|
||||
try {
|
||||
@@ -248,10 +256,24 @@ export async function startFromTool(deps: ToolDeps, req: ToolRequest): Promise<v
|
||||
deps.log("Agent Manager tool task failed", msg)
|
||||
deps.post({ type: "error", message: `Agent Manager tool task failed: ${msg}` })
|
||||
}
|
||||
deps.post({ type: "agentManager.multiVersionProgress", status: "creating", total, completed: state.ok, groupId })
|
||||
deps.post({
|
||||
type: "agentManager.multiVersionProgress",
|
||||
projectId: req.projectId,
|
||||
status: "creating",
|
||||
total,
|
||||
completed: state.ok,
|
||||
groupId,
|
||||
})
|
||||
}
|
||||
|
||||
deps.post({ type: "agentManager.multiVersionProgress", status: "done", total, completed: state.ok, groupId })
|
||||
deps.post({
|
||||
type: "agentManager.multiVersionProgress",
|
||||
projectId: req.projectId,
|
||||
status: "done",
|
||||
total,
|
||||
completed: state.ok,
|
||||
groupId,
|
||||
})
|
||||
if (state.ok === 0) deps.error(`Failed to start any Agent Manager sessions for request ${req.requestID}.`)
|
||||
deps.log(`Agent Manager tool request ${req.requestID} complete: ${state.ok}/${total}`)
|
||||
}
|
||||
@@ -299,6 +321,7 @@ export function parseToolRequest(value: unknown): ToolRequest | undefined {
|
||||
if (parsed.length !== limited.length) return undefined
|
||||
return {
|
||||
requestID: typeof value.requestID === "string" ? value.requestID : `am-${Date.now()}`,
|
||||
projectId: typeof value.projectId === "string" ? value.projectId : undefined,
|
||||
sessionID: typeof value.sessionID === "string" ? value.sessionID : undefined,
|
||||
directory: typeof value.directory === "string" ? value.directory : undefined,
|
||||
sandboxInheritanceToken:
|
||||
|
||||
@@ -32,6 +32,10 @@ describe("multi-project progress state", () => {
|
||||
|
||||
expect(first.busy().has("same")).toBe(true)
|
||||
expect(second.busy().has("same")).toBe(false)
|
||||
|
||||
second.setBusy(new Map([["same", { reason: "deleting" as const }]]))
|
||||
clearMultiVersionBusy(second, "group")
|
||||
expect(second.busy().get("same")?.reason).toBe("deleting")
|
||||
})
|
||||
|
||||
it("marks a newly created grouped worktree as busy in its project store", () => {
|
||||
@@ -42,4 +46,14 @@ describe("multi-project progress state", () => {
|
||||
|
||||
expect(store.busy().get("same")?.reason).toBe("setting-up")
|
||||
})
|
||||
|
||||
it("does not replace deletion progress when marking a grouped worktree", () => {
|
||||
const store = createProjectStore("a")
|
||||
store.applyState(state("a"))
|
||||
store.setBusy(new Map([["same", { reason: "deleting" as const }]]))
|
||||
|
||||
markMultiVersionBusy(store, "a-session")
|
||||
|
||||
expect(store.busy().get("same")?.reason).toBe("deleting")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { projectAdjacentHint } from "../../webview-ui/agent-manager/project-local-navigation"
|
||||
import { worktreeNavId } from "../../webview-ui/agent-manager/navigate"
|
||||
import { projectAdjacentHint, projectWorktreeRow } from "../../webview-ui/agent-manager/project-local-navigation"
|
||||
|
||||
describe("projectAdjacentHint", () => {
|
||||
it("does not leak a hint to another project with the same raw ID", () => {
|
||||
@@ -22,4 +23,44 @@ describe("projectAdjacentHint", () => {
|
||||
projectAdjacentHint("project-b", "project-b", "shared", "local", ["local", "other", "shared"], "prev", "next"),
|
||||
).toBe("")
|
||||
})
|
||||
|
||||
it("keeps shortcut and worktree keybinding values scoped for duplicate raw IDs", () => {
|
||||
const bindings = {
|
||||
previousSession: "Ctrl+Alt+Up",
|
||||
nextSession: "Ctrl+Alt+Down",
|
||||
closeWorktree: "Ctrl+Shift+W",
|
||||
openWorktree: "Ctrl+Shift+O",
|
||||
}
|
||||
const shortcuts = new Map([
|
||||
[worktreeNavId("project-a", "shared"), 2],
|
||||
[worktreeNavId("project-b", "shared"), 4],
|
||||
])
|
||||
const first = projectWorktreeRow({
|
||||
projectId: "project-a",
|
||||
activeProjectId: "project-a",
|
||||
worktreeId: "shared",
|
||||
activeId: "local",
|
||||
flatIds: ["local", "shared"],
|
||||
bindings,
|
||||
shortcuts,
|
||||
})
|
||||
const second = projectWorktreeRow({
|
||||
projectId: "project-b",
|
||||
activeProjectId: "project-a",
|
||||
worktreeId: "shared",
|
||||
activeId: "local",
|
||||
flatIds: ["local", "shared"],
|
||||
bindings,
|
||||
shortcuts,
|
||||
})
|
||||
|
||||
expect(first.navHint).toBe(bindings.nextSession)
|
||||
expect(second.navHint).toBe("")
|
||||
expect(first.shortcut).toBe(2)
|
||||
expect(second.shortcut).toBe(4)
|
||||
expect(first.closeKeybind).toBe(bindings.closeWorktree)
|
||||
expect(first.openKeybind).toBe(bindings.openWorktree)
|
||||
expect(second.closeKeybind).toBe(bindings.closeWorktree)
|
||||
expect(second.openKeybind).toBe(bindings.openWorktree)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,7 +20,6 @@ import type {
|
||||
} from "../src/types/messages"
|
||||
import type { LanguageContextValue } from "../src/context/language"
|
||||
import { useVSCode } from "../src/context/vscode"
|
||||
import { projectAdjacentHint, projectSidebarOrder } from "./project-local-navigation"
|
||||
import SectionHeader from "./SectionHeader"
|
||||
import { SidebarSectionHeader } from "./SidebarSectionHeader"
|
||||
import { WorktreeItem } from "./WorktreeItem"
|
||||
@@ -32,6 +31,7 @@ import { sectionAwareDetector } from "./section-dnd"
|
||||
import { ConstrainDragXAxis } from "./constrain-drag-x"
|
||||
import { createProjectStore, type ProjectStore } from "./project/store"
|
||||
import { randomColor } from "./section-colors"
|
||||
import { projectSidebarOrder, projectWorktreeRow } from "./project-local-navigation"
|
||||
|
||||
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
|
||||
|
||||
@@ -111,16 +111,16 @@ export const ProjectSidebarBody: Component<Props> = (props) => {
|
||||
const post = (message: Record<string, unknown>) =>
|
||||
vscode.postMessage({ ...message, projectId: props.project.id } as never)
|
||||
|
||||
const navHint = (id: string) =>
|
||||
projectAdjacentHint(
|
||||
props.project.id,
|
||||
props.selectedProject,
|
||||
id,
|
||||
props.selection ?? props.currentSessionID?.(),
|
||||
sidebarOrder(),
|
||||
props.bindings.previousSession ?? "",
|
||||
props.bindings.nextSession ?? "",
|
||||
)
|
||||
const row = (id: string) =>
|
||||
projectWorktreeRow({
|
||||
projectId: props.project.id,
|
||||
activeProjectId: props.selectedProject,
|
||||
worktreeId: id,
|
||||
activeId: props.selection ?? props.currentSessionID?.(),
|
||||
flatIds: sidebarOrder(),
|
||||
bindings: props.bindings,
|
||||
shortcuts: props.shortcutMap?.(),
|
||||
})
|
||||
|
||||
const scope = (kind: "section" | "worktree", id: string) => `${props.project.id}:${kind}:${id}`
|
||||
const parse = (kind: "section" | "worktree", value: unknown) => {
|
||||
@@ -229,6 +229,7 @@ export const ProjectSidebarBody: Component<Props> = (props) => {
|
||||
const renderWorktree = (worktree: WorktreeState, idx: () => number, list: WorktreeState[]) => {
|
||||
const label = () => firstOrderedTitle(sessions(worktree.id), store.tabOrder()[worktree.id], worktree.branch)
|
||||
const subtitle = () => (label() !== worktree.branch ? worktree.branch : undefined)
|
||||
const values = () => row(worktree.id)
|
||||
const sortable = createSortable(scope("worktree", worktree.id))
|
||||
void sortable
|
||||
return (
|
||||
@@ -236,7 +237,6 @@ export const ProjectSidebarBody: Component<Props> = (props) => {
|
||||
<WorktreeItem
|
||||
worktree={worktree}
|
||||
sidebarId={`${props.project.id}:${worktree.id}`}
|
||||
shortcut={props.shortcutMap?.().get(`${props.project.id}:wt:${worktree.id}`)}
|
||||
label={worktree.label || label()}
|
||||
subtitle={worktree.label ? (worktree.label !== worktree.branch ? worktree.branch : undefined) : subtitle()}
|
||||
active={active() && props.selection === worktree.id}
|
||||
@@ -245,7 +245,8 @@ export const ProjectSidebarBody: Component<Props> = (props) => {
|
||||
working={props.working?.(worktree.id) || runs()[worktree.id]?.state === "running"}
|
||||
stale={state()?.staleWorktreeIds?.includes(worktree.id) === true}
|
||||
stats={props.stats?.[worktree.id]}
|
||||
navHint={navHint(worktree.id)}
|
||||
shortcut={values().shortcut}
|
||||
navHint={values().navHint}
|
||||
sessions={sessions(worktree.id).length}
|
||||
grouped={isGrouped(worktree)}
|
||||
groupStart={isGroupStart(worktree, idx(), list)}
|
||||
@@ -253,8 +254,8 @@ export const ProjectSidebarBody: Component<Props> = (props) => {
|
||||
groupSize={worktree.groupId ? sorted().filter((item) => item.groupId === worktree.groupId).length : 0}
|
||||
renaming={renaming() === worktree.id}
|
||||
renameValue={name()}
|
||||
closeKeybind=""
|
||||
openKeybind=""
|
||||
closeKeybind={values().closeKeybind}
|
||||
openKeybind={values().openKeybind}
|
||||
pr={props.prs?.[worktree.id] ?? undefined}
|
||||
runStatus={runs()[worktree.id]}
|
||||
sections={sections()}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { adjacentHint } from "./navigate"
|
||||
import { adjacentHint, worktreeNavId } from "./navigate"
|
||||
import { buildSidebarOrder } from "./section-helpers"
|
||||
|
||||
export function projectSidebarOrder(...args: Parameters<typeof buildSidebarOrder>): string[] {
|
||||
@@ -17,3 +17,31 @@ export function projectAdjacentHint(
|
||||
if (projectId !== activeProjectId) return ""
|
||||
return adjacentHint(itemId, activeId, flatIds, prev, next)
|
||||
}
|
||||
|
||||
interface Input {
|
||||
projectId: string
|
||||
activeProjectId?: string
|
||||
worktreeId: string
|
||||
activeId?: string
|
||||
flatIds: string[]
|
||||
bindings: Record<string, string>
|
||||
shortcuts?: Map<string, number>
|
||||
}
|
||||
|
||||
/** Resolve the project-scoped values rendered by one worktree row. */
|
||||
export function projectWorktreeRow(input: Input) {
|
||||
return {
|
||||
shortcut: input.shortcuts?.get(worktreeNavId(input.projectId, input.worktreeId)),
|
||||
navHint: projectAdjacentHint(
|
||||
input.projectId,
|
||||
input.activeProjectId,
|
||||
input.worktreeId,
|
||||
input.activeId,
|
||||
input.flatIds,
|
||||
input.bindings.previousSession ?? "",
|
||||
input.bindings.nextSession ?? "",
|
||||
),
|
||||
closeKeybind: input.bindings.closeWorktree ?? "",
|
||||
openKeybind: input.bindings.openWorktree ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ProjectStore } from "./store"
|
||||
|
||||
/** Clear the loading indicators for every worktree in one multi-version group. */
|
||||
/** Clear setup indicators for every worktree in one multi-version group. */
|
||||
export function clearMultiVersionBusy(store: ProjectStore, groupId: string): void {
|
||||
const ids = new Set(
|
||||
store
|
||||
@@ -9,7 +9,7 @@ export function clearMultiVersionBusy(store: ProjectStore, groupId: string): voi
|
||||
.map((wt) => wt.id),
|
||||
)
|
||||
if (ids.size === 0) return
|
||||
store.setBusy((prev) => new Map([...prev].filter(([id]) => !ids.has(id))))
|
||||
store.setBusy((prev) => new Map([...prev].filter(([id, busy]) => !ids.has(id) || busy.reason === "deleting")))
|
||||
}
|
||||
|
||||
/** Keep a newly created grouped worktree showing progress until its prompt starts. */
|
||||
@@ -19,5 +19,8 @@ export function markMultiVersionBusy(store: ProjectStore, sessionId: string): vo
|
||||
if (!id) return
|
||||
const worktree = store.worktrees().find((item) => item.id === id)
|
||||
if (!worktree?.groupId) return
|
||||
store.setBusy((prev) => new Map([...prev, [id, { reason: "setting-up" as const }]]))
|
||||
store.setBusy((prev) => {
|
||||
if (prev.get(id)?.reason === "deleting") return prev
|
||||
return new Map([...prev, [id, { reason: "setting-up" as const }]])
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user