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
10 changes: 10 additions & 0 deletions .changeset/worktree-pool-prewarm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"kilo-code": minor
"@kilocode/cli": patch
---

Speed up Agent Manager worktree creation by pre-warming reusable worktrees and claiming a ready one instead of running a full checkout. Control the pre-warming in Agent Manager settings under "Pre-warm worktrees"; it is enabled by default and uses one extra checkout of disk space per open project.

Prepare snapshots during session creation to reduce first-prompt initialization work. Start no-script sessions after environment files are copied, while preserving setup-script completion before agent startup. Discarded worktrees now remove their checkpoint data instead of leaving it behind.

Resolve the primary checkout with one git call instead of four and discover agents and skills for a new worktree before the first prompt arrives, so the first response starts sooner.
6 changes: 6 additions & 0 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,12 @@
"scope": "application",
"description": "Prefix for automatically named Agent Manager branches, for example 'marius/' or 'feature/'. Explicit branch names are unchanged."
},
"kilo-code.new.agentManager.worktreePool": {
"type": "boolean",
"default": true,
"scope": "application",
"description": "Pre-warm a ready worktree in the background so new Agent Manager sessions start faster. Uses extra disk space equal to one checkout per open project. Turn off to create worktrees only on demand."
},
"kilo-code.new.experimental.multiProject": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3984,6 +3984,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
browserAutomation: this.browserAutomationSetting(),
"agentManager.autoBranchNaming": naming.get<boolean>("autoBranchNaming", true),
"agentManager.branchPrefix": naming.get<string>("branchPrefix", ""),
"agentManager.worktreePool": naming.get<boolean>("worktreePool", true),
"agentManager.pushFixes": pushFixes(),
}
}
Expand Down
84 changes: 45 additions & 39 deletions packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
deleteLifecycleWorktree,
promoteLifecycleSession,
removeStaleLifecycleWorktree,
removeWorktreeSnapshot,
type LifecycleHost,
} from "./provider-lifecycle"
import { Timing } from "./creation-timing"
import { normalizeBaseBranch } from "./base-branch"
import { handleBaseUpdate } from "./base-update"
import { pushFixes } from "../kilo-provider/push-fixes-settings"
Expand All @@ -30,14 +32,13 @@ import type { GitExecutable } from "../util/git-executable"
import { versionedName } from "./branch-name"
import { BranchNamingController } from "./branch-naming"
import { SetupScriptService } from "./SetupScriptService"
import { copyEnvFiles } from "./env-copy"
import { SessionTerminalManager } from "./SessionTerminalManager"
import { createTerminalHost } from "./terminal-host"
import { TerminalRouter } from "./terminal-routing"
import { discardWorktree as discard } from "./discard-worktree"
import { acquirePtyCleanup } from "./pty-cleanup"
import { executeVscodeTask } from "./task-runner"
import { runWorktreeSetupScript } from "./setup-script-task"
import { runLifecycleSetup } from "./provider-lifecycle"
import { RunController } from "./run/controller"
import { handleRunMessage } from "./run/message"
import { createRunController, createScriptTerminalRuntime, clearScriptTerminals } from "./script-terminal-runtime"
Expand All @@ -61,7 +62,7 @@ import { sandboxSessionMetadata } from "../shared/sandbox-session"
import { createOrchestrationBridge } from "./orchestration-setup"
import type { AgentManagerOrchestrationBridge } from "./orchestration-bridge"
import { pruneSubagents } from "./prune-subagents"
import { startSession } from "./mcp-warmup"
import { prepareDirectory, startSession } from "./mcp-warmup"
import { readTerminalFont, watchTerminalFont } from "./terminal-font"
import { DestinationState, handleDestination, watchTerminalDestination } from "./terminal-destination"
import { buildKeybindingMap } from "./format-keybinding"
Expand Down Expand Up @@ -951,6 +952,8 @@ export class AgentManagerProvider implements Disposable {
branch: string,
worktreeId?: string,
source?: { sandboxInheritanceToken?: string },
boot?: { at: number; metadata: () => Promise<Record<string, unknown>> },
timing?: Timing,
): Promise<Session | null> {
let client: KiloClient
try {
Expand Down Expand Up @@ -980,7 +983,11 @@ export class AgentManagerProvider implements Disposable {
})

try {
const metadata = await sandboxSessionMetadata(this.connectionService.sandboxPreference, client, worktreePath)
// Detached: preparation must not gate session creation or failure reporting.
void prepareDirectory(client, worktreePath).catch((err) => this.log("Worktree preparation failed:", err))
const metadata = await (boot?.metadata() ??
sandboxSessionMetadata(this.connectionService.sandboxPreference, client, worktreePath))
if (boot) timing?.mark("boot", boot.at)
const { data: session } = await startSession(
client,
worktreePath,
Expand All @@ -996,6 +1003,7 @@ export class AgentManagerProvider implements Disposable {
),
(...args) => this.log(...args),
)
timing?.mark("session")
return session
} catch (error) {
const err = getErrorMessage(error)
Expand Down Expand Up @@ -1094,14 +1102,18 @@ export class AgentManagerProvider implements Disposable {
const releasePtyCleanup = await this.acquirePtyCleanup(dir)
try {
await this.getWorktreeManager()?.removeWorktree(dir)
const root = this.getRoot()
if (root) await removeWorktreeSnapshot(this.lifecycleHost, root, dir)
this.getStateManager()?.removeWorktree(wid)
this.pushState()
} finally {
releasePtyCleanup()
}
},
setup: (dir, branch, id) => this.runSetupScriptForWorktree(dir, branch, id),
createSessionInWorktree: (dir, branch, id, source) => this.createSessionInWorktree(dir, branch, id, source),
hasScript: () => this.getSetupScriptService()?.hasScript() ?? false,
setup: (dir, branch, id, early) => this.runSetupScriptForWorktree(dir, branch, id, early),
createSessionInWorktree: (dir, branch, id, source, boot, timing) =>
this.createSessionInWorktree(dir, branch, id, source, boot, timing),
sessionMetadata: (client, dir) => sandboxSessionMetadata(this.connectionService.sandboxPreference, client, dir),
registerWorktreeSession: (sid, dir) => this.registerWorktreeSession(sid, dir),
notifyReady: (sid, result, wid) => this.notifyWorktreeReady(sid, result, wid),
Expand All @@ -1120,7 +1132,8 @@ export class AgentManagerProvider implements Disposable {
private async onCreateWorktree(baseBranch?: string, branchName?: string): Promise<null> {
const ctx = this.context
if (!ctx) return null
return createLifecycleWorktree(ctx, this.lifecycleHost, { baseBranch, branchName })
await createLifecycleWorktree(ctx, this.lifecycleHost, { baseBranch, branchName })
return null
}

/** Delete a worktree and dissociate its sessions. */
Expand Down Expand Up @@ -1221,41 +1234,32 @@ export class AgentManagerProvider implements Disposable {
}

/** Copy .env files and run the worktree setup script. Blocks until complete. Shows progress in overlay. */
private async runSetupScriptForWorktree(worktreePath: string, branch?: string, worktreeId?: string): Promise<void> {
private async runSetupScriptForWorktree(
worktreePath: string,
branch?: string,
worktreeId?: string,
early?: () => Promise<void>,
): Promise<void> {
const root = this.getRoot()
if (!root) return

// Always copy .env files from the main repo (before the setup script so it can override)
await copyEnvFiles(root, worktreePath, (msg) => this.outputChannel.appendLine(`[EnvCopy] ${msg}`))

try {
await runWorktreeSetupScript(
{
service: this.getSetupScriptService(),
destination: this.destination.value(),
projectId: this.context?.id,
worktreeId,
branch,
trusted: () => this.host.isTrusted(),
manager: this.scripts.manager,
vscode: executeVscodeTask,
log: (msg) => this.outputChannel.appendLine(`[SetupScript] ${msg}`),
post: (message) => this.postToWebview(message),
},
{ worktreePath, repoPath: root },
)
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
this.outputChannel.appendLine(`[AgentManager] Setup script error: ${msg}`)
this.postToWebview({
type: "agentManager.worktreeSetup",
status: "error",
message: `Setup script failed: ${msg}`,
await runLifecycleSetup(
{
service: this.getSetupScriptService(),
destination: this.destination.value(),
projectId: this.context?.id,
branch,
worktreeId,
})
}
branch,
trusted: () => this.host.isTrusted(),
manager: this.scripts.manager,
vscode: executeVscodeTask,
log: (msg) => this.outputChannel.appendLine(`[SetupScript] ${msg}`),
post: (message) => this.postToWebview(message),
},
{ worktreePath, repoPath: root },
(msg) => this.outputChannel.appendLine(msg),
early,
)
}

// Repo info
Expand Down Expand Up @@ -1449,8 +1453,10 @@ export class AgentManagerProvider implements Disposable {
private get lifecycleHost(): LifecycleHost {
return {
createOnDisk: (opts) => this.createWorktreeOnDisk(opts),
runSetup: (dir, branch, id) => this.runSetupScriptForWorktree(dir, branch, id),
createSession: (dir, branch, id) => this.createSessionInWorktree(dir, branch, id),
hasScript: () => this.getSetupScriptService()?.hasScript() ?? false,
runSetup: (dir, branch, id, early) => this.runSetupScriptForWorktree(dir, branch, id, early),
createSession: (dir, branch, id, boot, timing) =>
this.createSessionInWorktree(dir, branch, id, undefined, boot, timing),
notifyReady: (sid, result, id) => this.notifyWorktreeReady(sid, result, id),
sessions: {
register: (session) => this.panel?.sessions.registerSession(session),
Expand Down
Loading
Loading