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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ perfect commit message.
when you run `git commit -v`
- 🎯 Works from terminal or within Neovim (using vim-fugitive)
- 🤝 Non-intrusive - if you start typing, AI suggestions are added as comments instead
- 🔑 Uses `GEMINI_API_KEY`, `OPENAI_API_KEY`, or `ANTHROPIC_API_KEY` environment variables for authentication
- 🔑 Uses `GEMINI_API_KEY`, `OPENAI_API_KEY`, `COPILOT_TOKEN`, or `ANTHROPIC_API_KEY` environment variables for authentication
- 🔐 Copilot: automatically uses existing authentication from copilot.lua or copilot.vim (no token needed)
- ⚙️ Configurable model, temperature, and max tokens
- 🔄 Optional push prompt after successful commits
- ⬇️⬆️ Pull before push to reduce rejections (configurable with args)
Expand Down Expand Up @@ -91,7 +92,9 @@ export GEMINI_API_KEY="your-api-key-here"
export OPENAI_API_KEY="your-api-key-here"
```

**For Anthropic:**
**For Copilot:**

If you're already using [copilot.lua](https://github.com/zbirenbaum/copilot.lua) or [copilot.vim](https://github.com/github/copilot.vim), authentication is automatic, no setup needed. Otherwise:

```bash
export COPILOT_TOKEN="your-github-copilot-token-here"
Expand Down Expand Up @@ -127,8 +130,8 @@ require("ai_commit_msg").setup({
args = { "--rebase", "--autostash" }, -- arguments passed to `git pull`
},

-- Show spinner while generating
spinner = true,
-- Show spinner while generating (true for default, false to disable, or custom frames array)
spinner = true, -- Can also be false or { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }

-- Show notifications
notifications = true,
Expand Down Expand Up @@ -352,7 +355,7 @@ git config --global core.editor nvim

## Tips

- The plugin uses Gemini API, OpenAI Chat Completions API, and Anthropic Messages API directly
- The plugin uses Gemini API, Copilot Chat Completions API, OpenAI Chat Completions API, and Anthropic Messages API directly
- Lower temperature values (0.1-0.3) produce more consistent commit messages
- Higher temperature values (0.5-0.8) produce more creative variations
- The default model `gemini-2.5-flash-lite` provides excellent results at a very low cost
Expand Down
5 changes: 4 additions & 1 deletion lua/ai_commit_msg/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ local M = {}
local DEFAULT_PROMPT = [[{diff}]]
local DEFAULT_SYSTEM_PROMPT = require("ai_commit_msg.prompts").DEFAULT_SYSTEM_PROMPT

-- Default spinner frames
M.DEFAULT_SPINNER_FRAMES = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }

---@class ProviderConfig
---@field model string Model to use for this provider
---@field temperature number|nil Temperature for the model (0.0 to 1.0)
Expand All @@ -25,7 +28,7 @@ local DEFAULT_SYSTEM_PROMPT = require("ai_commit_msg.prompts").DEFAULT_SYSTEM_PR
---@field providers table<string, ProviderConfig> Provider-specific configurations
---@field auto_push_prompt boolean Whether to prompt for push after commit
---@field pull_before_push { enabled: boolean, args: string[] } Whether and how to run `git pull` before pushing
---@field spinner boolean Whether to show a spinner while generating
---@field spinner string[]|boolean Array of spinner frames to animate, true for default frames, or false to disable spinner
---@field notifications boolean Whether to show notifications
---@field context_lines number Number of surrounding lines to include in git diff
---@field keymaps table<string, string|false> Keymaps for commit buffer
Expand Down
74 changes: 50 additions & 24 deletions lua/ai_commit_msg/generator.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
local M = {}

