This plugin integrates command-line (CLI) AI coding agents into Neovim. It provides a unified workflow for interacting with AI assistants directly in your editor.
Using lazy.nvim:
{
"aweis89/ai-terminals.nvim",
dependencies = { "folke/snacks.nvim" },
opts = {
auto_terminal_keymaps = {
prefix = "<leader>a",
terminals = {
{name = "claude", key = "c"},
{name = "aider", key = "a"},
{name = "goose", key = "g"},
}
}
}
}This automatically generates consistent keymaps for all configured terminals:
<leader>acc- Toggle Claude terminal (double-tap, sends visual selection if active)<leader>acd- Send diagnostics to Claude<leader>acl- Add current file to Claude<leader>acL- Add all buffers to Claude<leader>acr- Run command and send output to Claude<leader>acm- Add comment for Claude to execute in background- Same pattern for
a(Aider:<leader>aaa),g(Goose:<leader>agg), etc.
For single terminal users: See the Single Terminal Setup section for shorter keymaps without terminal suffixes (e.g., <leader>a instead of <leader>ac).
toggle(name, position?): open/toggle a terminal; sends visual selection if active. See:h ai-terminals.toggle().send_term(name, text, opts?): send text to a specific named terminal; opts{ submit?, focus? }. See:h ai-terminals.send_term().send_diagnostics(name, opts?): send formatted diagnostics; opts{ submit?, prefix? }. See:h ai-terminals.send_diagnostics().add_files_to_terminal(name, files, opts?): add files to terminal. See Snacks Picker Integration for picker examples.send_command_output(name, cmd?, opts?): run a shell command and send stdout/exit code. See:h ai-terminals.send_command_output().open(name, position?, callback?): open a terminal and optionally run a callback with it. See:h ai-terminals.open().setup(opts): initialize and merge configuration with sensible defaults. See:h ai-terminals-configuration.
This plugin integrates with existing command-line AI tools. These CLIs are optional β install only the ones you plan to use. If you don't intend to use a given tool, you do not need to install it.
The tools below are preconfigured and ready to use. If they are installed and on
your PATH, you can use them immediately. You can also add your own custom
REPLs/CLIs β the plugin communicates via a PTY (Neovim terminal channels) and
tmux send-keys, so any interactive process that reads from the terminal/stdin
will work. See the Configuration section for the terminals table to add your
own entries.
Here are links to some of the tools mentioned in the default configuration:
- Aider: Aider
- Claude Code: Claude Code
- Goose: Goose
- Codex: Codex
- Cursor CLI: Cursor CLI
- Gemini CLI: Gemini CLI
If you choose to use any of these, make sure they are installed and accessible
in your system's PATH.
Most Neovim AI plugins implement editor-specific functionality for modifying files
and interacting with LLMs directly. ai-terminals.nvim takes a fundamentally
different approach: it focuses on the generic features of pre-existing terminal
CLI tools and integrates them into Neovim by creating a bridge to send data over.
Rather than reimplementing AI functionality within Neovim, this plugin leverages the robust ecosystems that already exist in terminal-based AI tools. It acts as a communication layer, providing:
- Universal CLI Integration: Works with any terminal-based AI tool that accepts stdin β from Aider and Claude CLI to custom scripts and future tools.
- Data Bridge Architecture: Creates a bridge between your editor context (code selections, diagnostics, file paths) and terminal AI agents.
- Tool Agnostic: Instead of locking you into specific AI services, it lets you use whatever CLI tools work best for your workflow.
- stdin as API: Leverages the universal stdin interface that all CLI tools provide, making integration straightforward and reliable.
- Composable Functions: Exposes core functions like
send,toggle, andsend_diagnosticsthat can be combined to create custom workflows - from sending Jira tickets to reviewing PRs to running tests and analyzing output.
This plugin is ideal for users who prefer terminal-based AI interaction and want a single, configurable way to manage them within Neovim.
- π Configurable Terminal Integration: Define and manage terminals for
various AI CLI tools (e.g., Claude, Goose, Cursor, Aider, custom scripts)
through a simple configuration table. Uses
Snacksfor terminal window management. - π File Modification Hooks:
- Real-time file reloading so active buffers always have the latest changes
- Format files using Neovim, supports Conform, None-LS and LSP fallback
- Notify on file changes so you don't need to babysit the terminal output
- π Send Visual Selection: Send the currently selected text (visual mode) to
the AI terminal, automatically wrapped in a markdown code block with the file
path and language type included. Each terminal can have a custom path header
template to format file paths according to the AI tool's preferences (e.g.,
@filenameor`filename`). - π©Ί Send Diagnostics: Send diagnostics (errors, warnings, etc.) for the
current buffer or visual selection to the AI terminal
(
:h ai-terminals.send_diagnostics), formatted with severity, line/column numbers, messages, and the corresponding source code lines. - π File Context Management: Generic functions to add files or buffers to any
terminal using configurable commands (
:h ai-terminals.add_files_to_terminal,:h ai-terminals.add_buffers_to_terminal). Picker integration with Snacks files can be selected from any of your existing file pickers (e.g.git_files,git_status,buffers,smart..) using a custom actions. - π¬ Background Comment Processing: Insert comments above the current line
that AI terminals execute in the background. By default, the terminal reads the
file, finds comments with the terminal-specific prefix (e.g.,
AIDER!,CLAUDE!), executes the instructions in the comment, and removes it β all without bringing the terminal into focus. This allows you to send prompts completely in the background while continuing to read code. UseM.comment(terminal_name)to trigger this workflow. The automated mappings create comment shortcuts for each terminal:<leader>ac<term-key>(e.g.,<leader>accfor Claude,<leader>acafor Aider). For Aider specifically, you can also use the legacyM.aider_comment(prefix, focus)function (:h ai-terminals.aider_comment,:h ai-terminals.comment). - π Diff Changes: View changes made by AI tools in vim diff tabs.
Requires
enable_diffing = truein your config. Each changed file opens in its own tab for review. Reopening the terminal window resets the changes. See:help diff-modefor vim's diff commands like:diffputand:diffgetto manipulate changes. Alternatively, usediff_changes({ delta = true })to view changes with the delta diff viewer in a terminal. Note: Git-based diff tools (like gitsigns.nvim or fugitive.vim) provide more feature-rich diff management and are recommended for most workflows (:h ai-terminals.diff_changes). - π Git Integration Recommendation: For tracking changes made by AI tools,
we recommend using established git plugins:
- gitsigns.nvim: Shows git changes in the sign column and provides inline diff views
- telescope.nvim or snacks.nvim git pickers: Browse git status with custom actions for staging, discarding, or viewing changes
- diffview.nvim: Comprehensive git diff viewer with side-by-side comparisons
The plugin provides generic file management functions that work with any terminal:
-
π Add Files to Terminal:
add_files_to_terminal(terminal_name, files, opts)- Send files to any terminal using its configured file commands
- Terminals with file_commands: Uses configured templates (e.g., Aider
uses
/addor/read-onlycommands with automatic submission) - Other terminals: Uses fallback
@file1 @file2format without submission - Options:
{ read_only = true }for read-only mode (Aider only)
-
π Add Buffers to Terminal:
add_buffers_to_terminal(terminal_name, opts)- Add all currently listed buffers to any terminal
- Filters out invalid, unloaded, or non-modifiable buffers
- Uses the same command templates as
add_files_to_terminal
File Picker Integration: These functions integrate with file
pickers like Snacks.nvim. You can configure picker actions to add selected files
directly to any terminal with keymaps like <localleader>aa for Aider or
<localleader>cc for Claude. See the Snacks Picker Integration
section below for a complete working example and the
picker integration recipe for additional approaches.
Automatically format buffers when the AI agent modifies them.
- Default: disabled
- Provider order when enabled:
- conform.nvim (if installed)
- none-ls/null-ls
- any attached LSP
- Formatting runs asynchronously.
Enable via setup (auto-detects the provider in the order above):
require("ai-terminals").setup({
trigger_formatting = {
enabled = true,
},
})Conform first, then LSP fallback (this is the default behavior):
require("ai-terminals").setup({
trigger_formatting = { enabled = true, timeout_ms = 5000 },
})
-- Internally tries: require("conform").format({ lsp_format = "never" })
-- If conform.nvim isn't available, tries none-ls/null-ls, then any LSP.Note: If conform.nvim is not installed or has no formatter for the filetype, it falls back to any attached LSP automatically. Formatting runs asynchronously (non-blocking).
Control what gets watched for external edits made by your AI agent.
- Default: enabled (
watch_cwd.enabled = true). - When enabled, the plugin watches the current working directory recursively and will:
- Automatically load files modified by the agent into Neovim as buffers (using
:baddandbufload), even if they were not previously open. - Attach LSP servers and filetype autocmds to newly loaded buffers.
- Reload all modified files with
:checktime. - Format modified files if
trigger_formatting.enabled = true(supports conform.nvim, none-ls/null-ls, or LSP formatting as fallback). - Show notifications when files are updated.
- Automatically load files modified by the agent into Neovim as buffers (using
- When disabled, only files that were already open in Neovim are reloaded/formatted.
Enable in setup:
require("ai-terminals").setup({
watch_cwd = { enabled = true }, -- watch entire CWD (recursively)
trigger_formatting = { enabled = true }, -- optional: auto-format on reload
})You can exclude paths from directory watching using glob patterns.
- Examples:
"**/.git/**","**/node_modules/**","**/.venv/**","**/*.log" - Matching is performed against the path relative to your current working directory.
- Ignored files will not be loaded into Neovim nor formatted when changed by the agent.
require("ai-terminals").setup({
watch_cwd = {
enabled = true,
ignore = {
"**/.git/**",
"**/node_modules/**",
"**/.venv/**",
"**/*.log",
},
-- Also merge ignore rules from <git root>/.gitignore
-- Negations (!) are supported; patterns are evaluated relative to repo root
gitignore = true,
},
trigger_formatting = { enabled = true },
})Notes
- .gitignore semantics supported: comments (#), negation (!), root-anchored patterns (leading β/β), directory-only (trailing β/β), and β**β.
- Only the repository root .gitignore is read; per-directory .gitignore files are not currently merged.
- Matching uses paths relative to the git root for .gitignore rules, and paths relative to your current working directory for
watch_cwd.ignore.
These functions still work but are deprecated in favor of the generic file management:
- β Add Files: (Deprecated) Use
add_files_to_terminal("aider", files, opts)instead (:h ai-terminals.aider_add_files). - β Add Buffers: (Deprecated) Use
add_buffers_to_terminal("aider", opts)instead (:h ai-terminals.aider_add_buffers).
- Snacks.nvim: Required for terminal window management. π¬
- Optional CLI tools (install on demand): Aider, Claude CLI, Goose, Codex CLI, Cursor CLI
You can optionally configure the plugin using the setup function. This allows
you to define your own terminals or override the default commands and settings.
The tmux backend is the preferred approach for this plugin as it provides
better performance and stability. When using the tmux backend
(backend = "tmux"), the plugin provides additional configuration options:
Prerequisites: To use the tmux backend, you need to install the tmux-toggle-popup plugin:
# Add to your tmux.conf
set -g @plugin "loichyan/tmux-toggle-popup"
set -g @popup-toggle-mode 'force-close'Default Keybinding:
C-h- Hide/toggle the tmux popup when it's in focusEscape- Hide/toggle the tmux popup when it's in focus
This keybinding is automatically configured when using the tmux backend and allows you to quickly hide the terminal popup from within tmux.
Credit: The tmux nvim bridge implementation is adapted from tmux-toggle-popup.nvim. The code has been integrated directly into this repository for additional control and to avoid external dependencies.
Each terminal can have a custom path_header_template that controls how file
paths are formatted when sending visual selections. This allows different AI
terminals to receive path information in their preferred format without
requiring additional tool calls.
Default Templates:
- Aider:
`%s`(wrapped in backticks for Aider's file reference format) - All other terminals:
@%s(prefixed with @ symbol)
Custom Template Example:
terminals = {
my_ai_tool = {
path_header_template = "File: %s", -- Custom format
},
}When you send a visual selection from src/main.js to a terminal, the path
header will be formatted according to that terminal's template:
- Aider receives:
`src/main.js` - Claude receives:
@src/main.js - Custom tool receives:
File: src/main.js
Built-in integration with Snacks.nvim lets you add the currently selected files in any picker to an AI terminal in a single keystroke β without overriding your own Snacks configuration.
Important: prefixes
- Toggle/diagnostics/etc. keymaps (set by
auto_terminal_keymaps) useauto_terminal_keymaps.prefix(default:<leader>a). - Picker actions use a separate
auto_terminal_keymaps.picker_prefixand default to<localleader>so they donβt conflict with toggles.
What you get
- Actions for every configured terminal:
<term>_addis generated for all entries inConfig.config.terminals;<term>_read_onlyis added only when that terminal definesfile_commands.add_files_readonly(e.g.,aider). - Safe defaults: default keymaps are added to common file pickers only when a key is not already defined.
- Absolute path resolution: uses
Snacks.picker.util.path(item)to resolve project-relative entries to full paths. - Claude directory special-case: selecting a single directory triggers
claude /add-dir <dir>.
Enable it (lazy.nvim)
return {
{
"folke/snacks.nvim",
opts = function(_, opts)
local sa = require("ai-terminals.snacks_actions")
return sa.apply(opts) -- merges actions + default keymaps; user options win
end,
},
}Default keymaps (added only if unset)
- Keys derive from your
auto_terminal_keymapsentries:{picker_prefix}a{key}β{terminal}_add{picker_prefix}A{key}β{terminal}_read_only(only when supported) Where{picker_prefix}isauto_terminal_keymaps.picker_prefix(defaults to<localleader>). Example: withpicker_prefix = "<localleader>"and{ name = "claude", key = "c" }, the picker maps use<localleader>acforclaude_add.
- If
auto_terminal_keymapsis not set, no default picker mappings are added (actions are still available to bind manually).
Applied to these pickers
buffers,files,git_diff,git_files,git_log_file,git_log,git_status,grep_buffers,grep_word,grep,projects,recent,smart,explorer.
Customize
- Mappings never override yours. To change keys, set them in your Snacks opts
as usual;
sa.apply()will leave existing mappings intact. - Add keys for other terminals by binding to the generated actions (e.g.,
goose_add,cursor_add). Example:
opts = {
picker = {
sources = {
files = {
win = {
input = {
keys = {
["<localleader>ag"] = { "goose_add", mode = { "n", "i" } },
},
},
},
},
},
},
}Notes
- Disable notifications from the integration with
vim.g.ai_terminals_snacks_actions_notify = false. - If you prefer full manual control, skip
sa.apply()and define your own actions/mappings;ai-terminals.add_files_to_terminal()works everywhere.
A concise lazy.nvim setup that relies on auto-generated keymaps. This keeps your config small while still exposing all the common actions.
-- lua/plugins/ai-terminals.lua
return {
{
"aweis89/ai-terminals.nvim",
dependencies = { "folke/snacks.nvim" },
opts = {
-- Optional: customize commands and per-terminal formatting
terminals = {
claude = { cmd = function() return "claude" end },
aider = { cmd = function() return "aider --watch-files" end, path_header_template = "`%s`" },
goose = { cmd = function() return string.format("GOOSE_CLI_THEME=%s goose", vim.o.background) end },
},
-- One line to get consistent mappings for all terminals
auto_terminal_keymaps = {
prefix = "<leader>a",
terminals = {
{ name = "claude", key = "c" },
{ name = "aider", key = "a" },
{ name = "goose", key = "g" },
-- { name = "cursor", key = "r", enabled = false }, -- example disabled
},
},
},
config = function(_, opts)
require("ai-terminals").setup(opts)
-- Optional: integrate Snacks pickers with add-file actions
local sa = require("ai-terminals.snacks_actions")
require("snacks").setup({}) -- or your existing Snacks opts
sa.apply(require("snacks").config) -- merges actions + safe default picker keys
end,
},
}What you get out of the box (with the config above):
<leader>acc/<leader>aaa/<leader>aggβ Toggle Claude/Aider/Goose (double-tap); sends visual selection.<leader>acdβ Send diagnostics to Claude. Same pattern for other terminals.<leader>acl/<leader>acLβ Add current file / all buffers to Claude.<leader>acrβ Prompt for a shell command and send output to Claude.<leader>acmβ Add comment for Claude to execute in background.- Picker adds:
<localleader>acadds the selected file(s) to Claude. Same fora(Aider),g(Goose), etc.
If you only use one AI tool, you can skip auto_terminal_keymaps and create simpler keymaps manually:
-- lua/plugins/ai-terminals.lua
return {
{
"aweis89/ai-terminals.nvim",
dependencies = { "folke/snacks.nvim" },
opts = {
terminals = {
claude = { cmd = "claude" },
},
},
config = function(_, opts)
require("ai-terminals").setup(opts)
local ai = require("ai-terminals")
-- Toggle terminal (sends visual selection if active)
vim.keymap.set({ "n", "v" }, "<leader>a", function() ai.toggle("claude") end,
{ desc = "Claude: Toggle terminal" })
-- Send diagnostics
vim.keymap.set({ "n", "v" }, "<leader>ad", function() ai.send_diagnostics("claude") end,
{ desc = "Claude: Send diagnostics" })
-- Add current file
vim.keymap.set("n", "<leader>al", function()
ai.add_files_to_terminal("claude", { vim.fn.expand("%") })
end, { desc = "Claude: Add current file" })
-- Add all buffers
vim.keymap.set("n", "<leader>aL", function() ai.add_buffers_to_terminal("claude") end,
{ desc = "Claude: Add all buffers" })
-- Run command and send output
vim.keymap.set("n", "<leader>ar", function() ai.send_command_output("claude") end,
{ desc = "Claude: Run command and send output" })
-- Add comment for background execution
vim.keymap.set("n", "<leader>ac", function() ai.comment("claude") end,
{ desc = "Claude: Add comment for AI to address" })
end,
},
}Keymaps generated:
<leader>aβ Toggle Claude terminal (sends visual selection if active)<leader>adβ Send diagnostics to Claude<leader>alβ Add current file to Claude<leader>aLβ Add all buffers to Claude<leader>arβ Run command and send output to Claude<leader>acβ Add comment for Claude to execute in background
This gives you shorter keymaps without the terminal-specific suffix. Simply replace "claude" with your preferred terminal name ("aider", "goose", etc.).
-
Add the plugin specification to your
lazy.nvimconfiguration:-- lua/plugins/ai-terminals.lua return { "aweis89/ai-terminals.nvim", dependencies = { "folke/snacks.nvim" }, opts = { auto_terminal_keymaps = { prefix = "<leader>a", terminals = { { name = "claude", key = "c" }, { name = "aider", key = "a" }, }, }, }, config = function(_, opts) require("ai-terminals").setup(opts) local sa = require("ai-terminals.snacks_actions") sa.apply(require("snacks").config) end, }
-
Restart Neovim or run
:Lazy sync.
If you are using packer.nvim, you only need to call the setup function in
your configuration if you want to customize the defaults.
-- In your Neovim configuration (e.g., lua/plugins.lua)
use({
"aweis89/ai-terminals.nvim",
requires = { "folke/snacks.nvim" },
config = function()
require("ai-terminals").setup({
auto_terminal_keymaps = {
prefix = "<leader>a",
terminals = {
{ name = "claude", key = "c" },
{ name = "aider", key = "a" },
},
},
})
-- Optional Snacks picker integration
local sa = require("ai-terminals.snacks_actions")
sa.apply(require("snacks").config)
end,
})Note on destroy_all: This function stops the underlying processes
associated with the AI terminals and closes their windows/buffers using the
underlying Snacks library's destroy() method. The next time you use toggle
or open for a specific AI tool, a completely new instance of that tool will be
started.
ai-terminals.nvim can be easily integrated with other Neovim plugins for
advanced workflows. Check the recipes directory for examples.
Contributions, issues, and feature requests are welcome! Please feel free to check the issues page.
This project is licensed under the MIT License - see the LICENSE file for details.