From 74f07f0a4b21a71d68c0e7c4b126f6a597eeeb1a Mon Sep 17 00:00:00 2001 From: Matheus de Jesus Date: Wed, 7 Jan 2026 22:56:24 -0300 Subject: [PATCH] feat: migrate to kiro-cli with settings and improvements - Switch from Amazon Q CLI to Kiro CLI - Add settings: model, showOutput, custom prompt - Add keybinding Ctrl+Shift+G - Add cancellation support - Add multi-repo picker - Add CLI installation check - Use claude-haiku-4.5 by default (2x faster) --- CHANGELOG.md | 17 ++++++ package-lock.json | 8 ++- package.json | 33 +++++++++- src/extension.ts | 149 +++++++++++++++++++++++++++++++--------------- src/prompts.ts | 8 --- src/utils.ts | 25 +------- 6 files changed, 158 insertions(+), 82 deletions(-) delete mode 100644 src/prompts.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 67db105..22d9ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 86e437f..e9da605 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "q-commit", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "q-commit", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "devDependencies": { "@types/node": "^20.x", @@ -676,6 +676,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -845,6 +846,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1583,6 +1585,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4499,6 +4502,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index caa7ba0..944b380 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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": [ { @@ -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 tags. Example: feat: add login", + "description": "Custom prompt for generating commit messages" + } + } } }, "scripts": { diff --git a/src/extension.ts b/src/extension.ts index ddbe90d..78281af 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,87 +1,143 @@ 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('model', 'claude-haiku-4.5'), + showOutput: config.get('showOutput', false), + prompt: config.get( + 'prompt', + 'Run git diff --cached and reply with ONLY a conventional commit message wrapped in tags. Example: feat: add login' + ), + }; +} -async function generateCommitMessage(repoPath: string): Promise { - 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 { + 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 { + 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}`); } } ); @@ -89,14 +145,13 @@ async function handleGenerateCommand() { 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(); } diff --git a/src/prompts.ts b/src/prompts.ts deleted file mode 100644 index c6f92e9..0000000 --- a/src/prompts.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const COMMIT_MESSAGE_PROMPT = `You are a VS Code extension that generates git commit messages. - -Analyze the staged changes using git diff --cached and generate a concise commit message following conventional commits format. - -IMPORTANT: You MUST wrap your final commit message in tags like this: -feat: add new feature - -Return ONLY the commit message wrapped in tags, nothing else after the tags.`; diff --git a/src/utils.ts b/src/utils.ts index d433f9a..95b9db2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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>/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>/s); + return match?.[1]?.trim() || null; }