local function get_spinner()
local spinner = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
return spinner[math.floor(vim.uv.hrtime() / (1e6 * 80)) % #spinner + 1]
local function get_spinner(spinner_frames)
return spinner_frames[math.floor(vim.uv.hrtime() / (1e6 * 80)) % #spinner_frames + 1]
end

local function notify(msg, level, config)
Expand All @@ -24,19 +23,34 @@ function M.generate(config, callback)
end)

local spinner_timer
local notif_id = "ai-commit-msg"
local notify_record
local notify_called = false

-- Resolve spinner frames: true uses default, table uses custom, false disables
local spinner_frames = nil
if config.spinner == true then
spinner_frames = require("ai_commit_msg.config").DEFAULT_SPINNER_FRAMES
elseif type(config.spinner) == "table" and #config.spinner > 0 then
spinner_frames = config.spinner
end

-- Start spinner if enabled
if config.spinner and config.notifications then
if spinner_frames and config.notifications then
local function update_spinner()
if not spinner_timer or spinner_timer:is_closing() then
return
end
vim.notify(get_spinner() .. " Generating commit message...", vim.log.levels.INFO, {
id = notif_id,
local opts = {
title = "AI Commit",
timeout = false,
})
hide_from_history = notify_called,
}
if notify_record then
opts.replace = notify_record.id
end
notify_record =
vim.notify(get_spinner(spinner_frames) .. " Generating commit message...", vim.log.levels.INFO, opts)
notify_called = true
end

spinner_timer = vim.uv.new_timer()
Expand Down Expand Up @@ -66,13 +80,16 @@ function M.generate(config, callback)
vim.schedule(function()
local error_msg = "Failed to get git diff: " .. (diff_res.stderr or "Unknown error")
vim.notify("ai-commit-msg.nvim: " .. error_msg, vim.log.levels.ERROR)
-- Clear spinner notification with error message
-- Replace spinner notification with error message
if config.notifications then
vim.notify("❌ " .. error_msg, vim.log.levels.ERROR, {
id = notif_id,
local opts = {
title = "AI Commit",
timeout = 3000,
})
}
if notify_record then
opts.replace = notify_record.id
end
vim.notify("❌ " .. error_msg, vim.log.levels.ERROR, opts)
end
if callback then
callback(false, error_msg)
Expand All @@ -93,13 +110,16 @@ function M.generate(config, callback)
vim.schedule(function()
local error_msg = "No staged changes to commit"
vim.notify("ai-commit-msg.nvim: " .. error_msg, vim.log.levels.WARN)
-- Clear spinner notification with warning message
-- Replace spinner notification with warning message
if config.notifications then
vim.notify("⚠️ " .. error_msg, vim.log.levels.WARN, {
id = notif_id,
local opts = {
title = "AI Commit",
timeout = 3000,
})
}
if notify_record then
opts.replace = notify_record.id
end
vim.notify("⚠️ " .. error_msg, vim.log.levels.WARN, opts)
end
if callback then
callback(false, error_msg)
Expand All @@ -125,13 +145,16 @@ function M.generate(config, callback)
vim.schedule(function()
if not success then
vim.notify("ai-commit-msg.nvim: " .. result, vim.log.levels.ERROR)
-- Clear spinner notification with error message
-- Replace spinner notification with error message
if config.notifications then
vim.notify("❌ " .. result, vim.log.levels.ERROR, {
id = notif_id,
local opts = {
title = "AI Commit",
timeout = 3000,
})
}
if notify_record then
opts.replace = notify_record.id
end
vim.notify("❌ " .. result, vim.log.levels.ERROR, opts)
end
if callback then
callback(false, result)
Expand All @@ -153,13 +176,16 @@ function M.generate(config, callback)
end

vim.notify("ai-commit-msg.nvim: Generated message: " .. result:sub(1, 50) .. "...", vim.log.levels.DEBUG)
-- Clear spinner notification with success message
-- Replace spinner notification with success message
if config.notifications then
vim.notify("✅ Commit message generated (" .. duration_cost_str .. ")", vim.log.levels.INFO, {
id = notif_id,
local opts = {
title = "AI Commit",
timeout = 2000,
})
}
if notify_record then
opts.replace = notify_record.id
end
vim.notify("✅ Commit message generated (" .. duration_cost_str .. ")", vim.log.levels.INFO, opts)
end
if callback then
callback(true, result)
Expand Down
Loading