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
5 changes: 5 additions & 0 deletions .changeset/agent-manager-update-from-base.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Ask the worktree agent to update from its saved base branch with `/update-from-base`, the worktree menu, or Command Palette, without stashing or discarding uncommitted work.
8 changes: 8 additions & 0 deletions packages/kilo-docs/pages/automate/agent-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ See [Authentication](/docs/getting-started/setup-authentication), [AI Providers]

Each Agent Manager session runs in an isolated git worktree on a separate branch, keeping your main branch clean.

### Update from the base branch

In a managed worktree's chat, type `/update-from-base` and select the action to ask its agent to fetch and merge the saved base branch. The worktree's right-click menu and **Agent Manager: Update from base** in the Command Palette run the same action.

The saved base stays the same if you switch branches in Local or change the project's default base. For example, a worktree created from `main` still updates from `main` when Local has `release` checked out. If you switch branches inside the managed worktree, the agent updates that worktree's current branch, not its original branch. Select the intended worktree before running the command; it does not update Local.

The agent uses the recorded remote, or the saved base branch's upstream if no remote was recorded. It asks for a source if the base is local-only or unavailable. The request prohibits stashing, discarding uncommitted work, and pushing. Existing merge or rebase operations and blocking dirty changes require your input. Normal tool approvals still apply.

### Worktree Location

Managed worktrees are created under `.kilo/worktrees/` in your project. Kilo also stores Agent Manager UI state in `.kilo/agent-manager.json`.
Expand Down
6 changes: 6 additions & 0 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@
"title": "Agent Manager: Open Worktree",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.updateFromBase",
"title": "Agent Manager: Update from base",
"category": "Kilo Code",
"enablement": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.openPR",
"title": "Agent Manager: Open Pull Request",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
type LifecycleHost,
} from "./provider-lifecycle"
import { normalizeBaseBranch } from "./base-branch"
import { handleBaseUpdate } from "./base-update"
import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type WorktreeStats } from "./GitStatsPoller"
import { PRStatusBridge } from "./pr-status-bridge"
import { createPollers, type ProjectPollers } from "./project/pollers"
Expand Down Expand Up @@ -517,6 +518,7 @@ export class AgentManagerProvider implements Disposable {
}
}
this.onBranchPrompt(m)
if (m.type === "agentManager.updateFromBase") return handleBaseUpdate(m, ctx, this.lifecycleHost)

