From 05ae51436c859fc529aa08b068666b0f12eb0ed6 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 3 Sep 2026 10:54:51 +0200 Subject: [PATCH 1/2] fix(vscode): clear completion indicators when opening results --- .../clear-completed-indicator-on-focus.md | 5 + packages/kilo-vscode/src/KiloProvider.ts | 25 ++ .../kilo-vscode/src/kilo-provider-utils.ts | 2 + .../cli-backend/connection-service.ts | 47 +++- .../fixtures/session-provider-activity.tsx | 123 ++++++++- .../kilo-provider-acknowledgement.test.ts | 244 ++++++++++++++++++ .../unit/kilo-provider-load-messages.test.ts | 1 + .../tests/unit/kilo-provider-utils.test.ts | 5 +- .../tests/unit/native-tab-title.test.ts | 6 +- .../presence-registration-contract.test.ts | 2 +- .../tests/unit/session-activity.test.ts | 13 + .../agent-manager/AgentManagerApp.tsx | 3 +- .../agent-manager/SubagentPanel.tsx | 3 +- packages/kilo-vscode/webview-ui/src/App.tsx | 7 +- .../webview-ui/src/context/session-types.ts | 1 + .../webview-ui/src/context/session.tsx | 35 ++- .../webview-ui/src/context/vscode.tsx | 13 +- .../src/types/messages/extension-messages.ts | 3 + .../src/types/messages/webview-messages.ts | 1 + .../webview-ui/src/utils/session-activity.ts | 4 +- 20 files changed, 523 insertions(+), 20 deletions(-) create mode 100644 .changeset/clear-completed-indicator-on-focus.md create mode 100644 packages/kilo-vscode/tests/unit/kilo-provider-acknowledgement.test.ts diff --git a/.changeset/clear-completed-indicator-on-focus.md b/.changeset/clear-completed-indicator-on-focus.md new file mode 100644 index 000000000000..65e4e010aa1b --- /dev/null +++ b/.changeset/clear-completed-indicator-on-focus.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Clear completed session indicators when you return to their chat across sidebar, editor, worktree, and subagent tabs. Keep indicators for sessions that finish while already open, and preserve unresolved input requests, errors, and turn diagnostics. diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 9a28be7c9a4a..b27631611285 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -412,6 +412,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private readonly anacondaDesktop = new AnacondaDesktopBridge() private sessionStatusMap = new Map() // Latest status used for destructive config warnings. private activity: Activity = "idle" + private active = false private caption: string | undefined private readonly epochs = new Map>() private readonly requests = new Map() @@ -441,6 +442,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private unsubscribeState: (() => void) | null = null private migrationCache: MigrationContext["migrationCache"] = new Map() private unsubscribeNotificationDismiss: (() => void) | null = null + private unsubscribeAcknowledged: (() => void) | null = null private unsubscribeLanguageChange: (() => void) | null = null private unsubscribeProfileChange: (() => void) | null = null private unsubscribeFavoritesChange: (() => void) | null = null @@ -582,6 +584,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper public setStreamVisibility(active: boolean): void { this.visibleTaskStreams.setActive(active) + this.active = active + if (!this.isWebviewReady) return + this.postMessage({ type: "webviewActiveChanged", active }) } public setProjectDirectory(directory: string | null): void { @@ -805,11 +810,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.setupWebviewMessageHandler(panel.webview) this.viewStateDisposable?.dispose() this.viewStateDisposable = this.visibleTaskStreams.bindPanel(panel, () => { + this.setStreamVisibility(panel.active && panel.visible) if (this.opts.disableViewedRegistration) return const id = this.contextSessionID this.streams.focus(panel.visible ? id : undefined) this.connectionService.registerVisible(this.instanceId, panel.visible && id ? [id] : []) }) + this.setStreamVisibility(panel.active && panel.visible) this.initializeConnection() } @@ -1014,8 +1021,20 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.initializeConnection() } + private acknowledge(message: Record): boolean { + if (message.type !== "acknowledgeSession") return false + if (typeof message.sessionID === "string" && typeof message.eventID === "string") { + this.connectionService.notifySessionAcknowledged(message.sessionID, message.eventID) + } + return true + } + private setupWebviewMessageHandler(webview: vscode.Webview): void { this.webviewMessageDisposable?.dispose() + this.unsubscribeAcknowledged?.() + this.unsubscribeAcknowledged = this.connectionService.onSessionAcknowledged((sessionID, eventID) => { + this.postMessage({ type: "sessionAcknowledged", sessionID, eventID }) + }) this.setFocusTarget("other") this.autocompleteConfigDisposable?.dispose() this.autocompleteConfigDisposable = watchAutocompleteConfig((msg) => this.postMessage(msg)) @@ -1030,6 +1049,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.telemetryStateDisposable?.dispose() this.telemetryStateDisposable = watchTelemetryState((msg) => this.postMessage(msg)) this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => { + if (this.acknowledge(message)) return const intercepted = await interceptMessage(message, { workspaceDir: (sid) => this.getWorkspaceDirectory(sid ?? this.currentSession?.id), post: (m) => this.postMessage(m), @@ -1109,6 +1129,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "webviewReady": console.log("[Kilo New] KiloProvider: ✅ webviewReady received") this.isWebviewReady = true + for (const event of this.connectionService.getPendingCompletions()) { + this.postMessage(mapSSEEventToWebviewMessage(event, event.properties.sessionID)) + } + this.postMessage({ type: "webviewActiveChanged", active: this.active }) this.visibleTaskStreams.clear() this.flushPendingKiloModel() await this.syncWebviewState("webviewReady") @@ -5469,6 +5493,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.unsubscribeClearPendingPrompts?.() this.unsubscribeDirectoryProvider?.() this.unsubscribeSandboxPreference?.() + this.unsubscribeAcknowledged?.() this.viewStateDisposable?.dispose() this.visibilityDisposable?.dispose() this.webviewMessageDisposable?.dispose() diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index 1aafaacf42f1..ad652cc2ea59 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -414,6 +414,7 @@ export type WebviewMessage = | { type: "sessionTurnClosed" sessionID: string + eventID: string reason: "completed" | "error" | "interrupted" | "superseded" parentID?: string } @@ -560,6 +561,7 @@ export function mapSSEEventToWebviewMessage(event: StreamEvent, sessionID: strin return { type: "sessionTurnClosed", sessionID: event.properties.sessionID, + eventID: event.id, reason: event.properties.reason, ...(event.properties.parentID ? { parentID: event.properties.parentID } : {}), } diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts index 2e67eb60cdd0..603e63921722 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode" import { ServerManager } from "./server-manager" -import { createKiloClient, type KiloClient } from "@kilocode/sdk/v2/client" +import { createKiloClient, type EventSessionTurnClose, type KiloClient } from "@kilocode/sdk/v2/client" import { SdkSSEAdapter, type SSEPayload } from "./sdk-sse-adapter" import type { ServerConfig } from "./types" import { createDuplicateEventFilter, resolveEventSessionId as resolveEventSessionIdPure } from "./connection-utils" @@ -12,6 +12,7 @@ type SSEEventListener = (event: SSEPayload, directory?: string) => void type StateListener = (state: ConnectionState, error?: Error) => void type SSEEventFilter = (event: SSEPayload, directory?: string) => boolean type NotificationDismissListener = (notificationId: string) => void +type SessionAcknowledgedListener = (sessionID: string, eventID: string) => void type LanguageChangeListener = (locale: string) => void type ProfileChangeListener = (data: unknown) => void type FavoritesChangeListener = (favorites: Array<{ providerID: string; modelID: string }>) => void @@ -100,6 +101,8 @@ export class KiloConnectionService { private readonly explicitAborts = new ExplicitAbortState() private readonly stateListeners: Set = new Set() private readonly notificationDismissListeners: Set = new Set() + private readonly sessionAcknowledgedListeners: Set = new Set() + private readonly completions = new Map() private readonly languageChangeListeners: Set = new Set() private readonly profileChangeListeners: Set = new Set() private readonly favoritesChangeListeners: Set = new Set() @@ -433,6 +436,25 @@ export class KiloConnectionService { } } + onSessionAcknowledged(listener: SessionAcknowledgedListener): () => void { + this.sessionAcknowledgedListeners.add(listener) + return () => { + this.sessionAcknowledgedListeners.delete(listener) + } + } + + notifySessionAcknowledged(sessionID: string, eventID: string): void { + const state = this.completions.get(sessionID) + if (state?.event?.id === eventID) state.event = undefined + for (const listener of this.sessionAcknowledgedListeners) { + listener(sessionID, eventID) + } + } + + getPendingCompletions() { + return [...this.completions.values()].flatMap((state) => (state.event ? [state.event] : [])) + } + /** * Subscribe to language change events broadcast from any KiloProvider. Returns unsubscribe function. */ @@ -691,6 +713,8 @@ export class KiloConnectionService { this.explicitAborts.clear() this.stateListeners.clear() this.notificationDismissListeners.clear() + this.sessionAcknowledgedListeners.clear() + this.completions.clear() this.profileChangeListeners.clear() this.favoritesChangeListeners.clear() this.clearPendingPromptsListeners.clear() @@ -897,6 +921,27 @@ export class KiloConnectionService { } private broadcast(event: SSEPayload, directory?: string): void { + if (event.type === "session.turn.close") { + const sid = event.properties.sessionID + const state = this.completions.get(sid) + if (!state || event.id > state.id) { + this.completions.set(sid, { + id: event.id, + event: event.properties.reason === "completed" ? event : undefined, + }) + } + } + if ( + event.type === "session.turn.open" || + (event.type === "session.status" && event.properties.status.type !== "idle") || + event.type === "session.error" || + event.type === "session.deleted" || + (event.type === "sync" && event.name === "session.deleted.1") + ) { + const sid = event.type === "sync" ? event.data.sessionID : event.properties.sessionID + const state = sid ? this.completions.get(sid) : undefined + if (sid && state && event.id > state.id) this.completions.set(sid, { id: event.id }) + } this.handlePermissionEvent(event, directory) this.handleQuestionEvent(event, directory) for (const listener of this.eventListeners) listener(event, directory) diff --git a/packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx b/packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx index 3b8975369139..6f483715ca06 100644 --- a/packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx +++ b/packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx @@ -4,9 +4,15 @@ import type { ModelSelection, WebviewMessage } from "../../webview-ui/src/types/ const window = new Window({ url: "http://localhost" }) Object.defineProperty(window, "origin", { value: window.location.origin }) +Object.defineProperty(window.document, "hasFocus", { value: () => true }) const sent: WebviewMessage[] = [] const api = { - postMessage: (message: WebviewMessage) => sent.push(message), + postMessage: (message: WebviewMessage) => { + sent.push(message) + if (message.type === "acknowledgeSession") { + queueMicrotask(() => post({ ...message, type: "sessionAcknowledged" })) + } + }, getState: () => undefined, setState: () => {}, } @@ -39,6 +45,7 @@ Object.assign(globalThis, { const { render } = await import("solid-js/web") const { For, Show, createEffect, createSignal } = await import("solid-js") +const { unwrap } = await import("solid-js/store") const { WorktreeItem } = await import("../../webview-ui/agent-manager/WorktreeItem") const { SubagentPanel } = await import("../../webview-ui/agent-manager/SubagentPanel") const { DragDropProvider, SortableProvider } = await import("@thisbeyond/solid-dnd") @@ -49,7 +56,7 @@ const { ConfigContext } = await import("../../webview-ui/src/context/config") const { LanguageContext } = await import("../../webview-ui/src/context/language") const { NotificationsProvider } = await import("../../webview-ui/src/context/notifications") const { ProviderProvider } = await import("../../webview-ui/src/context/provider") -const { SessionProvider, useSession } = await import("../../webview-ui/src/context/session") +const { SessionProvider, useSession, useSessionVisibility } = await import("../../webview-ui/src/context/session") const { post } = await import("../../webview-ui/src/utils/webview-message") const { terminal } = await import("../../webview-ui/src/context/session-outcome") const { PromptInput } = await import("../../webview-ui/src/components/chat/PromptInput") @@ -93,11 +100,20 @@ const ref = { value: undefined as ReturnType | undefined } const observed: (ModelSelection | null)[] = [] const [operation, setOperation] = createSignal(false) const [run, setRun] = createSignal(false) +const [inspected, setInspected] = createSignal(["task-child", "task-grand"]) const [inspector, setInspector] = createSignal(false) const [composer, setComposer] = createSignal(false) const [active, setActive] = createSignal("task-child") +const [review, setReview] = createSignal(false) +const [sharing, setSharing] = createSignal(false) +const peer = { value: undefined as ReturnType | undefined } +const Peer = () => { + peer.value = useSession() + return null +} const Probe = () => { const session = useSession() + useSessionVisibility(() => (review() ? undefined : session.currentSessionID())) ref.value = session createEffect(() => observed.push(session.selected())) const ids = ["root", "background"] @@ -118,6 +134,11 @@ const Probe = () => { } as Parameters[1] return ( + + + + + {(id) => renderTab(id, deps)} @@ -157,9 +178,9 @@ const Probe = () => { /> ["task-child", "task-grand"].map((id) => ({ id, title: id }))} + tabs={() => inspected().map((id) => ({ id, title: id }))} active={active} - visible={() => true} + visible={() => inspector() && inspected().length > 0} nextKeybind="" closeKeybind="" onSelect={setActive} @@ -211,8 +232,8 @@ const settle = async () => { await Promise.resolve() await window.happyDOM.waitUntilComplete() } -const emit = async (data: unknown) => { - post(structuredClone(data)) +const emit = async (data: { type: string; [key: string]: unknown }) => { + post(structuredClone(data.type === "sessionTurnClosed" ? { eventID: crypto.randomUUID(), ...data } : data)) await settle() } const state = (id: string) => { @@ -800,7 +821,13 @@ try { await settle() await catalog("org-a", [recommended.modelID], recommended.modelID) + setSharing(true) + await settle() + await emit({ type: "sessionsLoaded", sessions: unwrap(value.sessions()) }) value.setCurrentSessionID("root") + await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed", eventID: "seeded" }) + await check("root", "done") + await emit({ type: "webviewActiveChanged", active: true }) await check("root", "idle") await check("background", "idle") for (const update of [setOperation, setRun]) { @@ -922,13 +949,29 @@ try { await check("root", "idle") assert.equal(value.suggestions().length, 0) + await emit({ type: "webviewActiveChanged", active: true }) await emit({ type: "sessionTurnClosed", sessionID: "task-child", reason: "completed", parentID: "root" }) await check("task-child", "done") await check("root", "idle") + setInspected(["task-grand", "task-child"]) + await check("task-child", "done") + setInspected(["task-child"]) + await check("task-child", "done") + setInspected(["task-child", "task-grand"]) + await check("task-child", "done") + setActive("task-grand") + await settle() + const tab = host.querySelector('[data-tab-id="task-child"] [role="tab"]') + assert(tab) + tab.click() + await check("task-child", "idle") + assert.equal(peer.value?.activityFor("task-child"), "idle") + await emit({ type: "sessionTurnClosed", sessionID: "task-child", reason: "completed", parentID: "root" }) + await check("task-child", "done") setInspector(false) await settle() setInspector(true) - await check("task-child", "done") + await check("task-child", "idle") assert.equal(value.currentSessionID(), "root") await emit({ type: "sessionTurnClosed", sessionID: "task-child", reason: "error", parentID: "root" }) await check("task-child", "error") @@ -1135,6 +1178,72 @@ try { ) } + // "done" clears when the user switches TO that tab (the focus transition); + // an unresolved attention state ("waiting") never clears on focus. + await emit({ type: "sessionStatus", sessionID: "background", status: "idle" }) + await emit({ type: "sessionTurnClosed", sessionID: "background", reason: "completed" }) + await check("background", "done") + assert.equal(value.currentSessionID(), "root") + value.selectSession("background") + await check("background", "idle") + assert.equal(value.closeReason(), "completed") + assert.equal(peer.value?.activityFor("background"), "idle") + + setReview(true) + await settle() + await emit({ + type: "sessionTurnClosed", + sessionID: "background", + eventID: "review-completed", + reason: "completed", + }) + await check("background", "done") + setReview(false) + await check("background", "idle") + assert.equal(value.currentSessionID(), "background") + assert.equal(value.closeReason(), "completed") + assert.equal(peer.value?.activityFor("background"), "idle") + await emit({ + type: "sessionTurnClosed", + sessionID: "background", + eventID: "review-completed", + reason: "completed", + }) + await check("background", "idle") + + await emit({ type: "webviewActiveChanged", active: false }) + await emit({ + type: "sessionTurnClosed", + sessionID: "background", + eventID: "next-completed", + reason: "completed", + }) + await emit({ type: "sessionAcknowledged", sessionID: "background", eventID: "review-completed" }) + await check("background", "done") + value.selectSession("root") + value.selectSession("background") + await check("background", "done") + await emit({ type: "webviewActiveChanged", active: true }) + await check("background", "idle") + assert.equal(peer.value?.activityFor("background"), "idle") + + await emit({ + type: "questionRequest", + question: { + id: "attention", + sessionID: "background", + questions: [{ question: "Continue?", header: "Confirm", options: [] }], + }, + }) + await check("background", "waiting") + value.setCurrentSessionID("root") + await settle() + value.setCurrentSessionID("background") + await check("background", "waiting") + await emit({ type: "questionResolved", requestID: "attention" }) + value.setCurrentSessionID("root") + await settle() + await emit({ type: "sessionStatus", sessionID: "root", status: "busy" }) await emit({ type: "sessionStatus", sessionID: "root", status: "idle" }) await emit({ diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-acknowledgement.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-acknowledgement.test.ts new file mode 100644 index 000000000000..d21680aa8f3c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/kilo-provider-acknowledgement.test.ts @@ -0,0 +1,244 @@ +import { afterEach, describe, expect, it } from "bun:test" +import type { EventSessionTurnClose, Session } from "@kilocode/sdk/v2/client" +import { KiloProvider } from "../../src/KiloProvider" +import { KiloConnectionService } from "../../src/services/cli-backend/connection-service" +import type { SSEPayload } from "../../src/services/cli-backend/sdk-sse-adapter" + +const resources: { dispose(): void }[] = [] + +afterEach(() => { + for (const resource of resources.splice(0).reverse()) resource.dispose() +}) + +function create() { + const connection = new KiloConnectionService({} as never) + resources.push(connection) + return connection +} + +function emit(connection: KiloConnectionService, event: SSEPayload) { + const internal = connection as unknown as { broadcast(event: SSEPayload): void } + internal.broadcast(event) +} + +function completion(id = "evt-001"): EventSessionTurnClose { + return { id, type: "session.turn.close", properties: { sessionID: "s1", parentID: "parent", reason: "completed" } } +} + +function attach(connection: KiloConnectionService, blocked = true) { + const provider = new KiloProvider({} as never, connection) + resources.push(provider) + const internal = provider as unknown as { initConnectionPromise: Promise } + internal.initConnectionPromise = Promise.resolve() + const pending = Promise.withResolvers<(message: Record) => Promise>() + const sent: unknown[] = [] + const intercepts: unknown[] = [] + const webview = { + postMessage: async (message: unknown) => sent.push(message), + onDidReceiveMessage: (handler: (message: Record) => Promise) => { + pending.resolve(handler) + return { dispose: () => {} } + }, + } + provider.attachToWebview(webview as never, { + onBeforeMessage: async (message) => { + intercepts.push(message) + return blocked ? null : message + }, + }) + return { provider, webview, sent, intercepts, receive: pending.promise } +} + +describe("session acknowledgement host routing", () => { + it("broadcasts synchronously before interception and releases subscriptions on reattach and disposal", async () => { + const connection = create() + const sidebar = attach(connection) + const editor = attach(connection) + const receive = await sidebar.receive + const first = { type: "sessionAcknowledged", sessionID: "s1", eventID: "evt-first" } + const result = receive({ ...first, type: "acknowledgeSession" }) + + expect(connection.getConnectionState()).toBe("disconnected") + expect(sidebar.sent).toEqual([first]) + expect(editor.sent).toEqual([first]) + expect(sidebar.intercepts).toEqual([]) + await result + + editor.provider.attachToWebview(editor.webview as never) + const second = { ...first, eventID: "evt-second" } + connection.notifySessionAcknowledged(second.sessionID, second.eventID) + expect(sidebar.sent).toEqual([first, second]) + expect(editor.sent).toEqual([first, second]) + + sidebar.provider.dispose() + const third = { ...first, eventID: "evt-third" } + connection.notifySessionAcknowledged(third.sessionID, third.eventID) + expect(sidebar.sent).toEqual([first, second]) + expect(editor.sent).toEqual([first, second, third]) + + connection.dispose() + connection.notifySessionAcknowledged("s1", "evt-fourth") + expect(editor.sent).toEqual([first, second, third]) + }) + + it.each([ + { sessionID: "s1" }, + { eventID: "evt-first" }, + { sessionID: 1, eventID: "evt-first" }, + { sessionID: "s1", eventID: null }, + ])("ignores malformed acknowledgement %j", async (message) => { + const connection = create() + const sidebar = attach(connection) + const receive = await sidebar.receive + + await receive({ type: "acknowledgeSession", ...message }) + + expect(sidebar.sent).toEqual([]) + expect(sidebar.intercepts).toEqual([]) + }) +}) + +describe("pending completion replay", () => { + it("seeds a fresh webview before activation and readiness", async () => { + const connection = create() + const event = completion() + emit(connection, event) + emit(connection, { + id: "evt-002", + type: "session.status", + properties: { sessionID: "s1", status: { type: "idle" } }, + }) + const editor = attach(connection, false) + const ready = editor.provider.waitForReady() + const receive = await editor.receive + editor.provider.setStreamVisibility(true) + expect(editor.sent).toEqual([]) + + await receive({ type: "webviewReady" }) + await ready + + expect(editor.sent.slice(0, 2)).toEqual([ + { type: "sessionTurnClosed", sessionID: "s1", parentID: "parent", eventID: "evt-001", reason: "completed" }, + { type: "webviewActiveChanged", active: true }, + ]) + expect(connection.getPendingCompletions()).toEqual([event]) + }) + + it("does not replay acknowledged completions after duplicate close events", async () => { + const connection = create() + const event = completion() + emit(connection, event) + connection.notifySessionAcknowledged("s1", event.id) + emit(connection, event) + const editor = attach(connection, false) + const receive = await editor.receive + + await receive({ type: "webviewReady" }) + + expect(connection.getPendingCompletions()).toEqual([]) + expect(editor.sent.at(0)).toEqual({ type: "webviewActiveChanged", active: false }) + }) + + it("keeps the newer completion after a stale acknowledgement or close event", () => { + const connection = create() + const first = completion() + const second = completion("evt-002") + emit(connection, first) + emit(connection, second) + connection.notifySessionAcknowledged("s1", first.id) + emit(connection, first) + expect(connection.getPendingCompletions()).toEqual([second]) + }) + + it.each([ + { id: "evt-002", type: "session.turn.open", properties: { sessionID: "s1" } }, + { id: "evt-002", type: "session.status", properties: { sessionID: "s1", status: { type: "busy" } } }, + { id: "evt-002", type: "session.error", properties: { sessionID: "s1" } }, + { id: "evt-002", type: "session.deleted", properties: { sessionID: "s1", info: { id: "s1" } as Session } }, + { + id: "evt-002", + type: "sync", + name: "session.deleted.1", + seq: 2, + aggregateID: "s1", + data: { sessionID: "s1" }, + }, + { id: "evt-002", type: "session.turn.close", properties: { sessionID: "s1", reason: "interrupted" } }, + ] satisfies SSEPayload[])("clears the seed and rejects older closes after %j", (event) => { + const connection = create() + const previous = completion() + emit(connection, previous) + emit(connection, event) + expect(connection.getPendingCompletions()).toEqual([]) + emit(connection, previous) + expect(connection.getPendingCompletions()).toEqual([]) + }) + + it("keeps pending completions when untracked but discards them on service disposal", () => { + const connection = create() + const event = completion() + emit(connection, event) + connection.pruneSession("s1") + expect(connection.getPendingCompletions()).toEqual([event]) + connection.dispose() + expect(connection.getPendingCompletions()).toEqual([]) + }) +}) + +describe("webview activation host routing", () => { + it.each([true, false])("replays the latest stream activation (%s) when the webview is ready", async (active) => { + const sidebar = attach(create(), false) + const receive = await sidebar.receive + sidebar.provider.setStreamVisibility(!active) + sidebar.provider.setStreamVisibility(active) + expect(sidebar.sent).toEqual([]) + + await receive({ type: "webviewReady" }) + + expect(sidebar.sent).toContainEqual({ type: "webviewActiveChanged", active }) + sidebar.sent.length = 0 + sidebar.provider.setStreamVisibility(!active) + expect(sidebar.sent).toEqual([{ type: "webviewActiveChanged", active: !active }]) + }) + + it("keeps visible native panels registered when inactive and reports activation changes", async () => { + const connection = create() + const editor = attach(connection) + const internal = editor.provider as unknown as { + contextSessionID: string + isWebviewReady: boolean + _getHtmlForWebview: () => string + } + internal.contextSessionID = "s1" + internal._getHtmlForWebview = () => "" + const pending = Promise.withResolvers<() => void>() + const panel = { + webview: editor.webview, + active: false, + visible: true, + onDidChangeViewState: (handler: () => void) => { + pending.resolve(handler) + return { dispose: () => {} } + }, + } + editor.provider.resolveWebviewPanel(panel as never) + expect(editor.sent).toEqual([]) + internal.isWebviewReady = true + const change = await pending.promise + const presence = connection as unknown as { visible: Map> } + + change() + expect(editor.sent.at(-1)).toEqual({ type: "webviewActiveChanged", active: false }) + expect([...presence.visible.values()].flatMap((ids) => [...ids])).toEqual(["s1"]) + + panel.active = true + change() + expect(editor.sent.at(-1)).toEqual({ type: "webviewActiveChanged", active: true }) + expect([...presence.visible.values()].flatMap((ids) => [...ids])).toEqual(["s1"]) + + panel.visible = false + change() + expect(editor.sent.at(-1)).toEqual({ type: "webviewActiveChanged", active: false }) + expect([...presence.visible.values()].flatMap((ids) => [...ids])).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts index 73c36c665ffa..46018e645a66 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts @@ -212,6 +212,7 @@ function createConnection(client: ReturnType | null) { onEventFiltered: () => () => undefined, onStateChange: (_l: (s: State) => void) => () => undefined, onNotificationDismissed: () => () => undefined, + onSessionAcknowledged: () => () => undefined, onLanguageChanged: () => () => undefined, onProfileChanged: () => () => undefined, onFavoritesChanged: () => () => undefined, diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts index 2c8ce4fb6e65..10952ec99839 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts @@ -366,14 +366,14 @@ describe("mapSSEEventToWebviewMessage", () => { }) }) - it("maps session.turn.close to its terminal reason", () => { + it("maps session.turn.close with its event identity and terminal reason", () => { const event: EventSessionTurnClose = { id: "evt-turn", type: "session.turn.close", properties: { sessionID: "sess-1", reason: "interrupted" }, } const msg = mapSSEEventToWebviewMessage(event, "sess-1") - expect(msg).toEqual({ type: "sessionTurnClosed", sessionID: "sess-1", reason: "interrupted" }) + expect(msg).toEqual({ type: "sessionTurnClosed", sessionID: "sess-1", eventID: "evt-turn", reason: "interrupted" }) }) it("forwards the parent session ID when a child turn closes", () => { @@ -386,6 +386,7 @@ describe("mapSSEEventToWebviewMessage", () => { expect(mapSSEEventToWebviewMessage(event, "child")).toEqual({ type: "sessionTurnClosed", sessionID: "child", + eventID: "evt-child-turn", reason: "completed", parentID: "parent", }) diff --git a/packages/kilo-vscode/tests/unit/native-tab-title.test.ts b/packages/kilo-vscode/tests/unit/native-tab-title.test.ts index d1775b1c76a8..28bb19cab28d 100644 --- a/packages/kilo-vscode/tests/unit/native-tab-title.test.ts +++ b/packages/kilo-vscode/tests/unit/native-tab-title.test.ts @@ -25,7 +25,11 @@ describe("nativeTitle", () => { const listener: { current?: (message: { type: string; state: unknown }) => Promise } = {} const provider = new KiloProvider( { fsPath: "/extension" } as never, - { unregisterVisible: () => {}, unregisterAttached: () => {} } as never, + { + unregisterVisible: () => {}, + unregisterAttached: () => {}, + onSessionAcknowledged: () => () => {}, + } as never, undefined, { tabTitle: (title) => titles.push(title) }, ) diff --git a/packages/kilo-vscode/tests/unit/presence-registration-contract.test.ts b/packages/kilo-vscode/tests/unit/presence-registration-contract.test.ts index e97cf52c43c7..abfcc63f294d 100644 --- a/packages/kilo-vscode/tests/unit/presence-registration-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/presence-registration-contract.test.ts @@ -49,7 +49,7 @@ describe("KiloProvider editor-panel visible registration contract", () => { expect(body).toContain("this.contextSessionID") expect(body).toContain("panel.visible") expect(body).toContain("this.connectionService.registerVisible(this.instanceId,") - expect(body).not.toContain("panel.active") + expect(body).toContain("this.connectionService.registerVisible(this.instanceId, panel.visible && id ? [id] : [])") expect(body).not.toContain("this.currentSession") }) diff --git a/packages/kilo-vscode/tests/unit/session-activity.test.ts b/packages/kilo-vscode/tests/unit/session-activity.test.ts index 182036dda905..d46f200fe6a5 100644 --- a/packages/kilo-vscode/tests/unit/session-activity.test.ts +++ b/packages/kilo-vscode/tests/unit/session-activity.test.ts @@ -67,6 +67,19 @@ describe("activities", () => { expect(activities({ ...input, outcomes: { ...input.outcomes, root: { reason: "completed" } } }).root).toBe("done") }) + it("hides acknowledged completion without changing other activity", () => { + const input = { + parents, + statuses: {}, + outcomes: { root: { reason: "completed", seen: true }, child: { reason: "error", seen: true } }, + blocked: [], + disconnected: false, + } + expect(activities(input)).toEqual({ root: "idle", child: "error" }) + expect(activities({ ...input, blocked: ["root"] }).root).toBe("waiting") + expect(input.outcomes.root.reason).toBe("completed") + }) + it("shows disconnected active sessions as errors without changing idle or completed sessions", () => { const input = { parents, diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 1e8223642df2..6f9c0365b59c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -67,7 +67,7 @@ import { ImageModelsProvider } from "../src/context/image-models" import { NotificationsProvider } from "../src/context/notifications" import { FeedbackProvider } from "../src/context/feedback" import { MemoryProvider } from "../src/context/memory" -import { SessionProvider, useSession } from "../src/context/session" +import { SessionProvider, useSession, useSessionVisibility } from "../src/context/session" import { WorktreeModeProvider } from "../src/context/worktree-mode" import { DiffStyleProvider, useDiffStyle } from "../src/context/diff-style" import { ProviderShell } from "../src/context/provider-shell" @@ -905,6 +905,7 @@ const AgentManagerContent: Component = () => { ), ) reportVisibleSession(vscode, visibleSession) + useSessionVisibility(visibleSession) const worktreeLabel = (wt: WorktreeState): string => wt.label || firstOrderedTitle(sessionsForWorktree(wt.id), worktreeTabOrder()[wt.id], wt.branch) const worktreeSubtitle = (wt: WorktreeState): string | undefined => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx index 1f9f4f05ce8b..763752d8f346 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx @@ -12,7 +12,7 @@ import { DataBridge } from "../src/App" import { ChatView } from "../src/components/chat" import { ActivityIcon } from "../src/components/shared/ActivityIcon" import { useLanguage } from "../src/context/language" -import { SessionProvider, useSession } from "../src/context/session" +import { SessionProvider, useSession, useSessionVisibility } from "../src/context/session" import { description, label, type Activity } from "../src/utils/session-activity" import { SortableClosableTab } from "./ClosableTab" import { InspectorTabStrip } from "./InspectorTabStrip" @@ -136,6 +136,7 @@ const SubagentContent: Component Activity }> export const SubagentPanel: Component = (props) => { const session = useSession() + useSessionVisibility(() => (props.visible() ? props.active() : undefined)) return ( diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 4e718880d572..fc9f57d7c045 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -6,7 +6,7 @@ import { useVSCode } from "./context/vscode" import { useServer } from "./context/server" import { useProvider } from "./context/provider" import { WorkStyleProvider } from "./context/work-style" -import { useSession } from "./context/session" +import { useSession, useSessionVisibility } from "./context/session" import { LocalTabsProvider, useLocalTabs } from "./context/local-tabs" import { ProviderShell } from "./context/provider-shell" import { ChatView } from "./components/chat" @@ -231,6 +231,11 @@ const AppContent: Component = () => { strongest([session.currentSessionID(), ...(tabs?.ids() ?? [])].map(session.activityFor)), ) createEffect(() => vscode.postMessage({ type: "sessionActivity", state: activity() })) + useSessionVisibility(() => + !migration() && (currentView() === "newTask" || currentView() === "subAgentViewer") + ? session.currentSessionID() + : undefined, + ) const handleViewAction = (action: string) => { switch (action) { diff --git a/packages/kilo-vscode/webview-ui/src/context/session-types.ts b/packages/kilo-vscode/webview-ui/src/context/session-types.ts index 4e7c3acb10b4..ad70a2b82aad 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-types.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-types.ts @@ -68,6 +68,7 @@ export interface SessionContextValue { allStatusMap: () => Record activityFor: (sessionID: string | undefined) => Activity + acknowledge: (sessionID: string) => void inUseFor: (sessionID: string) => boolean // Parts for a specific message diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 191d1e6b0a37..523edf831681 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -17,7 +17,7 @@ import { batch, untrack, } from "solid-js" -import type { ParentComponent } from "solid-js" +import type { Accessor, ParentComponent } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" import { useVSCode } from "./vscode" import { useServer } from "./server" @@ -124,6 +124,8 @@ interface SessionStore { interface CloseState { reason: SessionCloseReason parentID?: string + eventID?: string + seen?: boolean } export const SessionContext = createContext() @@ -767,13 +769,19 @@ export const SessionProvider: ParentComponent = (props) => { } function closed(message: Extract) { + if (message.eventID && closeMap[message.sessionID]?.eventID === message.eventID) return if (message.reason === "completed" && closeMap[message.sessionID]?.reason === "error") return const ids = recoveries.get(message.sessionID) if (message.reason === "completed" && ids) { setStore("messages", message.sessionID, (msgs = []) => msgs.filter((msg) => !ids.has(msg.id))) } recoveries.delete(message.sessionID) - setCloseMap(message.sessionID, { reason: message.reason, parentID: message.parentID }) + setCloseMap(message.sessionID, { + reason: message.reason, + parentID: message.parentID, + eventID: message.eventID, + seen: false, + }) } function failed(id: string, message: Message) { @@ -1018,12 +1026,17 @@ export const SessionProvider: ParentComponent = (props) => { // Handle messages from extension onMount(() => { const unsubscribeProject = vscode.onMessage(trackAgentProject) + const unsubscribeAck = vscode.onMessage((message) => { + if (message.type !== "sessionAcknowledged") return + if (closeMap[message.sessionID]?.eventID === message.eventID) setCloseMap(message.sessionID, "seen", true) + }) const unsubscribe = vscode.onMessage((message) => { if (!isStaleAgentSession(message, agentProjectId())) handleExtensionMessage(message) }) setModelUsageReady(true) onCleanup(() => { unsubscribeProject() + unsubscribeAck() unsubscribe() }) }) @@ -1769,6 +1782,12 @@ export const SessionProvider: ParentComponent = (props) => { ) }) const activityFor = (id: string | undefined): Activity => (id ? (activityMap[id] ?? "idle") : "idle") + const acknowledge = (id: string) => { + const outcome = closeMap[id] + if (activityFor(id) !== "done" || !outcome?.eventID) return + setCloseMap(id, "seen", true) + vscode.postMessage({ type: "acknowledgeSession", sessionID: id, eventID: outcome.eventID }) + } const inUseFor = (id: string) => inUse(sessionFamily(id), statusMap, [...permissions(), ...questions()]) function handleTodoUpdated(sessionID: string, items: TodoItem[]) { @@ -2878,6 +2897,7 @@ export const SessionProvider: ParentComponent = (props) => { allParts, allStatusMap, activityFor, + acknowledge, inUseFor, recentModels: () => store.recentModels, modelUsageHistory: () => store.modelUsageHistory, @@ -2925,6 +2945,17 @@ export const SessionProvider: ParentComponent = (props) => { return {props.children} } +export function useSessionVisibility(visible: Accessor) { + const session = useSession() + const vscode = useVSCode() + const current = createMemo(() => (vscode.active() ? visible() : undefined)) + createEffect( + on(current, (id) => { + if (id) session.acknowledge(id) + }), + ) +} + export function useSession(): SessionContextValue { const context = useContext(SessionContext) if (!context) { diff --git a/packages/kilo-vscode/webview-ui/src/context/vscode.tsx b/packages/kilo-vscode/webview-ui/src/context/vscode.tsx index 71e7a1744654..90017faf4d01 100644 --- a/packages/kilo-vscode/webview-ui/src/context/vscode.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/vscode.tsx @@ -37,6 +37,7 @@ interface VSCodeContextValue { getState: () => T | undefined setState: (state: T) => void sidebarSide: () => "left" | "right" | undefined + active: () => boolean getModelSelectorExpanded: () => boolean setModelSelectorExpanded: (value: boolean) => void } @@ -89,12 +90,21 @@ export const VSCodeProvider: ParentComponent = (props) => { } window.addEventListener("message", messageListener) - const reportFocus = () => api.postMessage({ type: "webviewFocusChanged", focused: document.hasFocus() }) + const [ready, setReady] = createSignal(false) + const [active, setActive] = createSignal(document.hasFocus()) + const reportFocus = () => { + setActive(document.hasFocus()) + api.postMessage({ type: "webviewFocusChanged", focused: active() }) + } window.addEventListener("focus", reportFocus) window.addEventListener("blur", reportFocus) reportFocus() handlers.add((message) => { if (message?.type === "modelSelectorExpandedLoaded") setExpanded(message.value) + if (message?.type === "webviewActiveChanged") { + setActive(message.active) + setReady(true) + } }) api.postMessage({ type: "requestModelSelectorExpanded" }) @@ -120,6 +130,7 @@ export const VSCodeProvider: ParentComponent = (props) => { getState: () => api.getState() as T | undefined, setState: (state: T) => api.setState(state), sidebarSide: side, + active: () => ready() && active(), getModelSelectorExpanded: expanded, setModelSelectorExpanded: (value: boolean) => { setExpanded(value) diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index 24da4fc5ecb5..42d7690b958c 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -179,6 +179,7 @@ export interface SessionStatusMessage { export interface SessionTurnClosedMessage { type: "sessionTurnClosed" sessionID: string + eventID: string reason: SessionCloseReason parentID?: string } @@ -1491,6 +1492,8 @@ export interface AgentManagerBrowserDevtoolsMessage { } export type ExtensionMessage = + | { type: "sessionAcknowledged"; sessionID: string; eventID: string } + | { type: "webviewActiveChanged"; active: boolean } | DocumentResultMessage | DocumentOpenMessage | AgentManagerFocusContextRequestedMessage diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 6fba058c0dca..9059e3bac751 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -1520,6 +1520,7 @@ export interface DismissAgentMigrationBannerMessage { export type WebviewMessage = | { type: "sessionActivity"; state: Activity } + | { type: "acknowledgeSession"; sessionID: string; eventID: string } | DocumentRequestMessage | DocumentOpenFileMessage | DocumentCloseMessage diff --git a/packages/kilo-vscode/webview-ui/src/utils/session-activity.ts b/packages/kilo-vscode/webview-ui/src/utils/session-activity.ts index 2bd717b53a80..2367a9f2dbc4 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/session-activity.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/session-activity.ts @@ -23,7 +23,7 @@ export function activity(input: ActivityInput): Activity { export function activities(input: { parents: ReadonlyMap statuses: Record - outcomes: Record + outcomes: Record blocked: Iterable submitting?: Iterable disconnected: boolean @@ -41,7 +41,7 @@ export function activities(input: { blocked: blocked.has(id), disconnected: input.disconnected, errored: close === "error", - finished: close === "completed", + finished: close === "completed" && !input.outcomes[id]?.seen, }) result[id] = strongest([result[id] ?? "idle", own]) if (active === "idle") continue From c1da52af34bb8c1edda864afa5be9cb8f0d6698f Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 3 Sep 2026 11:31:01 +0200 Subject: [PATCH 2/2] fix(vscode): separate DOM focus from session activation --- .../fixtures/session-provider-activity.tsx | 24 ++++++++++++++++++- .../webview-ui/src/context/vscode.tsx | 15 ++++-------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx b/packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx index 6f483715ca06..8d8e6103fd66 100644 --- a/packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx +++ b/packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx @@ -4,7 +4,8 @@ import type { ModelSelection, WebviewMessage } from "../../webview-ui/src/types/ const window = new Window({ url: "http://localhost" }) Object.defineProperty(window, "origin", { value: window.location.origin }) -Object.defineProperty(window.document, "hasFocus", { value: () => true }) +const focused = { value: true } +Object.defineProperty(window.document, "hasFocus", { value: () => focused.value }) const sent: WebviewMessage[] = [] const api = { postMessage: (message: WebviewMessage) => { @@ -232,6 +233,18 @@ const settle = async () => { await Promise.resolve() await window.happyDOM.waitUntilComplete() } +const focus = async (value: boolean) => { + focused.value = value + window.dispatchEvent(new window.Event(value ? "focus" : "blur")) + await settle() + assert.deepEqual( + sent.findLast((message) => message.type === "webviewFocusChanged"), + { + type: "webviewFocusChanged", + focused: value, + }, + ) +} const emit = async (data: { type: string; [key: string]: unknown }) => { post(structuredClone(data.type === "sessionTurnClosed" ? { eventID: crypto.randomUUID(), ...data } : data)) await settle() @@ -1211,6 +1224,13 @@ try { }) await check("background", "idle") + await emit({ type: "sessionTurnClosed", sessionID: "background", reason: "completed" }) + await check("background", "done") + await focus(false) + await check("background", "done") + await focus(true) + await check("background", "done") + await emit({ type: "webviewActiveChanged", active: false }) await emit({ type: "sessionTurnClosed", @@ -1222,6 +1242,8 @@ try { await check("background", "done") value.selectSession("root") value.selectSession("background") + await focus(false) + await focus(true) await check("background", "done") await emit({ type: "webviewActiveChanged", active: true }) await check("background", "idle") diff --git a/packages/kilo-vscode/webview-ui/src/context/vscode.tsx b/packages/kilo-vscode/webview-ui/src/context/vscode.tsx index 90017faf4d01..5f324eb802df 100644 --- a/packages/kilo-vscode/webview-ui/src/context/vscode.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/vscode.tsx @@ -90,21 +90,14 @@ export const VSCodeProvider: ParentComponent = (props) => { } window.addEventListener("message", messageListener) - const [ready, setReady] = createSignal(false) - const [active, setActive] = createSignal(document.hasFocus()) - const reportFocus = () => { - setActive(document.hasFocus()) - api.postMessage({ type: "webviewFocusChanged", focused: active() }) - } + const [active, setActive] = createSignal(false) + const reportFocus = () => api.postMessage({ type: "webviewFocusChanged", focused: document.hasFocus() }) window.addEventListener("focus", reportFocus) window.addEventListener("blur", reportFocus) reportFocus() handlers.add((message) => { if (message?.type === "modelSelectorExpandedLoaded") setExpanded(message.value) - if (message?.type === "webviewActiveChanged") { - setActive(message.active) - setReady(true) - } + if (message?.type === "webviewActiveChanged") setActive(message.active) }) api.postMessage({ type: "requestModelSelectorExpanded" }) @@ -130,7 +123,7 @@ export const VSCodeProvider: ParentComponent = (props) => { getState: () => api.getState() as T | undefined, setState: (state: T) => api.setState(state), sidebarSide: side, - active: () => ready() && active(), + active, getModelSelectorExpanded: expanded, setModelSelectorExpanded: (value: boolean) => { setExpanded(value)