Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All notable changes to the "Q Commit" extension will be documented in this file.

## [0.0.3] - 2026-01-07

### Changed
- Switch from Amazon Q CLI to Kiro CLI
- Use claude-haiku-4.5 model by default (2x faster)
- Simplified prompt for faster execution

### Added
- Settings panel with configurable options:
- Model selection (haiku-4.5, sonnet-4, sonnet-4.5)
- Toggle output panel visibility
- Custom prompt support
- Keyboard shortcut: Ctrl+Shift+G (Cmd+Shift+G on Mac)
- Cancellable generation (click Cancel on notification)
- Multi-repository support (picker when multiple repos open)
- CLI installation check with friendly error message

## [0.0.2] - 2025-10-31

### Changed
Expand Down
8 changes: 6 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 31 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "q-commit",
"displayName": "Q Commit",
"description": "Generate commit messages using Amazon Q CLI",
"version": "0.0.2",
"description": "Generate commit messages using Kiro CLI",
"version": "0.0.3",
"publisher": "codeatlasdev",
"license": "MIT",
"icon": "icon.png",
Expand Down Expand Up @@ -37,6 +37,14 @@
"icon": "$(sparkle)"
}
],
"keybindings": [
{
"command": "q-commit.generate",
"key": "ctrl+shift+g",
"mac": "cmd+shift+g",
"when": "scmProvider == git"
}
],
"menus": {
"scm/title": [
{
Expand All @@ -45,6 +53,27 @@
"when": "scmProvider == git"
}
]
},
"configuration": {
"title": "Q Commit",
"properties": {
"q-commit.model": {
"type": "string",
"default": "claude-haiku-4.5",
"enum": ["claude-haiku-4.5", "claude-sonnet-4", "claude-sonnet-4.5"],
"description": "Model to use for generating commit messages"
},
"q-commit.showOutput": {
"type": "boolean",
"default": false,
"description": "Show output panel when generating commit messages"
},
"q-commit.prompt": {
"type": "string",
"default": "Run git diff --cached and reply with ONLY a conventional commit message wrapped in <commit></commit> tags. Example: <commit>feat: add login</commit>",
"description": "Custom prompt for generating commit messages"
}
}
}
},
"scripts": {
Expand Down
149 changes: 102 additions & 47 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,157 @@
import * as vscode from 'vscode';
import { exec } from 'child_process';
import { exec, ChildProcess } from 'child_process';
import { promisify } from 'util';
import { COMMIT_MESSAGE_PROMPT } from './prompts';
import { stripAnsi, extractCommitMessage } from './utils';

const execAsync = promisify(exec);
let outputChannel: vscode.OutputChannel;
let currentProcess: ChildProcess | null = null;

function getConfig() {
const config = vscode.workspace.getConfiguration('q-commit');
return {
model: config.get<string>('model', 'claude-haiku-4.5'),
showOutput: config.get<boolean>('showOutput', false),
prompt: config.get<string>(
'prompt',
'Run git diff --cached and reply with ONLY a conventional commit message wrapped in <commit></commit> tags. Example: <commit>feat: add login</commit>'
),
};
}

async function generateCommitMessage(repoPath: string): Promise<string> {
const { stdout, stderr } = await execAsync(
`q chat --no-interactive --trust-tools=fs_read,execute_bash "${COMMIT_MESSAGE_PROMPT.replace(/"/g, '\\"')}"`,
{ cwd: repoPath, maxBuffer: 10 * 1024 * 1024 }
);

if (stderr) {
outputChannel.appendLine('[STDERR] ' + stripAnsi(stderr));
async function checkCliInstalled(): Promise<boolean> {
try {
await execAsync('kiro-cli --version');
return true;
} catch {
return false;
}
}

outputChannel.appendLine('[STDOUT] Raw output:');
outputChannel.appendLine(stripAnsi(stdout));
async function generateCommitMessage(
repoPath: string,
token: vscode.CancellationToken
): Promise<string> {
const { model, prompt } = getConfig();

const message = extractCommitMessage(stdout);
return new Promise((resolve, reject) => {
const cmd = `kiro-cli chat --no-interactive --model ${model} --trust-tools=execute_bash "${prompt.replace(/"/g, '\\"')}"`;

if (!message) {
throw new Error('Could not extract commit message from output');
}
currentProcess = exec(
cmd,
{ cwd: repoPath, maxBuffer: 10 * 1024 * 1024 },
(error, stdout, stderr) => {
currentProcess = null;

if (token.isCancellationRequested) {
reject(new Error('Cancelled'));
return;
}

outputChannel.appendLine(`[INFO] Extracted message: "${message}"`);
return message;
if (error) {
reject(error);
return;
}

if (stderr) outputChannel.appendLine('[STDERR] ' + stripAnsi(stderr));
outputChannel.appendLine('[STDOUT] ' + stripAnsi(stdout));

const message = extractCommitMessage(stdout);
if (!message) {
reject(new Error('Could not extract commit message from output'));
return;
}

outputChannel.appendLine(`[INFO] Extracted: "${message}"`);
resolve(message);
}
);

token.onCancellationRequested(() => {
if (currentProcess) {
currentProcess.kill();
currentProcess = null;
}
});
});
}

async function handleGenerateCommand() {
if (currentProcess) {
vscode.window.showWarningMessage('Already generating a commit message...');
return;
}

const { showOutput } = getConfig();
outputChannel.clear();
outputChannel.show();
outputChannel.appendLine('[Q Commit] Starting...');
if (showOutput) outputChannel.show();

if (!(await checkCliInstalled())) {
vscode.window.showErrorMessage('Kiro CLI not found. Please install it first: https://kiro.dev');
return;
}

const gitExtension = vscode.extensions.getExtension('vscode.git');
if (!gitExtension) {
outputChannel.appendLine('[ERROR] Git extension not found');
vscode.window.showErrorMessage('Git extension not found');
return;
}

const git = gitExtension.exports.getAPI(1);

if (git.repositories.length === 0) {
outputChannel.appendLine('[ERROR] No git repository found');
vscode.window.showErrorMessage('No git repository found');
return;
}

const repo = git.repositories[0];
outputChannel.appendLine(`[INFO] Repository: ${repo.rootUri.fsPath}`);
let repo = git.repositories[0];
if (git.repositories.length > 1) {
type RepoItem = vscode.QuickPickItem & { repo: typeof repo };
const items: RepoItem[] = git.repositories.map((r: { rootUri: vscode.Uri }) => ({
label: r.rootUri.fsPath.split('/').pop() || '',
description: r.rootUri.fsPath,
repo: r,
}));
const selected = await vscode.window.showQuickPick(items, { placeHolder: 'Select repository' });
if (!selected) return;
repo = selected.repo;
}

if (repo.state.indexChanges.length === 0) {
outputChannel.appendLine('[WARN] No staged changes');
vscode.window.showWarningMessage('No staged changes');
return;
}

outputChannel.appendLine(`[INFO] Staged changes: ${repo.state.indexChanges.length} files`);

await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Generating commit message...',
cancellable: false,
cancellable: true,
},
async () => {
async (_, token) => {
try {
outputChannel.appendLine('[INFO] Executing Q CLI...');
const message = await generateCommitMessage(repo.rootUri.fsPath);

const message = await generateCommitMessage(repo.rootUri.fsPath, token);
repo.inputBox.value = message;
outputChannel.appendLine('[SUCCESS] Commit message generated!');
vscode.window.showInformationMessage('Commit message generated!');
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
outputChannel.appendLine('[ERROR] ' + errorMsg);
if (error instanceof Error && error.stack) {
outputChannel.appendLine('[STACK] ' + error.stack);
}
vscode.window.showErrorMessage(`Error: ${errorMsg}`);
if (error instanceof Error && error.message === 'Cancelled') return;
const msg = error instanceof Error ? error.message : 'Unknown error';
outputChannel.appendLine('[ERROR] ' + msg);
outputChannel.show();
vscode.window.showErrorMessage(`Error: ${msg}`);
}
}
);
}

export function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel('Q Commit');

const disposable = vscode.commands.registerCommand('q-commit.generate', handleGenerateCommand);

context.subscriptions.push(disposable, outputChannel);
context.subscriptions.push(
vscode.commands.registerCommand('q-commit.generate', handleGenerateCommand),
outputChannel
);
}

export function deactivate() {
if (outputChannel) {
outputChannel.dispose();
}
if (currentProcess) currentProcess.kill();
outputChannel?.dispose();
}
8 changes: 0 additions & 8 deletions src/prompts.ts

This file was deleted.

25 changes: 2 additions & 23 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,6 @@ export function stripAnsi(str: string): string {
}

export function extractCommitMessage(output: string): string | null {
const cleanOutput = stripAnsi(output);
const commitMatch = cleanOutput.match(/<commit>(.*?)<\/commit>/s);

if (commitMatch && commitMatch[1]) {
return commitMatch[1].trim();
}

// Fallback: extract after last tool output
const lines = cleanOutput.split('\n');
let lastToolIndex = -1;
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (line.includes('🛠️') || line.includes('●') || line.includes('↳') || line.includes('>')) {
lastToolIndex = i;
break;
}
}

const message = lines
.slice(lastToolIndex + 1)
.join('\n')
.trim();
return message || null;
const match = stripAnsi(output).match(/<commit>(.*?)<\/commit>/s);
return match?.[1]?.trim() || null;
}