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-settings-tab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Move Agent Manager worktree settings into the Kilo Settings editor and add project selection for multi-project workspaces.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion packages/kilo-vscode/script/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { spawn } from "node:child_process"
const win = process.platform === "win32"
const root = join(import.meta.dir, "..")
const repo = resolve(root, "..", "..")
const temp = tmpdir().trimEnd()

// ---------------------------------------------------------------------------
// Argument parsing
Expand Down Expand Up @@ -102,7 +103,7 @@ const base =
? expand(opts["state-dir"])
: isolated
? join(dev, "vscode")
: join(tmpdir(), `kilo-vscode-dev-${hash}`)
: join(temp, `kilo-vscode-dev-${hash}`)
const userDir = join(base, "user-data")
const extDir = join(base, "extensions")
const kilo =
Expand Down
93 changes: 92 additions & 1 deletion packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private ignoreControllerDir: string | null = null
private chatAutocomplete: ChatTextAreaAutocomplete | null = null
private projectDirectory: string | null | undefined
private settingsGeneration = 0
private indexingProjectId: string | undefined
private indexingSettingsRequest = 0
private indexingStatusRequest = 0
Expand Down Expand Up @@ -1048,6 +1049,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return
}
if (this.handleEditorOpenMessage(message)) return
if (await this.handleAgentManagerSettingsMessage(message)) return
if (
await handleWorkStyleMessage({
message,
Expand Down Expand Up @@ -1201,7 +1203,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
break
case "openSettingsPanel":
vscode.commands.executeCommand("kilo-code.new.settingsButtonClicked", message.tab)
vscode.commands.executeCommand("kilo-code.new.settingsButtonClicked", message.tab, message.projectId)
break
case "openKiloClaw":
vscode.commands.executeCommand("kilo-code.new.kiloClawOpen")
Expand Down Expand Up @@ -3714,6 +3716,94 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return this.projectDirectory ?? this.getRootDirectory()
}

private async handleAgentManagerSettingsMessage(
message: TypedWebviewMessage & { projectId?: string; branch?: string; requestId?: string },
): Promise<boolean> {
const handler = this.opts.agentManagerSettings
if (!handler) return false
const requestId = message.requestId
if (typeof requestId !== "string") return false
// Only Agent Manager settings messages participate in the generation
// guard: unrelated Settings-panel requests also carry requestId and must
// not invalidate an in-flight projects/branches response.
if (
message.type !== "requestAgentManagerSettings" &&
message.type !== "requestAgentManagerSettingsBranches" &&
message.type !== "setAgentManagerDefaultBaseBranch" &&
message.type !== "configureAgentManagerSetupScript"
)
return false
// The settings handler is project-scoped by projectId; the panel's own
// project directory (config bindings, saves, local-config opens) must
// stay untouched so Agent Manager tab traffic cannot expire unsaved
// config edits.
const generation = ++this.settingsGeneration
Comment thread
marius-kilocode marked this conversation as resolved.
if (message.type === "requestAgentManagerSettings") {
await this.sendAgentManagerSettings(message.projectId, false, generation, requestId)
return true
}
if (message.type === "requestAgentManagerSettingsBranches" && message.projectId) {
await this.sendAgentManagerSettingsBranches(message.projectId, generation, requestId)
return true
}
if (message.type === "setAgentManagerDefaultBaseBranch" && message.projectId) {
await handler.setDefaultBaseBranch(message.projectId, message.branch)
if (this.settingsGeneration !== generation) return true
await this.sendAgentManagerSettings(message.projectId, false, generation, requestId)
return true
}
if (message.type === "configureAgentManagerSetupScript" && message.projectId) {
await handler.configureSetupScript(message.projectId)
if (this.settingsGeneration !== generation) return true
await this.sendAgentManagerSettings(message.projectId, false, generation, requestId)
return true
}
return false
}

private async sendAgentManagerSettings(
projectId: string | undefined,
withBranches: boolean,
generation: number,
requestId: string,
): Promise<void> {
const handler = this.opts.agentManagerSettings
if (!handler) return
const projects = await handler.projects(projectId)
if (this.settingsGeneration !== generation) return
const selected = projects.some((project) => project.id === projectId) ? projectId : projects[0]?.id
const branch = selected ? await handler.defaultBranch(selected) : undefined
if (this.settingsGeneration !== generation) return
const items = selected
? projects.map((project) => (project.id === selected ? { ...project, defaultBranch: branch } : project))
: projects
this.postMessage({ type: "agentManagerSettingsLoaded", projects: items, projectId: selected, requestId })
if (withBranches && selected) await this.sendAgentManagerSettingsBranches(selected, generation, requestId)
}

private async sendAgentManagerSettingsBranches(
projectId: string,
generation: number,
requestId: string,
): Promise<void> {
const handler = this.opts.agentManagerSettings
if (!handler) return
const data = await handler.branches(projectId)
if (this.settingsGeneration !== generation) return
this.postMessage(
data
? { type: "agentManagerSettingsBranchesLoaded", ...data, requestId }
: {
type: "agentManagerSettingsBranchesLoaded",
projectId,
branches: [],
defaultBranch: "",
requestId,
error: true,
},
)
}

private bindingsFor(
directory: string,
targets:
Expand Down Expand Up @@ -5182,6 +5272,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// VS Code's native toolbar (restored in package.json) works everywhere.
topBar: this.opts.hideTopBar !== true && isCursorHost(),
topBarSurface: this.opts.topBarSurface === "tab" ? "tab_title" : "sidebar_title",
agentManagerSettings: this.opts.agentManagerSettings !== undefined,
})
}

