Skip to content
Closed
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
30 changes: 30 additions & 0 deletions packages/kilo-vscode/src/DiffViewerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as vscode from "vscode"
import type { FileDiff } from "@kilocode/sdk/v2/client"
import type { KiloConnectionService } from "./services/cli-backend"
import { buildWebviewHtml } from "./utils"
import { WebviewReadyRetry, showWebviewReloadWarning, type WebviewReadyRetryEvent } from "./webview-ready-retry"
import { TelemetryEventName, TelemetryProxy } from "./services/telemetry"
import { GitOps } from "./agent-manager/GitOps"
import {
appendOutput,
Expand All @@ -26,18 +28,40 @@ export class DiffViewerProvider implements vscode.Disposable {
private outputChannel: vscode.OutputChannel
private onSendComments: ((comments: unknown[], autoSend: boolean) => void) | undefined

private loader: WebviewReadyRetry

constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
) {
this.gitOps = new GitOps({ log: (...args) => this.log(...args) })
this.outputChannel = vscode.window.createOutputChannel("Kilo Diff Viewer")
this.loader = new WebviewReadyRetry({
name: "DiffViewer",
active: () => Boolean(this.panel),
html: () => (this.panel ? this.getHtml(this.panel.webview) : undefined),
load: (html) => {
if (!this.panel) return
this.panel.webview.html = html
},
warn: (msg) => this.log(msg),
log: (msg) => this.log(msg),
notify: () => showWebviewReloadWarning(),
capture: (event, props) => this.captureRetry(event, props),
})
}

private log(...args: unknown[]) {
appendOutput(this.outputChannel, "DiffViewer", ...args)
}

private captureRetry(event: WebviewReadyRetryEvent, props: Record<string, unknown>): void {
TelemetryProxy.capture(
event === "retry" ? TelemetryEventName.WEBVIEW_READY_RETRY : TelemetryEventName.WEBVIEW_READY_FAILED,
props,
)
}

public setCommentHandler(handler: (comments: unknown[], autoSend: boolean) => void): void {
this.onSendComments = handler
}
Expand Down Expand Up @@ -73,8 +97,12 @@ export class DiffViewerProvider implements vscode.Disposable {
panel.webview.onDidReceiveMessage((msg) => this.onMessage(msg), undefined, [])
panel.webview.html = this.getHtml(panel.webview)

// Detect service worker failures and retry (microsoft/vscode#125993)
this.loader.start()

panel.onDidDispose(() => {
this.log("Panel disposed")
this.loader.dispose()
this.stopDiffPolling()
this.panel = undefined
})
Expand All @@ -84,6 +112,7 @@ export class DiffViewerProvider implements vscode.Disposable {
const type = msg.type as string

if (type === "webviewReady") {
this.loader.done()
this.post({
type: "ready",
vscodeLanguage: vscode.env.language,
Expand Down Expand Up @@ -211,6 +240,7 @@ export class DiffViewerProvider implements vscode.Disposable {
}

public dispose(): void {
this.loader.dispose()
this.stopDiffPolling()
this.panel?.dispose()
this.outputChannel.dispose()
Expand Down
42 changes: 39 additions & 3 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import type { EditorContext } from "./services/cli-backend/types"
import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController"
import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete"
import { buildWebviewHtml } from "./utils"
import { TelemetryProxy, type TelemetryPropertiesProvider } from "./services/telemetry"
import { WebviewReadyRetry, showWebviewReloadWarning, type WebviewReadyRetryEvent } from "./webview-ready-retry"
import { TelemetryEventName, TelemetryProxy, type TelemetryPropertiesProvider } from "./services/telemetry"
import {
sessionToWebview,
indexProvidersById,
Expand Down Expand Up @@ -170,6 +171,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private statsPoller: GitStatsPoller | null = null
private cachedStats: unknown = null

private loader: WebviewReadyRetry

/** Optional interceptor called before the standard message handler.
* Return null to consume the message, or return a (possibly transformed) message. */
private onBeforeMessage: ((msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>) | null = null
Expand All @@ -187,6 +190,19 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
) {
this.projectDirectory = options?.projectDirectory
this.slimEditMetadata = options?.slimEditMetadata ?? true
this.loader = new WebviewReadyRetry({
name: "KiloProvider",
active: () => Boolean(this.webview),
html: () => (this.webview ? this._getHtmlForWebview(this.webview) : undefined),
load: (html) => {
if (!this.webview) return
this.webview.html = html
},
warn: (msg) => console.warn(msg),
log: (msg) => console.error(msg),
notify: () => showWebviewReloadWarning(),
capture: (event, props) => this.captureRetry(event, props),
})

TelemetryProxy.getInstance().setProvider(this)
}
Expand All @@ -209,6 +225,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}

private captureRetry(event: WebviewReadyRetryEvent, props: Record<string, unknown>): void {
TelemetryProxy.capture(
event === "retry" ? TelemetryEventName.WEBVIEW_READY_RETRY : TelemetryEventName.WEBVIEW_READY_FAILED,
props,
)
}

/**
* Convenience getter that returns the shared SDK KiloClient or null if not yet connected.
* Preserves the existing null-check pattern used throughout handler methods.
Expand Down Expand Up @@ -341,6 +364,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.statsPoller?.setEnabled(webviewView.visible)
})

// Detect service worker failures and retry (microsoft/vscode#125993)
this.loader.start()

// Initialize connection to CLI backend
this.initializeConnection()
}
Expand All @@ -363,6 +389,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Handle messages from webview (shared handler)
this.setupWebviewMessageHandler(panel.webview)

// Detect service worker failures and retry (microsoft/vscode#125993)
this.loader.start()

this.initializeConnection()
}

Expand Down Expand Up @@ -433,18 +462,23 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
* Attach to a webview that already has its own HTML set.
* Sets up message handling and connection without overriding HTML content.
*
* @param options.html - Optional HTML generator for retrying panels with custom bundles.
* @param options.onBeforeMessage - Optional interceptor called before the standard handler.
* Return null to consume the message (stop propagation), or return the message
* (possibly transformed) to continue with standard handling.
*/
public attachToWebview(
webview: vscode.Webview,
options?: { onBeforeMessage?: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null> },
options?: {
html?: () => string
onBeforeMessage?: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
},
): void {
this.isWebviewReady = false
this.webview = webview
this.onBeforeMessage = options?.onBeforeMessage ?? null
this.setupWebviewMessageHandler(webview)
this.loader.start(options?.html)
this.initializeConnection()
}

Expand All @@ -469,7 +503,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper

switch (message.type) {
case "webviewReady":
console.log("[Kilo New] KiloProvider: ✅ webviewReady received")
console.log("[Kilo New] KiloProvider: webviewReady received")
this.loader.done()
this.isWebviewReady = true
await this.syncWebviewState("webviewReady")
this.flushPendingReviewComments()
Expand Down Expand Up @@ -3021,6 +3056,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
* Does NOT kill the server — that's the connection service's job.
*/
dispose(): void {
this.loader.dispose()
this.statsPoller?.stop()
this.unsubscribeEvent?.()
this.unsubscribeState?.()
Expand Down
18 changes: 10 additions & 8 deletions packages/kilo-vscode/src/agent-manager/vscode-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,21 @@ export class VscodeHost implements Host {
dark: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-dark.svg"),
}

const port = this.connectionService.getServerInfo()?.port
panel.webview.html = buildWebviewHtml(panel.webview, {
scriptUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "agent-manager.js")),
styleUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "agent-manager.css")),
iconsBaseUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
title: "Agent Manager",
port,
})
const html = () =>
buildWebviewHtml(panel.webview, {
scriptUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "agent-manager.js")),
styleUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "agent-manager.css")),
iconsBaseUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
title: "Agent Manager",
port: this.connectionService.getServerInfo()?.port,
})
panel.webview.html = html()

const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
slimEditMetadata: true,
})
provider.attachToWebview(panel.webview, {
html,
onBeforeMessage: opts.onBeforeMessage,
})

Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/src/services/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export enum TelemetryEventName {
AUTO_PURGE_FAILED = "Auto Purge Failed",
MANUAL_PURGE_TRIGGERED = "Manual Purge Triggered",
WEBVIEW_MEMORY_USAGE = "Webview Memory Usage",
WEBVIEW_READY_RETRY = "Webview Ready Retry",
WEBVIEW_READY_FAILED = "Webview Ready Failed",
MEMORY_WARNING_SHOWN = "Memory Warning Shown",
ASK_APPROVAL = "Ask Approval",
NOTIFICATION_CLICKED = "Notification Clicked",
Expand Down
140 changes: 140 additions & 0 deletions packages/kilo-vscode/src/webview-ready-retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as vscode from "vscode"

export const WEBVIEW_READY_TIMEOUT = 8_000
export const WEBVIEW_READY_RETRIES = 3

type Scheduler = {
set(callback: () => void, timeout: number): unknown
clear(timer: unknown): void
}

export type WebviewReadyRetryEvent = "retry" | "failed"

export type WebviewReadyRetryOptions = {
name: string
active?: () => boolean
html: () => string | undefined
load: (html: string) => void
log?: (message: string) => void
warn?: (message: string) => void
notify?: () => void
capture?: (event: WebviewReadyRetryEvent, props: Record<string, unknown>) => void
scheduler?: Scheduler
timeout?: number
retries?: number
}

const scheduler: Scheduler = {
set: (callback, timeout) => setTimeout(callback, timeout),
clear: (timer) => clearTimeout(timer as ReturnType<typeof setTimeout>),
}

export class WebviewReadyRetry {
private timer: unknown = null
private attempts = 0
private ready = false
private html: () => string | undefined

constructor(private readonly opts: WebviewReadyRetryOptions) {
this.html = opts.html
}

start(html?: () => string | undefined): void {
this.stop()
this.ready = false
this.attempts = 0
this.html = html ?? this.opts.html
this.arm()
}

done(): void {
this.ready = true
this.stop()
}

dispose(): void {
this.stop()
}

private get active(): boolean {
return this.opts.active?.() ?? true
}

private get delay(): number {
return (this.opts.timeout ?? WEBVIEW_READY_TIMEOUT) * (this.attempts + 1)
}

private get retries(): number {
return this.opts.retries ?? WEBVIEW_READY_RETRIES
}

private get scheduler(): Scheduler {
return this.opts.scheduler ?? scheduler
}

private arm(): void {
this.timer = this.scheduler.set(() => this.retry(), this.delay)
}

private retry(): void {
const delay = this.delay
this.timer = null

if (this.ready || !this.active) return

if (this.attempts >= this.retries) {
this.log(
`webview not ready after ${this.retries} retries - likely VS Code service worker bug ` +
`(microsoft/vscode#125993). Try "Developer: Reload Window".`,
)
this.opts.capture?.("failed", { name: this.opts.name, retries: this.retries })
this.opts.notify?.()
return
}

this.attempts++
this.warn(`webview not ready after ${delay}ms, retrying (${this.attempts}/${this.retries})`)
this.opts.capture?.("retry", {
name: this.opts.name,
attempt: this.attempts,
retries: this.retries,
timeout: delay,
})

const html = this.html()
if (this.ready || !this.active || html === undefined) return

this.opts.load(html)
if (this.ready || !this.active) return

this.arm()
}

private stop(): void {
const timer = this.timer
if (timer === null) return

this.scheduler.clear(timer)
this.timer = null
}

private log(message: string): void {
this.opts.log?.(`[Kilo New] ${this.opts.name}: ${message}`)
}

private warn(message: string): void {
this.opts.warn?.(`[Kilo New] ${this.opts.name}: ${message}`)
}
}

export function showWebviewReloadWarning(): void {
void vscode.window
.showWarningMessage(
"Kilo Code failed to load. This is a known VS Code issue. Try reloading the window.",
"Reload Window",
)
.then((choice) => {
if (choice !== "Reload Window") return
void vscode.commands.executeCommand("workbench.action.reloadWindow")
})
}
Loading
Loading