16 Commits

Author SHA1 Message Date
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
10 changed files with 195 additions and 97 deletions
+19 -8
View File
@@ -1,6 +1,8 @@
# Zemit # 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.
![banner](/assets/git-banner.png)
## Ce que ça fait ## Ce que ça fait
@@ -16,8 +18,17 @@ Tu peux interrompre la génération à tout moment depuis le même panneau.
## Installation ## 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). 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é. 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...** 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 | | Paramètre | Description | Défaut |
|---|---|---| |---|---|---|
| `aiCommit.provider` | Fournisseur d'IA : `anthropic`, `openai` ou `ollama` | `anthropic` | | `zemit.provider` | Fournisseur d'IA : `anthropic`, `openai` ou `ollama` | `anthropic` |
| `aiCommit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ | | `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ |
| `aiCommit.model` | Modèle à utiliser | `claude-haiku-4-5-20251001` | | `zemit.model` | Modèle à utiliser | `claude-sonnet-4-6` |
| `aiCommit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ | | `zemit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ |
| `aiCommit.commitStyle` | Style du message : `conventional` ou `simple` | `conventional` | | `zemit.commitStyle` | Style du message : `conventional` ou `simple` | `conventional` |
| `aiCommit.maxDiffSize` | Taille maximale du diff envoyé à l'IA (en caractères) | `5000` | | `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. 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: 216 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", "name": "zemit",
"version": "1.1.0", "version": "1.1.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "zemit", "name": "zemit",
"version": "1.1.0", "version": "1.1.3",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"devDependencies": { "devDependencies": {
"@types/node": "^18.0.0", "@types/node": "^18.0.0",
@@ -1038,9 +1038,9 @@
} }
}, },
"node_modules/@vscode/vsce": { "node_modules/@vscode/vsce": {
"version": "3.8.1", "version": "3.9.1",
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.8.1.tgz", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz",
"integrity": "sha512-Ij1i53rvR2Z/BR8tdESNqb5l5GNvOLQIWSbE1NnRnXQrvJu/xhK8nVfe6vXKdI6L7/fUwzlqqB1gOjt901mTmg==", "integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1078,7 +1078,7 @@
"vsce": "vsce" "vsce": "vsce"
}, },
"engines": { "engines": {
"node": ">= 22" "node": ">= 20"
}, },
"optionalDependencies": { "optionalDependencies": {
"keytar": "^7.7.0" "keytar": "^7.7.0"
+23 -23
View File
@@ -1,8 +1,8 @@
{ {
"name": "zemit", "name": "zemit",
"version": "1.1.0", "version": "1.1.4",
"displayName": "Zemit", "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": { "repository": {
"type": "git", "type": "git",
"url": "https://git.hyko.cx/hykocx/zemit" "url": "https://git.hyko.cx/hykocx/zemit"
@@ -31,7 +31,7 @@
"configuration": { "configuration": {
"title": "Zemit", "title": "Zemit",
"properties": { "properties": {
"aiCommit.provider": { "zemit.provider": {
"type": "string", "type": "string",
"enum": [ "enum": [
"anthropic", "anthropic",
@@ -41,35 +41,35 @@
"default": "anthropic", "default": "anthropic",
"description": "Fournisseur d'IA utilisé pour générer les messages de commit." "description": "Fournisseur d'IA utilisé pour générer les messages de commit."
}, },
"aiCommit.apiKey": { "zemit.apiKey": {
"type": "string", "type": "string",
"default": "", "default": "",
"markdownDescription": "Clé API du fournisseur sélectionné. Non requise pour Ollama." "markdownDescription": "Clé API du fournisseur sélectionné. Non requise pour Ollama."
}, },
"aiCommit.model": { "zemit.model": {
"type": "string", "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é." "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", "type": "string",
"default": "", "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." "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", "type": "string",
"enum": [ "enum": [
"conventional", "zemit-v1",
"simple" "zemit-v2"
], ],
"enumDescriptions": [ "enumDescriptions": [
"Format Conventional Commits (feat:, fix:, chore:, etc.)", "Zemit V1 Format Conventional Commits classique",
"Description courte sur une ligne" "Zemit V2 Conventional Commits compact avec résumé de fichiers et corps structuré"
], ],
"default": "conventional", "default": "zemit-v2",
"description": "Format du message de commit généré." "description": "Version du prompt utilisé pour générer le message de commit."
}, },
"aiCommit.maxDiffSize": { "zemit.maxDiffSize": {
"type": "number", "type": "number",
"default": 5000, "default": 5000,
"minimum": 500, "minimum": 500,
@@ -80,19 +80,19 @@
}, },
"commands": [ "commands": [
{ {
"command": "aiCommit.generateCommitMessage", "command": "zemit.generateCommitMessage",
"title": "Générer un message de commit", "title": "Générer un message de commit",
"category": "Zemit", "category": "Zemit",
"icon": "$(sparkle)" "icon": "$(sparkle)"
}, },
{ {
"command": "aiCommit.abortGeneration", "command": "zemit.abortGeneration",
"title": "Arrêter la génération", "title": "Arrêter la génération",
"category": "Zemit", "category": "Zemit",
"icon": "$(debug-stop)" "icon": "$(debug-stop)"
}, },
{ {
"command": "aiCommit.selectModel", "command": "zemit.selectModel",
"title": "Sélectionner un modèle", "title": "Sélectionner un modèle",
"category": "Zemit", "category": "Zemit",
"icon": "$(list-selection)" "icon": "$(list-selection)"
@@ -101,14 +101,14 @@
"menus": { "menus": {
"scm/title": [ "scm/title": [
{ {
"command": "aiCommit.generateCommitMessage", "command": "zemit.generateCommitMessage",
"group": "navigation@1", "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", "group": "navigation@1",
"when": "config.git.enabled && scmProvider == git && aiCommit.isGenerating" "when": "config.git.enabled && scmProvider == git && zemit.isGenerating"
} }
] ]
} }
@@ -116,7 +116,7 @@
"scripts": { "scripts": {
"build": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --platform=node --target=node18", "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", "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": { "devDependencies": {
"@types/node": "^18.0.0", "@types/node": "^18.0.0",
+19 -18
View File
@@ -1,6 +1,7 @@
import * as path from "path" import * as path from "path"
import * as vscode from "vscode" import * as vscode from "vscode"
import { getGitDiffStagedFirst } from "./git" import { getGitContext, getGitDiffStagedFirst } from "./git"
import { LATEST_PROMPT_VERSION } from "./prompts"
import { createProvider } from "./providers" import { createProvider } from "./providers"
let abortController: AbortController | undefined let abortController: AbortController | undefined
@@ -9,19 +10,19 @@ export async function generateCommitMsg(scm?: vscode.SourceControl): Promise<voi
try { try {
const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports
if (!gitExtension) { if (!gitExtension) {
throw new Error("Git extension not found") throw new Error("Extension Git introuvable")
} }
const git = gitExtension.getAPI(1) const git = gitExtension.getAPI(1)
if (git.repositories.length === 0) { 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 invoked from SCM panel button, a specific repo is provided
if (scm) { if (scm) {
const repository = git.getRepository(scm.rootUri) const repository = git.getRepository(scm.rootUri)
if (!repository) { 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) await generateForRepository(repository)
return return
@@ -36,7 +37,7 @@ export async function generateCommitMsg(scm?: vscode.SourceControl): Promise<voi
export function abortGeneration(): void { export function abortGeneration(): void {
abortController?.abort() abortController?.abort()
vscode.commands.executeCommand("setContext", "aiCommit.isGenerating", false) vscode.commands.executeCommand("setContext", "zemit.isGenerating", false)
} }
// ─── Multi-repo orchestration ───────────────────────────────────────────────── // ─── Multi-repo orchestration ─────────────────────────────────────────────────
@@ -45,7 +46,7 @@ async function orchestrateMultiRepo(repos: unknown[]): Promise<void> {
const reposWithChanges = await filterReposWithChanges(repos) const reposWithChanges = await filterReposWithChanges(repos)
if (reposWithChanges.length === 0) { 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 return
} }
@@ -62,12 +63,12 @@ async function orchestrateMultiRepo(repos: unknown[]): Promise<void> {
items.unshift({ items.unshift({
label: "$(git-commit) All repositories with changes", 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, repo: null as any,
}) })
const selection = await vscode.window.showQuickPick(items, { 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 if (!selection) return
@@ -99,34 +100,34 @@ async function filterReposWithChanges(repos: unknown[]): Promise<unknown[]> {
async function generateForRepository(repository: any): Promise<void> { async function generateForRepository(repository: any): Promise<void> {
const repoPath = repository.rootUri.fsPath const repoPath = repository.rootUri.fsPath
const repoName = repoPath.split(path.sep).pop() || "repository" const repoName = repoPath.split(path.sep).pop() || "repository"
const diff = await getGitDiffStagedFirst(repoPath) const { diff, stat } = await getGitContext(repoPath)
await vscode.window.withProgress( await vscode.window.withProgress(
{ {
location: vscode.ProgressLocation.SourceControl, location: vscode.ProgressLocation.SourceControl,
title: `Zemit: Generating commit message for ${repoName}...`, title: `Zemit : Génération du message de commit pour ${repoName}...`,
cancellable: true, cancellable: true,
}, },
(_progress, token) => performGeneration(repository.inputBox, diff, token), (_progress, token) => performGeneration(repository.inputBox, diff, stat, token),
) )
} }
async function performGeneration( async function performGeneration(
inputBox: any, inputBox: any,
diff: string, diff: string,
stat: string,
token: vscode.CancellationToken, token: vscode.CancellationToken,
): Promise<void> { ): Promise<void> {
const config = vscode.workspace.getConfiguration("aiCommit") const config = vscode.workspace.getConfiguration("zemit")
const maxDiffSize = config.get<number>("maxDiffSize", 5000) 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 = const truncatedDiff =
diff.length > maxDiffSize ? diff.substring(0, maxDiffSize) + "\n\n[Diff truncated due to size]" : diff 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() || "" const existingNote = inputBox.value?.trim() || ""
let prompt = truncatedDiff let prompt = stat ? `## Changed files\n${stat}\n\n## Diff\n${truncatedDiff}` : truncatedDiff
if (existingNote) { if (existingNote) {
prompt = `Developer note (use if relevant): ${existingNote}\n\n${prompt}` prompt = `Developer note (use if relevant): ${existingNote}\n\n${prompt}`
} }
@@ -135,7 +136,7 @@ async function performGeneration(
token.onCancellationRequested(() => abortController?.abort()) token.onCancellationRequested(() => abortController?.abort())
try { try {
await vscode.commands.executeCommand("setContext", "aiCommit.isGenerating", true) await vscode.commands.executeCommand("setContext", "zemit.isGenerating", true)
const provider = createProvider(config) const provider = createProvider(config)
let response = "" let response = ""
@@ -147,10 +148,10 @@ async function performGeneration(
} }
if (!inputBox.value) { if (!inputBox.value) {
throw new Error("The AI returned an empty response") throw new Error("L'IA n'a retourné aucune réponse")
} }
} finally { } 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 { export function activate(context: vscode.ExtensionContext): void {
context.subscriptions.push( context.subscriptions.push(
vscode.commands.registerCommand("aiCommit.generateCommitMessage", (scm?: vscode.SourceControl) => vscode.commands.registerCommand("zemit.generateCommitMessage", (scm?: vscode.SourceControl) =>
generateCommitMsg(scm), generateCommitMsg(scm),
), ),
vscode.commands.registerCommand("aiCommit.abortGeneration", () => abortGeneration()), vscode.commands.registerCommand("zemit.abortGeneration", () => abortGeneration()),
vscode.commands.registerCommand("aiCommit.selectModel", () => selectModel()), vscode.commands.registerCommand("zemit.selectModel", () => selectModel()),
) )
} }
async function selectModel(): Promise<void> { async function selectModel(): Promise<void> {
const config = vscode.workspace.getConfiguration("aiCommit") const config = vscode.workspace.getConfiguration("zemit")
let models: string[] let models: string[]
try { try {
models = await vscode.window.withProgress( 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), () => fetchAvailableModels(config),
) )
} catch (err) { } catch (err) {
@@ -28,25 +28,25 @@ async function selectModel(): Promise<void> {
} }
if (models.length === 0) { 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 return
} }
const currentModel = config.get<string>("model", "") const currentModel = config.get<string>("model", "")
const items = models.map((id) => ({ const items = models.map((id) => ({
label: id, label: id,
description: id === currentModel ? "current" : undefined, description: id === currentModel ? "actuel" : undefined,
})) }))
const picked = await vscode.window.showQuickPick(items, { const picked = await vscode.window.showQuickPick(items, {
placeHolder: "Select a model", placeHolder: "Sélectionner un modèle",
matchOnDescription: false, matchOnDescription: false,
}) })
if (!picked) return if (!picked) return
await config.update("model", picked.label, vscode.ConfigurationTarget.Global) 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 {} 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> { export async function getGitDiff(cwd: string, stagedOnly = false): Promise<string> {
if (!(await isGitInstalled())) { if (!(await isGitInstalled())) {
throw new Error("Git is not installed") throw new Error("Git n'est pas installé")
} }
if (!(await isGitRepo(cwd))) { if (!(await isGitRepo(cwd))) {
throw new Error("Not a git repository") throw new Error("Pas un dépôt git")
} }
let diff = "" let diff = ""
@@ -43,17 +43,15 @@ export async function getGitDiff(cwd: string, stagedOnly = false): Promise<strin
if (await hasCommits(cwd)) { if (await hasCommits(cwd)) {
const { stdout: staged } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd }) const { stdout: staged } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd })
diff = staged.trim() diff = staged.trim()
}
if (!stagedOnly && !diff) { if (!stagedOnly && !diff) {
const { stdout: unstaged } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd }) const { stdout: unstaged } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd })
diff = unstaged.trim() diff = unstaged.trim()
} }
} else {
// New repo with no commits yet — show everything that's staged/tracked // First commit — no HEAD exists yet
if (!diff) { const { stdout: cached } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd })
const { stdout: newRepo } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd }) diff = cached.trim()
diff = newRepo.trim()
} }
// Include untracked (new) files when not limiting to staged only // 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) { 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 return diff
@@ -85,3 +83,55 @@ export async function getGitDiffStagedFirst(cwd: string): Promise<string> {
return await getGitDiff(cwd, false) 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" import * as vscode from "vscode"
import { SYSTEM_PROMPT, getPrompt } from "./prompts"
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).`
export interface AIProvider { export interface AIProvider {
generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> 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> { 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 url = `${this.baseUrl}/chat/completions`
const response = await fetch(url, { const response = await fetch(url, {
@@ -70,7 +58,7 @@ class AnthropicProvider implements AIProvider {
) {} ) {}
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> { 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 url = `${this.baseUrl}/messages`
const response = await fetch(url, { const response = await fetch(url, {
@@ -164,13 +152,13 @@ function extractAnthropicDelta(data: string): string | null {
export function createProvider(config: vscode.WorkspaceConfiguration): AIProvider { export function createProvider(config: vscode.WorkspaceConfiguration): AIProvider {
const provider = config.get<string>("provider", "anthropic") const provider = config.get<string>("provider", "anthropic")
const apiKey = config.get<string>("apiKey", "") 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", "") const customBaseUrl = config.get<string>("baseUrl", "")
switch (provider) { switch (provider) {
case "anthropic": { case "anthropic": {
const baseUrl = customBaseUrl || "https://api.anthropic.com/v1" 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) return new AnthropicProvider(apiKey, model, baseUrl)
} }
case "ollama": { case "ollama": {
@@ -179,7 +167,7 @@ export function createProvider(config: vscode.WorkspaceConfiguration): AIProvide
} }
default: { default: {
const baseUrl = customBaseUrl || "https://api.openai.com/v1" 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) return new OpenAICompatibleProvider(apiKey, model, baseUrl)
} }
} }
@@ -195,7 +183,7 @@ export async function fetchAvailableModels(config: vscode.WorkspaceConfiguration
switch (provider) { switch (provider) {
case "anthropic": { case "anthropic": {
const baseUrl = customBaseUrl || "https://api.anthropic.com/v1" 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`, { const response = await fetch(`${baseUrl}/models`, {
headers: { headers: {
"x-api-key": apiKey, "x-api-key": apiKey,
@@ -208,7 +196,7 @@ export async function fetchAvailableModels(config: vscode.WorkspaceConfiguration
} }
case "openai": { case "openai": {
const baseUrl = customBaseUrl || "https://api.openai.com/v1" 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`, { const response = await fetch(`${baseUrl}/models`, {
headers: { Authorization: `Bearer ${apiKey}` }, headers: { Authorization: `Bearer ${apiKey}` },
}) })