Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 954ca5bd47 | |||
| 030a7b1472 | |||
| 05879f9171 | |||
| ca73d9683f | |||
| e309fa3acd | |||
| c870350c11 | |||
| f823dc4803 | |||
| cbceea72fa | |||
| 4471c2b2b2 | |||
| f5f6a2f703 | |||
| f63b6770d2 | |||
| a90ab0eca3 | |||
| 1bdd31ae0c | |||
| 0697e279ca | |||
| 718cdf87c9 | |||
| bab40a8304 | |||
| 102861ccfa | |||
| 76aa5b0444 | |||
| 7839a27d3a | |||
| 1f9ed36194 | |||
| 4f59c77e8f | |||
| a102b0d714 | |||
| b9e0273d57 | |||
| 7c4f5a9189 |
+7
-3
@@ -1,6 +1,10 @@
|
|||||||
src/**
|
.vscode/**
|
||||||
node_modules/**
|
.git/**
|
||||||
.gitignore
|
.gitignore
|
||||||
|
node_modules/**
|
||||||
|
*.vsix
|
||||||
|
assets/**
|
||||||
|
src/**
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
cline-main/**
|
|
||||||
**/*.map
|
**/*.map
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Zemit are documented here.
|
||||||
|
|
||||||
|
## [1.1.5] - 2026-05-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Claude Code CLI provider (local, no API key required)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- File deletions were not detected when generating commit messages (`--diff-filter=d` excluded deleted files)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.4] - 2026-04-24
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved prompt structure: diff is now accompanied by a `--name-status` summary of changed files
|
||||||
|
- Updated README description and settings table to reflect new prompt version
|
||||||
|
- Updated extension icon and banner image
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.3] - 2026-04-15
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Refactored prompt system into a dedicated `prompts` module with enhanced commit instructions
|
||||||
|
- Updated app icon and git banner images
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.2] - 2026-04-15
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated extension icon
|
||||||
|
- Shortened marketplace description
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.1] - 2026-04-15
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Translated all UI messages to French
|
||||||
|
- Updated default model to `claude-sonnet-4-6`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.0] - 2026-04-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Dedicated `providers` module to support multiple AI backends
|
||||||
|
- Dedicated `git` module for staged/unstaged diff handling
|
||||||
|
- VS Code extension entry point with model selection
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Refined conventional commit prompt to limit body usage
|
||||||
|
- Extracted commit generation logic into its own module
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-04-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release of Zemit
|
||||||
|
- AI-powered commit message generation using the Anthropic Claude API
|
||||||
|
- Support for staged and unstaged git diffs
|
||||||
|
- Conventional commit format by default
|
||||||
|
- GNU General Public License v3.0
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Ce que ça fait
|
## 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.
|
Tu peux interrompre la génération à tout moment depuis le même panneau.
|
||||||
|
|
||||||
@@ -12,12 +14,21 @@ Tu peux interrompre la génération à tout moment depuis le même panneau.
|
|||||||
|
|
||||||
- VS Code 1.85 ou plus récent
|
- VS Code 1.85 ou plus récent
|
||||||
- Un dépôt Git avec des changements stagés
|
- Un dépôt Git avec des changements stagés
|
||||||
- Une clé API pour le fournisseur choisi (non requise pour Ollama)
|
- Une clé API pour le fournisseur choisi (non requise pour Ollama et Claude Code)
|
||||||
|
|
||||||
## 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`, `ollama` ou `claudecode` | `anthropic` |
|
||||||
| `aiCommit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ |
|
| `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama et Claude Code) | _(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.promptVersion` | Version du prompt | `zemit-v2` |
|
||||||
| `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.
|
||||||
|
|
||||||
@@ -50,6 +61,18 @@ Pour arrêter une génération en cours, clique sur l'icône d'arrêt au même e
|
|||||||
- **Anthropic** : modèles Claude (Haiku, Sonnet, Opus)
|
- **Anthropic** : modèles Claude (Haiku, Sonnet, Opus)
|
||||||
- **OpenAI** : modèles GPT
|
- **OpenAI** : modèles GPT
|
||||||
- **Ollama** : modèles locaux, aucune clé requise
|
- **Ollama** : modèles locaux, aucune clé requise
|
||||||
|
- **Claude Code** : utilise le CLI `claude` installé localement, aucune clé API requise — l'authentification est gérée par Claude Code lui-même
|
||||||
|
|
||||||
|
### Utiliser Claude Code
|
||||||
|
|
||||||
|
Assure-toi que le CLI `claude` est installé et accessible (`~/.local/bin/claude` ou dans ton `PATH`), puis configure :
|
||||||
|
|
||||||
|
```json
|
||||||
|
"zemit.provider": "claudecode",
|
||||||
|
"zemit.model": "claude-sonnet-4-6"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note : le provider Claude Code ne supporte pas le streaming — le message apparaît d'un coup à la fin de la génération, contrairement aux providers API.
|
||||||
|
|
||||||
## Développement
|
## Développement
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 465 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.2 KiB |
Generated
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "zemit",
|
"name": "zemit",
|
||||||
"version": "1.1.0",
|
"version": "1.1.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "zemit",
|
"name": "zemit",
|
||||||
"version": "1.1.0",
|
"version": "1.1.5",
|
||||||
"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"
|
||||||
|
|||||||
+32
-25
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "zemit",
|
"name": "zemit",
|
||||||
"version": "1.1.0",
|
"version": "1.1.5",
|
||||||
"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,45 +31,52 @@
|
|||||||
"configuration": {
|
"configuration": {
|
||||||
"title": "Zemit",
|
"title": "Zemit",
|
||||||
"properties": {
|
"properties": {
|
||||||
"aiCommit.provider": {
|
"zemit.provider": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"anthropic",
|
"anthropic",
|
||||||
"openai",
|
"openai",
|
||||||
"ollama"
|
"ollama",
|
||||||
|
"claudecode"
|
||||||
|
],
|
||||||
|
"enumDescriptions": [
|
||||||
|
"Anthropic API (clé API requise)",
|
||||||
|
"OpenAI API (clé API requise)",
|
||||||
|
"Ollama – instance locale",
|
||||||
|
"Claude Code CLI – installation locale, sans clé API"
|
||||||
],
|
],
|
||||||
"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 et Claude Code."
|
||||||
},
|
},
|
||||||
"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 +87,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 +108,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 +123,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
@@ -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
@@ -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 {}
|
||||||
|
|||||||
+61
-11
@@ -32,28 +32,26 @@ 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 = ""
|
||||||
|
|
||||||
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", { 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", { 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", { 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", { 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", { 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", { 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 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
+77
-20
@@ -1,17 +1,6 @@
|
|||||||
|
import { spawn } from "child_process"
|
||||||
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 +16,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 +59,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, {
|
||||||
@@ -103,6 +92,69 @@ class AnthropicProvider implements AIProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Claude Code CLI ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class ClaudeCodeProvider implements AIProvider {
|
||||||
|
constructor(private readonly model: string) {}
|
||||||
|
|
||||||
|
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
|
||||||
|
const instruction = getPrompt(style)
|
||||||
|
const input = `<input>${instruction}\n\n${diff}</input>`
|
||||||
|
|
||||||
|
// ~/.local/bin may not be in PATH inside the VSCode extension host
|
||||||
|
const env = { ...process.env, PATH: `${process.env.HOME ?? ""}/.local/bin:${process.env.PATH ?? ""}` }
|
||||||
|
|
||||||
|
const proc = spawn("claude", [
|
||||||
|
"-p", "--output-format", "text",
|
||||||
|
"--model", this.model,
|
||||||
|
"--system-prompt", SYSTEM_PROMPT,
|
||||||
|
], { env })
|
||||||
|
|
||||||
|
const onAbort = () => proc.kill()
|
||||||
|
signal.addEventListener("abort", onAbort, { once: true })
|
||||||
|
|
||||||
|
proc.stdin.end(input)
|
||||||
|
|
||||||
|
let stderrData = ""
|
||||||
|
proc.stderr.on("data", (chunk: Buffer) => { stderrData += chunk.toString() })
|
||||||
|
|
||||||
|
// When spawn fails (e.g. ENOENT), 'close' never fires — resolve via 'error' too
|
||||||
|
let spawnError: Error | undefined
|
||||||
|
const closeOrError = new Promise<number | null>((resolve) => {
|
||||||
|
proc.on("close", resolve)
|
||||||
|
proc.on("error", (err: Error) => {
|
||||||
|
spawnError = err
|
||||||
|
proc.stdout.destroy() // Unblock the for-await below
|
||||||
|
resolve(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const chunk of proc.stdout) {
|
||||||
|
yield (chunk as Buffer).toString()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// stdout may throw if destroyed due to a spawn error — handled below
|
||||||
|
} finally {
|
||||||
|
signal.removeEventListener("abort", onAbort)
|
||||||
|
}
|
||||||
|
|
||||||
|
await closeOrError
|
||||||
|
|
||||||
|
if (spawnError) {
|
||||||
|
const isNotFound = (spawnError as NodeJS.ErrnoException).code === "ENOENT"
|
||||||
|
throw new Error(isNotFound
|
||||||
|
? "Claude Code CLI introuvable. Assurez-vous que 'claude' est installé et accessible (ex. ~/.local/bin)."
|
||||||
|
: `Claude Code CLI: ${spawnError.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitCode = await closeOrError
|
||||||
|
if (exitCode !== 0 && !signal.aborted) {
|
||||||
|
throw new Error(`Claude Code CLI error: ${stderrData || `exit code ${exitCode}`}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── SSE parsing helpers ─────────────────────────────────────────────────────
|
// ─── SSE parsing helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function* parseSSEStream(
|
async function* parseSSEStream(
|
||||||
@@ -164,22 +216,25 @@ 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 "claudecode": {
|
||||||
|
return new ClaudeCodeProvider(model)
|
||||||
|
}
|
||||||
case "ollama": {
|
case "ollama": {
|
||||||
const baseUrl = customBaseUrl || "http://localhost:11434/v1"
|
const baseUrl = customBaseUrl || "http://localhost:11434/v1"
|
||||||
return new OpenAICompatibleProvider("ollama", model, baseUrl)
|
return new OpenAICompatibleProvider("ollama", model, baseUrl)
|
||||||
}
|
}
|
||||||
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 +250,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 +263,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}` },
|
||||||
})
|
})
|
||||||
@@ -219,6 +274,8 @@ export async function fetchAvailableModels(config: vscode.WorkspaceConfiguration
|
|||||||
.filter((id) => /^(gpt-|o1|o3|chatgpt-)/.test(id))
|
.filter((id) => /^(gpt-|o1|o3|chatgpt-)/.test(id))
|
||||||
.sort()
|
.sort()
|
||||||
}
|
}
|
||||||
|
case "claudecode":
|
||||||
|
return []
|
||||||
case "ollama": {
|
case "ollama": {
|
||||||
const baseUrl = customBaseUrl || "http://localhost:11434/v1"
|
const baseUrl = customBaseUrl || "http://localhost:11434/v1"
|
||||||
const response = await fetch(`${baseUrl}/models`)
|
const response = await fetch(`${baseUrl}/models`)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"lib": ["ES2020"],
|
"lib": ["ES2020"],
|
||||||
|
"types": ["node"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user