Merge pull request #13369 from Kilo-Org/migrate-settings-to-vscode-multiproject-support

feat(vscode): add project-scoped Agent Manager settings tab
This commit is contained in:
Marius
2026-08-24 16:24:42 +02:00
committed by GitHub
54 changed files with 1098 additions and 284 deletions
+5
View File
@@ -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.
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82b91b6fcb81a7500e37dfcc335d2977955cab4f786efa30f35880c09cd8987c
size 34732
oid sha256:e9b217741df7ecac772898be685b192360d65c8709fce6ff65c133aba50715e2
size 34695
+2 -1
View File
@@ -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
@@ -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 =
+92 -1
View File
@@ -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
@@ -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,
@@ -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")
@@ -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
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:
@@ -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,
})
}
@@ -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"
@@ -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"
@@ -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
}
@@ -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 {
@@ -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)
@@ -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)
}
})
@@ -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)
})
}
@@ -166,5 +180,6 @@ export class SettingsEditorProvider implements vscode.Disposable {
this.panels.clear()
this.providers.clear()
this.tabs.clear()
this.projects.clear()
}
}
@@ -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"
@@ -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(
@@ -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({
@@ -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(),
}
@@ -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
@@ -31,6 +31,8 @@ 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
@@ -38,6 +40,10 @@ export interface ProjectMessageDeps {
/** 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
@@ -0,0 +1,168 @@
import type { BranchListItem } from "../git-import"
import { normalizeBaseBranch } from "../base-branch"
import { initContextState } from "./init"
import type { ProjectContext } from "./context"
import type { ProjectContexts, ProjectSnapshot } from "./contexts"
export interface SettingsProject {
id: string
root: string
label: string
pinned: boolean
missing: boolean
defaultBaseBranch?: string
defaultBranch?: string
setupScriptPath?: string
}
export interface SettingsBranches {
projectId: string
branches: BranchListItem[]
defaultBranch: string
configuredBaseBranch?: string
setupScriptPath?: string
}
export interface SettingsHandler {
projects(projectId?: string): Promise<SettingsProject[]>
projectDirectory(projectId: string): string | undefined
defaultBranch(projectId: string): Promise<string | undefined>
branches(projectId: string): Promise<SettingsBranches | undefined>
setDefaultBaseBranch(projectId: string, branch?: string): Promise<void>
configureSetupScript(projectId: string): Promise<void>
}
export function createSettingsHandler(opts: {
contexts: ProjectContexts
open: (path: string) => Promise<void>
push: (ctx: ProjectContext) => void
log: (...args: unknown[]) => void
}): SettingsHandler {
return {
projects: (projectId) => loadSettingsProjects(opts.contexts, projectId, opts.log),
projectDirectory: (id) => settingsDirectory(opts.contexts, id),
defaultBranch: (id) => settingsDefaultBranch(opts.contexts, id, opts.log),
branches: (id) => settingsBranches(opts.contexts, id, opts.log, opts.push),
setDefaultBaseBranch: (id, branch) => setSettingsDefaultBaseBranch(opts.contexts, id, branch, opts.log, opts.push),
configureSetupScript: (id) => configureSettingsSetupScript(opts.contexts, id, opts.open, opts.log),
}
}
export function settingsProjects(contexts: ProjectContexts): SettingsProject[] {
return contexts.snapshots().map((project) => settingsProject(contexts, project))
}
async function loadSettingsProjects(
contexts: ProjectContexts,
projectId: string | undefined,
log: (...args: unknown[]) => void,
): Promise<SettingsProject[]> {
const snapshots = contexts.snapshots()
const selected = snapshots.some((project) => project.id === projectId) ? projectId : snapshots.at(0)?.id
const ctx = selected ? contexts.resolve(selected) : undefined
if (ctx && !ctx.missing()) {
const init = await initContextState(ctx, log)
if (init.ok && init.current) ctx.setupService()
}
return settingsProjects(contexts)
}
function settingsProject(contexts: ProjectContexts, project: ProjectSnapshot): SettingsProject {
const ctx = contexts.get(project.id)
return {
id: project.id,
root: project.root,
label: project.label,
pinned: project.pinned,
missing: project.missing,
defaultBaseBranch: ctx?.peekState()?.getDefaultBaseBranch(),
setupScriptPath: ctx?.peekSetup()?.resolveScript()?.path,
}
}
export function settingsDirectory(contexts: ProjectContexts, projectId: string): string | undefined {
return contexts.resolve(projectId)?.root
}
export async function settingsDefaultBranch(
contexts: ProjectContexts,
projectId: string,
log: (...args: unknown[]) => void,
): Promise<string | undefined> {
const ctx = contexts.resolve(projectId)
if (!ctx || ctx.missing()) return undefined
return ctx
.worktreeManager()
.defaultBranch()
.catch((err) => {
log(`Failed to load settings default branch for ${projectId}:`, err)
return undefined
})
}
export async function settingsBranches(
contexts: ProjectContexts,
projectId: string,
log: (...args: unknown[]) => void,
push: (ctx: ProjectContext) => void,
): Promise<SettingsBranches | undefined> {
const ctx = contexts.resolve(projectId)
if (!ctx || ctx.missing()) return undefined
const init = await initContextState(ctx, log)
if (!init.ok || !init.current) return undefined
const result = await ctx
.worktreeManager()
.listBranches()
.catch((err) => {
log(`Failed to load settings branches for ${projectId}:`, err)
return undefined
})
if (!result) return undefined
const configured = ctx.peekState()?.getDefaultBaseBranch()
const valid = configured && result.branches.some((branch) => branch.name === configured)
if (configured && !valid) {
ctx.peekState()?.setDefaultBaseBranch(undefined)
push(ctx)
}
return {
projectId,
branches: result.branches,
defaultBranch: result.defaultBranch,
configuredBaseBranch: valid ? configured : undefined,
setupScriptPath: ctx.setupService().resolveScript()?.path,
}
}
export async function setSettingsDefaultBaseBranch(
contexts: ProjectContexts,
projectId: string,
branch: string | undefined,
log: (...args: unknown[]) => void,
push: (ctx: ProjectContext) => void,
): Promise<void> {
const ctx = contexts.resolve(projectId)
if (!ctx || ctx.missing()) return
const init = await initContextState(ctx, log)
if (!init.ok || !init.current) return
ctx.peekState()?.setDefaultBaseBranch(normalizeBaseBranch(branch))
push(ctx)
}
export async function configureSettingsSetupScript(
contexts: ProjectContexts,
projectId: string,
open: (path: string) => Promise<void>,
log: (...args: unknown[]) => void,
): Promise<void> {
const ctx = contexts.resolve(projectId)
if (!ctx || ctx.missing()) return
const service = ctx.setupService()
try {
if (!service.hasScript()) await service.createDefaultScript()
const script = service.resolveScript()
if (!script) return
await open(script.path)
} catch (err) {
log(`Failed to open setup script for ${projectId}:`, err)
}
}
@@ -12,10 +12,13 @@ import { ProjectRegistry } from "./registry"
import type { ProjectContext, ProjectInitResult } from "./context"
import { ProjectContexts, type ProjectSnapshot } from "./contexts"
import type { ProjectMessageDeps } from "./messages"
import { createSettingsHandler, type SettingsHandler } from "./settings"
export interface ProjectWiring {
registry: ProjectRegistry
contexts: ProjectContexts
/** Project-scoped settings handler shared with the Kilo Settings editor. */
settings: SettingsHandler
messages: ProjectMessageDeps
/** Payload for the agentManager.projects webview message. */
snapshots(): { type: "agentManager.projects"; multiProject: boolean; projects: ProjectSnapshot[] }
@@ -35,10 +38,10 @@ export function createProjectWiring(opts: {
ready: (ctx: ProjectContext) => Promise<ProjectInitResult>
/** Push the project catalog to the webview. */
push: () => void
/** Push one project's state (or every context when omitted) to the webview. */
pushState: (ctx?: ProjectContext) => void
/** Re-derive the pinned project after workspace folder changes. */
changed: () => void
/** Re-push worktree state (e.g. after the flag toggles). */
refresh: () => void
/** Acknowledge an atomically validated sidebar selection. */
selected: (target: import("./route").SidebarTarget) => void
}): ProjectWiring {
@@ -64,8 +67,15 @@ export function createProjectWiring(opts: {
push: opts.push,
selected: opts.selected,
error: (message) => opts.host.showError(message),
openSettings: (tab, projectId) => opts.host.openSettings(tab, projectId),
log: opts.log,
}
const settings = createSettingsHandler({
contexts,
open: (path) => opts.host.openDocument(path),
push: opts.pushState,
log: opts.log,
})
const listeners: Disposable[] = [
opts.host.onDidChangeWorkspaceFolders(() => opts.changed()),
opts.host.onDidChangeMultiProject((enabled) => {
@@ -74,12 +84,13 @@ export function createProjectWiring(opts: {
if (pinned) opts.activate(pinned)
}
opts.push()
opts.refresh()
opts.pushState()
}),
]
return {
registry,
contexts,
settings,
messages,
snapshots: () => ({
type: "agentManager.projects",
@@ -673,6 +673,12 @@ interface RenameWorktreeIn {
label: string
}
interface OpenSettingsPanelIn {
type: "openSettingsPanel"
tab?: string
projectId?: string
}
interface RequestStateIn {
type: "agentManager.requestState"
}
@@ -1093,6 +1099,7 @@ export type AgentManagerInMessage =
| RequestRepoInfoIn
| CreateMultiVersionIn
| RenameWorktreeIn
| OpenSettingsPanelIn
| RequestStateIn
| RequestBranchesIn
| SetTabOrderIn
@@ -320,6 +320,10 @@ export class VscodeHost implements Host {
void vscode.env.openExternal(vscode.Uri.parse(url))
}
openSettings(tab?: string, projectId?: string): void {
void vscode.commands.executeCommand("kilo-code.new.settingsButtonClicked", tab, projectId)
}
refreshGit(): void {
void vscode.commands.executeCommand("git.refresh")
}
+5 -3
View File
@@ -295,7 +295,9 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(diffVirtualProvider)
// Create standalone editor providers (open in editor area, not sidebar)
const settingsEditorProvider = new SettingsEditorProvider(context.extensionUri, connectionService, context)
const settingsEditorProvider = new SettingsEditorProvider(context.extensionUri, connectionService, context, {
...agentManagerProvider.settings,
})
settingsEditorProvider.setRemoteService(remoteService)
const marketplacePanelProvider = new MarketplacePanelProvider(context.extensionUri, connectionService, context)
context.subscriptions.push(settingsEditorProvider, marketplacePanelProvider)
@@ -429,8 +431,8 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.profileButtonClicked", () => {
settingsEditorProvider.openPanel("profile")
}),
vscode.commands.registerCommand("kilo-code.new.settingsButtonClicked", (tab?: string) => {
settingsEditorProvider.openPanel("settings", tab)
vscode.commands.registerCommand("kilo-code.new.settingsButtonClicked", (tab?: string, projectId?: string) => {
settingsEditorProvider.openPanel("settings", tab, projectId)
}),
vscode.commands.registerCommand("kilo-code.new.openIndexingSettings", () => {
settingsEditorProvider.openPanel("settings", "indexing")
@@ -1,4 +1,7 @@
import type { ProjectRouteService } from "../agent-manager/project/route"
import type { SettingsHandler } from "../agent-manager/project/settings"
export type AgentManagerSettingsHandler = SettingsHandler
export type KiloProviderOptions = {
/** Context key updated from focus events reported by this provider's webview. */
@@ -47,4 +50,6 @@ export type KiloProviderOptions = {
hideTopBar?: boolean
/** Reports "Open in Tab" as the top bar's telemetry surface instead of the sidebar default. */
topBarSurface?: "tab"
/** Project-aware settings used by the standalone Agent Manager settings tab. */
agentManagerSettings?: AgentManagerSettingsHandler
}
+2 -1
View File
@@ -65,6 +65,7 @@ export function buildWebviewHtml(
/** Sidebar top bar visibility and telemetry surface for the shared webview bundle (App.tsx). Unused by the Agent Manager bundle. */
topBar?: boolean
topBarSurface?: string
agentManagerSettings?: boolean
},
): string {
const nonce = getNonce()
@@ -107,7 +108,7 @@ export function buildWebviewHtml(
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}">window.ICONS_BASE_URI = "${opts.iconsBaseUri}"; window.KILO_SHIKI_WORKER_URI = "${opts.workerUri}"; window.KILO_MARKDOWN_SHIKI_WORKER_URI = "${markdownWorkerUri}"; window.KILO_TOP_BAR = ${opts.topBar !== false}; window.KILO_TOP_BAR_SURFACE = "${opts.topBarSurface ?? "sidebar_title"}";</script>
<script nonce="${nonce}">window.ICONS_BASE_URI = "${opts.iconsBaseUri}"; window.KILO_SHIKI_WORKER_URI = "${opts.workerUri}"; window.KILO_MARKDOWN_SHIKI_WORKER_URI = "${markdownWorkerUri}"; window.KILO_TOP_BAR = ${opts.topBar !== false}; window.KILO_TOP_BAR_SURFACE = "${opts.topBarSurface ?? "sidebar_title"}"; window.KILO_AGENT_MANAGER_SETTINGS = ${opts.agentManagerSettings === true};</script>
<script nonce="${nonce}" src="${opts.scriptUri}"></script>
</body>
</html>`
@@ -59,7 +59,6 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/ClosableTab.tsx"),
path.join(ROOT, "webview-ui/agent-manager/InspectorTabStrip.tsx"),
path.join(ROOT, "webview-ui/agent-manager/ProjectBranchDialog.tsx"),
path.join(ROOT, "webview-ui/agent-manager/DefaultBaseBranchDialog.tsx"),
path.join(ROOT, "webview-ui/agent-manager/tab-rendering.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/TerminalTab.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/SideTerminalPanel.tsx"),
@@ -904,35 +903,6 @@ describe("KiloProvider — pending session refresh on reconnect", () => {
})
// ---------------------------------------------------------------------------
// handleChangeDefaultBaseBranch — listener leak fix
// ---------------------------------------------------------------------------
describe("Agent Manager — dialog listener cleanup", () => {
const tsx = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/DefaultBaseBranchDialog.tsx"), "utf-8")
/**
* Regression: handleChangeDefaultBaseBranch subscribes to vscode.onMessage
* for branch data. Previously unsub() was only called inside selectBranch()
* and the Escape keydown handler. If the dialog closed via backdrop click or
* external dialog.close(), the listener leaked and stacked on every reopen.
*
* The fix ties unsub() to the dialog component's Solid cleanup so it always
* disposes regardless of how the dialog closes.
*/
it("DefaultBaseBranchDialog disposes its message listener on cleanup", () => {
expect(tsx).toContain("const unsub = vscode.onMessage")
expect(tsx).toContain("onCleanup(unsub)")
})
it("select does not manually call unsub (handled by onCleanup)", () => {
const selStart = tsx.indexOf("const select =")
expect(selStart, "select must exist").toBeGreaterThan(-1)
const selEnd = tsx.indexOf("}", selStart + 40)
const selBody = tsx.slice(selStart, selEnd + 1)
expect(selBody, "select should not call unsub() directly").not.toContain("unsub()")
})
})
describe("SetupScriptRunner — task execution model", () => {
const runner = fs.readFileSync(SETUP_SCRIPT_RUNNER_FILE, "utf-8")
const taskAdapter = fs.readFileSync(path.join(ROOT, "src/agent-manager/task-runner.ts"), "utf-8")
@@ -0,0 +1,50 @@
import { describe, expect, it } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"
const ROOT = join(import.meta.dir, "..", "..")
describe("Agent Manager settings navigation", () => {
const actions = readFileSync(join(ROOT, "webview-ui", "agent-manager", "ProjectActions.tsx"), "utf8")
const project = readFileSync(join(ROOT, "webview-ui", "agent-manager", "ProjectSidebarBody.tsx"), "utf8")
const settings = readFileSync(join(ROOT, "webview-ui", "src", "components", "settings", "Settings.tsx"), "utf8")
const branchDialog = readFileSync(join(ROOT, "webview-ui", "agent-manager", "ProjectBranchDialog.tsx"), "utf8")
it("opens the project settings tab with the owning project id", () => {
expect(actions).toContain("onClick={props.onSettings}")
expect(project).toContain('tab: "agentManager"')
expect(project).toContain("projectId: props.project.id")
})
it("renders a project selector and project-scoped settings controls", () => {
expect(settings).toContain('type: "requestAgentManagerSettings"')
expect(settings).toContain('type: "requestAgentManagerSettingsBranches"')
expect(settings).toContain('type: "setAgentManagerDefaultBaseBranch"')
expect(settings).toContain('type: "configureAgentManagerSetupScript"')
})
it("keeps the repository default branch selectable without an empty value", () => {
expect(settings).toContain("<ProjectBranchDialog")
expect(branchDialog).toContain('label: language.t("agentManager.worktree.defaultBaseBranchAuto")')
expect(branchDialog).toContain("onSelect: () => select()")
})
it("places the Agent Manager settings tab under Auto-Approve", () => {
const autoApprove = settings.indexOf('value="autoApprove"')
const agentManager = settings.indexOf('value="agentManager"')
const browser = settings.indexOf('value="browser"')
expect(autoApprove).toBeGreaterThan(-1)
expect(autoApprove).toBeLessThan(agentManager)
expect(agentManager).toBeLessThan(browser)
})
it("loads branches only when the default branch picker opens", () => {
expect(settings).toContain("loadBranches(id)")
expect(settings).toContain("dialog.show(() =>")
})
it("opens the existing branch picker dialog", () => {
expect(settings).toContain("dialog.show(() =>")
expect(settings).toContain("<ProjectBranchDialog")
})
})
@@ -78,7 +78,6 @@ import { ProviderShell } from "../src/context/provider-shell"
import { ChatView } from "../src/components/chat"
import HistoryView from "../src/components/history/HistoryView"
import { NewWorktreeDialog } from "./NewWorktreeDialog"
import { DefaultBaseBranchDialog } from "./DefaultBaseBranchDialog"
import { createModeRouter } from "./mode-router"
import { ProjectList } from "./ProjectList"
import { SidebarBody } from "./SidebarBody"
@@ -258,9 +257,6 @@ const AgentManagerContent: Component = () => {
const [sessionsLoaded, setSessionsLoaded] = createSignal(false)
const [isGitRepo, setIsGitRepo] = createSignal(true)
const [repoDetectedBranch, setRepoDetectedBranch] = createSignal<string | undefined>()
const defaultBaseBranch = () => registry.active().defaultBaseBranch()
const setDefaultBaseBranch = (v: Parameters<Setter<string | undefined>>[0]) =>
registry.active().setDefaultBaseBranch(v)
const [projectList, setProjectList] = createSignal<AgentProjectSnapshot[]>([])
const [multiProject, setMultiProject] = createSignal(false)
const [currentProjectId, setCurrentProjectId] = createSignal<string | undefined>()
@@ -274,7 +270,9 @@ const AgentManagerContent: Component = () => {
)
const isActivePayload = (pid: string | undefined) =>
projectList().length === 0 || pid === undefined || pid === activeProjectId()
const repoDefaultBranch = () => defaultBaseBranch() ?? repoDetectedBranch() ?? "main"
const repoDefaultBranch = () => repoDetectedBranch() ?? "main"
const DEFAULT_SIDEBAR_WIDTH = 260
const MIN_SIDEBAR_WIDTH = 200
const MAX_SIDEBAR_WIDTH_RATIO = 0.4
@@ -1757,23 +1755,6 @@ const AgentManagerContent: Component = () => {
const revertCtl = createRevertFile(diffScopeId, diffCtx, () => review.scope(), vscode, showToast, t, activeProjectId)
const handleConfigureSetupScript = () => {
vscode.postMessage({ type: "agentManager.configureSetupScript" })
}
const setupScript = metrics.click("configure_setup_script", "worktree_settings", handleConfigureSetupScript)
const handleChangeDefaultBaseBranch = () => {
dialog.show(() => (
<DefaultBaseBranchDialog
selected={defaultBaseBranch()}
detected={repoDetectedBranch()}
onSelect={setDefaultBaseBranch}
onDetected={setRepoDetectedBranch}
onClose={() => dialog.close()}
/>
))
}
const handleShowKeyboardShortcuts = () => {
const categories = buildShortcutCategories(kb(), t)
dialog.show(() => (
@@ -2364,8 +2345,7 @@ const AgentManagerContent: Component = () => {
onNewWorktree={showNewWorktreeDialog}
onNewSection={newSection}
onShortcuts={metrics.click("keyboard_shortcuts", "worktrees_header", handleShowKeyboardShortcuts)}
onSetup={setupScript}
onBranch={handleChangeDefaultBaseBranch}
projectId={activeProjectId()}
sections={sections}
sortedWorktrees={sortedWorktrees}
worktrees={worktrees}
@@ -1,110 +0,0 @@
/** @jsxImportSource solid-js */
import { createMemo, createSignal, onCleanup, type Component } from "solid-js"
import { Dialog } from "@kilocode/kilo-ui/dialog"
import { BranchSelect } from "../src/components/shared/BranchSelect"
import { useLanguage } from "../src/context/language"
import { useVSCode } from "../src/context/vscode"
import type { AgentManagerBranchesMessage, BranchInfo } from "../src/types/messages"
interface Props {
selected?: string
detected?: string
onSelect: (branch?: string) => void
onDetected: (branch: string) => void
onClose: () => void
}
export const DefaultBaseBranchDialog: Component<Props> = (props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const [search, setSearch] = createSignal("")
const [branches, setBranches] = createSignal<BranchInfo[]>([])
const [loading, setLoading] = createSignal(true)
const [highlighted, setHighlighted] = createSignal(-1)
const filtered = createMemo(() => {
const value = search().toLowerCase()
return value ? branches().filter((branch) => branch.name.toLowerCase().includes(value)) : branches()
})
const select = (branch?: string) => {
vscode.postMessage({ type: "agentManager.setDefaultBaseBranch", branch })
props.onSelect(branch)
props.onClose()
}
const unsub = vscode.onMessage((message) => {
if (message.type !== "agentManager.branches") return
const event = message as AgentManagerBranchesMessage
setBranches(event.branches)
if (event.defaultBranch) props.onDetected(event.defaultBranch)
setLoading(false)
})
onCleanup(unsub)
vscode.postMessage({ type: "agentManager.requestBranches" })
const keydown = (event: KeyboardEvent) => {
const items = filtered()
const total = items.length + 1
if (event.key === "ArrowDown") {
event.preventDefault()
event.stopPropagation()
setHighlighted((value) => Math.min(value + 1, total - 2))
return
}
if (event.key === "ArrowUp") {
event.preventDefault()
event.stopPropagation()
setHighlighted((value) => Math.max(value - 1, -1))
return
}
if (event.key === "Enter") {
event.preventDefault()
event.stopPropagation()
const index = highlighted()
if (index === -1) {
select()
return
}
const branch = items[index]
if (branch) select(branch.name)
return
}
if (event.key !== "Escape") return
event.preventDefault()
event.stopPropagation()
props.onClose()
}
return (
<Dialog title={t("agentManager.worktree.defaultBaseBranch")} fit>
<div class="am-default-base-branch">
<BranchSelect
branches={filtered()}
loading={loading()}
search={search()}
onSearch={(value) => {
setSearch(value)
setHighlighted(-1)
}}
onSelect={(branch) => select(branch.name)}
onSearchKeyDown={keydown}
selected={props.selected}
highlighted={highlighted()}
onHighlight={setHighlighted}
searchPlaceholder={t("agentManager.dialog.searchBranches")}
emptyLabel={t("agentManager.import.noMatchingBranches")}
loadingLabel={t("agentManager.import.loadingBranches")}
defaultLabel={t("agentManager.dialog.branchBadge.default")}
remoteLabel={t("agentManager.dialog.branchBadge.remote")}
defaultName={props.selected}
autoOption={{
label: t("agentManager.worktree.defaultBaseBranchAuto"),
hint: props.detected,
active: !props.selected,
highlighted: highlighted() === -1,
onSelect: () => select(),
}}
/>
</div>
</Dialog>
)
}
@@ -16,8 +16,7 @@ interface Props {
onCreate: () => void
onNew: () => void
onSection: () => void
onSetup: () => void
onBranch: () => void
onSettings: () => void
}
export const ProjectActions: Component<Props> = (props) => (
@@ -70,27 +69,12 @@ export const ProjectActions: Component<Props> = (props) => (
</DropdownMenu.Portal>
</DropdownMenu>
</div>
<DropdownMenu gutter={4} placement="bottom-end">
<DropdownMenu.Trigger
as={IconButton}
icon="settings-gear"
size="small"
variant="ghost"
label={props.t("agentManager.worktree.settings")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content class="am-split-menu">
<DropdownMenu.Item onSelect={props.onSetup}>
<DropdownMenu.ItemLabel>{props.t("agentManager.worktree.setupScript")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={props.onBranch}>
<DropdownMenu.ItemLabel>
{props.t("agentManager.worktree.defaultBaseBranch")}: {props.branch}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<IconButton
icon="settings-gear"
size="small"
variant="ghost"
label={props.t("agentManager.worktree.settings")}
onClick={props.onSettings}
/>
</div>
)
@@ -1,67 +1,92 @@
/** @jsxImportSource solid-js */
import { createMemo, createSignal, onCleanup, onMount, type Component } from "solid-js"
import { createMemo, createSignal, type Component } from "solid-js"
import { Dialog } from "@kilocode/kilo-ui/dialog"
import "./agent-manager.css"
import "./agent-manager-review.css"
import { BranchSelect } from "../src/components/shared/BranchSelect"
import { useLanguage } from "../src/context/language"
import { useVSCode } from "../src/context/vscode"
import type { AgentManagerBranchesMessage, BranchInfo } from "../src/types/messages"
import type { BranchInfo } from "../src/types/messages"
interface Props {
projectId: string
selected?: string
detected?: string
branches: () => BranchInfo[]
loading: () => boolean
onSelect: (branch?: string) => void
onClose: () => void
}
export const ProjectBranchDialog: Component<Props> = (props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const language = useLanguage()
const [search, setSearch] = createSignal("")
const [branches, setBranches] = createSignal<BranchInfo[]>([])
const [loading, setLoading] = createSignal(true)
const [highlighted, setHighlighted] = createSignal(-1)
const filtered = createMemo(() => {
const value = search().toLowerCase()
return value ? branches().filter((branch) => branch.name.toLowerCase().includes(value)) : branches()
const items = props.branches()
return value ? items.filter((branch) => branch.name.toLowerCase().includes(value)) : items
})
const select = (branch?: string) => {
vscode.postMessage({ type: "agentManager.setDefaultBaseBranch", projectId: props.projectId, branch })
props.onSelect(branch)
props.onClose()
}
const keydown = (event: KeyboardEvent) => {
const items = filtered()
const total = items.length + 1
if (event.key === "ArrowDown") {
event.preventDefault()
event.stopPropagation()
setHighlighted((value) => Math.min(value + 1, total - 2))
return
}
if (event.key === "ArrowUp") {
event.preventDefault()
event.stopPropagation()
setHighlighted((value) => Math.max(value - 1, -1))
return
}
if (event.key === "Enter") {
event.preventDefault()
event.stopPropagation()
const index = highlighted()
if (index === -1) {
select()
return
}
const branch = items[index]
if (branch) select(branch.name)
return
}
if (event.key !== "Escape") return
event.preventDefault()
event.stopPropagation()
props.onClose()
}
const unsubscribe = vscode.onMessage((message) => {
if (message.type !== "agentManager.branches") return
const event = message as AgentManagerBranchesMessage
if (event.projectId !== props.projectId) return
setBranches(event.branches)
setLoading(false)
})
onCleanup(unsubscribe)
onMount(() => vscode.postMessage({ type: "agentManager.requestBranches", projectId: props.projectId }))
return (
<Dialog title={t("agentManager.worktree.defaultBaseBranch")} fit>
<Dialog title={language.t("agentManager.worktree.defaultBaseBranch")} fit>
<div class="am-default-base-branch">
<BranchSelect
branches={filtered()}
loading={loading()}
loading={props.loading()}
search={search()}
onSearch={(value) => {
setSearch(value)
setHighlighted(-1)
}}
onSelect={(branch) => select(branch.name)}
onSearchKeyDown={keydown}
selected={props.selected}
highlighted={highlighted()}
onHighlight={setHighlighted}
searchPlaceholder={t("agentManager.dialog.searchBranches")}
emptyLabel={t("agentManager.import.noMatchingBranches")}
loadingLabel={t("agentManager.import.loadingBranches")}
defaultLabel={t("agentManager.dialog.branchBadge.default")}
remoteLabel={t("agentManager.dialog.branchBadge.remote")}
searchPlaceholder={language.t("agentManager.dialog.searchBranches")}
emptyLabel={language.t("agentManager.import.noMatchingBranches")}
loadingLabel={language.t("agentManager.import.loadingBranches")}
defaultLabel={language.t("agentManager.dialog.branchBadge.default")}
remoteLabel={language.t("agentManager.dialog.branchBadge.remote")}
defaultName={props.selected ?? props.detected}
autoOption={{
label: t("agentManager.worktree.defaultBaseBranchAuto"),
label: language.t("agentManager.worktree.defaultBaseBranchAuto"),
hint: props.detected,
active: !props.selected,
highlighted: highlighted() === -1,
@@ -18,7 +18,6 @@ import { SidebarSearchMenu, type SidebarSearchMenuRef } from "./SidebarSearchMen
import type { SidebarSearchItem } from "./sidebar-search"
import { LOCAL } from "./navigate"
import { NewWorktreeDialog } from "./NewWorktreeDialog"
import { ProjectBranchDialog } from "./ProjectBranchDialog"
import type { ProjectStore } from "./project/store"
import type { ModeRouter } from "./mode-router"
@@ -155,15 +154,6 @@ export const ProjectList: Component<Props> = (props) => {
/>
))
}
const defaultBranch = (projectId: string, selected?: string, detected?: string) =>
dialog.show(() => (
<ProjectBranchDialog
projectId={projectId}
selected={selected}
detected={detected}
onClose={() => dialog.close()}
/>
))
return (
<ProjectsSection
projects={props.projects}
@@ -240,7 +230,6 @@ export const ProjectList: Component<Props> = (props) => {
onSelectWorktree={(projectId, worktreeId) => select({ projectId, kind: "worktree", worktreeId })}
onSelectSession={(projectId, sessionId) => select({ projectId, kind: "session", sessionId })}
onNewWorktree={newWorktree}
onDefaultBranch={defaultBranch}
shortcutMap={props.shortcutMap}
/>
)}
@@ -57,7 +57,6 @@ interface Props {
onSelectWorktree: (projectId: string, worktreeId: string) => void
onSelectSession: (projectId: string, sessionId: string) => void
onNewWorktree: (projectId: string) => void
onDefaultBranch: (projectId: string, selected?: string, detected?: string) => void
shortcutMap?: () => Map<string, number>
}
@@ -366,8 +365,9 @@ export const ProjectSidebarBody: Component<Props> = (props) => {
onCreate={() => post({ type: "agentManager.createWorktree" })}
onNew={() => props.onNewWorktree(props.project.id)}
onSection={() => createSection()}
onSetup={() => post({ type: "agentManager.configureSetupScript" })}
onBranch={() => props.onDefaultBranch(props.project.id, state()?.defaultBaseBranch, props.local?.branch)}
onSettings={() =>
vscode.postMessage({ type: "openSettingsPanel", tab: "agentManager", projectId: props.project.id })
}
/>
}
/>
@@ -63,8 +63,6 @@ export interface SidebarBodyProps {
onNewWorktree: () => void
onNewSection: () => void
onShortcuts: () => void
onSetup: () => void
onBranch: () => void
sections: () => SectionState[]
sortedWorktrees: () => WorktreeState[]
worktrees: () => WorktreeState[]
@@ -205,8 +203,9 @@ export const SidebarBody: Component<SidebarBodyProps> = (props) => {
onNew={props.onNewWorktree}
onSection={props.onNewSection}
onShortcuts={props.onShortcuts}
onSetup={props.onSetup}
onBranch={props.onBranch}
onSettings={() =>
vscode.postMessage({ type: "openSettingsPanel", tab: "agentManager", projectId: props.projectId })
}
/>
}
/>
@@ -25,8 +25,7 @@ interface WorktreeSectionActionsProps {
onNew: () => void
onSection: () => void
onShortcuts: () => void
onSetup: () => void
onBranch: () => void
onSettings: () => void
}
export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (props) => (
@@ -108,28 +107,13 @@ export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (p
onClick={props.onShortcuts}
/>
</TooltipKeybind>
<DropdownMenu gutter={4} placement="bottom-end">
<DropdownMenu.Trigger
as={IconButton}
icon="settings-gear"
size="small"
variant="ghost"
label={props.t("agentManager.worktree.settings")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content class="am-split-menu">
<DropdownMenu.Item onSelect={props.onSetup}>
<DropdownMenu.ItemLabel>{props.t("agentManager.worktree.setupScript")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={props.onBranch}>
<DropdownMenu.ItemLabel>
{props.t("agentManager.worktree.defaultBaseBranch")}: {props.branch}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<IconButton
icon="settings-gear"
size="small"
variant="ghost"
label={props.t("agentManager.worktree.settings")}
onClick={props.onSettings}
/>
</Show>
</div>
)
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "الجلسات",
"agentManager.projects": "المشاريع",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "المشروع",
"agentManager.settings.project.description": "اختر repository الذي تريد تعديل إعدادات worktree الخاصة به.",
"agentManager.settings.project.empty": "لا تتوفر أي مشاريع في Agent Manager.",
"agentManager.settings.defaultBaseBranch.description": "branch المستخدم كنقطة بداية لـ worktrees جديدة.",
"agentManager.settings.setupScript.description": "شغّل قبل أن يبدأ agent في worktree جديد.",
"agentManager.settings.setupScript.create": "إنشاء script",
"agentManager.settings.setupScript.edit": "تحرير script",
"agentManager.project.add": "إضافة مشروع",
"agentManager.project.remove": "إزالة من Agent Manager",
"agentManager.project.missing": "المستودع غير موجود",
@@ -5,6 +5,15 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESSÕES",
"agentManager.projects": "PROJETOS",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Projeto",
"agentManager.settings.project.description":
"Escolha o repository cujas configurações de worktree você deseja editar.",
"agentManager.settings.project.empty": "Nenhum projeto do Agent Manager está disponível.",
"agentManager.settings.defaultBaseBranch.description": "O branch usado como ponto de partida para novos worktrees.",
"agentManager.settings.setupScript.description": "Execute antes que um agent comece em um novo worktree.",
"agentManager.settings.setupScript.create": "Criar script",
"agentManager.settings.setupScript.edit": "Editar script",
"agentManager.project.add": "Adicionar projeto",
"agentManager.project.remove": "Remover do Agent Manager",
"agentManager.project.missing": "Repositório não encontrado",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESIJE",
"agentManager.projects": "PROJEKTI",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Projekat",
"agentManager.settings.project.description": "Izaberite repository čije worktree postavke želite urediti.",
"agentManager.settings.project.empty": "Nema dostupnih projekata u Agent Manager.",
"agentManager.settings.defaultBaseBranch.description": "branch koji se koristi kao početna tačka za nove worktrees.",
"agentManager.settings.setupScript.description": "Pokrenite prije nego što agent počne u novom worktree.",
"agentManager.settings.setupScript.create": "Kreiraj script",
"agentManager.settings.setupScript.edit": "Uredi script",
"agentManager.project.add": "Dodaj projekat",
"agentManager.project.remove": "Ukloni iz Agent Manager-a",
"agentManager.project.missing": "Repozitorij nije pronađen",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESSIONER",
"agentManager.projects": "PROJEKTER",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Projekt",
"agentManager.settings.project.description": "Vælg det repository, hvis worktree-indstillinger du vil redigere.",
"agentManager.settings.project.empty": "Der er ingen tilgængelige projekter i Agent Manager.",
"agentManager.settings.defaultBaseBranch.description": "Den branch, der bruges som udgangspunkt for nye worktrees.",
"agentManager.settings.setupScript.description": "Kør, før en agent starter i et nyt worktree.",
"agentManager.settings.setupScript.create": "Opret script",
"agentManager.settings.setupScript.edit": "Rediger script",
"agentManager.project.add": "Tilføj projekt",
"agentManager.project.remove": "Fjern fra Agent Manager",
"agentManager.project.missing": "Repository ikke fundet",
@@ -5,6 +5,16 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SITZUNGEN",
"agentManager.projects": "PROJEKTE",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Projekt",
"agentManager.settings.project.description":
"Wählen Sie das repository aus, dessen worktree-Einstellungen Sie bearbeiten möchten.",
"agentManager.settings.project.empty": "Keine Agent Manager-Projekte verfügbar.",
"agentManager.settings.defaultBaseBranch.description":
"Der branch, der als Ausgangspunkt für neue worktrees verwendet wird.",
"agentManager.settings.setupScript.description": "Vor dem Start eines agent in einem neuen worktree ausführen.",
"agentManager.settings.setupScript.create": "Script erstellen",
"agentManager.settings.setupScript.edit": "Script bearbeiten",
"agentManager.project.add": "Projekt hinzufügen",
"agentManager.project.remove": "Aus Agent Manager entfernen",
"agentManager.project.missing": "Repository nicht gefunden",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESSIONS",
"agentManager.projects": "PROJECTS",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Project",
"agentManager.settings.project.description": "Choose the repository whose worktree settings you want to edit.",
"agentManager.settings.project.empty": "No Agent Manager projects are available.",
"agentManager.settings.defaultBaseBranch.description": "The branch used as the starting point for new worktrees.",
"agentManager.settings.setupScript.description": "Run before an agent starts in a new worktree.",
"agentManager.settings.setupScript.create": "Create script",
"agentManager.settings.setupScript.edit": "Edit script",
"agentManager.project.add": "Add project",
"agentManager.project.remove": "Remove from Agent Manager",
"agentManager.project.missing": "Repository not found",
@@ -5,6 +5,15 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESIONES",
"agentManager.projects": "PROYECTOS",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Proyecto",
"agentManager.settings.project.description": "Elige el repository cuyos ajustes de worktree quieres editar.",
"agentManager.settings.project.empty": "No hay proyectos de Agent Manager disponibles.",
"agentManager.settings.defaultBaseBranch.description":
"El branch que se usa como punto de partida para nuevos worktrees.",
"agentManager.settings.setupScript.description": "Ejecutar antes de que un agent se inicie en un nuevo worktree.",
"agentManager.settings.setupScript.create": "Crear script",
"agentManager.settings.setupScript.edit": "Editar script",
"agentManager.project.add": "Añadir proyecto",
"agentManager.project.remove": "Eliminar de Agent Manager",
"agentManager.project.missing": "Repositorio no encontrado",
@@ -5,6 +5,15 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "جلسات",
"agentManager.projects": "پروژه‌ها",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "پروژه",
"agentManager.settings.project.description":
"repository موردنظر را انتخاب کنید تا تنظیمات worktree آن را ویرایش کنید.",
"agentManager.settings.project.empty": "هیچ پروژه‌ای در Agent Manager در دسترس نیست.",
"agentManager.settings.defaultBaseBranch.description": "branch مورد استفاده به‌عنوان نقطه شروع برای worktrees جدید.",
"agentManager.settings.setupScript.description": "قبل از شروع agent در یک worktree جدید اجرا شود.",
"agentManager.settings.setupScript.create": "ایجاد script",
"agentManager.settings.setupScript.edit": "ویرایش script",
"agentManager.project.add": "افزودن پروژه",
"agentManager.project.remove": "حذف از Agent Manager",
"agentManager.project.missing": "مخزن یافت نشد",
+10
View File
@@ -5,6 +5,16 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESSIONS",
"agentManager.projects": "PROJETS",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Projet",
"agentManager.settings.project.description":
"Choisissez le repository dont vous souhaitez modifier les paramètres du worktree.",
"agentManager.settings.project.empty": "Aucun projet Agent Manager n'est disponible.",
"agentManager.settings.defaultBaseBranch.description":
"Le branch utilisé comme point de départ pour les nouveaux worktrees.",
"agentManager.settings.setupScript.description": "Exécuter avant qu'un agent ne démarre dans un nouveau worktree.",
"agentManager.settings.setupScript.create": "Créer script",
"agentManager.settings.setupScript.edit": "Modifier script",
"agentManager.project.add": "Ajouter un projet",
"agentManager.project.remove": "Retirer d'Agent Manager",
"agentManager.project.missing": "Dépôt introuvable",
+10
View File
@@ -5,6 +5,16 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREE",
"agentManager.section.sessions": "SESSIONI",
"agentManager.projects": "PROGETTI",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Progetto",
"agentManager.settings.project.description":
"Scegli il repository di cui vuoi modificare le impostazioni del worktree.",
"agentManager.settings.project.empty": "Non sono disponibili progetti di Agent Manager.",
"agentManager.settings.defaultBaseBranch.description":
"Il branch usato come punto di partenza per i nuovi worktrees.",
"agentManager.settings.setupScript.description": "Esegui prima che un agent inizi in un nuovo worktree.",
"agentManager.settings.setupScript.create": "Crea script",
"agentManager.settings.setupScript.edit": "Modifica script",
"agentManager.project.add": "Aggiungi progetto",
"agentManager.project.remove": "Rimuovi da Agent Manager",
"agentManager.project.missing": "Repository non trovata",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "セッション",
"agentManager.projects": "プロジェクト",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "プロジェクト",
"agentManager.settings.project.description": "編集する worktree 設定の repository を選択してください。",
"agentManager.settings.project.empty": "利用可能な Agent Manager プロジェクトはありません。",
"agentManager.settings.defaultBaseBranch.description": "新しい worktrees の開始点として使用する branch。",
"agentManager.settings.setupScript.description": "新しい worktree で agent が開始する前に実行します。",
"agentManager.settings.setupScript.create": "script を作成",
"agentManager.settings.setupScript.edit": "script を編集",
"agentManager.project.add": "プロジェクトを追加",
"agentManager.project.remove": "Agent Managerから削除",
"agentManager.project.missing": "リポジトリが見つかりません",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "세션",
"agentManager.projects": "프로젝트",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "프로젝트",
"agentManager.settings.project.description": "편집하려는 worktree 설정의 repository를 선택하세요.",
"agentManager.settings.project.empty": "사용 가능한 Agent Manager 프로젝트가 없습니다.",
"agentManager.settings.defaultBaseBranch.description": "새 worktrees의 시작점으로 사용할 branch입니다.",
"agentManager.settings.setupScript.description": "새 worktree에서 agent가 시작하기 전에 실행합니다.",
"agentManager.settings.setupScript.create": "script 만들기",
"agentManager.settings.setupScript.edit": "script 편집",
"agentManager.project.add": "프로젝트 추가",
"agentManager.project.remove": "Agent Manager에서 제거",
"agentManager.project.missing": "저장소를 찾을 수 없음",
@@ -5,6 +5,15 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESSIES",
"agentManager.projects": "PROJECTEN",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Project",
"agentManager.settings.project.description": "Kies de repository waarvan je de worktree-instellingen wilt bewerken.",
"agentManager.settings.project.empty": "Er zijn geen Agent Manager-projecten beschikbaar.",
"agentManager.settings.defaultBaseBranch.description":
"De branch die als startpunt voor nieuwe worktrees wordt gebruikt.",
"agentManager.settings.setupScript.description": "Uitvoeren voordat een agent in een nieuwe worktree start.",
"agentManager.settings.setupScript.create": "script maken",
"agentManager.settings.setupScript.edit": "script bewerken",
"agentManager.project.add": "Project toevoegen",
"agentManager.project.remove": "Verwijderen uit Agent Manager",
"agentManager.project.missing": "Repository niet gevonden",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "ØKTER",
"agentManager.projects": "PROSJEKTER",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Prosjekt",
"agentManager.settings.project.description": "Velg repository hvis worktree-innstillinger du vil redigere.",
"agentManager.settings.project.empty": "Ingen Agent Manager-prosjekter er tilgjengelige.",
"agentManager.settings.defaultBaseBranch.description": "branch som brukes som utgangspunkt for nye worktrees.",
"agentManager.settings.setupScript.description": "Kjør før en agent starter i et nytt worktree.",
"agentManager.settings.setupScript.create": "Opprett script",
"agentManager.settings.setupScript.edit": "Rediger script",
"agentManager.project.add": "Legg til prosjekt",
"agentManager.project.remove": "Fjern fra Agent Manager",
"agentManager.project.missing": "Repository ikke funnet",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESJE",
"agentManager.projects": "PROJEKTY",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Projekt",
"agentManager.settings.project.description": "Wybierz repository, którego ustawienia worktree chcesz edytować.",
"agentManager.settings.project.empty": "Brak dostępnych projektów Agent Manager.",
"agentManager.settings.defaultBaseBranch.description": "branch używany jako punkt wyjścia dla nowych worktrees.",
"agentManager.settings.setupScript.description": "Uruchom przed tym, jak agent rozpocznie pracę w nowym worktree.",
"agentManager.settings.setupScript.create": "Utwórz script",
"agentManager.settings.setupScript.edit": "Edytuj script",
"agentManager.project.add": "Dodaj projekt",
"agentManager.project.remove": "Usuń z Agent Manager",
"agentManager.project.missing": "Nie znaleziono repozytorium",
@@ -5,6 +5,15 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "СЕССИИ",
"agentManager.projects": "ПРОЕКТЫ",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Проект",
"agentManager.settings.project.description": "Выберите repository, настройки worktree которого хотите изменить.",
"agentManager.settings.project.empty": "Нет доступных проектов Agent Manager.",
"agentManager.settings.defaultBaseBranch.description":
"branch, используемый как отправная точка для новых worktrees.",
"agentManager.settings.setupScript.description": "Запустите перед тем, как agent начнёт работу в новом worktree.",
"agentManager.settings.setupScript.create": "Создать script",
"agentManager.settings.setupScript.edit": "Изменить script",
"agentManager.project.add": "Добавить проект",
"agentManager.project.remove": "Удалить из Agent Manager",
"agentManager.project.missing": "Репозиторий не найден",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "เซสชัน",
"agentManager.projects": "โปรเจกต์",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "โปรเจกต์",
"agentManager.settings.project.description": "เลือก repository ที่มีการตั้งค่า worktree ที่คุณต้องการแก้ไข",
"agentManager.settings.project.empty": "ไม่มีโปรเจกต์ Agent Manager ที่พร้อมใช้งาน",
"agentManager.settings.defaultBaseBranch.description": "branch ที่ใช้เป็นจุดเริ่มต้นสำหรับ worktrees ใหม่",
"agentManager.settings.setupScript.description": "เรียกใช้ก่อนที่ agent จะเริ่มทำงานใน worktree ใหม่",
"agentManager.settings.setupScript.create": "สร้าง script",
"agentManager.settings.setupScript.edit": "แก้ไข script",
"agentManager.project.add": "เพิ่มโปรเจกต์",
"agentManager.project.remove": "ลบออกจาก Agent Manager",
"agentManager.project.missing": "ไม่พบ Repository",
@@ -5,6 +5,15 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREE'LER",
"agentManager.section.sessions": "OTURUMLAR",
"agentManager.projects": "PROJELER",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Proje",
"agentManager.settings.project.description": "Worktree ayarlarını düzenlemek istediğiniz repository'yi seçin.",
"agentManager.settings.project.empty": "Kullanılabilir Agent Manager projesi yok.",
"agentManager.settings.defaultBaseBranch.description":
"Yeni worktrees için başlangıç noktası olarak kullanılan branch.",
"agentManager.settings.setupScript.description": "Yeni bir worktree'de agent başlamadan önce çalıştırın.",
"agentManager.settings.setupScript.create": "script oluştur",
"agentManager.settings.setupScript.edit": "script düzenle",
"agentManager.project.add": "Proje ekle",
"agentManager.project.remove": "Agent Manager'dan kaldır",
"agentManager.project.missing": "Depo bulunamadı",
@@ -5,6 +5,15 @@ export const dict = {
"agentManager.section.worktrees": "РОБОЧІ ДЕРЕВА",
"agentManager.section.sessions": "СЕСІЇ",
"agentManager.projects": "ПРОЄКТИ",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "Проєкт",
"agentManager.settings.project.description": "Виберіть repository, налаштування worktree якого потрібно змінити.",
"agentManager.settings.project.empty": "Немає доступних проєктів Agent Manager.",
"agentManager.settings.defaultBaseBranch.description":
"branch, який використовується як початкова точка для нових worktrees.",
"agentManager.settings.setupScript.description": "Запускайте перед початком роботи agent у новому worktree.",
"agentManager.settings.setupScript.create": "Створити script",
"agentManager.settings.setupScript.edit": "Редагувати script",
"agentManager.project.add": "Додати проєкт",
"agentManager.project.remove": "Видалити з Agent Manager",
"agentManager.project.missing": "Репозиторій не знайдено",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "会话",
"agentManager.projects": "项目",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "项目",
"agentManager.settings.project.description": "选择要编辑其 worktree 设置的 repository。",
"agentManager.settings.project.empty": "没有可用的 Agent Manager 项目。",
"agentManager.settings.defaultBaseBranch.description": "用于新 worktrees 起始点的 branch。",
"agentManager.settings.setupScript.description": "在 agent 于新 worktree 中启动前运行。",
"agentManager.settings.setupScript.create": "创建 script",
"agentManager.settings.setupScript.edit": "编辑 script",
"agentManager.project.add": "添加项目",
"agentManager.project.remove": "从 Agent Manager 移除",
"agentManager.project.missing": "未找到仓库",
@@ -5,6 +5,14 @@ export const dict = {
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "工作階段",
"agentManager.projects": "專案",
"agentManager.settings.title": "Agent Manager",
"agentManager.settings.project.title": "專案",
"agentManager.settings.project.description": "選擇要編輯其 worktree 設定的 repository。",
"agentManager.settings.project.empty": "沒有可用的 Agent Manager 專案。",
"agentManager.settings.defaultBaseBranch.description": "用作新 worktrees 起始點的 branch。",
"agentManager.settings.setupScript.description": "在 agent 於新的 worktree 中啟動前執行。",
"agentManager.settings.setupScript.create": "建立 script",
"agentManager.settings.setupScript.edit": "編輯 script",
"agentManager.project.add": "新增專案",
"agentManager.project.remove": "從 Agent Manager 移除",
"agentManager.project.missing": "找不到儲存庫",
+9 -1
View File
@@ -220,6 +220,7 @@ export const DataBridge: Component<{ children: any }> = (props) => {
const AppContent: Component = () => {
const [currentView, setCurrentView] = createSignal<ViewType>("newTask")
const [settingsTab, setSettingsTab] = createSignal<string | undefined>()
const [agentManagerProjectId, setAgentManagerProjectId] = createSignal<string | undefined>()
// legacy-migration: state-driven flag independent of currentView to avoid
// race conditions with SettingsEditorProvider's navigate messages.
const [migrationNeeded, setMigrationNeeded] = createSignal(false)
@@ -294,6 +295,7 @@ const AppContent: Component = () => {
if (message?.type === "navigate" && message.view && VALID_VIEWS.has(message.view)) {
console.log("[Kilo New] App: 🧭 navigate:", message.view, message.tab ? `tab=${message.tab}` : "")
if (message.tab) setSettingsTab(message.tab)
setAgentManagerProjectId(message.projectId)
setCurrentView(message.view as ViewType)
vscode.postMessage({ type: "settingsTabChanged", tab: message.tab })
}
@@ -341,7 +343,11 @@ const AppContent: Component = () => {
// VS Code's native title bar toolbar already covers those. Defaults to
// true only when unset entirely (e.g. Storybook, which doesn't render the
// real page HTML).
const host = window as { KILO_TOP_BAR?: boolean; KILO_TOP_BAR_SURFACE?: string }
const host = window as {
KILO_TOP_BAR?: boolean
KILO_TOP_BAR_SURFACE?: string
KILO_AGENT_MANAGER_SETTINGS?: boolean
}
const showTopBar = host.KILO_TOP_BAR !== false
const topBarSurface = host.KILO_TOP_BAR_SURFACE ?? "sidebar_title"
@@ -396,6 +402,8 @@ const AppContent: Component = () => {
<Match when={currentView() === "settings"}>
<Settings
tab={settingsTab()}
agentManagerProjectId={agentManagerProjectId()}
agentManagerSettings={host.KILO_AGENT_MANAGER_SETTINGS === true}
onTabChange={setSettingsTab}
onMigrationClick={(source) => {
setMigrationSource(source)
@@ -1,7 +1,8 @@
import { Component, createSignal, createEffect, createMemo, on, Show } from "solid-js"
import { Component, createSignal, createEffect, createMemo, on, Show, onCleanup } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import { Tabs } from "@kilocode/kilo-ui/tabs"
import { Button } from "@kilocode/kilo-ui/button"
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { showToast } from "@kilocode/kilo-ui/toast"
import { useVSCode } from "../../context/vscode"
@@ -29,13 +30,192 @@ import * as Sandboxing from "./sandboxing"
import { useServer } from "../../context/server"
import type { MigrationSource } from "../../types/messages"
import { configMessage } from "../../utils/open-config"
import type {
AgentManagerSettingsBranchesLoadedMessage,
AgentManagerSettingsLoadedMessage,
AgentManagerSettingsProject,
ExtensionMessage,
} from "../../types/messages"
import { Select } from "@kilocode/kilo-ui/select"
import { Card } from "@kilocode/kilo-ui/card"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import SettingsRow from "./SettingsRow"
import { ProjectBranchDialog } from "../../../agent-manager/ProjectBranchDialog"
export interface SettingsProps {
tab?: string
agentManagerProjectId?: string
agentManagerSettings?: boolean
onTabChange?: (tab: string) => void
onMigrationClick?: (source: MigrationSource) => void // legacy-migration
}
const AgentManagerTab: Component<{ projectId?: string }> = (props) => {
const language = useLanguage()
const vscode = useVSCode()
const dialog = useDialog()
const [projects, setProjects] = createSignal<AgentManagerSettingsProject[]>([])
const remembered = vscode.getState<{ agentManagerProjectId?: string }>()?.agentManagerProjectId
const [selected, setSelected] = createSignal<string | undefined>(props.projectId ?? remembered)
const [branches, setBranches] = createSignal<AgentManagerSettingsBranchesLoadedMessage>()
const [branchLoading, setBranchLoading] = createSignal(false)
const [loading, setLoading] = createSignal(true)
let count = 0
let latestSettings = ""
let latestBranches = ""
const issue = () => {
return `${Date.now() * 1000 + ++count}`
}
const scriptPath = () =>
projects().find((item) => item.id === selected())?.setupScriptPath ?? branches()?.setupScriptPath
const load = (projectId?: string) => {
if (!projectId) return
setSelected(projectId)
setBranches(undefined)
setBranchLoading(false)
vscode.setState({ ...vscode.getState<Record<string, unknown>>(), agentManagerProjectId: projectId })
latestSettings = issue()
latestBranches = latestSettings
vscode.postMessage({ type: "requestAgentManagerSettings", projectId, requestId: latestSettings })
}
const loadBranches = (projectId?: string) => {
if (!projectId || (branches()?.projectId === projectId && !branches()?.error) || branchLoading()) return
setBranchLoading(true)
latestBranches = issue()
vscode.postMessage({ type: "requestAgentManagerSettingsBranches", projectId, requestId: latestBranches })
}
createEffect(() => {
if (props.projectId && props.projectId !== selected()) load(props.projectId)
})
const onMessage = (message: ExtensionMessage) => {
if (message.type === "agentManagerSettingsLoaded") {
const event = message as AgentManagerSettingsLoadedMessage
if (event.requestId !== latestSettings) return
setProjects(event.projects)
const current = selected()
const next = event.projects.some((project) => project.id === current) ? current : event.projectId
setSelected(next)
setLoading(false)
return
}
if (message.type === "agentManagerSettingsBranchesLoaded") {
const event = message as AgentManagerSettingsBranchesLoadedMessage
if (event.requestId !== latestBranches) return
if (event.projectId === selected()) {
setBranches(event.error ? undefined : event)
setBranchLoading(false)
}
}
}
const initial = props.projectId ?? remembered
const timeout = setTimeout(() => setLoading(false), 3000)
const unsubscribe = vscode.onMessage(onMessage)
onCleanup(() => {
unsubscribe()
clearTimeout(timeout)
})
latestSettings = issue()
vscode.postMessage({ type: "requestAgentManagerSettings", projectId: initial, requestId: latestSettings })
const project = () => projects().find((item) => item.id === selected())
const projectText = (item: AgentManagerSettingsProject) => `${item.label} - ${item.root}`
const branchLabel = () => {
const data = branches()
const configured = data ? data.configuredBaseBranch : project()?.defaultBaseBranch
if (configured) return configured
const detected = data?.defaultBranch ?? project()?.defaultBranch
return `${language.t("agentManager.worktree.defaultBaseBranchAuto")}${detected ? ` (${detected})` : ""}`
}
const setBranch = (branch?: string) => {
const id = selected()
if (!id) return
setBranches((current) => (current ? { ...current, configuredBaseBranch: branch } : current))
latestSettings = issue()
latestBranches = latestSettings
vscode.postMessage({ type: "setAgentManagerDefaultBaseBranch", projectId: id, branch, requestId: latestSettings })
}
const openBranches = () => {
const id = selected()
if (!id || project()?.missing) return
loadBranches(id)
dialog.show(() => (
<ProjectBranchDialog
selected={branches()?.configuredBaseBranch ?? project()?.defaultBaseBranch}
detected={branches()?.defaultBranch ?? project()?.defaultBranch}
branches={() => branches()?.branches ?? []}
loading={branchLoading}
onClose={() => dialog.close()}
onSelect={setBranch}
/>
))
}
return (
<Show when={!loading()} fallback={<Spinner />}>
<div class="settings-agent-manager">
<Card>
<SettingsRow
title={language.t("agentManager.settings.project.title")}
description={language.t("agentManager.settings.project.description")}
>
<Select
options={projects()}
current={project()}
value={(item) => item.id}
label={projectText}
onSelect={(item) => load(item?.id)}
variant="secondary"
size="small"
triggerVariant="settings"
triggerProps={{ title: project()?.root }}
children={(item) => (item ? <span title={projectText(item)}>{projectText(item)}</span> : "")}
/>
</SettingsRow>
<Show when={project()} fallback={<p>{language.t("agentManager.settings.project.empty")}</p>}>
<SettingsRow
title={language.t("agentManager.worktree.defaultBaseBranch")}
description={language.t("agentManager.settings.defaultBaseBranch.description")}
>
<Button
variant="secondary"
size="small"
class="settings-branch-trigger"
title={branchLabel()}
disabled={project()?.missing || branchLoading()}
onClick={openBranches}
>
<span class="settings-branch-trigger-label">{branchLabel()}</span>
<Icon name="selector" size="small" />
</Button>
</SettingsRow>
<SettingsRow
title={language.t("agentManager.worktree.setupScript")}
description={language.t("agentManager.settings.setupScript.description")}
last
>
<Button
variant="secondary"
size="small"
icon="edit"
disabled={project()?.missing}
onClick={() =>
vscode.postMessage({
type: "configureAgentManagerSetupScript",
projectId: selected()!,
requestId: (latestSettings = issue()),
})
}
>
{scriptPath()
? language.t("agentManager.settings.setupScript.edit")
: language.t("agentManager.settings.setupScript.create")}
</Button>
</SettingsRow>
</Show>
</Card>
</div>
</Show>
)
}
const Settings: Component<SettingsProps> = (props) => {
const server = useServer()
const language = useLanguage()
@@ -151,6 +331,12 @@ const Settings: Component<SettingsProps> = (props) => {
<Icon name="checklist" />
<span class="label">{language.t("settings.autoApprove.title")}</span>
</Tabs.Trigger>
<Show when={props.agentManagerSettings}>
<Tabs.Trigger value="agentManager" aria-label={language.t("agentManager.settings.title")}>
<Icon name="settings-gear" />
<span class="label">{language.t("agentManager.settings.title")}</span>
</Tabs.Trigger>
</Show>
<Tabs.Trigger value="browser" aria-label={language.t("settings.webTools.title")}>
<Icon name="window-cursor" />
<span class="label">{language.t("settings.webTools.title")}</span>
@@ -222,6 +408,12 @@ const Settings: Component<SettingsProps> = (props) => {
<h3>{language.t("settings.autoApprove.title")}</h3>
<AutoApproveTab />
</Tabs.Content>
<Show when={props.agentManagerSettings}>
<Tabs.Content value="agentManager">
<h3>{language.t("agentManager.settings.title")}</h3>
<AgentManagerTab projectId={props.agentManagerProjectId} />
</Tabs.Content>
</Show>
<Tabs.Content value="browser">
<h3>{language.t("settings.webTools.title")}</h3>
<BrowserTab />
@@ -115,6 +115,16 @@
}
}
.settings-branch-trigger {
max-width: 160px;
}
.settings-branch-trigger-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Wider inputs inside cards with data-variant="wide-input" (e.g. edit mode config) */
[data-variant="wide-input"] [data-slot="settings-row-input"] {
flex: 1 1 0%;
@@ -411,6 +411,36 @@ export interface NavigateMessage {
type: "navigate"
view: "newTask" | "marketplace" | "history" | "profile" | "settings" | "subAgentViewer"
tab?: string
projectId?: string
}
export interface AgentManagerSettingsProject {
id: string
root: string
label: string
pinned: boolean
missing: boolean
defaultBaseBranch?: string
defaultBranch?: string
setupScriptPath?: string
}
export interface AgentManagerSettingsLoadedMessage {
type: "agentManagerSettingsLoaded"
projects: AgentManagerSettingsProject[]
projectId?: string
requestId: string
}
export interface AgentManagerSettingsBranchesLoadedMessage {
type: "agentManagerSettingsBranchesLoaded"
projectId: string
branches: BranchInfo[]
defaultBranch: string
requestId: string
error?: boolean
configuredBaseBranch?: string
setupScriptPath?: string
}
export interface IndexingStatusLoadedMessage {
@@ -1420,6 +1450,8 @@ export type ExtensionMessage =
| DeviceAuthFailedMessage
| DeviceAuthCancelledMessage
| NavigateMessage
| AgentManagerSettingsLoadedMessage
| AgentManagerSettingsBranchesLoadedMessage
| IndexingStatusLoadedMessage
| IndexingSettingsLoadedMessage
| ChatSettingsLoadedMessage
@@ -234,6 +234,32 @@ export interface CompactRequest {
export interface OpenSettingsPanelRequest {
type: "openSettingsPanel"
tab?: string
projectId?: string
}
export interface RequestAgentManagerSettingsMessage {
type: "requestAgentManagerSettings"
projectId?: string
requestId: string
}
export interface RequestAgentManagerSettingsBranchesMessage {
type: "requestAgentManagerSettingsBranches"
projectId: string
requestId: string
}
export interface SetAgentManagerDefaultBaseBranchMessage {
type: "setAgentManagerDefaultBaseBranch"
projectId: string
branch?: string
requestId: string
}
export interface ConfigureAgentManagerSetupScriptMessage {
type: "configureAgentManagerSetupScript"
projectId: string
requestId: string
}
export interface OpenProfilePanelRequest {
@@ -1488,6 +1514,10 @@ export type WebviewMessage =
| RefreshProviderUsageMessage
| OpenExternalRequest
| OpenSettingsPanelRequest
| RequestAgentManagerSettingsMessage
| RequestAgentManagerSettingsBranchesMessage
| SetAgentManagerDefaultBaseBranchMessage
| ConfigureAgentManagerSetupScriptMessage
| OpenProfilePanelRequest
| OpenVSCodeSettingsRequest
| OpenConfigFileRequest
+86
View File
@@ -244,11 +244,15 @@ import type {
McpAuthRemoveResponses,
McpAuthStartErrors,
McpAuthStartResponses,
McpCallToolErrors,
McpCallToolResponses,
McpConnectErrors,
McpConnectResponses,
McpDisconnectErrors,
McpDisconnectResponses,
McpLocalConfig,
McpReadResourceErrors,
McpReadResourceResponses,
McpRemoteConfig,
McpStatusErrors,
McpStatusResponses,
@@ -3184,6 +3188,88 @@ export class Mcp extends HeyApiClient {
})
}
/**
* Read MCP resource
*
* Read a resource from a connected MCP server by URI. Used by MCP Apps to load UI resources.
*/
public readResource<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
uri?: string
server?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "uri" },
{ in: "body", key: "server" },
],
},
],
)
return (options?.client ?? this.client).post<McpReadResourceResponses, McpReadResourceErrors, ThrowOnError>({
url: "/experimental/resource/read",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* Call MCP tool
*
* Call a tool on a connected MCP server. Used by MCP Apps for widget-initiated tool calls.
*/
public callTool<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
server?: string
name?: string
arguments?: {
[key: string]: unknown
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "server" },
{ in: "body", key: "name" },
{ in: "body", key: "arguments" },
],
},
],
)
return (options?.client ?? this.client).post<McpCallToolResponses, McpCallToolErrors, ThrowOnError>({
url: "/experimental/mcp/call-tool",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
private _auth?: Auth2
get auth(): Auth2 {
return (this._auth ??= new Auth2({ client: this.client }))
+84
View File
@@ -12124,6 +12124,90 @@ export type McpDisconnectResponses = {
export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses]
export type McpReadResourceData = {
body?: {
uri: string
server: string
}
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/experimental/resource/read"
}
export type McpReadResourceErrors = {
/**
* BadRequest | InvalidRequestError
*/
400: EffectHttpApiErrorBadRequest | InvalidRequestError
/**
* Not found
*/
404: NotFoundError
}
export type McpReadResourceError = McpReadResourceErrors[keyof McpReadResourceErrors]
export type McpReadResourceResponses = {
/**
* Resource content
*/
200: {
uri: string
mimeType?: string
text?: string
blob?: string
}
}
export type McpReadResourceResponse = McpReadResourceResponses[keyof McpReadResourceResponses]
export type McpCallToolData = {
body?: {
server: string
name: string
arguments?: {
[key: string]: unknown
}
}
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/experimental/mcp/call-tool"
}
export type McpCallToolErrors = {
/**
* BadRequest | InvalidRequestError
*/
400: EffectHttpApiErrorBadRequest | InvalidRequestError
/**
* Not found
*/
404: NotFoundError
}
export type McpCallToolError = McpCallToolErrors[keyof McpCallToolErrors]
export type McpCallToolResponses = {
/**
* Tool call result
*/
200: {
content: Array<unknown>
isError?: boolean
structuredContent?: {
[key: string]: unknown
}
}
}
export type McpCallToolResponse = McpCallToolResponses[keyof McpCallToolResponses]
export type ProjectListData = {
body?: never
path?: never