20 Commits

Author SHA1 Message Date
hykocx e309fa3acd chore: bump version to 1.1.4 2026-04-24 12:38:38 -04:00
hykocx c870350c11 chore(config): update vscodeignore to exclude additional build artifacts 2026-04-24 12:38:01 -04:00
hykocx f823dc4803 docs(readme): update banner image and replace asset file
- update README.md to reference new preview.jpg instead of git-banner.png
- add assets/preview.jpg as replacement banner image
- remove deprecated assets/git-banner.png
2026-04-24 12:32:06 -04:00
hykocx cbceea72fa docs(readme): update description and settings table to reflect new prompt version
- update diff description to mention file summary context
- replace `zemit.commitStyle` setting with `zemit.promptVersion` in settings table
2026-04-24 12:26:06 -04:00
hykocx 4471c2b2b2 refactor(git,commitGenerator): add git context with stat and improve prompt structure
- introduce `getGitContext` returning both diff and name-status stat output
- update `generateForRepository` and `performGeneration` to consume new context
- prefix prompt with changed files stat block when available
- replace `commitStyle` config key with `promptVersion` using `LATEST_PROMPT_VERSION`
- fix first-commit diff logic to use else-branch instead of fallthrough checks
- update extension icon
2026-04-24 12:22:01 -04:00
hykocx f5f6a2f703 chore: bump version to 1.1.4 and update dependencies
- upgrade @vscode/vsce from 3.8.1 to 3.9.1 (drops node >= 22 requirement to >= 20)
- rename config key `zemit.commitStyle` to `zemit.promptVersion` with new enum values `zemit-v1`/`zemit-v2`
- bump package version from 1.1.3 to 1.1.4
2026-04-24 12:21:53 -04:00
hykocx f63b6770d2 docs(assets): update git banner and app icon images 2026-04-15 18:12:09 -04:00
hykocx a90ab0eca3 docs(readme): add banner image and fix asset base urls for packaging 2026-04-15 18:04:57 -04:00
hykocx 1bdd31ae0c chore: bump version to 1.1.3 2026-04-15 17:58:59 -04:00
hykocx 0697e279ca refactor(prompts): extract prompts to dedicated module and enhance instructions 2026-04-15 17:58:45 -04:00
hykocx 718cdf87c9 chore: update icon image 2026-04-15 16:04:07 -04:00
hykocx bab40a8304 chore: bump version to 1.1.2 and update description 2026-04-15 15:15:03 -04:00
hykocx 102861ccfa chore: update icon image 2026-04-15 15:14:57 -04:00
hykocx 76aa5b0444 docs: shorten description in README and package.json 2026-04-15 14:58:15 -04:00
hykocx 7839a27d3a refactor: rename config namespace from aiCommit to zemit 2026-04-15 14:45:35 -04:00
hykocx 1f9ed36194 refactor: translate UI messages to French 2026-04-15 14:42:57 -04:00
hykocx 4f59c77e8f chore: bump version from 1.1.0 to 1.1.1 2026-04-15 14:38:55 -04:00
hykocx a102b0d714 refactor: rename command and config prefix from aiCommit to zemit 2026-04-15 14:34:55 -04:00
hykocx b9e0273d57 chore: update default model to claude-sonnet-4-6 2026-04-15 14:32:23 -04:00
hykocx 7c4f5a9189 docs: add marketplace installation instructions to README 2026-04-15 14:30:31 -04:00
11 changed files with 203 additions and 101 deletions
+7 -3
View File
@@ -1,6 +1,10 @@
src/**
node_modules/**
.vscode/**
.git/**
.gitignore
node_modules/**
*.vsix
assets/**
src/**
tsconfig.json
cline-main/**
**/*.map
package-lock.json
+20 -9
View File
@@ -1,10 +1,12 @@
# Zemit
Génère des messages de commit dans VSCode à partir du diff en cours, via un modèle d'IA.
Génère des messages de commit, via un modèle d'IA, directement dans VSCode.
![preview](/assets/preview.jpg)
## Ce que ça fait
Zemit lit le diff stagé dans ton dépôt Git et envoie le contenu à un modèle d'IA pour produire un message de commit. Le message apparaît directement dans le champ de saisie du panneau Source Control.
Zemit lit le diff stagé et le résumé des fichiers modifiés dans ton dépôt Git, puis envoie ce contexte à un modèle d'IA pour produire un message de commit. Le message apparaît directement dans le champ de saisie du panneau Source Control.
Tu peux interrompre la génération à tout moment depuis le même panneau.
@@ -16,8 +18,17 @@ Tu peux interrompre la génération à tout moment depuis le même panneau.
## Installation
### Via le marketplace (recommandé)
Recherche **Zemit** dans l'onglet Extensions de ton éditeur, ou installe directement depuis :
- **VS Code** : [marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=hykocx.zemit)
- **VSCodium / Open VSX** : [open-vsx.org](https://open-vsx.org/extension/hykocx/zemit)
### Via un fichier VSIX
1. Télécharge le fichier `.vsix` depuis les [releases](https://git.hyko.cx/hykocx/zemit/releases).
2. Dans VS Code, ouvre la palette de commandes (`Ctrl+Shift+P` / `Cmd+Shift+P`).
2. Ouvre la palette de commandes (`Ctrl+Shift+P` / `Cmd+Shift+P`).
3. Cherche **Extensions: Installer depuis un fichier VSIX...** et sélectionne le fichier téléchargé.
Ou via le menu **Extensions** (icône en barre latérale) → `...`**Installer depuis un fichier VSIX...**
@@ -28,12 +39,12 @@ Les paramètres se trouvent dans les préférences VS Code sous **Zemit**.
| Paramètre | Description | Défaut |
|---|---|---|
| `aiCommit.provider` | Fournisseur d'IA : `anthropic`, `openai` ou `ollama` | `anthropic` |
| `aiCommit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ |
| `aiCommit.model` | Modèle à utiliser | `claude-haiku-4-5-20251001` |
| `aiCommit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ |
| `aiCommit.commitStyle` | Style du message : `conventional` ou `simple` | `conventional` |
| `aiCommit.maxDiffSize` | Taille maximale du diff envoyé à l'IA (en caractères) | `5000` |
| `zemit.provider` | Fournisseur d'IA : `anthropic`, `openai` ou `ollama` | `anthropic` |
| `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ |
| `zemit.model` | Modèle à utiliser | `claude-sonnet-4-6` |
| `zemit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ |
| `zemit.promptVersion` | Version du prompt | `zemit-v2` |
| `zemit.maxDiffSize` | Taille maximale du diff envoyé à l'IA (en caractères) | `5000` |
Pour choisir un modèle parmi ceux disponibles chez ton fournisseur, lance la commande **Zemit: Select Model** depuis la palette de commandes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "zemit",
"version": "1.1.0",
"version": "1.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "zemit",
"version": "1.1.0",
"version": "1.1.4",
"license": "GPL-3.0-only",
"devDependencies": {
"@types/node": "^18.0.0",
@@ -1038,9 +1038,9 @@
}
},
"node_modules/@vscode/vsce": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.8.1.tgz",
"integrity": "sha512-Ij1i53rvR2Z/BR8tdESNqb5l5GNvOLQIWSbE1NnRnXQrvJu/xhK8nVfe6vXKdI6L7/fUwzlqqB1gOjt901mTmg==",
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz",
"integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1078,7 +1078,7 @@
"vsce": "vsce"
},
"engines": {
"node": ">= 22"
"node": ">= 20"
},
"optionalDependencies": {
"keytar": "^7.7.0"
+23 -23
View File
@@ -1,8 +1,8 @@
{
"name": "zemit",
"version": "1.1.0",
"version": "1.1.4",
"displayName": "Zemit",
"description": "Génère des messages de commit dans VSCode à partir du diff en cours, via un modèle d'IA.",
"description": "Génère des messages de commit, via un modèle d'IA, directement dans VSCode.",
"repository": {
"type": "git",
"url": "https://git.hyko.cx/hykocx/zemit"
@@ -31,7 +31,7 @@
"configuration": {
"title": "Zemit",
"properties": {
"aiCommit.provider": {
"zemit.provider": {
"type": "string",
"enum": [
"anthropic",
@@ -41,35 +41,35 @@
"default": "anthropic",
"description": "Fournisseur d'IA utilisé pour générer les messages de commit."
},
"aiCommit.apiKey": {
"zemit.apiKey": {
"type": "string",
"default": "",
"markdownDescription": "Clé API du fournisseur sélectionné. Non requise pour Ollama."
},
"aiCommit.model": {
"zemit.model": {
"type": "string",
"default": "claude-haiku-4-5-20251001",
"default": "claude-sonnet-4-6",
"markdownDescription": "Modèle à utiliser. Lance **Zemit : Sélectionner un modèle** pour parcourir les modèles disponibles du fournisseur configuré."
},
"aiCommit.baseUrl": {
"zemit.baseUrl": {
"type": "string",
"default": "",
"markdownDescription": "URL de base personnalisée (ex. `http://localhost:11434/v1` pour Ollama). Laisser vide pour utiliser la valeur par défaut du fournisseur."
},
"aiCommit.commitStyle": {
"zemit.promptVersion": {
"type": "string",
"enum": [
"conventional",
"simple"
"zemit-v1",
"zemit-v2"
],
"enumDescriptions": [
"Format Conventional Commits (feat:, fix:, chore:, etc.)",
"Description courte sur une ligne"
"Zemit V1 Format Conventional Commits classique",
"Zemit V2 Conventional Commits compact avec résumé de fichiers et corps structuré"
],
"default": "conventional",
"description": "Format du message de commit généré."
"default": "zemit-v2",
"description": "Version du prompt utilisé pour générer le message de commit."
},
"aiCommit.maxDiffSize": {
"zemit.maxDiffSize": {
"type": "number",
"default": 5000,
"minimum": 500,
@@ -80,19 +80,19 @@
},
"commands": [
{
"command": "aiCommit.generateCommitMessage",
"command": "zemit.generateCommitMessage",
"title": "Générer un message de commit",
"category": "Zemit",
"icon": "$(sparkle)"
},
{
"command": "aiCommit.abortGeneration",
"command": "zemit.abortGeneration",
"title": "Arrêter la génération",
"category": "Zemit",
"icon": "$(debug-stop)"
},
{
"command": "aiCommit.selectModel",
"command": "zemit.selectModel",
"title": "Sélectionner un modèle",
"category": "Zemit",
"icon": "$(list-selection)"
@@ -101,14 +101,14 @@
"menus": {
"scm/title": [
{
"command": "aiCommit.generateCommitMessage",
"command": "zemit.generateCommitMessage",
"group": "navigation@1",
"when": "config.git.enabled && scmProvider == git && !aiCommit.isGenerating"
"when": "config.git.enabled && scmProvider == git && !zemit.isGenerating"
},
{
"command": "aiCommit.abortGeneration",
"command": "zemit.abortGeneration",
"group": "navigation@1",
"when": "config.git.enabled && scmProvider == git && aiCommit.isGenerating"
"when": "config.git.enabled && scmProvider == git && zemit.isGenerating"
}
]
}
@@ -116,7 +116,7 @@
"scripts": {
"build": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --platform=node --target=node18",
"watch": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --platform=node --target=node18 --watch",
"package": "npm run build && vsce package"
"package": "npm run build && vsce package --baseContentUrl https://git.hyko.cx/hykocx/zemit/raw/branch/main --baseImagesUrl https://git.hyko.cx/hykocx/zemit/raw/branch/main"
},
"devDependencies": {
"@types/node": "^18.0.0",
+19 -18
View File
@@ -1,6 +1,7 @@
import * as path from "path"
import * as vscode from "vscode"
import { getGitDiffStagedFirst } from "./git"
import { getGitContext, getGitDiffStagedFirst } from "./git"
import { LATEST_PROMPT_VERSION } from "./prompts"
import { createProvider } from "./providers"
let abortController: AbortController | undefined
@@ -9,19 +10,19 @@ export async function generateCommitMsg(scm?: vscode.SourceControl): Promise<voi
try {
const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports
if (!gitExtension) {
throw new Error("Git extension not found")
throw new Error("Extension Git introuvable")
}
const git = gitExtension.getAPI(1)
if (git.repositories.length === 0) {
throw new Error("No Git repositories available")
throw new Error("Aucun dépôt Git disponible")
}
// If invoked from SCM panel button, a specific repo is provided
if (scm) {
const repository = git.getRepository(scm.rootUri)
if (!repository) {
throw new Error("Repository not found for the selected SCM")
throw new Error("Dépôt introuvable pour le SCM sélectionné")
}
await generateForRepository(repository)
return
@@ -36,7 +37,7 @@ export async function generateCommitMsg(scm?: vscode.SourceControl): Promise<voi
export function abortGeneration(): void {
abortController?.abort()
vscode.commands.executeCommand("setContext", "aiCommit.isGenerating", false)
vscode.commands.executeCommand("setContext", "zemit.isGenerating", false)
}
// ─── Multi-repo orchestration ─────────────────────────────────────────────────
@@ -45,7 +46,7 @@ async function orchestrateMultiRepo(repos: unknown[]): Promise<void> {
const reposWithChanges = await filterReposWithChanges(repos)
if (reposWithChanges.length === 0) {
vscode.window.showInformationMessage("[Zemit] No changes found in any repository.")
vscode.window.showInformationMessage("[Zemit] Aucune modification trouvée dans les dépôts.")
return
}
@@ -62,12 +63,12 @@ async function orchestrateMultiRepo(repos: unknown[]): Promise<void> {
items.unshift({
label: "$(git-commit) All repositories with changes",
description: `Generate for ${reposWithChanges.length} repositories`,
description: `Générer pour ${reposWithChanges.length} dépôts`,
repo: null as any,
})
const selection = await vscode.window.showQuickPick(items, {
placeHolder: "Select a repository to generate a commit message for",
placeHolder: "Sélectionner un dépôt pour générer un message de commit",
})
if (!selection) return
@@ -99,34 +100,34 @@ async function filterReposWithChanges(repos: unknown[]): Promise<unknown[]> {
async function generateForRepository(repository: any): Promise<void> {
const repoPath = repository.rootUri.fsPath
const repoName = repoPath.split(path.sep).pop() || "repository"
const diff = await getGitDiffStagedFirst(repoPath)
const { diff, stat } = await getGitContext(repoPath)
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.SourceControl,
title: `Zemit: Generating commit message for ${repoName}...`,
title: `Zemit : Génération du message de commit pour ${repoName}...`,
cancellable: true,
},
(_progress, token) => performGeneration(repository.inputBox, diff, token),
(_progress, token) => performGeneration(repository.inputBox, diff, stat, token),
)
}
async function performGeneration(
inputBox: any,
diff: string,
stat: string,
token: vscode.CancellationToken,
): Promise<void> {
const config = vscode.workspace.getConfiguration("aiCommit")
const config = vscode.workspace.getConfiguration("zemit")
const maxDiffSize = config.get<number>("maxDiffSize", 5000)
const style = config.get<string>("commitStyle", "conventional")
const style = config.get<string>("promptVersion", LATEST_PROMPT_VERSION)
const truncatedDiff =
diff.length > maxDiffSize ? diff.substring(0, maxDiffSize) + "\n\n[Diff truncated due to size]" : diff
// Include any existing user note in the prompt
const existingNote = inputBox.value?.trim() || ""
let prompt = truncatedDiff
let prompt = stat ? `## Changed files\n${stat}\n\n## Diff\n${truncatedDiff}` : truncatedDiff
if (existingNote) {
prompt = `Developer note (use if relevant): ${existingNote}\n\n${prompt}`
}
@@ -135,7 +136,7 @@ async function performGeneration(
token.onCancellationRequested(() => abortController?.abort())
try {
await vscode.commands.executeCommand("setContext", "aiCommit.isGenerating", true)
await vscode.commands.executeCommand("setContext", "zemit.isGenerating", true)
const provider = createProvider(config)
let response = ""
@@ -147,10 +148,10 @@ async function performGeneration(
}
if (!inputBox.value) {
throw new Error("The AI returned an empty response")
throw new Error("L'IA n'a retourné aucune réponse")
}
} finally {
await vscode.commands.executeCommand("setContext", "aiCommit.isGenerating", false)
await vscode.commands.executeCommand("setContext", "zemit.isGenerating", false)
}
}
+9 -9
View File
@@ -4,21 +4,21 @@ import { fetchAvailableModels } from "./providers"
export function activate(context: vscode.ExtensionContext): void {
context.subscriptions.push(
vscode.commands.registerCommand("aiCommit.generateCommitMessage", (scm?: vscode.SourceControl) =>
vscode.commands.registerCommand("zemit.generateCommitMessage", (scm?: vscode.SourceControl) =>
generateCommitMsg(scm),
),
vscode.commands.registerCommand("aiCommit.abortGeneration", () => abortGeneration()),
vscode.commands.registerCommand("aiCommit.selectModel", () => selectModel()),
vscode.commands.registerCommand("zemit.abortGeneration", () => abortGeneration()),
vscode.commands.registerCommand("zemit.selectModel", () => selectModel()),
)
}
async function selectModel(): Promise<void> {
const config = vscode.workspace.getConfiguration("aiCommit")
const config = vscode.workspace.getConfiguration("zemit")
let models: string[]
try {
models = await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: "Zemit: Fetching available models…", cancellable: false },
{ location: vscode.ProgressLocation.Notification, title: "Zemit : Récupération des modèles disponibles…", cancellable: false },
() => fetchAvailableModels(config),
)
} catch (err) {
@@ -28,25 +28,25 @@ async function selectModel(): Promise<void> {
}
if (models.length === 0) {
vscode.window.showWarningMessage("[Zemit] No models found for the configured provider.")
vscode.window.showWarningMessage("[Zemit] Aucun modèle trouvé pour le fournisseur configuré.")
return
}
const currentModel = config.get<string>("model", "")
const items = models.map((id) => ({
label: id,
description: id === currentModel ? "current" : undefined,
description: id === currentModel ? "actuel" : undefined,
}))
const picked = await vscode.window.showQuickPick(items, {
placeHolder: "Select a model",
placeHolder: "Sélectionner un modèle",
matchOnDescription: false,
})
if (!picked) return
await config.update("model", picked.label, vscode.ConfigurationTarget.Global)
vscode.window.showInformationMessage(`[Zemit] Model set to ${picked.label}`)
vscode.window.showInformationMessage(`[Zemit] Modèle défini sur ${picked.label}`)
}
export function deactivate(): void {}
+59 -9
View File
@@ -32,10 +32,10 @@ async function hasCommits(cwd: string): Promise<boolean> {
export async function getGitDiff(cwd: string, stagedOnly = false): Promise<string> {
if (!(await isGitInstalled())) {
throw new Error("Git is not installed")
throw new Error("Git n'est pas installé")
}
if (!(await isGitRepo(cwd))) {
throw new Error("Not a git repository")
throw new Error("Pas un dépôt git")
}
let diff = ""
@@ -43,17 +43,15 @@ export async function getGitDiff(cwd: string, stagedOnly = false): Promise<strin
if (await hasCommits(cwd)) {
const { stdout: staged } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd })
diff = staged.trim()
}
if (!stagedOnly && !diff) {
const { stdout: unstaged } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd })
diff = unstaged.trim()
}
// New repo with no commits yet — show everything that's staged/tracked
if (!diff) {
const { stdout: newRepo } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd })
diff = newRepo.trim()
} else {
// First commit — no HEAD exists yet
const { stdout: cached } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd })
diff = cached.trim()
}
// Include untracked (new) files when not limiting to staged only
@@ -72,7 +70,7 @@ export async function getGitDiff(cwd: string, stagedOnly = false): Promise<strin
}
if (!diff) {
throw new Error("No changes found to generate a commit message from")
throw new Error("Aucune modification trouvée pour générer un message de commit")
}
return diff
@@ -85,3 +83,55 @@ export async function getGitDiffStagedFirst(cwd: string): Promise<string> {
return await getGitDiff(cwd, false)
}
}
export async function getGitContext(cwd: string): Promise<{ diff: string; stat: string }> {
if (!(await isGitInstalled())) throw new Error("Git n'est pas installé")
if (!(await isGitRepo(cwd))) throw new Error("Pas un dépôt git")
let diff = ""
let stat = ""
let includeUntracked = false
if (await hasCommits(cwd)) {
const { stdout: stagedDiff } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd })
diff = stagedDiff.trim()
if (diff) {
const { stdout: stagedStat } = await execAsync("git --no-pager diff --staged --name-status", { cwd })
stat = stagedStat.trim()
} else {
includeUntracked = true
const { stdout: allDiff } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd })
diff = allDiff.trim()
const { stdout: allStat } = await execAsync("git --no-pager diff HEAD --name-status", { cwd })
stat = allStat.trim()
}
} else {
// First commit — no HEAD exists yet
includeUntracked = true
const { stdout: cachedDiff } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd })
diff = cachedDiff.trim()
const { stdout: cachedStat } = await execAsync("git --no-pager diff --cached --name-status", { cwd })
stat = cachedStat.trim()
}
if (includeUntracked) {
const { stdout: untrackedList } = await execAsync("git ls-files --others --exclude-standard", { cwd })
const untrackedFiles = untrackedList.trim().split("\n").filter(Boolean)
for (const file of untrackedFiles) {
const result = await execAsync(
`git --no-pager diff --no-index -- /dev/null ${JSON.stringify(file)}`,
{ cwd },
).catch((e: any) => ({ stdout: e.stdout as string | undefined }))
if (result.stdout) diff += (diff ? "\n" : "") + result.stdout
stat += (stat ? "\n" : "") + `A\t${file}`
}
}
diff = diff.trim()
stat = stat.trim()
if (!diff) throw new Error("Aucune modification trouvée pour générer un message de commit")
return { diff, stat }
}
+48
View File
@@ -0,0 +1,48 @@
export const LATEST_PROMPT_VERSION = "zemit-v2"
export const SYSTEM_PROMPT =
"You are a git commit message generator. Output only the commit message — no explanation, no preamble, no backticks, no quotes. All messages must be written in English."
export const ZEMIT_V1 = `Based on the provided git diff, generate a concise and descriptive commit message following the Conventional Commits format:
<type>(<scope>): <short description>
Types: feat, fix, refactor, style, docs, test, chore, perf, revert
- feat: new feature
- fix: bug fix
- refactor: code restructuring without behavior change
- style: formatting only (spaces, commas, no logic change)
- docs: documentation only
- test: add or update tests
- chore: maintenance, dependencies, build config
- perf: performance improvement
- revert: revert a previous commit (format: revert: revert "<original message>")
Rules:
- Scope is optional, specifies the affected area (e.g. auth, api, storage, ui, config)
- Description: lowercase, no trailing period, in English
- One commit = one intention, do not mix fix and refactor
- For breaking changes, add ! after the type and a BREAKING CHANGE: footer
- Only include a body if there are multiple distinct changes to explain; for a single focused change, output the title only`
export const ZEMIT_V2 = `Generate a git commit message following Conventional Commits format:
<type>(<scope>): <short description>
Types: feat | fix | refactor | style | docs | test | chore | perf
Scope: optional, affected module (e.g. auth, api, ui, config)
Description: lowercase, imperative mood, no trailing period
Body rules:
- Single focused change → title only
- Multiple files or distinct concerns → title + blank line + concise bullet body listing each change
- Breaking change: add ! after type + "BREAKING CHANGE: <detail>" footer`
const PROMPT_REGISTRY: Record<string, string> = {
"zemit-v1": ZEMIT_V1,
"zemit-v2": ZEMIT_V2,
}
export function getPrompt(version: string): string {
return PROMPT_REGISTRY[version] ?? PROMPT_REGISTRY[LATEST_PROMPT_VERSION]
}
+8 -20
View File
@@ -1,17 +1,5 @@
import * as vscode from "vscode"
const SYSTEM_PROMPT =
"You are a helpful assistant that generates informative git commit messages based on git diffs output. Skip preamble and remove all backticks surrounding the commit message."
const CONVENTIONAL_INSTRUCTION = `Based on the provided git diff, generate a concise and descriptive commit message.
The commit message should:
1. Have a short title (50-72 characters)
2. Follow the Conventional Commits format (feat:, fix:, chore:, docs:, refactor:, test:, style:, etc.)
3. Be clear and informative
4. Only include a body description if there are multiple distinct changes to explain, if the diff represents a single focused change, output the title only`
const SIMPLE_INSTRUCTION = `Based on the provided git diff, generate a short and clear one-line commit message (50-72 characters).`
import { SYSTEM_PROMPT, getPrompt } from "./prompts"
export interface AIProvider {
generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string>
@@ -27,7 +15,7 @@ class OpenAICompatibleProvider implements AIProvider {
) {}
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
const instruction = style === "conventional" ? CONVENTIONAL_INSTRUCTION : SIMPLE_INSTRUCTION
const instruction = getPrompt(style)
const url = `${this.baseUrl}/chat/completions`
const response = await fetch(url, {
@@ -70,7 +58,7 @@ class AnthropicProvider implements AIProvider {
) {}
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
const instruction = style === "conventional" ? CONVENTIONAL_INSTRUCTION : SIMPLE_INSTRUCTION
const instruction = getPrompt(style)
const url = `${this.baseUrl}/messages`
const response = await fetch(url, {
@@ -164,13 +152,13 @@ function extractAnthropicDelta(data: string): string | null {
export function createProvider(config: vscode.WorkspaceConfiguration): AIProvider {
const provider = config.get<string>("provider", "anthropic")
const apiKey = config.get<string>("apiKey", "")
const model = config.get<string>("model", "claude-haiku-4-5-20251001")
const model = config.get<string>("model", "claude-sonnet-4-6")
const customBaseUrl = config.get<string>("baseUrl", "")
switch (provider) {
case "anthropic": {
const baseUrl = customBaseUrl || "https://api.anthropic.com/v1"
if (!apiKey) throw new Error("Anthropic API key is required. Set it in Settings → Zemit AI Commit.")
if (!apiKey) throw new Error("Clé API Anthropic requise. Configurez-la dans Paramètres → Zemit AI Commit.")
return new AnthropicProvider(apiKey, model, baseUrl)
}
case "ollama": {
@@ -179,7 +167,7 @@ export function createProvider(config: vscode.WorkspaceConfiguration): AIProvide
}
default: {
const baseUrl = customBaseUrl || "https://api.openai.com/v1"
if (!apiKey) throw new Error("OpenAI API key is required. Set it in Settings → Zemit AI Commit.")
if (!apiKey) throw new Error("Clé API OpenAI requise. Configurez-la dans Paramètres → Zemit AI Commit.")
return new OpenAICompatibleProvider(apiKey, model, baseUrl)
}
}
@@ -195,7 +183,7 @@ export async function fetchAvailableModels(config: vscode.WorkspaceConfiguration
switch (provider) {
case "anthropic": {
const baseUrl = customBaseUrl || "https://api.anthropic.com/v1"
if (!apiKey) throw new Error("Anthropic API key is required. Set it in Settings → Zemit AI Commit.")
if (!apiKey) throw new Error("Clé API Anthropic requise. Configurez-la dans Paramètres → Zemit AI Commit.")
const response = await fetch(`${baseUrl}/models`, {
headers: {
"x-api-key": apiKey,
@@ -208,7 +196,7 @@ export async function fetchAvailableModels(config: vscode.WorkspaceConfiguration
}
case "openai": {
const baseUrl = customBaseUrl || "https://api.openai.com/v1"
if (!apiKey) throw new Error("OpenAI API key is required. Set it in Settings → Zemit AI Commit.")
if (!apiKey) throw new Error("Clé API OpenAI requise. Configurez-la dans Paramètres → Zemit AI Commit.")
const response = await fetch(`${baseUrl}/models`, {
headers: { Authorization: `Bearer ${apiKey}` },
})