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/quiet-project-session-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Load the correct Agent Manager session history on the first worktree switch between projects.
3 changes: 3 additions & 0 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.trackedSessionIds.add(session.id)
this.postMessage({
type: "sessionCreated",
projectId: this.opts.projectQualifier?.()?.projectId,
session: this.sessionToWebview(session),
...(activate ? { activate: true } : {}),
})
Expand Down Expand Up @@ -1946,6 +1947,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Notify webview of the new session
this.postMessage({
type: "sessionCreated",
projectId: this.opts.projectQualifier?.()?.projectId,
session: this.sessionToWebview(this.currentSession!),
})
} catch (error) {
Expand Down Expand Up @@ -3472,6 +3474,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.trackedSessionIds.add(session.id)
this.postMessage({
type: "sessionCreated",
projectId: this.opts.projectQualifier?.()?.projectId,
session: this.sessionToWebview(session),
draftID,
})
Expand Down
25 changes: 21 additions & 4 deletions packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { forkSession } from "./fork-session"
import { AgentManagerVisiblePresence } from "./am-visible-presence"
import { continueInWorktree } from "./continue-in-worktree"
import { WorktreeDiffController } from "./worktree-diff-controller"
import { sendDiffBranches as postDiffBranches } from "./project/diff-branches"
import { WorktreeImporter } from "./worktree-importer"
import {
createWorktreeOnDisk,
Expand Down Expand Up @@ -221,6 +222,7 @@ export class AgentManagerProvider implements Disposable {
localDiffFile: local.file,
post: (msg) => this.postToWebview(msg),
log: (...args) => this.log(...args),
projectId: () => this.context?.id,
})
const pollers = createPollers({
git: this.gitOps,
Expand Down Expand Up @@ -272,7 +274,8 @@ export class AgentManagerProvider implements Disposable {
pushState: (ctx) => this.pushState(ctx),
hasPanelSession: (id) => this.panelSessions.has(id),
closeSession: (id) => this.onCloseSession(id),
postSessionClosed: (id) => this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id }),
postSessionClosed: (id, projectId) =>
this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id, projectId }),
log: (...args) => this.log(...args),
})
this.unsubTool = this.connectionService.onEventFiltered(
Expand Down Expand Up @@ -829,6 +832,7 @@ export class AgentManagerProvider implements Disposable {
}

private onDiffMessage(m: AgentManagerInMessage): Record<string, unknown> | null | undefined {
if ("projectId" in m && m.projectId && m.projectId !== this.context?.id) return null
if (m.type === "agentManager.requestWorktreeDiff") {
void this.diffs.request(composeDiffId(m.sessionId, normalizeScope(m.scope)))
return null
Expand All @@ -854,14 +858,14 @@ export class AgentManagerProvider implements Disposable {
return null
}
if (m.type === "agentManager.requestDiffBranches") {
void this.diffs.postBranches(composeDiffId(m.sessionId, normalizeScope(m.scope)))
void this.sendDiffBranches(m.sessionId, m.scope, this.context?.id)
return null
}
if (m.type === "agentManager.setDiffBaseBranch") {
void this.diffs
.setBase(composeDiffId(m.sessionId, normalizeScope(m.scope)), m.branch)
.catch((err) => this.log("Failed to set diff base:", err instanceof Error ? err.message : String(err)))
.then(() => void this.diffs.postBranches(composeDiffId(m.sessionId, normalizeScope(m.scope))))
.then(() => void this.sendDiffBranches(m.sessionId, m.scope, this.context?.id))
return null
}
if (m.type === "agentManager.openFile") {
Expand All @@ -871,6 +875,17 @@ export class AgentManagerProvider implements Disposable {
if (m.type === "agentManager.requestDocument") return this.diffs.document(m.sessionId, m.file, m.contextKey)
}

private async sendDiffBranches(sessionId: string, scope?: string, projectId = this.context?.id): Promise<void> {
return postDiffBranches(
this.diffs,
(message) => this.postToWebview(message),
(...args) => this.log(...args),
sessionId,
scope,
projectId,
)
}

private onBridgeMessage(m: AgentManagerInMessage): Record<string, unknown> | null | undefined {
if (m.type !== "openFile") return undefined

Expand Down Expand Up @@ -1163,6 +1178,7 @@ export class AgentManagerProvider implements Disposable {
notifyForked: (s, from, wt) =>
this.postToWebview({
type: "agentManager.sessionForked",
projectId: this.context?.id,
sessionId: s.id,
forkedFromId: from,
worktreeId: wt,
Expand Down Expand Up @@ -1265,7 +1281,7 @@ export class AgentManagerProvider implements Disposable {
try {
const branch = await manager.currentBranch()
const defaultBranch = await manager.defaultBranch()
this.postToWebview({ type: "agentManager.repoInfo", branch, defaultBranch })
this.postToWebview({ type: "agentManager.repoInfo", branch, defaultBranch, projectId: this.context?.id })
} catch (error) {
this.log(`Failed to get current branch: ${error}`)
}
Expand Down Expand Up @@ -1306,6 +1322,7 @@ export class AgentManagerProvider implements Disposable {
this.pushState()
this.postToWebview({
type: "agentManager.sessionAdded",
projectId: this.context?.id,
sessionId: session.id,
worktreeId: worktree.id,
})
Expand Down
4 changes: 2 additions & 2 deletions packages/kilo-vscode/src/agent-manager/orchestration-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface OrchestrationBridgeDeps {
pushState: (ctx?: ProjectContext) => void
hasPanelSession: (id: string) => boolean
closeSession: (id: string) => Promise<unknown>
postSessionClosed: (id: string) => void
postSessionClosed: (id: string, projectId?: string) => void
log: (...args: unknown[]) => void
}

Expand Down Expand Up @@ -57,7 +57,7 @@ export function createOrchestrationBridge(deps: OrchestrationBridgeDeps): AgentM
} else {
await deps.closeSession(id)
}
deps.postSessionClosed(id)
deps.postSessionClosed(id, ctx?.id)
},
directories: () => {
const all: string[] = []
Expand Down
100 changes: 55 additions & 45 deletions packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface PRBridgeHost {
openExternal(url: string): void
log(...args: unknown[]): void
semaphore?: Semaphore
projectId?: () => string | undefined
}

/** Minimal panel surface needed by the bridge (subset of PanelContext). */
Expand Down Expand Up @@ -51,6 +52,7 @@ export class PRStatusBridge {
openExternal: (url: string) => void
log: (...args: unknown[]) => void
semaphore?: Semaphore
projectId?: () => string | undefined
}): PRStatusBridge {
return new PRStatusBridge(opts)
}
Expand Down Expand Up @@ -81,61 +83,63 @@ export class PRStatusBridge {
/** Handle an incoming webview message. Returns true if handled. */
handleMessage(m: Record<string, unknown>): boolean {
if (m.type === "agentManager.refreshPR") {
if (typeof m.projectId === "string" && m.projectId !== this.host.projectId?.()) return true
this.poller.refresh(m.worktreeId as string)
return true
}
if (m.type === "agentManager.openPR") {
const url = (m.url as string) ?? this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)?.prUrl
if (url) this.host.openExternal(url)
return true
}
const isResolve = m.type === "agentManager.resolveComment"
const isUnresolve = m.type === "agentManager.unresolveComment"
if (isResolve || isUnresolve) {
const id = m.worktreeId as string
const threadId = m.threadId as string
const wt = this.host.getWorktrees().find((w: Worktree) => w.id === id)
const cwd = wt?.path ?? this.host.getWorkspaceRoot()
const resultType = isResolve ? "agentManager.resolveCommentResult" : "agentManager.unresolveCommentResult"
if (!cwd) {
this.host.log("resolveComment: no cwd for worktree", id)
this.host.postToWebview({
type: resultType,
worktreeId: id,
threadId,
success: false,
})
const explicit = typeof m.url === "string" ? m.url : undefined
if (explicit) {
this.host.openExternal(explicit)
return true
}
const action = isResolve ? resolveComment : unresolveComment
action(threadId, cwd).then(
() => {
this.host.postToWebview({
type: resultType,
worktreeId: id,
threadId,
success: true,
})
// Refresh PR data after successful mutation to get updated comment state
this.poller.refresh(id)
},
(err: unknown) => {
const msg = err instanceof Error ? err.message : String(err)
this.host.log(`${resultType} failed: ${msg}`)
this.host.postToWebview({
type: resultType,
worktreeId: id,
threadId,
success: false,
error: ghErrorReason(msg),
})
},
)
if (typeof m.projectId === "string" && m.projectId !== this.host.projectId?.()) return true
const url = this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)?.prUrl
if (url) this.host.openExternal(url)
return true
}
if (m.type === "agentManager.resolveComment" || m.type === "agentManager.unresolveComment")
return this.handleComment(m)
return false
}

private handleComment(m: Record<string, unknown>): boolean {
if (typeof m.projectId === "string" && m.projectId !== this.host.projectId?.()) return true
const id = m.worktreeId as string
const threadId = m.threadId as string
const projectId = typeof m.projectId === "string" ? m.projectId : this.host.projectId?.()
Comment thread
marius-kilocode marked this conversation as resolved.
const wt = this.host.getWorktrees().find((w) => w.id === id)
const cwd = wt?.path ?? this.host.getWorkspaceRoot()
const resolve = m.type === "agentManager.resolveComment"
const resultType = resolve ? "agentManager.resolveCommentResult" : "agentManager.unresolveCommentResult"
const result = (success: boolean, error?: string) =>
this.host.postToWebview({
type: resultType,
...(projectId ? { projectId } : {}),
worktreeId: id,
threadId,
success,
...(error ? { error } : {}),
})
if (!cwd) {
this.host.log("resolveComment: no cwd for worktree", id)
result(false)
return true
}
const action = resolve ? resolveComment : unresolveComment
action(threadId, cwd).then(
() => {
result(true)
this.poller.refresh(id)
},
(err: unknown) => {
this.host.log(`${resultType} failed: ${err instanceof Error ? err.message : String(err)}`)
result(false, ghErrorReason(err instanceof Error ? err.message : String(err)))
},
)
return true
}

/** Remove cached status for a deleted worktree. */
remove(worktreeId: string): void {
this.cache.delete(worktreeId)
Expand Down Expand Up @@ -189,6 +193,7 @@ function reportError(
worktreeId: id,
pr: null,
error: err,
...(host.projectId?.() ? { projectId: host.projectId() } : {}),
} as AgentManagerOutMessage)
// Always forward auth/missing errors so the webview can show a toast,
// regardless of whether prior data exists. Deduplicate per error type
Expand All @@ -210,7 +215,12 @@ function accept(bridge: PRStatusBridge, host: PRBridgeHost, id: string, pr: PRSt
return
}
const merged = pr && prev ? mergePRStatus(prev, pr) : pr
const msg = { type: "agentManager.prStatus", worktreeId: id, pr: merged } as AgentManagerOutMessage
const msg = {
type: "agentManager.prStatus",
worktreeId: id,
pr: merged,
...(host.projectId?.() ? { projectId: host.projectId() } : {}),
} as AgentManagerOutMessage
bridge["cache"].set(id, msg)
if (pr && branch !== undefined) bridge["branches"].set(id, branch)
if (!pr) bridge["branches"].delete(id)
Expand Down
30 changes: 30 additions & 0 deletions packages/kilo-vscode/src/agent-manager/project/diff-branches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { AgentManagerOutMessage } from "../types"
import { composeDiffId, normalizeScope } from "../diff-scope"
import type { WorktreeDiffController } from "../worktree-diff-controller"

export async function sendDiffBranches(
diffs: WorktreeDiffController,
post: (message: AgentManagerOutMessage) => void,
log: (...args: unknown[]) => void,
sessionId: string,
scope?: string,
projectId?: string,
): Promise<void> {
const id = composeDiffId(sessionId, normalizeScope(scope))
const result = await diffs.branches(id).catch((err) => {
log("Failed to list diff branches:", err instanceof Error ? err.message : String(err))
return undefined
})
if (!result) return
post({
type: "agentManager.diffBranches",
projectId,
sessionId: id,
branches: result.branches,
defaultBranch: result.defaultBranch,
autoBase: result.autoBase,
currentBase: result.currentBase,
isAuto: result.isAuto,
currentBranch: result.currentBranch,
})
}
7 changes: 6 additions & 1 deletion packages/kilo-vscode/src/agent-manager/project/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,12 @@ function rememberTarget(projectId: string, target: SidebarTarget, deps: ProjectM
// Never persist a target the project does not have: the webview can race a
// project switch and still hold the previous project's selection.
if (target.kind === "worktree" && !state.getWorktree(target.worktreeId)) return
if (target.kind === "session" && !state.getSession(target.sessionId)) return
if (
target.kind === "session" &&
!state.getSession(target.sessionId) &&
!deps.contexts.get(projectId)?.hasLiveSession(target.sessionId)
)
return
state.setActiveTarget(target)
}

Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/src/agent-manager/project/pollers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function createPollerPair(ctx: ProjectContext, deps: PollerDeps): PollerPair {
openExternal: deps.openExternal,
log: deps.log,
semaphore: deps.semaphore,
projectId: () => ctx.id,
})
return { stats, pr }
}
Expand Down Expand Up @@ -209,6 +210,7 @@ export function createPollers(opts: {
openExternal: opts.openExternal,
log: opts.log,
semaphore: opts.semaphore,
projectId: opts.activeId,
})
const projects = new ProjectPollers({
git: opts.git,
Expand Down
Loading
Loading