Skip to content

feat: add commit message generation#327

Draft
markijbema wants to merge 13 commits intodevfrom
mark/feat-commit-message-generation
Draft

feat: add commit message generation#327
markijbema wants to merge 13 commits intodevfrom
mark/feat-commit-message-generation

Conversation

@markijbema
Copy link
Contributor

What

AI-powered commit message generation using Conventional Commits format. Uses small_model from the configured provider — no user model selection needed.

Three surfaces

  1. CLI commandkilo commit: generates a commit message from staged changes and commits
  2. HTTP routePOST /commit-message: backend endpoint for commit message generation
  3. VS Code SCM panel button — integrates into the Source Control view for one-click commit message generation

Architecture

Shared core module in packages/opencode/src/commit-message/ handles git context gathering (diff, staged files) and LLM-based message generation. The VS Code extension and CLI both consume this via the HTTP route.

New files

  • packages/opencode/src/commit-message/generate.ts — core generation logic
  • packages/opencode/src/commit-message/git-context.ts — git diff/staged file gathering
  • packages/opencode/src/commit-message/types.ts — shared types
  • packages/opencode/src/commit-message/index.ts — barrel export
  • packages/opencode/src/commit-message/__tests__/ — unit tests
  • packages/opencode/src/server/routes/commit-message.ts — HTTP route handler
  • packages/opencode/src/cli/cmd/commit.ts — CLI command
  • packages/kilo-vscode/src/services/commit-message/index.ts — VS Code SCM integration
  • packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts — tests
  • packages/kilo-vscode/docs/commit-message-implementation-plan.md — implementation plan
  • packages/kilo-vscode/docs/commit-message-reimplementation.md — design doc

Modified files

  • packages/opencode/src/server/server.ts — register new route
  • packages/opencode/src/index.ts — export commit-message module
  • packages/kilo-vscode/src/services/cli-backend/http-client.ts — add generateCommitMessage() method
  • packages/kilo-vscode/src/extension.ts — register SCM command
  • packages/kilo-vscode/package.json — add SCM menu contribution

private readonly baseUrl: string
private readonly authHeader: string
private readonly authUsername = "opencode"
private readonly authUsername = "kilo"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why this broke, but it seems like this was changed in the backend breaking the vscode extension.

The mockGitContext variable was declared without a type annotation,
causing TypeScript to infer a narrow type from the initial assignment
(status: "modified"). This made reassignment with status: "added"
fail with TS2322. Adding the GitContext type allows all valid status
values.
@markijbema
Copy link
Contributor Author

Run from extension or commandline:
CleanShot 2026-02-16 at 17 58 55

