diff --git a/.changeset/wise-kites-smile.md b/.changeset/wise-kites-smile.md new file mode 100644 index 00000000000..4630adf3c9f --- /dev/null +++ b/.changeset/wise-kites-smile.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix stale indexing status updates in the VS Code extension on Windows so Settings and the chat indicator refresh while indexing progresses. Directory matching for indexing SSE events now handles Windows path casing, and indexing status pushes are no longer dropped if they arrive before the initial config payload. diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index cf3c33479e2..8d9c6f2defa 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -39,6 +39,7 @@ import { resolveContextDirectory, resolveNewSessionDirectory, resolveWorkspaceDirectory, + sameDirectory, SessionStreamScheduler, type SessionRefreshContext, } from "./kilo-provider-utils" @@ -3085,7 +3086,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (event.type === "server.instance.disposed") { const props = event.properties as Record | null const dir = typeof props?.directory === "string" ? props.directory : undefined - if (dir && path.resolve(dir) !== path.resolve(this.getWorkspaceDirectory())) return + if (dir && !sameDirectory(dir, this.getWorkspaceDirectory())) return void this.reloadAfterAuthChange() return } @@ -3131,8 +3132,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper handleNetworkEvent(event.type as string, event.properties as any, this.client, (s) => this.getWorkspaceDirectory(s)) if (event.type === "indexing.status" && directory) { - const current = path.resolve(this.getWorkspaceDirectory(this.currentSession?.id)) - if (path.resolve(directory) !== current) return + if (!sameDirectory(directory, this.getWorkspaceDirectory(this.currentSession?.id))) return } const msg = mapSSEEventToWebviewMessage(event, sessionID) diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index caa1f565e18..d0d7df5ff1e 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -3,6 +3,7 @@ import { prettifyError } from "zod/v4" import type { CloudSessionMessage, IndexingStatus } from "./services/cli-backend/types" import type { PartBatch, PartUpdate } from "./kilo-provider/session-stream-scheduler" import type { PartRemove } from "./shared/stream-messages" +import * as path from "path" export { SessionStreamScheduler } from "./kilo-provider/session-stream-scheduler" @@ -359,6 +360,17 @@ export function resolveNewSessionDirectory(input: { }) } +export function sameDirectory(a: string, b: string): boolean { + if (!a || !b) return false + + const left = path.resolve(a) + const right = path.resolve(b) + if (path.relative(left, right) === "") return true + + if (process.platform !== "win32") return false + return path.relative(left.toLowerCase(), right.toLowerCase()) === "" +} + export type WebviewMessage = | PartUpdate | PartBatch diff --git a/packages/kilo-vscode/tests/unit/indexing-utils.test.ts b/packages/kilo-vscode/tests/unit/indexing-utils.test.ts index 8405bf23f39..cd91df1ecff 100644 --- a/packages/kilo-vscode/tests/unit/indexing-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/indexing-utils.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "bun:test" -import { formatIndexingLabel, indexingTone } from "../../webview-ui/src/context/indexing-utils" +import { + applyIndexingStatusMessage, + formatIndexingLabel, + indexingTone, +} from "../../webview-ui/src/context/indexing-utils" import { mapSSEEventToWebviewMessage } from "../../src/kilo-provider-utils" import { configFeatures } from "../../src/features" import type { EventIndexingStatus, IndexingStatus } from "@kilocode/sdk/v2/client" @@ -120,3 +124,54 @@ describe("indexing feature detection", () => { expect(configFeatures({}).indexing).toBe(false) }) }) + +describe("indexing status message handling", () => { + it("applies indexing status regardless of feature-toggle race", () => { + let status = makeStatus() + let loading = true + const applied = applyIndexingStatusMessage( + { + type: "indexingStatusLoaded", + status: makeStatus({ + state: "In Progress", + message: "Indexing", + processedFiles: 5, + totalFiles: 20, + percent: 25, + }), + }, + (next) => { + status = next + }, + (next) => { + loading = next + }, + ) + + expect(applied).toBe(true) + expect(status.state).toBe("In Progress") + expect(status.percent).toBe(25) + expect(loading).toBe(false) + }) + + it("ignores unrelated extension messages", () => { + let status = makeStatus() + let loading = true + const applied = applyIndexingStatusMessage( + { + type: "connectionState", + state: "connected", + }, + (next) => { + status = next + }, + (next) => { + loading = next + }, + ) + + expect(applied).toBe(false) + expect(status.state).toBe("Disabled") + expect(loading).toBe(true) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-indexing-refresh.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-indexing-refresh.test.ts index a8b1981daa4..e3854e53015 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-indexing-refresh.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-indexing-refresh.test.ts @@ -7,6 +7,8 @@ const { KiloProvider } = await import("../../src/KiloProvider") type Internals = { connectionState: "connecting" | "connected" | "disconnected" | "error" currentSession: { id: string } | null + cachedIndexingStatusMessage: unknown + handleEvent: (event: unknown, directory?: string) => void reloadAfterAuthChange: () => Promise handleUpdateConfig: (partial: Partial) => Promise fetchAndSendConfig: () => Promise @@ -140,4 +142,42 @@ describe("KiloProvider indexing refresh", () => { globalThis.fetch = original } }) + + it("forwards indexing.status when directory only differs by Windows drive casing", () => { + const provider = new KiloProvider( + {} as never, + { + resolveEventSessionId: () => undefined, + } as never, + ) + const internal = provider as unknown as Internals + provider.setSessionDirectory("ses_worktree", "C:/Repo/Work") + internal.currentSession = { id: "ses_worktree" } + + const desc = Object.getOwnPropertyDescriptor(process, "platform") + Object.defineProperty(process, "platform", { value: "win32", configurable: true }) + try { + internal.handleEvent( + { + type: "indexing.status", + properties: { + status: { + state: "Complete", + message: "Done", + processedFiles: 10, + totalFiles: 10, + percent: 100, + }, + }, + }, + "c:/repo/work", + ) + } finally { + if (desc) Object.defineProperty(process, "platform", desc) + } + + const msg = internal.cachedIndexingStatusMessage as { type?: string; status?: { state?: string } } | undefined + expect(msg?.type).toBe("indexingStatusLoaded") + expect(msg?.status?.state).toBe("Complete") + }) }) diff --git a/packages/kilo-vscode/tests/unit/sameDirectory.test.ts b/packages/kilo-vscode/tests/unit/sameDirectory.test.ts new file mode 100644 index 00000000000..cb864a7003a --- /dev/null +++ b/packages/kilo-vscode/tests/unit/sameDirectory.test.ts @@ -0,0 +1,31 @@ +import { afterEach, describe, expect, it } from "bun:test" +import { sameDirectory } from "../../src/kilo-provider-utils" + +const platform = Object.getOwnPropertyDescriptor(process, "platform") + +function setPlatform(value: string) { + Object.defineProperty(process, "platform", { value, configurable: true }) +} + +afterEach(() => { + if (platform) Object.defineProperty(process, "platform", platform) +}) + +describe("sameDirectory", () => { + it("matches identical paths", () => { + expect(sameDirectory("/repo/pkg", "/repo/pkg")).toBe(true) + }) + + it("matches trailing slash differences", () => { + expect(sameDirectory("/repo/pkg", "/repo/pkg/")).toBe(true) + }) + + it("matches Windows drive-letter case differences", () => { + setPlatform("win32") + expect(sameDirectory("C:/Repo/Work", "c:/repo/work")).toBe(true) + }) + + it("returns false for different directories", () => { + expect(sameDirectory("/repo/pkg-a", "/repo/pkg-b")).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/context/indexing-utils.ts b/packages/kilo-vscode/webview-ui/src/context/indexing-utils.ts index 59def4fc6fb..5f045684e41 100644 --- a/packages/kilo-vscode/webview-ui/src/context/indexing-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/indexing-utils.ts @@ -1,4 +1,4 @@ -import type { IndexingStatus } from "../types/messages" +import type { ExtensionMessage, IndexingStatus } from "../types/messages" export function formatIndexingLabel(status: IndexingStatus): string { if (status.state === "In Progress") { @@ -24,3 +24,14 @@ export function indexingTone(status: IndexingStatus): "muted" | "warning" | "suc if (status.state === "Standby") return "muted" return "muted" } + +export function applyIndexingStatusMessage( + message: ExtensionMessage, + setStatus: (status: IndexingStatus) => void, + setLoading: (value: boolean) => void, +): boolean { + if (message.type !== "indexingStatusLoaded") return false + setStatus(message.status) + setLoading(false) + return true +} diff --git a/packages/kilo-vscode/webview-ui/src/context/indexing.tsx b/packages/kilo-vscode/webview-ui/src/context/indexing.tsx index 227a51d3cdf..cce967f7d09 100644 --- a/packages/kilo-vscode/webview-ui/src/context/indexing.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/indexing.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, createMemo, createSignal, onCleanup, createE import type { ParentComponent, Accessor } from "solid-js" import { useVSCode } from "./vscode" import type { ExtensionMessage, IndexingStatus } from "../types/messages" -import { formatIndexingLabel, indexingTone } from "./indexing-utils" +import { applyIndexingStatusMessage, formatIndexingLabel, indexingTone } from "./indexing-utils" import { useConfig } from "./config" interface IndexingContextValue { @@ -31,10 +31,7 @@ export const IndexingProvider: ParentComponent = (props) => { const [loading, setLoading] = createSignal(true) const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { - if (!features().indexing) return - if (message.type !== "indexingStatusLoaded") return - setStatus(message.status) - setLoading(false) + applyIndexingStatusMessage(message, setStatus, setLoading) }) onCleanup(unsubscribe)