From 6617b2433c96a7dabcbddd0bc5645a7a50c9ce49 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 29 Jul 2026 09:28:08 +0200 Subject: [PATCH] refactor(agent-manager): consolidate lifecycle deps into ctx plus a capability host The worktree lifecycle handlers took a 27-field closure bag that mixed project-state accessors with provider capabilities, a service-locator pattern with the same hidden-dependency problem as the ambient scope it replaced. Handlers now receive the ProjectContext directly for everything that is project state (peekState, worktreeManager, stale set, root, readiness via initContextState), and a small LifecycleHost carries only what genuinely belongs to the provider: the shared creation helpers, the panel session facade, route registration, poller skips, the diff controller, telemetry, and the webview boundary. The provider resolves ctx explicitly at each handler boundary. --- .../src/agent-manager/AgentManagerProvider.ts | 55 +++-- .../src/agent-manager/provider-lifecycle.ts | 228 ++++++++++-------- .../tests/unit/agent-manager-arch.test.ts | 14 +- .../unit/agent-manager-close-session.test.ts | 10 + 4 files changed, 174 insertions(+), 133 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 5e366f615df..c4a5f93ca5e 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -18,7 +18,7 @@ import { deleteLifecycleWorktree, promoteLifecycleSession, removeStaleLifecycleWorktree, - type LifecycleDeps, + type LifecycleHost, } from "./provider-lifecycle" import { normalizeBaseBranch } from "./base-branch" import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type WorktreeStats } from "./GitStatsPoller" @@ -1039,27 +1039,37 @@ export class AgentManagerProvider implements Disposable { /** Create a new worktree with an auto-created first session. */ private async onCreateWorktree(baseBranch?: string, branchName?: string): Promise { - return createLifecycleWorktree(this.lifecycle, { baseBranch, branchName }) + const ctx = this.context + if (!ctx) return null + return createLifecycleWorktree(ctx, this.lifecycleHost, { baseBranch, branchName }) } /** Delete a worktree and dissociate its sessions. */ private async onDeleteWorktree(worktreeId: string): Promise { - return deleteLifecycleWorktree(this.lifecycle, worktreeId) + const ctx = this.context + if (!ctx) return null + return deleteLifecycleWorktree(ctx, this.lifecycleHost, worktreeId) } /** Remove a stale worktree entry from state without touching the filesystem. */ private async onRemoveStaleWorktree(worktreeId: string): Promise { - return removeStaleLifecycleWorktree(this.lifecycle, worktreeId) + const ctx = this.context + if (!ctx) return null + return removeStaleLifecycleWorktree(ctx, this.lifecycleHost, worktreeId) } /** Promote a session: create a worktree and move the session into it. */ private async onPromoteSession(sessionId: string): Promise { - return promoteLifecycleSession(this.lifecycle, sessionId) + const ctx = this.context + if (!ctx) return null + return promoteLifecycleSession(ctx, this.lifecycleHost, sessionId) } /** Add a new session to an existing worktree. */ private async onAddSessionToWorktree(worktreeId: string, sessionId?: string): Promise { - return addSessionToLifecycleWorktree(this.lifecycle, worktreeId, sessionId) + const ctx = this.context + if (!ctx) return null + return addSessionToLifecycleWorktree(ctx, this.lifecycleHost, worktreeId, sessionId) } private onForkSession(sessionId: string, worktreeId?: string, messageId?: string) { @@ -1089,7 +1099,9 @@ export class AgentManagerProvider implements Disposable { /** Stop a session and remove it from Agent Manager. */ private async onCloseSession(sessionId: string): Promise { - return closeLifecycleSession(this.lifecycle, sessionId) + const ctx = this.context + if (!ctx) return null + return closeLifecycleSession(ctx, this.lifecycleHost, sessionId) } // Multi-version worktree creation @@ -1513,37 +1525,34 @@ export class AgentManagerProvider implements Disposable { } // Manager accessors — repository-bound services are owned by the active ProjectContext (immutable per root). - private get lifecycle(): LifecycleDeps { + /** Provider capabilities for the worktree lifecycle handlers (state stays in ProjectContext). */ + private get lifecycleHost(): LifecycleHost { return { - waitReady: (context) => this.waitForStateReady(context), createOnDisk: (opts) => this.createWorktreeOnDisk(opts), - setup: (dir, branch, id) => this.runSetupScriptForWorktree(dir, branch, id), + runSetup: (dir, branch, id) => this.runSetupScriptForWorktree(dir, branch, id), createSession: (dir, branch, id) => this.createSessionInWorktree(dir, branch, id), - state: () => this.getStateManager(), - manager: () => this.getWorktreeManager(), + notifyReady: (sid, result, id) => this.notifyWorktreeReady(sid, result, id), + sessions: { + register: (session) => this.panel?.sessions.registerSession(session), + clearDirectory: (sid) => this.panel?.sessions.clearSessionDirectory(sid), + directories: () => this.panel?.sessions.getSessionDirectories(), + abort: (ids) => this.panel?.sessions.abortSessions(ids) ?? Promise.resolve(), + forget: (sid) => void this.panelSessions.delete(sid), + }, push: () => this.pushState(), register: (sid, dir) => this.registerWorktreeSession(sid, dir), - ready: (sid, result, id) => this.notifyWorktreeReady(sid, result, id), - registerSession: (session) => this.panel?.sessions.registerSession(session), - clearDirectory: (sid) => this.panel?.sessions.clearSessionDirectory(sid), - directories: () => this.panel?.sessions.getSessionDirectories(), - abort: (ids) => this.panel?.sessions.abortSessions(ids) ?? Promise.resolve(), - forgetPanel: (sid) => void this.panelSessions.delete(sid), skipStats: (id) => this.statsPoller.skipWorktree(id), removePR: (id) => this.prBridge.remove(id), removeRun: (id) => this.run.remove(id), forgetName: (id) => this.naming.forget(id), - stale: () => this.staleWorktreeIds, - clearStale: (id) => this.clearStaleTracking(id), - stopDiffsFor: (path, orphaned) => { + stopDiffs: (path, orphaned) => { if (this.diffs.shouldStopForWorktree(path, orphaned)) this.diffs.stop() }, capture: (event, props) => this.host.capture(event, props), autoName: () => this.host.autoBranchNaming(), client: () => this.connectionService.getClient(), metadata: (client, dir) => sandboxSessionMetadata(this.connectionService.sandboxPreference, client, dir), - root: () => this.getRoot(), - post: (msg) => this.postToWebview(msg as unknown as AgentManagerOutMessage), + post: (msg) => this.postToWebview(msg), log: (...args) => this.log(...args), } } diff --git a/packages/kilo-vscode/src/agent-manager/provider-lifecycle.ts b/packages/kilo-vscode/src/agent-manager/provider-lifecycle.ts index 6b813a56693..4ee6713f0b3 100644 --- a/packages/kilo-vscode/src/agent-manager/provider-lifecycle.ts +++ b/packages/kilo-vscode/src/agent-manager/provider-lifecycle.ts @@ -1,217 +1,233 @@ import type { KiloClient, Session } from "@kilocode/sdk/v2/client" import { getErrorMessage } from "../kilo-provider-utils" -import type { WorktreeManager, CreateWorktreeResult } from "./WorktreeManager" +import type { AgentManagerOutMessage } from "./types" import { PLATFORM } from "./constants" -import type { ManagedSession, WorktreeStateManager } from "./WorktreeStateManager" +import { initContextState } from "./project-init" +import type { ProjectContext } from "./project-context" +import type { ManagedSession } from "./WorktreeStateManager" +import type { CreateWorktreeResult, WorktreeManager } from "./WorktreeManager" import type { CreateWorktreeOnDiskOptions, CreateWorktreeOnDiskResult } from "./worktree-create" import { recordPromotionHandoff } from "./promotion-handoff" import { stopSessionProcesses } from "../kilo-provider/background-process" -/** Everything the worktree lifecycle handlers read from the provider. */ -export interface LifecycleDeps { - waitReady: (context: string) => Promise +/** + * Provider capabilities the worktree lifecycle needs beyond project state. + * State is reached through the ProjectContext the handler receives; this host + * only carries what genuinely belongs to the provider: shared creation + * helpers, the panel session facade, route registration, poller skips, the + * diff controller, telemetry, and the webview boundary. + */ +export interface LifecycleHost { createOnDisk: (opts?: CreateWorktreeOnDiskOptions) => Promise - setup: (dir: string, branch: string, id: string) => Promise + runSetup: (dir: string, branch: string, id: string) => Promise createSession: (dir: string, branch: string, id: string) => Promise - state: () => WorktreeStateManager | undefined - manager: () => WorktreeManager | undefined + notifyReady: (sessionId: string, result: CreateWorktreeResult, worktreeId?: string) => void + sessions: { + register: (session: Session) => void + clearDirectory: (sessionId: string) => void + directories: () => ReadonlyMap | undefined + abort: (sessionIds: string[]) => Promise + forget: (sessionId: string) => void + } push: () => void register: (sessionId: string, dir: string) => void - ready: (sessionId: string, result: CreateWorktreeResult, worktreeId?: string) => void - registerSession: (session: Session) => void - clearDirectory: (sessionId: string) => void - directories: () => ReadonlyMap | undefined - abort: (sessionIds: string[]) => Promise - forgetPanel: (sessionId: string) => void skipStats: (worktreeId: string) => void removePR: (worktreeId: string) => void removeRun: (worktreeId: string) => void forgetName: (worktreeId: string) => void - stale: () => Set - clearStale: (worktreeId: string) => void - stopDiffsFor: (path: string, orphaned: ManagedSession[]) => void + stopDiffs: (path: string, orphaned: ManagedSession[]) => void capture: (event: string, props: Record) => void autoName: () => { enabled: boolean } client: () => KiloClient metadata: (client: KiloClient, dir: string) => Promise> - root: () => string | undefined - post: (message: Record) => void + post: (message: AgentManagerOutMessage) => void log: (...args: unknown[]) => void } /** Create a new worktree with an auto-created first session. */ export async function createLifecycleWorktree( - deps: LifecycleDeps, + ctx: ProjectContext, + host: LifecycleHost, opts: { baseBranch?: string; branchName?: string }, ): Promise { - await deps.waitReady("onCreateWorktree") + await initContextState(ctx, host.log) - const created = await deps.createOnDisk({ baseBranch: opts.baseBranch, branchName: opts.branchName }) + const created = await host.createOnDisk({ baseBranch: opts.baseBranch, branchName: opts.branchName }) if (!created) return null // Run setup script for new worktree (blocks until complete, shows in overlay) - await deps.setup(created.result.path, created.result.branch, created.worktree.id) + await host.runSetup(created.result.path, created.result.branch, created.worktree.id) - const session = await deps.createSession(created.result.path, created.result.branch, created.worktree.id) + const session = await host.createSession(created.result.path, created.result.branch, created.worktree.id) if (!session) { - const state = deps.state() - const manager = deps.manager() - state?.removeWorktree(created.worktree.id) - await manager?.removeWorktree(created.result.path) - deps.push() + ctx.peekState()?.removeWorktree(created.worktree.id) + await ctx.worktreeManager().removeWorktree(created.result.path) + host.push() return null } - const state = deps.state()! + const state = ctx.peekState()! state.addSession(session.id, created.worktree.id) - if (!opts.branchName && deps.autoName().enabled) state.armAutoName(created.worktree.id, session.id) - deps.register(session.id, created.result.path) + if (!opts.branchName && host.autoName().enabled) state.armAutoName(created.worktree.id, session.id) + host.register(session.id, created.result.path) // Push state before registerSession so the webview's sessionCreated handler // sees the worktree mapping and routes the session to the worktree tab. - deps.ready(session.id, created.result, created.worktree.id) - deps.registerSession(session) - deps.capture("Agent Manager Session Started", { + host.notifyReady(session.id, created.result, created.worktree.id) + host.sessions.register(session) + host.capture("Agent Manager Session Started", { source: PLATFORM, sessionId: session.id, worktreeId: created.worktree.id, branch: created.result.branch, }) - deps.log(`Created worktree ${created.worktree.id} with session ${session.id}`) + host.log(`Created worktree ${created.worktree.id} with session ${session.id}`) return null } /** Delete a worktree and dissociate its sessions. */ -export async function deleteLifecycleWorktree(deps: LifecycleDeps, worktreeId: string): Promise { - const manager = deps.manager() - const state = deps.state() - if (!manager || !state) return null +export async function deleteLifecycleWorktree( + ctx: ProjectContext, + host: LifecycleHost, + worktreeId: string, +): Promise { + const state = ctx.peekState() + if (!state) return null const worktree = state.getWorktree(worktreeId) if (!worktree) { - deps.log(`Worktree ${worktreeId} not found in state`) + host.log(`Worktree ${worktreeId} not found in state`) return null } // Remove from state BEFORE disk removal so pollers immediately stop targeting this worktree. // Pre-emptive skip covers any in-flight poll that already captured getWorktrees(). - deps.skipStats(worktreeId) - deps.removePR(worktreeId) - deps.removeRun(worktreeId) - deps.forgetName(worktreeId) + host.skipStats(worktreeId) + host.removePR(worktreeId) + host.removeRun(worktreeId) + host.forgetName(worktreeId) const orphaned = state.removeWorktree(worktreeId) - deps.stopDiffsFor(worktree.path, orphaned) - for (const s of orphaned) deps.clearDirectory(s.id) - deps.push() + host.stopDiffs(worktree.path, orphaned) + for (const s of orphaned) host.sessions.clearDirectory(s.id) + host.push() // Disk removal after state is clean — pollers no longer reference this worktree. const branch = worktree.branchOwned === false ? undefined : (worktree.originalBranch ?? worktree.branch) try { - await manager.removeWorktree(worktree.path, branch) + await ctx.worktreeManager().removeWorktree(worktree.path, branch) } catch (error) { - deps.log(`Failed to remove worktree from disk: ${error}`) + host.log(`Failed to remove worktree from disk: ${error}`) } - deps.log(`Deleted worktree ${worktreeId}${branch ? ` (${branch})` : ""}`) + host.log(`Deleted worktree ${worktreeId}${branch ? ` (${branch})` : ""}`) return null } /** Remove a stale worktree entry from state without touching the filesystem. */ -export async function removeStaleLifecycleWorktree(deps: LifecycleDeps, worktreeId: string): Promise { - const state = deps.state() +export async function removeStaleLifecycleWorktree( + ctx: ProjectContext, + host: LifecycleHost, + worktreeId: string, +): Promise { + const state = ctx.peekState() if (!state) return null - if (!deps.stale().has(worktreeId)) { - deps.log(`Ignored stale removal for non-stale worktree ${worktreeId}`) + if (!ctx.stale.has(worktreeId)) { + host.log(`Ignored stale removal for non-stale worktree ${worktreeId}`) return null } const worktree = state.getWorktree(worktreeId) if (!worktree) { - deps.clearStale(worktreeId) - deps.push() + ctx.stale.delete(worktreeId) + host.push() return null } - deps.forgetName(worktreeId) + host.forgetName(worktreeId) const orphaned = state.removeWorktree(worktreeId) - deps.stopDiffsFor(worktree.path, orphaned) - for (const session of orphaned) deps.clearDirectory(session.id) - deps.clearStale(worktreeId) - deps.push() - deps.log(`Removed stale worktree entry ${worktreeId} (${worktree.branch})`) + host.stopDiffs(worktree.path, orphaned) + for (const session of orphaned) host.sessions.clearDirectory(session.id) + ctx.stale.delete(worktreeId) + host.push() + host.log(`Removed stale worktree entry ${worktreeId} (${worktree.branch})`) return null } /** Promote a session: create a worktree and move the session into it. */ -export async function promoteLifecycleSession(deps: LifecycleDeps, sessionId: string): Promise { - await deps.waitReady("onPromoteSession") - const created = await deps.createOnDisk({}) +export async function promoteLifecycleSession( + ctx: ProjectContext, + host: LifecycleHost, + sessionId: string, +): Promise { + await initContextState(ctx, host.log) + const created = await host.createOnDisk({}) if (!created) return null // Run setup script for new worktree (blocks until complete, shows in overlay) - await deps.setup(created.result.path, created.result.branch, created.worktree.id) + await host.runSetup(created.result.path, created.result.branch, created.worktree.id) - const state = deps.state()! + const state = ctx.peekState()! if (!state.getSession(sessionId)) { state.addSession(sessionId, created.worktree.id) } else { state.moveSession(sessionId, created.worktree.id) } - deps.register(sessionId, created.result.path) + host.register(sessionId, created.result.path) try { await recordPromotionHandoff({ - client: deps.client(), + client: host.client(), sessionId, directory: created.result.path, branch: created.result.branch, }) } catch (err) { - deps.log("Failed to record worktree promotion handoff:", getErrorMessage(err)) + host.log("Failed to record worktree promotion handoff:", getErrorMessage(err)) } - deps.ready(sessionId, created.result, created.worktree.id) - deps.log(`Promoted session ${sessionId} to worktree ${created.worktree.id}`) + host.notifyReady(sessionId, created.result, created.worktree.id) + host.log(`Promoted session ${sessionId} to worktree ${created.worktree.id}`) return null } /** Add a new or existing session to an existing worktree. */ export async function addSessionToLifecycleWorktree( - deps: LifecycleDeps, + ctx: ProjectContext, + host: LifecycleHost, worktreeId: string, sessionId?: string, ): Promise { let client: KiloClient try { - client = deps.client() + client = host.client() } catch (err) { - deps.log("onAddSessionToWorktree: client not available:", err) - deps.post({ type: "error", message: "Not connected to CLI backend" }) + host.log("onAddSessionToWorktree: client not available:", err) + host.post({ type: "error", message: "Not connected to CLI backend" }) return null } - const state = deps.state() + const state = ctx.peekState() if (!state) return null const worktree = state.getWorktree(worktreeId) if (!worktree) { - deps.log(`Worktree ${worktreeId} not found`) + host.log(`Worktree ${worktreeId} not found`) return null } if (sessionId) { if (state.getSession(sessionId)) state.moveSession(sessionId, worktreeId) else state.addSession(sessionId, worktreeId) - deps.register(sessionId, worktree.path) - deps.push() - deps.post({ type: "agentManager.sessionAdded", sessionId, worktreeId }) - deps.capture("Agent Manager Session Started", { + host.register(sessionId, worktree.path) + host.push() + host.post({ type: "agentManager.sessionAdded", sessionId, worktreeId }) + host.capture("Agent Manager Session Started", { source: PLATFORM, sessionId, worktreeId, existing: true, }) - deps.log(`Added existing session ${sessionId} to worktree ${worktreeId}`) + host.log(`Added existing session ${sessionId} to worktree ${worktreeId}`) return null } let session: Session try { - const metadata = await deps.metadata(client, worktree.path) + const metadata = await host.metadata(client, worktree.path) const { data } = await client.session.create( { directory: worktree.path, platform: PLATFORM, metadata }, { throwOnError: true }, @@ -219,8 +235,8 @@ export async function addSessionToLifecycleWorktree( session = data } catch (error) { const err = getErrorMessage(error) - deps.post({ type: "error", message: `Failed to create session: ${err}` }) - deps.capture("Agent Manager Session Error", { + host.post({ type: "error", message: `Failed to create session: ${err}` }) + host.capture("Agent Manager Session Error", { source: PLATFORM, error: err, context: "addSessionToWorktree", @@ -230,35 +246,41 @@ export async function addSessionToLifecycleWorktree( } state.addSession(session.id, worktreeId) - deps.register(session.id, worktree.path) - deps.push() - deps.post({ type: "agentManager.sessionAdded", sessionId: session.id, worktreeId }) - deps.registerSession(session) + host.register(session.id, worktree.path) + host.push() + host.post({ type: "agentManager.sessionAdded", sessionId: session.id, worktreeId }) + host.sessions.register(session) - deps.capture("Agent Manager Session Started", { + host.capture("Agent Manager Session Started", { source: PLATFORM, sessionId: session.id, worktreeId, }) - deps.log(`Added session ${session.id} to worktree ${worktreeId}`) + host.log(`Added session ${session.id} to worktree ${worktreeId}`) return null } /** Stop a session and remove it from Agent Manager. */ -export async function closeLifecycleSession(deps: LifecycleDeps, sessionId: string): Promise { - const state = deps.state() - const dir = state?.directoryFor(sessionId) ?? deps.directories()?.get(sessionId) ?? deps.root() ?? process.cwd() - await deps.abort([sessionId]) - deps.forgetPanel(sessionId) +export async function closeLifecycleSession( + ctx: ProjectContext, + host: LifecycleHost, + sessionId: string, +): Promise { + const state = ctx.peekState() + const dir = state?.directoryFor(sessionId) ?? host.sessions.directories()?.get(sessionId) ?? ctx.root ?? process.cwd() + await host.sessions.abort([sessionId]) + host.sessions.forget(sessionId) try { - await stopSessionProcesses(deps.client(), sessionId, dir) + await stopSessionProcesses(host.client(), sessionId, dir) } catch (err) { - deps.log("onCloseSession: client not available:", err) + host.log("onCloseSession: client not available:", err) } state?.removeSession(sessionId) - deps.clearDirectory(sessionId) - if (state) deps.push() - deps.log(`Closed session ${sessionId}`) + host.sessions.clearDirectory(sessionId) + if (state) host.push() + host.log(`Closed session ${sessionId}`) return null } + +export type { WorktreeManager } diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index c6201c3df5e..f0301e4abbb 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -270,7 +270,7 @@ describe("Agent Manager Provider Messages", () => { expect(body).toContain("closedDrafts.add(sessionId)") expect(body).toContain('vscode.postMessage({ type: "agentManager.closeSession", sessionId })') expect(body).not.toContain('type: "agentManager.forgetSession"') - expect(getMethodBody("onCloseSession")).toContain("await deps.abort([sessionId])") + expect(getMethodBody("onCloseSession")).toContain("await host.sessions.abort([sessionId])") expect(text).toContain("if (created.draftID && closedDrafts.delete(created.draftID)) return") }) @@ -517,10 +517,10 @@ describe("Agent Manager Provider — onMessage routing", () => { */ it("onDeleteWorktree removes from disk, state, clears orphans, and pushes", () => { const text = body("onDeleteWorktree") - expect(text).toContain("manager.removeWorktree") + expect(text).toContain("worktreeManager().removeWorktree") expect(text).toContain("state.removeWorktree") - expect(text).toContain("deps.clearDirectory") - expect(text).toContain("deps.push()") + expect(text).toContain("sessions.clearDirectory") + expect(text).toContain("host.push()") }) // -- onCreateWorktree invariants ------------------------------------------- @@ -532,8 +532,8 @@ describe("Agent Manager Provider — onMessage routing", () => { */ it("onCreateWorktree runs setup script before creating session", () => { const text = body("onCreateWorktree") - const setupIdx = text.indexOf("deps.setup(") - const sessionIdx = text.indexOf("deps.createSession(") + const setupIdx = text.indexOf("host.runSetup(") + const sessionIdx = text.indexOf("host.createSession(") expect(setupIdx, "setup script call must exist").toBeGreaterThan(-1) expect(sessionIdx, "session creation call must exist").toBeGreaterThan(-1) expect(setupIdx, "setup script must run before session creation").toBeLessThan(sessionIdx) @@ -568,7 +568,7 @@ describe("Agent Manager Provider — onMessage routing", () => { */ it("onPromoteSession runs setup script before modifying session", () => { const text = body("onPromoteSession") - const setupIdx = text.indexOf("deps.setup(") + const setupIdx = text.indexOf("host.runSetup(") const moveIdx = text.indexOf("moveSession") expect(setupIdx).toBeGreaterThan(-1) expect(moveIdx).toBeGreaterThan(-1) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-close-session.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-close-session.test.ts index 5575c78bb48..6ad70d26c8b 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-close-session.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-close-session.test.ts @@ -3,6 +3,8 @@ import { describe, expect, it, mock } from "bun:test" const { AgentManagerProvider } = await import("../../src/agent-manager/AgentManagerProvider") type Manager = { + projectScope: { current: () => unknown; run: (ctx: unknown, fn: () => unknown) => unknown } + contexts: { active: () => unknown } connectionService: { getClient: () => unknown } panel: { postMessage: (message: unknown) => void @@ -61,6 +63,14 @@ function createManager(options?: { dir?: string; panelDir?: string; state?: bool manager.getRoot = () => "/repo" manager.pushState = mock(() => undefined) manager.log = mock(() => undefined) + const ctx = { + peekState: () => (options?.state === false ? undefined : state), + root: "/repo", + stale: new Set(), + worktreeManager: () => ({}), + } + manager.projectScope = { current: () => ctx, run: (_: unknown, fn: () => unknown) => fn() } + manager.contexts = { active: () => ctx } return { manager, stopped, aborted, cleared, removed, messages, events } }