@markijbema markijbema marked this pull request as ready for review February 16, 2026 17:00
connectionService: KiloConnectionService,
): vscode.Disposable[] {
const command = vscode.commands.registerCommand("kilo-code.new.generateCommitMessage", async () => {
const extension = vscode.extensions.getExtension<GitExtensionExports>("vscode.git")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Ensure the built-in Git extension is activated before accessing exports

vscode.extensions.getExtension('vscode.git') can return an extension object with exports undefined until it’s activated. Consider await extension.activate() before calling extension.exports.getAPI(1) to avoid incorrectly reporting "No Git repository found".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: connectionService.getHttpClient() appears to throw, so this null-check won’t work

packages/kilo-vscode/src/services/cli-backend/connection-service.ts defines getHttpClient(): HttpClient and throws when not connected. This line will throw before reaching the if (!client) branch, so the command will likely fail without showing "Kilo backend is not connected".

Consider wrapping this call in a try/catch (or adding a non-throwing accessor) so the user gets the intended error message.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: --staged-only option is defined but not used

The command builder defines staged-only (default true), but the handler always calls generateCommitMessage({ path: process.cwd() }) without using args['staged-only']. This will surprise users and makes the option misleading.

Either wire this flag through to the underlying git-context selection logic, or remove the option until it’s supported.

function parseNameStatus(output: string): Array<{ status: string; path: string }> {
if (!output) return []
return output.split("\n").map((line) => {
const [status, ...rest] = line.split("\t")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Rename entries from git diff --name-status are not parsed correctly

For rename status lines (e.g. R100 old.ts new.ts), rest.join('\t') produces a combined string like old.ts new.ts. Downstream, this entry.path is treated as a single path (lock-file filtering, selectedFiles filtering, and git diff ... -- entry.path), which will break for renames.

Consider parsing rename lines into a structured shape (old/new), and using the new path for .path (and/or storing both).

})
}

function mapStatus(code: string): FileChange["status"] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: FileChange['status'] includes untracked but it’s never produced

mapStatus() maps ?? / ? to added, while FileChange['status'] allows untracked. If untracked is intentionally modeled, consider returning untracked for ?? and adjusting handling; otherwise consider removing untracked from the union to avoid confusion.

validator(
"json",
z.object({
path: z.string().meta({ description: "Workspace/repo path" }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Route accepts an arbitrary filesystem path from the client

This endpoint uses the caller-provided path as the repo directory for git operations. Even though this server is local + authenticated, it still broadens the API surface (any client that can reach the backend can point it at arbitrary directories).

Consider restricting path to the active instance/workspace directory (e.g. from the instance context / directory header) or validating that path is within an allowed workspace root.

@kiloconnect
Copy link
Contributor

kiloconnect bot commented Feb 16, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 0
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

CRITICAL

File Line Issue
packages/opencode/src/commit-message/git-context.ts 137 git() trims leading whitespace; can corrupt git status --porcelain parsing (fixed-column format)
packages/kilo-vscode/src/services/commit-message/index.ts 65 Potential runtime error if getHttpClient() returns undefined/null instead of throwing before client.generateCommitMessage(...)
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
(none) (n/a) (n/a)
Files Reviewed (9 files)

})

it("shows error when no git repository is found", async () => {
vi.mocked(vscode.extensions.getExtension).mockReturnValue({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Mocked vscode.git extension is missing isActive/activate, which can cause the command callback to throw

registerCommitMessageService() now checks if (!extension.isActive) await extension.activate(). In this test, the mocked extension object only provides exports, so extension.activate will be undefined and the callback will throw before reaching the "No Git repository found" branch.

Add isActive: true (or activate: vi.fn().mockResolvedValue(undefined) + isActive: false) to the mocked extension return values so the test exercises the intended path.

}),
},
} as any)
vi.mocked(mockConnectionService.getHttpClient as any).mockReturnValue(null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Test setup implies getHttpClient() returns null, but the production code assumes it throws (and never null-checks)

This test does mockReturnValue(null) and expects an error toast, but registerCommitMessageService() only handles the disconnected case via try/catch and then calls client.generateCommitMessage(...) unconditionally. If getHttpClient() ever returned null without throwing, this would become a runtime error instead of a user-facing message.

Either (a) update the test to mockImplementation(() => { throw new Error('...') }) to match the connection service contract, or (b) add an explicit if (!client) guard in the command implementation.

return
}

const path = folder.uri.fsPath
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Use the git repository root path instead of the first workspace folder

When VS Code has multiple workspace folders (or the repo is not the first folder), using workspaceFolders[0] can generate a commit message for the wrong directory. Since you already have repository.rootUri, using that is more precise.

Suggested change
const path = folder.uri.fsPath
const path = repository.rootUri.fsPath

@blacksmith-sh
Copy link

blacksmith-sh bot commented Feb 16, 2026

Found 12 test failures on Blacksmith runners:

Failures

Test View Logs
e2e/models/models-visibility.spec.ts/hiding a model removes it from the model picker View Logs
e2e/prompt/prompt.spec.ts/can send a prompt and receive a reply View Logs
e2e/session/session-undo-redo.spec.ts/
slash redo clears revert and restores latest state
View Logs
e2e/session/session-undo-redo.spec.ts/slash undo sets revert and restores prior prompt View Logs
e2e/settings/settings-models.spec.ts/hiding a model removes it from the model picker View Logs
e2e/settings/settings-models.spec.ts/
showing a hidden model restores it to the model picker
View Logs
e2e\models\models-visibility.spec.ts/hiding a model removes it from the model picker
View Logs
e2e\prompt\prompt.spec.ts/can send a prompt and receive a reply ────────────
View Logs
e2e\session\session-undo-redo.spec.ts/
slash redo clears revert and restores latest state
View Logs
e2e\session\session-undo-redo.spec.ts/
slash undo sets revert and restores prior prompt
View Logs
e2e\settings\settings-models.spec.ts/hiding a model removes it from the model picker
View Logs
e2e\settings\settings-models.spec.ts/
showing a hidden model restores it to the model picker
View Logs

Fix in Cursor

stdout: "pipe",
stderr: "pipe",
})
return result.stdout.toString().trim()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: git() trims leading whitespace, which can corrupt git status --porcelain parsing

Using packages/opencode/src/commit-message/git-context.ts:137, .trim() can remove the leading space of the first porcelain line (e.g. " M file"), shifting the fixed-column layout that parsePorcelain() relies on (slice(0,2) / slice(3)). This can drop the first character of the filepath (and then subsequent git diff -- <path> calls target the wrong file).

Suggested change
return result.stdout.toString().trim()
return result.stdout.toString().trimEnd()

.withProgress(
{ location: vscode.ProgressLocation.SourceControl, title: "Generating commit message..." },
async () => {
const message = await client.generateCommitMessage(path, undefined, previousMessage)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Possible runtime error if getHttpClient() returns undefined/null instead of throwing

packages/kilo-vscode/src/services/commit-message/index.ts:65 calls client.generateCommitMessage(...), but client is declared as HttpClient | undefined and the code only handles the throwing case. If connectionService.getHttpClient() returns undefined/null when disconnected, this will throw and show a generic failure message. Consider explicitly checking client after the call and returning early with a targeted error message.

@markijbema markijbema marked this pull request as draft February 17, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments