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
16 changes: 12 additions & 4 deletions packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)) {
Comment thread
marius-kilocode marked this conversation as resolved.
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()
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/kilo-vscode/src/agent-manager/GitOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 }
}),
)

Expand Down
13 changes: 13 additions & 0 deletions packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions packages/kilo-vscode/tests/unit/git-stats-poller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 }],
Expand All @@ -154,15 +154,18 @@ 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 () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gsp-presence-fail-"))
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 }],
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
})
Expand Down
Loading