diff --git a/.changeset/commit-message-no-changes-error.md b/.changeset/commit-message-no-changes-error.md new file mode 100644 index 00000000000..f012b297238 --- /dev/null +++ b/.changeset/commit-message-no-changes-error.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Show a clear "No changes found to generate a commit message for" error instead of a generic "Unexpected server error" when there is nothing to commit. The endpoint now returns a typed 422, and the extension surfaces the real message directly. diff --git a/packages/kilo-vscode/src/services/commit-message/index.ts b/packages/kilo-vscode/src/services/commit-message/index.ts index ecb8cd2e4a8..fccd4062f7b 100644 --- a/packages/kilo-vscode/src/services/commit-message/index.ts +++ b/packages/kilo-vscode/src/services/commit-message/index.ts @@ -119,7 +119,7 @@ export function registerCommitMessageService( } const msg = getErrorMessage(error) console.error("[Kilo New] Failed to generate commit message:", msg) - vscode.window.showErrorMessage(`Failed to generate commit message: ${msg}`) + vscode.window.showErrorMessage(msg || "Failed to generate commit message. Please try again.") }) }, ) diff --git a/packages/opencode/src/kilocode/commit-message/generate.ts b/packages/opencode/src/kilocode/commit-message/generate.ts index c3e9b5a4dc0..6cef27efced 100644 --- a/packages/opencode/src/kilocode/commit-message/generate.ts +++ b/packages/opencode/src/kilocode/commit-message/generate.ts @@ -10,6 +10,13 @@ import { getGitContext } from "./git-context" const log = Log.create({ service: "commit-message" }) +export class NoChangesError extends Error { + constructor() { + super("No changes found to generate a commit message for") + this.name = "CommitMessageNoChanges" + } +} + export const CommitMessageRuntime = { context(repoPath: string, selectedFiles?: string[]) { return getGitContext(repoPath, selectedFiles) @@ -146,7 +153,7 @@ const TIMEOUT_MS = 30_000 export async function generateCommitMessage(request: CommitMessageRequest): Promise { const ctx = await CommitMessageRuntime.context(request.path, request.selectedFiles) if (ctx.files.length === 0) { - throw new Error("No changes found to generate a commit message for") + throw new NoChangesError() } log.info("generating", { diff --git a/packages/opencode/src/kilocode/commit-message/index.ts b/packages/opencode/src/kilocode/commit-message/index.ts index 45fbb0f73a2..7afa0f9f844 100644 --- a/packages/opencode/src/kilocode/commit-message/index.ts +++ b/packages/opencode/src/kilocode/commit-message/index.ts @@ -1,2 +1,2 @@ -export { generateCommitMessage } from "./generate" +export { generateCommitMessage, NoChangesError } from "./generate" export type { CommitMessageRequest, CommitMessageResponse, GitContext, FileChange } from "./types" diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/commit-message.ts b/packages/opencode/src/kilocode/server/httpapi/groups/commit-message.ts index cd1c611f661..dbc4a9d567e 100644 --- a/packages/opencode/src/kilocode/server/httpapi/groups/commit-message.ts +++ b/packages/opencode/src/kilocode/server/httpapi/groups/commit-message.ts @@ -24,6 +24,13 @@ const CommitMessageResponse = Schema.Struct({ message: Schema.String, }) +export class CommitMessageNoChangesError extends Schema.ErrorClass( + "CommitMessageNoChangesError", +)( + { message: Schema.String }, + { httpApiStatus: 422 }, +) {} + export const CommitMessageApi = HttpApi.make("commit-message") .add( HttpApiGroup.make("commit-message") @@ -32,7 +39,7 @@ export const CommitMessageApi = HttpApi.make("commit-message") query: WorkspaceRoutingQuery, payload: CommitMessagePayload, success: described(CommitMessageResponse, "Generated commit message"), - error: HttpApiError.BadRequest, + error: [HttpApiError.BadRequest, CommitMessageNoChangesError], }).annotateMerge( OpenApi.annotations({ identifier: "commitMessage.generate", diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/commit-message.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/commit-message.ts index d1c4366a0e2..a7e610a2d77 100644 --- a/packages/opencode/src/kilocode/server/httpapi/handlers/commit-message.ts +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/commit-message.ts @@ -3,8 +3,8 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { EffectBridge } from "@/effect/bridge" import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api" import { Config } from "@/config/config" -import { generateCommitMessage } from "@/kilocode/commit-message" -import { CommitMessagePayload } from "../groups/commit-message" +import { generateCommitMessage, NoChangesError } from "@/kilocode/commit-message" +import { CommitMessageNoChangesError, CommitMessagePayload } from "../groups/commit-message" export const commitMessageHandlers = HttpApiBuilder.group(InstanceHttpApi, "commit-message", (handlers) => Effect.gen(function* () { @@ -22,6 +22,13 @@ export const commitMessageHandlers = HttpApiBuilder.group(InstanceHttpApi, "comm previousMessage: ctx.payload.previousMessage, prompt, }), + ).pipe( + Effect.catchDefect((defect) => { + if (defect instanceof NoChangesError) { + return Effect.fail(new CommitMessageNoChangesError({ message: defect.message })) + } + return Effect.die(defect) + }), ) return { message: result.message } }) diff --git a/packages/opencode/test/kilocode/commit-message/generate.test.ts b/packages/opencode/test/kilocode/commit-message/generate.test.ts index d58873302d7..f0e0c6d63d4 100644 --- a/packages/opencode/test/kilocode/commit-message/generate.test.ts +++ b/packages/opencode/test/kilocode/commit-message/generate.test.ts @@ -42,7 +42,7 @@ mock.module("@opencode-ai/core/util/log", () => ({ }), })) -import { CommitMessageRuntime, generateCommitMessage } from "../../../src/kilocode/commit-message/generate" +import { CommitMessageRuntime, generateCommitMessage, NoChangesError } from "../../../src/kilocode/commit-message/generate" const context = spyOn(CommitMessageRuntime, "context").mockImplementation(async (repoPath, selectedFiles) => { captured = { path: repoPath, selected: selectedFiles } @@ -133,11 +133,14 @@ describe("commit-message.generate", () => { }) describe("error on no changes", () => { - test("throws when no git changes are found", async () => { + test("throws NoChangesError when no git changes are found", async () => { mockGitContext = { branch: "main", recentCommits: [], files: [] } - await expect(generateCommitMessage({ path: "/repo" })).rejects.toThrow( - "No changes found to generate a commit message for", - ) + const err = await generateCommitMessage({ path: "/repo" }).catch((e) => e) + expect(err).toBeInstanceOf(NoChangesError) + if (err instanceof NoChangesError) { + expect(err.message).toBe("No changes found to generate a commit message for") + expect(err.name).toBe("CommitMessageNoChanges") + } }) }) diff --git a/packages/opencode/test/kilocode/server/commit-message-no-changes.test.ts b/packages/opencode/test/kilocode/server/commit-message-no-changes.test.ts new file mode 100644 index 00000000000..b511d620108 --- /dev/null +++ b/packages/opencode/test/kilocode/server/commit-message-no-changes.test.ts @@ -0,0 +1,24 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { Server } from "../../../src/server/server" +import { resetDatabase } from "../../fixture/db" +import { disposeAllInstances, tmpdir } from "../../fixture/fixture" + +afterEach(async () => { + await disposeAllInstances() + await resetDatabase() +}) + +describe("commit-message httpapi", () => { + test("returns 422 with the real message when there are no changes", async () => { + await using tmp = await tmpdir({ git: true }) + + const res = await Server.Default().app.request("/commit-message", { + method: "POST", + headers: { "content-type": "application/json", "x-kilo-directory": tmp.path }, + body: JSON.stringify({ path: tmp.path }), + }) + + expect(res.status).toBe(422) + expect(await res.json()).toEqual({ message: "No changes found to generate a commit message for" }) + }) +}) diff --git a/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts b/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts index 061f268b62b..635329314cd 100644 --- a/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts +++ b/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts @@ -465,6 +465,16 @@ export const kiloScenarios: Scenario[] = [ .post("/commit-message", "commitMessage.generate") .at((ctx) => ({ path: "/commit-message", headers: ctx.headers(), body: {} })) .status(400), + http.protected + .post("/commit-message", "commitMessage.generate") + .at((ctx) => ({ path: "/commit-message", headers: ctx.headers(), body: { path: directory(ctx) } })) + .json(422, (body) => { + object(body) + check( + body.message === "No changes found to generate a commit message for", + "no changes should surface a real 422 message, not a masked 500", + ) + }), http.protected .post("/session/{sessionID}/branch-name", "branchName.generate") .at((ctx) => ({ diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0e9eb535849..f504dbb00c7 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1693,6 +1693,7 @@ export type Config = { continue_loop_on_deny?: boolean sandbox?: boolean sandbox_restrict_network?: boolean + sandbox_writable_paths?: Array mcp_timeout?: number policies?: Array } @@ -2412,6 +2413,10 @@ export type BackgroundProcessLogs = { output: string } +export type CommitMessageNoChangesError = { + message: string +} + export type ConfigOverlayResponse = { scope: "global" | "project" effective: Config @@ -10101,6 +10106,10 @@ export type CommitMessageGenerateErrors = { * BadRequest | InvalidRequestError */ 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * CommitMessageNoChangesError + */ + 422: CommitMessageNoChangesError } export type CommitMessageGenerateError = CommitMessageGenerateErrors[keyof CommitMessageGenerateErrors]