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
6 changes: 6 additions & 0 deletions packages/kilo-vscode/esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ async function main() {
// Build KiloClaw webview (SolidJS, standalone chat panel)
const kiloClawCtx = await createBrowserWebviewContext("webview-ui/kiloclaw/index.tsx", "dist/kiloclaw.js")

// Build Marketplace webview (SolidJS, standalone catalog panel)
const marketplaceCtx = await createBrowserWebviewContext("webview-ui/marketplace/index.tsx", "dist/marketplace.js")

// Build Diff Viewer webview (SolidJS, reuses Agent Manager diff components)
const diffViewerCtx = await createBrowserWebviewContext("webview-ui/diff-viewer/index.tsx", "dist/diff-viewer.js")

Expand All @@ -229,6 +232,7 @@ async function main() {
diffViewerCtx.watch(),
diffVirtualCtx.watch(),
kiloClawCtx.watch(),
marketplaceCtx.watch(),
shikiWorkerCtx.watch(),
])
} else {
Expand All @@ -237,6 +241,7 @@ async function main() {
webviewCtx.rebuild(),
agentManagerCtx.rebuild(),
kiloClawCtx.rebuild(),
marketplaceCtx.rebuild(),
diffViewerCtx.rebuild(),
diffVirtualCtx.rebuild(),
shikiWorkerCtx.rebuild(),
Expand All @@ -248,6 +253,7 @@ async function main() {
diffViewerCtx.dispose(),
diffVirtualCtx.dispose(),
kiloClawCtx.dispose(),
marketplaceCtx.dispose(),
shikiWorkerCtx.dispose(),
])
}
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"webview-ui/diff-viewer/index.tsx",
"webview-ui/diff-virtual/index.tsx",
"webview-ui/kiloclaw/index.tsx",
"webview-ui/marketplace/index.tsx",
"webview-ui/pierre-worker.ts",
"webview-ui/src/index.tsx",
"src/**/__tests__/**/*.{ts,spec.ts}",
Expand Down
223 changes: 20 additions & 203 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,7 @@ import { GitOps } from "./agent-manager/GitOps"
import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller"
import { diffSummary as localDiffSummary } from "./agent-manager/local-diff"
import { getWorkspaceRoot } from "./review-utils"
import {
MarketplaceService,
type MarketplaceItem,
type AgentMarketplaceItem,
type RemoveResult,
} from "./services/marketplace"
import { createMarketplaceRemover, removeAgent, removeMcp } from "./kilo-provider/remove-config-item"
import type { RemoteStatusService } from "./services/RemoteStatusService"
import { resolveProjectDirectory } from "./project-directory"
import { getBusySessionCount, seedSessionStatuses } from "./session-status"
Expand Down Expand Up @@ -236,10 +231,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private viewStateDisposable: vscode.Disposable | null = null
private visibilityDisposable: vscode.Disposable | null = null
private autoApproveBridge: ReturnType<typeof createAutoApproveBridge> | null = null
private readonly marketplaceRemove = createMarketplaceRemover()

private ignoreController: FileIgnoreController | null = null
private ignoreControllerDir: string | null = null
private marketplace: MarketplaceService | null = null
private chatAutocomplete: ChatTextAreaAutocomplete | null = null
private projectDirectory: string | null | undefined
private slimEditMetadata = true
Expand Down Expand Up @@ -384,6 +379,21 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}

private get removeConfigItemCtx() {
return {
connection: this.connectionService,
project: () => this.getProjectDirectory(this.currentSession?.id),
directory: () => this.getWorkspaceDirectory(),
remove: this.marketplaceRemove,
refresh: async () => {
this.cachedAgentsMessage = null
this.cachedConfigMessage = null
await Promise.all([this.fetchAndSendAgents(), this.fetchAndSendConfig()])
},
storage: this.extensionContext?.globalStorageUri,
}
}

