From 1f03f6eaf5f0e6ade1ee50b2049c3b3bce6e4bba Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:26:31 +0000 Subject: [PATCH 1/7] feat(kilo-vscode): auto-connect to Kilo backend when client is missing move repository path initialization earlier and attempt backend connect using repo path as workspace dir when client retrieval fails --- .../src/services/commit-message/index.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/kilo-vscode/src/services/commit-message/index.ts b/packages/kilo-vscode/src/services/commit-message/index.ts index 4c57712edcc..f855731b1b4 100644 --- a/packages/kilo-vscode/src/services/commit-message/index.ts +++ b/packages/kilo-vscode/src/services/commit-message/index.ts @@ -53,21 +53,27 @@ export function registerCommitMessageService( return } + const path = repository.rootUri.fsPath + let client: KiloClient | undefined try { client = connectionService.getClient() - } catch (err) { - console.error("[Kilo New] Failed to get client:", err) - vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") - return + } catch { + // Backend not yet started — connect now using the repository path as the workspace dir. + try { + await connectionService.connect(path) + client = connectionService.getClient() + } catch (err) { + console.error("[Kilo New] Failed to connect to Kilo backend:", err) + vscode.window.showErrorMessage("Failed to connect to Kilo backend. Please try again.") + return + } } if (!client) { vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") return } - const path = repository.rootUri.fsPath - const previousMessage = lastWorkspacePath === path ? lastGeneratedMessage : undefined const controller = new AbortController() From ca4cfb0c59ee6d34b38488b239bc8bfb9bcc71d2 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:48:54 +0000 Subject: [PATCH 2/7] test(vscode): update commit message tests for auto-connect behaviour - Add connect() mock to the shared mockConnectionService - Replace old 'not connected' error test with one that verifies the error shown when connect() itself fails - Add new test covering successful auto-connect + message generation - Fix pre-existing broken token mock missing onCancellationRequested in the matching/fallback repository tests --- .../commit-message/__tests__/index.spec.ts | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts index 124002d8028..d72b86015f9 100644 --- a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts +++ b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts @@ -55,6 +55,7 @@ describe("commit-message service", () => { mockConnectionService = { getClient: vi.fn().mockReturnValue(mockClient), + connect: vi.fn().mockResolvedValue(undefined), } as any }) @@ -115,7 +116,7 @@ describe("commit-message service", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("No Git repository found") }) - it("shows error when backend is not connected", async () => { + it("shows error when backend fails to connect", async () => { ;(vscode.extensions.getExtension as Mock).mockReturnValue({ isActive: true, activate: vi.fn().mockResolvedValue(undefined), @@ -128,14 +129,44 @@ describe("commit-message service", () => { ;(mockConnectionService.getClient as Mock).mockImplementation(() => { throw new Error("Not connected") }) + ;(mockConnectionService.connect as Mock).mockRejectedValue(new Error("Connect failed")) await commandCallback() expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - "Kilo backend is not connected. Please wait for the connection to establish.", + "Failed to connect to Kilo backend. Please try again.", ) }) + it("auto-connects backend and generates message when client not yet ready", async () => { + const mockInputBox = { value: "" } + ;(vscode.extensions.getExtension as Mock).mockReturnValue({ + isActive: true, + activate: vi.fn().mockResolvedValue(undefined), + exports: { + getAPI: () => ({ + repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/auto-connect-repo" } }], + }), + }, + }) + // First call throws (not started), after connect() succeeds the second call returns the client + ;(mockConnectionService.getClient as Mock) + .mockImplementationOnce(() => { + throw new Error("Not connected") + }) + .mockReturnValue(mockClient) + + const mockToken = { onCancellationRequested: vi.fn() } + ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { + await task({}, mockToken) + }) + + await commandCallback() + + expect(mockConnectionService.connect).toHaveBeenCalledWith("/auto-connect-repo") + expect(mockInputBox.value).toBe("feat: add new feature") + }) + it("calls commitMessage.generate on the SDK client with repository root path", async () => { const mockInputBox = { value: "" } ;(vscode.extensions.getExtension as Mock).mockReturnValue({ @@ -229,7 +260,7 @@ describe("commit-message service", () => { } as any) vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { - await task({} as any, {} as any) + await task({} as any, { onCancellationRequested: vi.fn() } as any) }) // Simulate SCM title/input passing the SourceControl for the worktree repo @@ -254,7 +285,7 @@ describe("commit-message service", () => { } as any) vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { - await task({} as any, {} as any) + await task({} as any, { onCancellationRequested: vi.fn() } as any) }) const scmArg = { rootUri: { fsPath: "/nonexistent-repo" } } as vscode.SourceControl From bedd10297c2005b8f355547bfe2d489eb4cb2817 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:00:31 +0000 Subject: [PATCH 3/7] feat(vscode): add getClientAsync() to KiloConnectionService for lazy auto-connect Adds a getClientAsync() method that auto-connects using the first VS Code workspace folder if the backend is not yet started, eliminating the need for callers to manually call connect() before getClient(). --- .../src/services/cli-backend/connection-service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 beb7fe6ad82..1eff5a586bd 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts @@ -91,6 +91,19 @@ export class KiloConnectionService { return this.client } + /** + * Get the shared SDK client, auto-connecting if not yet started. + * Uses the first VS Code workspace folder as the working directory. + * Throws if no workspace is open or if the connection fails. + */ + async getClientAsync(): Promise { + if (this.client) return this.client + const dir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + if (!dir) throw new Error("No workspace folder open") + await this.connect(dir) + return this.client! + } + /** * Get server info (port). Returns null if not connected. */ From 0a35b3a9e1e67fb5ffda45f2c81832603ecfc6a1 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:00:36 +0000 Subject: [PATCH 4/7] refactor(vscode): simplify commit message command to use getClientAsync() Replace manual connect() + getClient() try/catch with a single getClientAsync() call, which handles lazy startup internally. --- .../commit-message/__tests__/index.spec.ts | 16 +++----------- .../src/services/commit-message/index.ts | 21 +++++-------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts index d72b86015f9..f35427f1513 100644 --- a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts +++ b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts @@ -54,8 +54,7 @@ describe("commit-message service", () => { } mockConnectionService = { - getClient: vi.fn().mockReturnValue(mockClient), - connect: vi.fn().mockResolvedValue(undefined), + getClientAsync: vi.fn().mockResolvedValue(mockClient), } as any }) @@ -126,10 +125,7 @@ describe("commit-message service", () => { }), }, }) - ;(mockConnectionService.getClient as Mock).mockImplementation(() => { - throw new Error("Not connected") - }) - ;(mockConnectionService.connect as Mock).mockRejectedValue(new Error("Connect failed")) + ;(mockConnectionService.getClientAsync as Mock).mockRejectedValue(new Error("Connect failed")) await commandCallback() @@ -149,12 +145,6 @@ describe("commit-message service", () => { }), }, }) - // First call throws (not started), after connect() succeeds the second call returns the client - ;(mockConnectionService.getClient as Mock) - .mockImplementationOnce(() => { - throw new Error("Not connected") - }) - .mockReturnValue(mockClient) const mockToken = { onCancellationRequested: vi.fn() } ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { @@ -163,7 +153,7 @@ describe("commit-message service", () => { await commandCallback() - expect(mockConnectionService.connect).toHaveBeenCalledWith("/auto-connect-repo") + expect(mockConnectionService.getClientAsync).toHaveBeenCalled() expect(mockInputBox.value).toBe("feat: add new feature") }) diff --git a/packages/kilo-vscode/src/services/commit-message/index.ts b/packages/kilo-vscode/src/services/commit-message/index.ts index f855731b1b4..71a7796cb1e 100644 --- a/packages/kilo-vscode/src/services/commit-message/index.ts +++ b/packages/kilo-vscode/src/services/commit-message/index.ts @@ -1,5 +1,4 @@ import * as vscode from "vscode" -import type { KiloClient } from "@kilocode/sdk/v2/client" import type { KiloConnectionService } from "../cli-backend/connection-service" import { getErrorMessage } from "../../kilo-provider-utils" @@ -55,22 +54,12 @@ export function registerCommitMessageService( const path = repository.rootUri.fsPath - let client: KiloClient | undefined + let client: KiloClient try { - client = connectionService.getClient() - } catch { - // Backend not yet started — connect now using the repository path as the workspace dir. - try { - await connectionService.connect(path) - client = connectionService.getClient() - } catch (err) { - console.error("[Kilo New] Failed to connect to Kilo backend:", err) - vscode.window.showErrorMessage("Failed to connect to Kilo backend. Please try again.") - return - } - } - if (!client) { - vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") + client = await connectionService.getClientAsync() + } catch (err) { + console.error("[Kilo New] Failed to connect to Kilo backend:", err) + vscode.window.showErrorMessage("Failed to connect to Kilo backend. Please try again.") return } From 1ecd016b936b626ba3604b441c377cfb104545db Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:01:51 +0000 Subject: [PATCH 5/7] fix(vscode): remove unused KiloClient type annotation after getClientAsync refactor --- packages/kilo-vscode/src/services/commit-message/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/services/commit-message/index.ts b/packages/kilo-vscode/src/services/commit-message/index.ts index 71a7796cb1e..bc54384c079 100644 --- a/packages/kilo-vscode/src/services/commit-message/index.ts +++ b/packages/kilo-vscode/src/services/commit-message/index.ts @@ -54,7 +54,7 @@ export function registerCommitMessageService( const path = repository.rootUri.fsPath - let client: KiloClient + let client try { client = await connectionService.getClientAsync() } catch (err) { From a12ae3fa849c6cb57813fe03dff1dd1b582ba73a Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:55:31 +0000 Subject: [PATCH 6/7] fix(vscode): accept optional dir in getClientAsync, pass repo path from commit message command getClientAsync() now accepts an optional directory parameter so callers that already know their working directory (e.g. the SCM commit message command, which resolves it from the Git extension) can pass it directly. This fixes a regression where single-file Git windows with no workspace folders would fail because workspaceFolders[0] was undefined. --- .../src/services/cli-backend/connection-service.ts | 13 +++++++------ .../src/services/commit-message/index.ts | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) 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 1eff5a586bd..155790ac6e1 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts @@ -93,14 +93,15 @@ export class KiloConnectionService { /** * Get the shared SDK client, auto-connecting if not yet started. - * Uses the first VS Code workspace folder as the working directory. - * Throws if no workspace is open or if the connection fails. + * Accepts an optional directory to use as the workspace root; falls back + * to the first VS Code workspace folder. Throws if neither is available + * or if the connection fails. */ - async getClientAsync(): Promise { + async getClientAsync(dir?: string): Promise { if (this.client) return this.client - const dir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath - if (!dir) throw new Error("No workspace folder open") - await this.connect(dir) + const root = dir ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + if (!root) throw new Error("No workspace folder open") + await this.connect(root) return this.client! } diff --git a/packages/kilo-vscode/src/services/commit-message/index.ts b/packages/kilo-vscode/src/services/commit-message/index.ts index bc54384c079..bdd397327b4 100644 --- a/packages/kilo-vscode/src/services/commit-message/index.ts +++ b/packages/kilo-vscode/src/services/commit-message/index.ts @@ -56,7 +56,7 @@ export function registerCommitMessageService( let client try { - client = await connectionService.getClientAsync() + client = await connectionService.getClientAsync(path) } catch (err) { console.error("[Kilo New] Failed to connect to Kilo backend:", err) vscode.window.showErrorMessage("Failed to connect to Kilo backend. Please try again.") From ce642ac348b54cb3c3ec03578f5a5b4006ae9a75 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Mon, 13 Apr 2026 14:14:11 +0200 Subject: [PATCH 7/7] refactor(vscode): use getClientAsync in AutocompleteModel instead of manual state checks --- packages/kilo-vscode/eslint.config.mjs | 2 +- .../autocomplete/AutocompleteModel.ts | 19 ++++------- .../__tests__/AutocompleteModel.spec.ts | 34 ++++++++++++------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs index f82b5a9d931..f0d48197663 100644 --- a/packages/kilo-vscode/eslint.config.mjs +++ b/packages/kilo-vscode/eslint.config.mjs @@ -34,7 +34,7 @@ export default [ { files: ["src/KiloProvider.ts"], rules: { - "max-lines": ["error", 3200], + "max-lines": ["error", 3400], }, }, eslintConfigPrettier, diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts index ea0570c216f..8fd77bd4a49 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts @@ -54,12 +54,7 @@ export class AutocompleteModel { throw new Error("Connection service is not available") } - const state = this.connectionService.getConnectionState() - if (state !== "connected") { - throw new Error(`CLI backend is not connected (state: ${state})`) - } - - const client = this.connectionService.getClient() + const client = await this.connectionService.getClientAsync() let cost = 0 let inputTokens = 0 @@ -146,13 +141,13 @@ export class AutocompleteModel { * Returns false on any error (not connected, fetch failed, etc.). */ public async hasBalance(): Promise { - if (!this.connectionService || this.connectionService.getConnectionState() !== "connected") { + if (!this.connectionService) return false + try { + const client = await this.connectionService.getClientAsync() + const result = await client.kilo.profile().catch(() => null) + return (result?.data?.balance?.balance ?? 0) > 0 + } catch { return false } - const result = await this.connectionService - .getClient() - .kilo.profile() - .catch(() => null) - return (result?.data?.balance?.balance ?? 0) > 0 } } diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts index d94bcd0612e..e98fc60cc67 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts +++ b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts @@ -1,20 +1,30 @@ -import { describe, it, expect, vi } from "vitest" +import { describe, it, expect, vi, beforeEach } from "vitest" import { AutocompleteModel } from "../AutocompleteModel" import type { KiloConnectionService } from "../../cli-backend" +const mockClient = { + kilo: { + fim: vi.fn(), + }, +} + function createMockConnectionService(state: "connecting" | "connected" | "disconnected" | "error" = "connected") { return { getConnectionState: vi.fn().mockReturnValue(state), - getClient: vi.fn().mockReturnValue({ - kilo: { - fim: vi.fn(), - }, - }), + getClient: vi.fn().mockReturnValue(mockClient), + getClientAsync: + state === "connected" + ? vi.fn().mockResolvedValue(mockClient) + : vi.fn().mockRejectedValue(new Error(`CLI backend is not connected (state: ${state})`)), onStateChange: vi.fn().mockReturnValue(() => {}), } as unknown as KiloConnectionService } describe("AutocompleteModel", () => { + beforeEach(() => { + mockClient.kilo.fim.mockReset() + }) + describe("constructor", () => { it("defaults profileName and profileType to null", () => { const model = new AutocompleteModel() @@ -98,7 +108,7 @@ describe("AutocompleteModel", () => { const connection = createMockConnectionService("disconnected") const model = new AutocompleteModel(connection) await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow( - "CLI backend is not connected (state: disconnected)", + "CLI backend is not connected", ) }) @@ -113,8 +123,7 @@ describe("AutocompleteModel", () => { ] const connection = createMockConnectionService("connected") - const client = (connection as any).getClient() - client.kilo.fim.mockResolvedValue({ + mockClient.kilo.fim.mockResolvedValue({ stream: (async function* () { for (const chunk of chunks) yield chunk })(), @@ -136,8 +145,7 @@ describe("AutocompleteModel", () => { it("passes model parameters to fim call", async () => { const connection = createMockConnectionService("connected") - const client = (connection as any).getClient() - client.kilo.fim.mockResolvedValue({ + mockClient.kilo.fim.mockResolvedValue({ stream: (async function* () {})(), }) @@ -145,7 +153,7 @@ describe("AutocompleteModel", () => { const signal = new AbortController().signal await model.generateFimResponse("pre", "suf", vi.fn(), signal) - expect(client.kilo.fim).toHaveBeenCalledWith( + expect(mockClient.kilo.fim).toHaveBeenCalledWith( { prefix: "pre", suffix: "suf", @@ -153,7 +161,7 @@ describe("AutocompleteModel", () => { maxTokens: 256, temperature: 0.2, }, - { signal }, + expect.objectContaining({ signal }), ) }) })