Expand Down
35 changes: 25 additions & 10 deletions packages/kilo-vscode/src/SettingsEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { KiloProvider } from "./KiloProvider"
import { resolvePanelProjectDirectory } from "./project-directory"
import type { KiloConnectionService } from "./services/cli-backend"
import type { RemoteStatusService } from "./services/RemoteStatusService"
import type { AgentManagerSettingsHandler } from "./kilo-provider/options"

type PanelView = "settings" | "profile" | "indexing"

Expand All @@ -27,15 +28,18 @@ export class SettingsEditorProvider implements vscode.Disposable {
private panels = new Map<PanelView, vscode.WebviewPanel>()
private providers = new Map<PanelView, KiloProvider>()
private tabs = new Map<PanelView, string>()
private projects = new Map<PanelView, string>()
private remoteService: RemoteStatusService | null = null

constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
private readonly context: vscode.ExtensionContext,
private readonly agentManagerSettings?: AgentManagerSettingsHandler,
) {}

private getProjectDirectory(): string | null {
private getProjectDirectory(projectId?: string): string | null {
if (projectId) return this.agentManagerSettings?.projectDirectory(projectId) ?? null
const editor = vscode.window.activeTextEditor
const active =
editor?.document.uri.scheme === "file"
Expand All @@ -53,19 +57,22 @@ export class SettingsEditorProvider implements vscode.Disposable {
return view
}

openPanel(view: PanelView, tab?: string): void {
openPanel(view: PanelView, tab?: string, projectId?: string): void {
if (tab) this.tabs.set(view, tab)
if (projectId) this.projects.set(view, projectId)
else this.projects.delete(view)

const projectDirectory = this.getProjectDirectory()
const projectDirectory = this.getProjectDirectory(projectId)
const existing = this.panels.get(view)
if (existing) {
this.providers.get(view)?.setProjectDirectory(projectDirectory)
if (tab) {
const provider = this.providers.get(view)
provider?.postMessage({ type: "navigate", view, tab })
}
existing.reveal(vscode.ViewColumn.Active)
this.providers.get(view)?.postMessage({ type: "navigate", view, ...(tab ? { tab } : {}) })
this.providers.get(view)?.postMessage({
type: "navigate",
view,
...(tab ? { tab } : {}),
...(projectId ? { projectId } : {}),
})
return
}

Expand All @@ -90,7 +97,7 @@ export class SettingsEditorProvider implements vscode.Disposable {
panel.dispose()
return
}
this.wirePanel(panel, view, this.getProjectDirectory())
this.wirePanel(panel, view, this.getProjectDirectory(this.projects.get(view)))
}

private wirePanel(panel: vscode.WebviewPanel, view: PanelView, projectDirectory: string | null): void {
Expand All @@ -104,6 +111,7 @@ export class SettingsEditorProvider implements vscode.Disposable {
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
projectDirectory,
hideTopBar: true,
agentManagerSettings: view === "settings" ? this.agentManagerSettings : undefined,
})
if (this.remoteService) {
provider.setRemoteService(this.remoteService)
Expand All @@ -123,7 +131,12 @@ export class SettingsEditorProvider implements vscode.Disposable {
if (msg.type === "webviewReady") {
// Small delay to let KiloProvider's own webviewReady handler finish first
setTimeout(() => {
provider.postMessage({ type: "navigate", view, tab: this.tabs.get(view) })
provider.postMessage({
type: "navigate",
view,
tab: this.tabs.get(view),
projectId: this.projects.get(view),
})
}, 50)
}
})
Expand All @@ -148,6 +161,7 @@ export class SettingsEditorProvider implements vscode.Disposable {
this.panels.delete(view)
this.providers.delete(view)
this.tabs.delete(view)
this.projects.delete(view)
})
}

Expand All @@ -166,5 +180,6 @@ export class SettingsEditorProvider implements vscode.Disposable {
this.panels.clear()
this.providers.clear()
this.tabs.clear()
this.projects.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import { ProjectContexts } from "./project/contexts"
import { hydrateExpanded } from "./project/hydrate"
import { createMultiVersion, type MultiVersionHost } from "./provider-multi-version"
import { handleProjectMessage, type ProjectMessageDeps } from "./project/messages"
import { createProjectWiring } from "./project/wiring"
import { createProjectWiring, type ProjectWiring } from "./project/wiring"
import { ProjectScope } from "./project/scope"
import type { AgentManagerOutMessage, AgentManagerInMessage } from "./types"
import type { Host, PanelContext, OutputHandle, Disposable } from "./host"
Expand Down Expand Up @@ -116,10 +116,9 @@ export class AgentManagerProvider implements Disposable {
private destination = new DestinationState()
private closing: Promise<void> | undefined
private onVisibilityChange: ((visible: boolean) => void) | undefined
// Tracks sessions owned by this panel until they are explicitly closed.
private panelSessions = new Set<string>()
private busySessions = new Set<string>()

readonly settings: ProjectWiring["settings"]
/** Session ID most recently loaded via `loadMessages`; updated synchronously. */
private activeSessionId: string | undefined
private visiblePresence = new AgentManagerVisiblePresence(
Expand Down Expand Up @@ -195,12 +194,13 @@ export class AgentManagerProvider implements Disposable {
expand: (ctx) => this.initExpanded(ctx),
ready: (ctx) => initContextState(ctx, (...args) => this.log(...args)),
push: () => this.pushProjects(),
pushState: (ctx) => this.pushState(ctx),
changed: () => this.onWorkspaceChanged(),
refresh: () => this.pushState(),
selected: (target) => this.postToWebview({ type: "agentManager.selectionActivated", target }),
})
this.registry = wiring.registry
this.contexts = wiring.contexts
this.settings = wiring.settings
this.projects = wiring.messages
this.unsubProjects = () => wiring.dispose()
this.naming = new BranchNamingController({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function createMockHost(): Host {
copyToClipboard: vi.fn(),
capture: vi.fn(),
openExternal: vi.fn(),
openSettings: vi.fn(),
refreshGit: vi.fn(),
dispose: vi.fn(),
}
Expand Down
3 changes: 3 additions & 0 deletions packages/kilo-vscode/src/agent-manager/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ export interface Host {
/** Open a URL in the user's default browser. */
openExternal(url: string): void

/** Open Kilo Settings, optionally focused on a tab and project. */
openSettings(tab?: string, projectId?: string): void

/** Ask VS Code's git extension to re-scan repositories (e.g. after worktree ref migration). */
refreshGit(): void

Expand Down
6 changes: 6 additions & 0 deletions packages/kilo-vscode/src/agent-manager/project/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@ export interface ProjectMessageDeps {
selected: (target: SidebarTarget) => void
/** Show a user-facing error. */
error: (message: string) => void
/** Open the Kilo Settings editor, optionally on a tab and project. */
openSettings: (tab?: string, projectId?: string) => void
/** Ensure a context's repository state is ready (no-op once initialized). */
ready: (ctx: ProjectContext) => Promise<ProjectInitResult>
log: (...args: unknown[]) => void
}

/** Handle a project-management message. Returns true when the message was consumed. */
export async function handleProjectMessage(m: AgentManagerInMessage, deps: ProjectMessageDeps): Promise<boolean> {
if (m.type === "openSettingsPanel") {
deps.openSettings(m.tab, m.projectId)
return true
}
if (m.type === "agentManager.requestProjects") {
deps.push()
return true
Expand Down
Loading
Loading