private async syncWebviewState(reason: string): Promise<void> {
const serverInfo = this.connectionService.getServerInfo()
console.log("[Kilo New] KiloProvider: 🔄 syncWebviewState()", {
Expand Down Expand Up @@ -1105,44 +1115,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
})
break
}
case "fetchMarketplaceData": {
await this.handleFetchMarketplaceData()
break
}
case "filterMarketplaceItems": {
// Client-side filtering — no server action needed
break
}
case "installMarketplaceItem": {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const scope = message.mpInstallOptions?.target ?? "project"
const result = await this.getMarketplace().install(message.mpItem, message.mpInstallOptions, workspace)
if (result.success) {
await this.invalidateAfterMarketplaceChange(scope)
}
this.postMessage({
type: "marketplaceInstallResult",
success: result.success,
slug: result.slug,
error: result.error,
})
break
}
case "removeInstalledMarketplaceItem": {
const scope = message.mpInstallOptions?.target ?? "project"
const result = await this.removeMarketplaceItem(message.mpItem, scope)
this.postMessage({
type: "marketplaceRemoveResult",
success: result.success,
slug: result.slug,
error: result.error,
})
break
}
case "dismissAgentMigrationBanner": {
await this.extensionContext?.globalState.update("kilo.agentMigrationBannerDismissed", true)
break
}
}
})
this.webviewMessageDisposable = watchFontSizeConfig((msg) => this.postMessage(msg), this.webviewMessageDisposable)
Expand All @@ -1156,15 +1128,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
})
}

private async handleFetchMarketplaceData(): Promise<void> {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
const skills = await this.fetchCliSkills()
const data = await mp.fetchData(workspace, skills)
const dismissed = this.extensionContext?.globalState.get<boolean>("kilo.agentMigrationBannerDismissed") ?? false
this.postMessage({ type: "marketplaceData", ...data, showAgentMigrationBanner: !dismissed })
}

/**
* Initialize connection to the CLI backend server.
* Subscribes to the shared KiloConnectionService.
Expand Down Expand Up @@ -1914,18 +1877,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}

private async fetchCliSkills(): Promise<Array<{ name: string; location: string }> | undefined> {
if (!this.client) return undefined
try {
const dir = this.getWorkspaceDirectory()
const { data } = await retry(() => this.client!.app.skills({ directory: dir }, { throwOnError: true }))
return data
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to fetch CLI skills for marketplace:", error)
return undefined
}
}

/**
* Remove a skill via the CLI backend (deletes from disk + clears cache), then refresh.
* Returns true on success, false on failure.
Expand Down Expand Up @@ -1969,77 +1920,18 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
} catch {
// fall through to kilo.json removal
}
const stub: AgentMarketplaceItem = {
id: name,
type: "agent",
name,
description: "",
content: { mode: "primary", description: "", prompt: "" },
}
if (!(await this.removeMarketplaceItemFromAllScopes(stub))) {
if (!(await removeAgent(this.removeConfigItemCtx, name))) {
console.error("[Kilo New] KiloProvider: Failed to remove agent:", name)
}
}

private async handleRemoveMcp(name: string): Promise<void> {
// Remove from legacy files first so that the subsequent invalidation
// causes the CLI to re-read config without the legacy entry.
await this.removeLegacyMcp(name)

const stub = { id: name, type: "mcp" as const, name, description: "", url: "", content: "" }
const removed = await this.removeMarketplaceItemFromAllScopes(stub)
const removed = await removeMcp(this.removeConfigItemCtx, name)
if (!removed) {
console.error("[Kilo New] KiloProvider: Failed to remove MCP server:", name)
}
}

/**
* Remove an MCP server from legacy config files (.kilo/mcp.json, .kilocode/mcp.json,
* and the VS Code global storage mcp_settings.json). These files are read by the
* CLI-side McpMigrator and merged into config at the lowest precedence level.
* Returns true if the entry was found and removed from at least one file.
*/
private async removeLegacyMcp(name: string): Promise<boolean> {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const files: vscode.Uri[] = []

// Project-level legacy files
if (workspace) {
files.push(vscode.Uri.file(path.join(workspace, ".kilo", "mcp.json")))
files.push(vscode.Uri.file(path.join(workspace, ".kilocode", "mcp.json")))
}

// Global legacy file (VS Code extension global storage)
const storage = this.extensionContext?.globalStorageUri
if (storage) {
files.push(vscode.Uri.joinPath(storage, "settings", "mcp_settings.json"))
}

let removed = false
for (const uri of files) {
const bytes = await vscode.workspace.fs.readFile(uri).then(
(b) => b,
() => null,
)
if (!bytes) continue

try {
const parsed = JSON.parse(Buffer.from(bytes).toString("utf8")) as Record<string, unknown>
const servers = parsed.mcpServers as Record<string, unknown> | undefined
if (!servers?.[name]) continue

delete servers[name]
const content = Buffer.from(JSON.stringify(parsed, null, 2), "utf8")
await vscode.workspace.fs.writeFile(uri, content)
removed = true
} catch (err) {
console.warn("[Kilo New] KiloProvider: Failed to remove legacy MCP from", uri.fsPath, err)
}
}

return removed
}

