diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index ba9c1911b7e..51548eb1be7 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -43,6 +43,8 @@ export class AgentManagerProvider implements vscode.Disposable { private diffInterval: ReturnType | undefined private diffSessionId: string | undefined private lastDiffHash: string | undefined + private validationInterval: ReturnType | undefined + private validating = false private statsPoller: WorktreeStatsPoller private cachedDiffTarget: { directory: string; baseBranch: string } | undefined @@ -107,6 +109,7 @@ export class AgentManagerProvider implements vscode.Disposable { this.panel.onDidDispose(() => { this.log("Panel disposed") + this.stopWorktreeValidation() this.statsPoller.stop() this.stopDiffPolling() this.provider?.dispose() @@ -131,7 +134,12 @@ export class AgentManagerProvider implements vscode.Disposable { // Validate worktree directories still exist (handles manual deletion) const root = this.getWorkspaceRoot() - if (root) await state.validate(root) + if (root) { + const orphaned = await state.validate(root) + for (const s of orphaned) { + this.provider?.clearSessionDirectory(s.id) + } + } // Register all worktree sessions with KiloProvider for (const worktree of state.getWorktrees()) { @@ -148,6 +156,10 @@ export class AgentManagerProvider implements vscode.Disposable { if (state.getSessions().length > 0) { this.provider?.refreshSessions() } + + // Poll for externally deleted worktrees while the panel is open. + // Guard against panel being disposed while initializeState() was awaiting. + if (this.panel) this.startWorktreeValidation() } // --------------------------------------------------------------------------- @@ -1486,6 +1498,52 @@ export class AgentManagerProvider implements vscode.Disposable { this.cachedDiffTarget = undefined } + // --------------------------------------------------------------------------- + // Worktree validation polling + // --------------------------------------------------------------------------- + + private startWorktreeValidation(): void { + this.stopWorktreeValidation() + this.validationInterval = setInterval(() => { + void this.validateWorktrees() + }, 10_000) + } + + private stopWorktreeValidation(): void { + if (this.validationInterval) { + clearInterval(this.validationInterval) + this.validationInterval = undefined + } + } + + private async validateWorktrees(): Promise { + if (this.validating) return + this.validating = true + try { + const state = this.getStateManager() + const root = this.getWorkspaceRoot() + if (!state || !root) return + + const orphaned = await state.validate(root) + if (orphaned.length === 0) return + + for (const s of orphaned) { + this.provider?.clearSessionDirectory(s.id) + } + + // Stop diff polling if it targets an orphaned session + if (this.diffSessionId && orphaned.some((s) => s.id === this.diffSessionId)) { + this.stopDiffPolling() + } + + this.pushState() + } catch (error) { + this.log("Worktree validation failed:", error) + } finally { + this.validating = false + } + } + private postToWebview(message: Record): void { if (this.panel?.webview) void this.panel.webview.postMessage(message) } @@ -1526,6 +1584,7 @@ export class AgentManagerProvider implements vscode.Disposable { } public dispose(): void { + this.stopWorktreeValidation() this.stopDiffPolling() this.statsPoller.stop() this.terminalManager.dispose() diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts index 9b135fb07b2..d2cc67703e6 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts @@ -280,23 +280,26 @@ export class WorktreeStateManager { } } - /** Remove worktrees whose directories no longer exist on disk. */ - async validate(root: string): Promise { - let changed = false + /** Remove worktrees whose directories no longer exist on disk. Returns orphaned sessions. */ + async validate(root: string): Promise { + const orphaned: ManagedSession[] = [] for (const wt of [...this.worktrees.values()]) { const resolved = path.isAbsolute(wt.path) ? wt.path : path.join(root, wt.path) if (!fs.existsSync(resolved)) { this.log(`Worktree ${wt.id} directory missing (${resolved}), removing`) - this.removeWorktree(wt.id) - changed = true + orphaned.push(...this.removeWorktree(wt.id)) } } - if (changed) await this.save() + // removeWorktree() already queues a save per call — just wait for completion + if (orphaned.length > 0) await this.flush() + return orphaned } - /** Wait for any in-flight save to complete without triggering a new one. */ + /** Wait for all in-flight and queued saves to complete without triggering a new one. */ async flush(): Promise { - if (this.saving) await this.saving + while (this.saving || this.pendingSave) { + await (this.saving ?? Promise.resolve()) + } } async save(): Promise {