mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
feat(agent-manager): sync worktree branch from git worktree list (#8534)
Piggyback on the existing `git worktree list --porcelain` call in probeWorktreePresence to detect branch changes (e.g. after checking out a PR branch). Zero additional git calls — the branch info was already returned but discarded.
This commit is contained in:
@@ -695,11 +695,11 @@ export class AgentManagerProvider implements Disposable {
|
||||
this.pushState()
|
||||
// Disk removal after state is clean — pollers no longer reference this worktree.
|
||||
try {
|
||||
await manager.removeWorktree(worktree.path, worktree.branch)
|
||||
await manager.removeWorktree(worktree.path, worktree.originalBranch ?? worktree.branch)
|
||||
} catch (error) {
|
||||
this.log(`Failed to remove worktree from disk: ${error}`)
|
||||
}
|
||||
this.log(`Deleted worktree ${worktreeId} (${worktree.branch})`)
|
||||
this.log(`Deleted worktree ${worktreeId} (${worktree.originalBranch ?? worktree.branch})`)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1444,12 +1444,20 @@ export class AgentManagerProvider implements Disposable {
|
||||
const entries = result.worktrees.filter((item) => ids.has(item.worktreeId))
|
||||
if (entries.length === 0) return
|
||||
|
||||
// Sync branches from git worktree list (no extra git calls)
|
||||
let branchChanged = false
|
||||
for (const entry of entries) {
|
||||
if (entry.branch && state.updateWorktreeBranch(entry.worktreeId, entry.branch)) {
|
||||
branchChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
const next = new Set(entries.filter((entry) => entry.missing).map((entry) => entry.worktreeId))
|
||||
const changed =
|
||||
const staleChanged =
|
||||
next.size !== this.staleWorktreeIds.size || [...next].some((worktreeId) => !this.staleWorktreeIds.has(worktreeId))
|
||||
this.staleWorktreeIds = next
|
||||
|
||||
if (changed) {
|
||||
if (staleChanged || branchChanged) {
|
||||
this.pushState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,14 +130,14 @@ export class GitOps {
|
||||
}
|
||||
|
||||
/** Return the set of worktree paths for the repo, excluding bare entries. */
|
||||
async listWorktreePaths(cwd: string): Promise<Set<string>> {
|
||||
async listWorktreePaths(cwd: string): Promise<Map<string, string>> {
|
||||
const raw = await this.raw(["worktree", "list", "--porcelain"], cwd)
|
||||
const paths = new Set<string>()
|
||||
const result = new Map<string, string>()
|
||||
for (const entry of parseWorktreeList(raw)) {
|
||||
if (entry.bare) continue
|
||||
paths.add(normalizePath(entry.path))
|
||||
result.set(normalizePath(entry.path), entry.branch)
|
||||
}
|
||||
return paths
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface LocalStats {
|
||||
export interface WorktreePresence {
|
||||
worktreeId: string
|
||||
missing: boolean
|
||||
/** Current branch from `git worktree list`, if available. */
|
||||
branch?: string
|
||||
}
|
||||
|
||||
export interface WorktreePresenceResult {
|
||||
@@ -231,7 +233,8 @@ export class GitStatsPoller {
|
||||
() => false,
|
||||
)
|
||||
const missing = !exists || !tracked.has(normalized)
|
||||
return { worktreeId: wt.id, missing }
|
||||
const branch = tracked.get(normalized)
|
||||
return { worktreeId: wt.id, missing, branch }
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ export interface Worktree {
|
||||
prUrl?: string
|
||||
/** Cached PR state for correct badge color on reload (open/merged/closed/draft). */
|
||||
prState?: string
|
||||
/** Original branch created with the worktree, used for cleanup on deletion.
|
||||
* Set automatically when `branch` is updated via live sync. */
|
||||
originalBranch?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,6 +172,16 @@ export class WorktreeStateManager {
|
||||
return wt
|
||||
}
|
||||
|
||||
updateWorktreeBranch(id: string, branch: string): boolean {
|
||||
const wt = this.worktrees.get(id)
|
||||
if (!wt || wt.branch === branch) return false
|
||||
if (!wt.originalBranch) wt.originalBranch = wt.branch
|
||||
this.log(`Updated worktree ${id} branch: ${wt.branch} → ${branch}`)
|
||||
wt.branch = branch
|
||||
void this.save()
|
||||
return true
|
||||
}
|
||||
|
||||
updateWorktreeLabel(id: string, label: string): void {
|
||||
const wt = this.worktrees.get(id)
|
||||
if (!wt) return
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as fs from "fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import type { KiloClient } from "@kilocode/sdk/v2/client"
|
||||
import { GitStatsPoller } from "../../src/agent-manager/GitStatsPoller"
|
||||
import { GitStatsPoller, type WorktreePresenceResult } from "../../src/agent-manager/GitStatsPoller"
|
||||
import { GitOps } from "../../src/agent-manager/GitOps"
|
||||
import type { Worktree } from "../../src/agent-manager/WorktreeStateManager"
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("GitStatsPoller", () => {
|
||||
const wtPath = path.join(root, "wt-a")
|
||||
fs.mkdirSync(wtPath, { recursive: true })
|
||||
|
||||
const presence: Array<{ worktrees: Array<{ worktreeId: string; missing: boolean }>; degraded: boolean }> = []
|
||||
const presence: WorktreePresenceResult[] = []
|
||||
|
||||
const poller = new GitStatsPoller({
|
||||
getWorktrees: () => [{ ...worktree("a"), path: wtPath }],
|
||||
@@ -154,7 +154,10 @@ describe("GitStatsPoller", () => {
|
||||
poller.stop()
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
|
||||
expect(presence[0]).toEqual({ worktrees: [{ worktreeId: "a", missing: false }], degraded: false })
|
||||
expect(presence[0]).toEqual({
|
||||
worktrees: [{ worktreeId: "a", missing: false, branch: "branch-a" }],
|
||||
degraded: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("emits degraded probe when git worktree listing fails", async () => {
|
||||
@@ -162,7 +165,7 @@ describe("GitStatsPoller", () => {
|
||||
const wtPath = path.join(root, "wt-a")
|
||||
fs.mkdirSync(wtPath, { recursive: true })
|
||||
|
||||
const presence: Array<{ worktrees: Array<{ worktreeId: string; missing: boolean }>; degraded: boolean }> = []
|
||||
const presence: WorktreePresenceResult[] = []
|
||||
|
||||
const poller = new GitStatsPoller({
|
||||
getWorktrees: () => [{ ...worktree("a"), path: wtPath }],
|
||||
@@ -199,7 +202,7 @@ describe("GitStatsPoller", () => {
|
||||
|
||||
const calls: string[] = []
|
||||
const emitted: Array<Array<{ worktreeId: string; additions: number; deletions: number; commits: number }>> = []
|
||||
const presence: Array<{ worktrees: Array<{ worktreeId: string; missing: boolean }>; degraded: boolean }> = []
|
||||
const presence: WorktreePresenceResult[] = []
|
||||
|
||||
const client = {
|
||||
worktree: {
|
||||
@@ -240,8 +243,8 @@ describe("GitStatsPoller", () => {
|
||||
expect(calls.some((cwd) => cwd === wtBPath)).toBe(false)
|
||||
expect(presence[0]).toEqual({
|
||||
worktrees: [
|
||||
{ worktreeId: "a", missing: false },
|
||||
{ worktreeId: "b", missing: true },
|
||||
{ worktreeId: "a", missing: false, branch: "branch-a" },
|
||||
{ worktreeId: "b", missing: true, branch: undefined },
|
||||
],
|
||||
degraded: false,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user