private async fetchAndSendMcpStatus(): Promise<void> {
if (!this.client) {
if (this.cachedMcpStatusMessage) {
Expand All @@ -2061,75 +1953,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}

/**
* Remove a marketplace item from a single scope and invalidate CLI caches.
*/
private async removeMarketplaceItem(item: MarketplaceItem, scope: "project" | "global"): Promise<RemoveResult> {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const result = await this.getMarketplace().remove(item, scope, workspace)
if (result.success) {
await this.invalidateAfterMarketplaceChange(scope)
}
return result
}

/**
* Remove a marketplace item from both project and global scopes.
* mp.remove returns success even when the entry doesn't exist (no-op),
* so we must attempt both scopes to cover dual-scope installations.
* Returns true if at least one scope removal succeeded.
*/
private async removeMarketplaceItemFromAllScopes(item: MarketplaceItem): Promise<boolean> {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
const project = await mp.remove(item, "project", workspace)
const global = await mp.remove(item, "global", workspace)

if (project.success || global.success) {
const scope = global.success ? "global" : "project"
await this.invalidateAfterMarketplaceChange(scope)
return true
}
return false
}

/**
* Invalidate CLI caches and refresh the webview after a marketplace install/remove.
*
* For global scope: uses global.config.update with the freshly-written config file
* contents rather than global.dispose. This goes through Config.updateGlobal() which
* calls Config.global.reset() to invalidate the lazy-cached global config, ensuring
* the newly installed/removed MCP entry is visible on the next config.get call.
* (global.dispose alone is not sufficient on older CLI versions that lack the
* Config.global.reset() call in the dispose handler.)
*
* For project scope: instance.dispose is sufficient because the per-instance
* Config.state is cleared and re-reads all files (including global) on next access.
*/
private async invalidateAfterMarketplaceChange(scope: "project" | "global"): Promise<void> {
if (!this.client) return
if (scope === "global") {
// Use global.config.update with an empty config to trigger Config.updateGlobal()
// which calls Config.global.reset(). This invalidates the lazy-cached global
// config in the CLI process so it re-reads kilo.json from disk.
// An empty object merge is a no-op for the file content but resets the cache.
// (global.dispose alone is insufficient on older CLI versions that lack
// the Config.global.reset() call in the dispose handler.)
await this.client.global.config.update({ config: {} }).catch((e: unknown) => {
console.warn("[Kilo New] global.config.update after marketplace change failed:", e)
})
}
// Always dispose the per-project instance so it rebuilds state from
// the (possibly updated) global + project config on the next request.
const dir = this.getWorkspaceDirectory()
await this.client.instance.dispose({ directory: dir }).catch((e: unknown) => {
console.warn("[Kilo New] instance.dispose() after marketplace change failed:", e)
})
this.cachedAgentsMessage = null
this.cachedConfigMessage = null
await Promise.all([this.fetchAndSendAgents(), this.fetchAndSendConfig()])
}

/**
* Fetch backend config and send to webview.
*/
Expand Down Expand Up @@ -3432,12 +3255,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper

// legacy-migration end ---------------------------------------------------------

private getMarketplace(): MarketplaceService {
if (this.marketplace) return this.marketplace
this.marketplace = new MarketplaceService()
return this.marketplace
}

// ── Worktree stats polling (sidebar diff badge) ──────────────────
private startStatsPolling(): void {
this.statsPoller?.stop()
Expand Down Expand Up @@ -3503,6 +3320,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.sessionStatusMap.clear()
this.ignoreController?.dispose()
this.chatAutocomplete?.dispose()
;(this.marketplace?.dispose(), disposeGitChangesTarget())
disposeGitChangesTarget()
}
}
Loading
Loading