Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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/persist-project-accordion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Allow Agent Manager project headers to collapse or expand their project body, and persist that state across panel opens and VS Code restarts.
26 changes: 19 additions & 7 deletions packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ export class AgentManagerProvider implements Disposable {
this.panelSessions.clear()
void ctx.sessions.abortSessions(ids).catch((err) => this.log("Failed to abort sessions on panel close:", err))
this.statsPoller.stop()
this.projectPollers.dispose()
this.prBridge.poller.stop()
this.diffs.stop()
this.activeSessionId = undefined
Expand Down Expand Up @@ -450,12 +451,14 @@ export class AgentManagerProvider implements Disposable {

/** Initialize an expanded background project and push its state (no panel wiring). */
private initExpanded(ctx: ProjectContext): void {
void initContextState(ctx, (...args) => this.log(...args)).then((result) => {
if (!result.current) return
registerProjectSessions(ctx, this.panel?.sessions)
this.pushState(ctx)
this.projectPollers.sync(this.contexts)
})
void initContextState(ctx, (...args) => this.log(...args))
.then((result) => {
if (!result.current) return
registerProjectSessions(ctx, this.panel?.sessions)
this.pushState(ctx)
this.projectPollers.sync(this.contexts)
})
.catch((err) => this.log("Failed to initialize expanded project:", err))
}

// Message interceptor
Expand Down Expand Up @@ -1567,11 +1570,20 @@ export class AgentManagerProvider implements Disposable {
}

private pushProjects(): void {
const projects = this.contexts.snapshots()
this.postToWebview({
type: "agentManager.projects",
multiProject: this.host.multiProject(),
projects: this.contexts.snapshots(),
projects,
})
if (this.panel) {
Comment thread
marius-kilocode marked this conversation as resolved.
for (const project of projects) {
if (project.active || !project.expanded || !project.trusted || project.missing) continue
Comment thread
marius-kilocode marked this conversation as resolved.
if (this.contexts.get(project.id)) continue
const ctx = this.contexts.expand(project.id)
if (ctx) this.initExpanded(ctx)
Comment thread
marius-kilocode marked this conversation as resolved.
}
}
this.projectPollers.sync(this.contexts)
}

Expand Down
39 changes: 26 additions & 13 deletions packages/kilo-vscode/src/agent-manager/project/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface ContextsOptions {
registry: {
list(): StoredProject[]
get(id: string): StoredProject | undefined
expanded?(id: string): boolean | undefined
}
/** Registry trust lookup for non-pinned projects. */
trusted: (id: string) => boolean
Expand All @@ -44,7 +45,7 @@ interface ContextsOptions {
export class ProjectContexts {
private readonly contexts = new Map<string, ProjectContext>()
private activeId: string | undefined
private readonly expanded = new Set<string>()
private readonly expansion = new Map<string, boolean>()

constructor(private readonly opts: ContextsOptions) {}

Expand Down Expand Up @@ -83,15 +84,15 @@ export class ProjectContexts {
const pinned = this.pinned()
if (pinned) {
this.activeId = pinned.id
this.expanded.add(pinned.id)
this.rememberExpansion(pinned.id, true)
return pinned
}
if (!this.opts.enabled()) return undefined
const first = this.opts.registry.list().find((p) => this.opts.trusted(p.id))
if (!first) return undefined
const ctx = this.ensure(first.id, first.root, false)
this.activeId = ctx.id
this.expanded.add(ctx.id)
this.rememberExpansion(ctx.id, false)
return ctx
}

Expand Down Expand Up @@ -140,27 +141,33 @@ export class ProjectContexts {
}

isExpanded(id: string): boolean {
return this.expanded.has(id)
const value = this.expansion.get(id)
if (value !== undefined) return value
const stored = this.opts.registry.expanded?.(id)
if (stored !== undefined) return stored
const root = this.opts.workspaceRoot()
return root !== undefined && projectIdFor(canonicalizePath(root)) === id
}

/** Make a project the active context and expand it. Returns undefined when not allowed. */
activate(id: string): ProjectContext | undefined {
const ctx = this.usableCtx(id)
if (!ctx) return undefined
this.activeId = id
this.rememberExpansion(id, false)
return ctx
}

/** Expand a project without activating it. Returns undefined when not allowed. */
expand(id: string): ProjectContext | undefined {
const ctx = this.usableCtx(id)
if (!ctx) return undefined
this.expanded.add(id)
this.expansion.set(id, true)
return ctx
}

collapse(id: string): void {
this.expanded.delete(id)
this.expansion.set(id, false)
if (this.isActive(id)) return
this.contexts.get(id)?.suspend()
}
Expand All @@ -169,10 +176,10 @@ export class ProjectContexts {
disable(): ProjectContext | undefined {
const pinned = this.pinned()
this.activeId = pinned?.id
if (pinned) this.expanded.add(pinned.id)
if (pinned) this.expansion.set(pinned.id, true)
for (const ctx of this.contexts.values()) {
if (ctx.pinned) continue
this.expanded.delete(ctx.id)
this.expansion.set(ctx.id, false)
ctx.suspend()
// Match remove()/syncPinned(): drop the routes too, otherwise the shared
// route service accumulates entries for every disabled project.
Expand All @@ -194,7 +201,7 @@ export class ProjectContexts {
async remove(id: string): Promise<boolean> {
const ctx = this.contexts.get(id)
if (!ctx || ctx.pinned) return false
this.expanded.delete(id)
this.expansion.delete(id)
if (this.activeId === id) this.activeId = undefined
this.contexts.delete(id)
this.opts.remove?.(id)
Expand All @@ -215,7 +222,7 @@ export class ProjectContexts {
for (const [id, ctx] of [...this.contexts]) {
if (!ctx.pinned) continue
this.contexts.delete(id)
this.expanded.delete(id)
this.expansion.delete(id)
if (this.activeId === id) this.activeId = undefined
this.opts.remove?.(id)
ctx.suspend()
Expand All @@ -241,26 +248,32 @@ export class ProjectContexts {
const id = ctx?.id ?? stored!.id
const root = ctx?.root ?? stored!.root
const pinned = ctx?.pinned ?? false
const missing = ctx ? ctx.missing() : !(this.opts.deps.exists ?? fs.existsSync)(root)
return {
id,
root,
label: stored?.label || path.basename(root) || root,
pinned,
active: this.isActive(id),
expanded: this.isExpanded(id),
expanded: !missing && this.isExpanded(id),
initialized: ctx?.loaded ?? false,
trusted: pinned || (stored?.trusted ?? false),
missing: ctx ? ctx.missing() : !(this.opts.deps.exists ?? fs.existsSync)(root),
missing,
}
}

private rememberExpansion(id: string, fallback: boolean): void {
if (this.expansion.has(id)) return
this.expansion.set(id, this.opts.registry.expanded?.(id) ?? fallback)
}

async dispose(): Promise<void> {
for (const ctx of this.contexts.values()) {
this.opts.remove?.(ctx.id)
await ctx.dispose()
}
this.contexts.clear()
this.expanded.clear()
this.expansion.clear()
this.activeId = undefined
}
}
10 changes: 8 additions & 2 deletions packages/kilo-vscode/src/agent-manager/project/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,15 @@ function selectProject(id: string, deps: ProjectMessageDeps): void {

async function setExpanded(id: string, expanded: boolean, deps: ProjectMessageDeps): Promise<void> {
if (disabled(deps)) return
const ctx = expanded ? deps.contexts.usable(id) : deps.contexts.resolve(id)
if (!ctx) {
deps.push()
return
}
await deps.registry.setExpanded(id, expanded)
if (expanded) {
const ctx = deps.contexts.expand(id)
if (ctx) deps.expand(ctx)
const next = deps.contexts.expand(id)
if (next) deps.expand(next)
}
if (!expanded) deps.contexts.collapse(id)
deps.push()
Expand Down
Loading
Loading