const worktree = await this.onWorktreeMessage(m)
if (worktree !== undefined) return worktree
Expand Down Expand Up @@ -1632,9 +1634,7 @@ export class AgentManagerProvider implements Disposable {

/** Open a worktree directory directly in VS Code. */
private openWorktreeDirectory(worktreeId: string): void {
const state = this.getStateManager()
if (!state) return
const worktree = state.getWorktree(worktreeId)
const worktree = this.getStateManager()?.getWorktree(worktreeId)
if (!worktree) return
const target = path.normalize(worktree.path)
if (!fs.existsSync(target)) {
Expand Down
101 changes: 101 additions & 0 deletions packages/kilo-vscode/src/agent-manager/base-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { randomUUID } from "crypto"
import type { ProjectContext } from "./project/context"
import type { LifecycleHost } from "./provider-lifecycle"
import type { Worktree } from "./WorktreeStateManager"
import { prompt } from "./orchestration-domain"
import { startSession } from "./mcp-warmup"
import { PLATFORM } from "./constants"

import type { BaseUpdateRequest } from "../../webview-ui/src/types/messages/agent-manager"

type Host = Pick<LifecycleHost, "client" | "metadata" | "register" | "sessions" | "push" | "notify" | "log">
const pending = new WeakSet<Worktree>()

export function baseUpdatePrompt(worktree: Worktree): string {
return [
`Update the current branch in worktree ${JSON.stringify(worktree.path)} from its saved base branch ${JSON.stringify(worktree.parentBranch)}. Do not use today's project default or the worktree's own upstream.`,
worktree.remote
? `Use the recorded remote ${JSON.stringify(worktree.remote)} and exact base ref ${JSON.stringify(`refs/heads/${worktree.parentBranch}`)}.`
: `Resolve the upstream of the saved base branch ${JSON.stringify(worktree.parentBranch)} to its remote and exact branch ref. If the base is local-only or cannot be resolved, stop and ask me which source to use. Do not guess a remote or silently use a local branch.`,
"Check the worktree's current branch and Git status first. Do not switch branches. If HEAD is detached or a merge or rebase is already in progress, stop and ask rather than starting a competing operation.",
"Fetch the exact remote base, then resolve FETCH_HEAD^{commit} and merge that freshly fetched commit ID. If fetch or ref resolution fails, stop. Never merge a stale tracking ref, switch sources silently, or use git pull.",
"Never use git stash, --autostash, or automatic stashing. Disable merge.autoStash for the merge. Do not discard, overwrite, stage, or commit pre-existing edits. If uncommitted changes in this worktree block the merge, stop and ask how to preserve them.",
"Resolve conflicts while preserving both branches' intent. If the intended resolution is unclear, stop and ask. Then run relevant tests, lint, and type checks. Keep normal tool permissions and approvals. Do not push, merge a PR, or apply this worktree into the base.",
].join("\n\n")
}

export async function handleBaseUpdate(msg: BaseUpdateRequest, ctx: ProjectContext, host: Host): Promise<null> {
const state = ctx.peekState()
const worktree = state?.getWorktree(msg.worktreeId)
if (!state || !worktree || (msg.projectId && msg.projectId !== ctx.id)) {
host.notify("Select an available managed worktree to update from base.")
return null
}
if (pending.has(worktree)) return null
pending.add(worktree)
try {
const generation = ctx.generation
const client = host.client()
const target = await (async () => {
const statuses = await client.session.status({ directory: worktree.path }, { throwOnError: true })
const sessions = state.getSessions(worktree.id)
const selected = msg.sessionId ? state.getSession(msg.sessionId) : undefined
if (msg.sessionId && selected?.worktreeId !== worktree.id)
throw new Error("The target session changed worktrees.")
const busy = sessions
.filter((session) => (statuses.data[session.id]?.type ?? "idle") !== "idle")
.map((session) => session.id)
const id = selected?.id ?? sessions.find((session) => busy.includes(session.id))?.id ?? sessions.at(0)?.id
if (busy.some((item) => item !== id))
throw new Error("Another session in this worktree is busy. Wait for it to finish.")
return id
})()
const current = () => {
if (!ctx.isCurrent(generation) || state.getWorktree(worktree.id) !== worktree)
throw new Error("The worktree is no longer available.")
}
current()
const id =
target ??
(await (async () => {
const metadata = await host.metadata(client, worktree.path)
const { data } = await startSession(
client,
worktree.path,
() => {
current()
if (state.getSessions(worktree.id).length)
throw new Error("A session was added. Try Update from base again.")
return client.session.create(
{ directory: worktree.path, platform: PLATFORM, metadata },
{ throwOnError: true },
)
},
host.log,
)
current()
state.addSession(data.id, worktree.id)
host.register(data.id, worktree.path)
ctx.invalidateSessions()
host.push()
return data.id
})())
current()
if (state.getSession(id)?.worktreeId !== worktree.id) throw new Error("The target session changed worktrees.")
host.sessions.registerSessionRoute?.({ projectId: ctx.id, sessionId: id }, worktree.path, generation)
await prompt({
client,
root: ctx.root,
state,
sessionID: id,
text: baseUpdatePrompt(worktree),
messageID: randomUUID(),
})
} catch (err) {
host.log("Update from base failed:", err)
host.notify(err instanceof Error ? err.message : String(err))
} finally {
pending.delete(worktree)
}
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
export const STATE_GATED = new Set<string>([
"agentManager.createWorktree",
"agentManager.updateFromBase",
"agentManager.promoteSession",
"agentManager.createMultiVersion",
"agentManager.deleteWorktree",
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/src/agent-manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,7 @@ interface BrowserRequestIn {

/** All messages the Agent Manager expects from the webview (onMessage input). */
export type AgentManagerInMessage =
| import("../../webview-ui/src/types/messages/agent-manager").BaseUpdateRequest
| CreateWorktreeIn
| RequestProjectsIn
| AddProjectIn
Expand Down
3 changes: 3 additions & 0 deletions packages/kilo-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,9 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.agentManager.openWorktree", () => {
agentManagerProvider.postMessage({ type: "action", action: "openWorktree" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.updateFromBase", () => {
agentManagerProvider.postMessage({ type: "action", action: "updateFromBase" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.openPR", () => {
agentManagerProvider.postMessage({ type: "action", action: "openPR" })
}),
Expand Down
Loading
Loading