Thanks to visit codestin.com
Credit goes to github.com

Skip to content

aweis89/ai-terminals.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ€– AI Terminals Neovim Plugin

StandWithPalestine test lint

Screenshot 2025-07-27 at 11 04 59β€―AM

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.

⚑ Quick Start

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).

Public API

  • 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.

πŸ”Œ Integrations

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:

If you choose to use any of these, make sure they are installed and accessible in your system's PATH.

πŸ€” Motivation

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, and send_diagnostics that 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.

✨ Features

βš™οΈ Generic Features (Works with any terminal-based AI agent)

  • πŸ”Œ 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 Snacks for 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., @filename or `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. Use M.comment(terminal_name) to trigger this workflow. The automated mappings create comment shortcuts for each terminal: <leader>ac<term-key> (e.g., <leader>acc for Claude, <leader>aca for Aider). For Aider specifically, you can also use the legacy M.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 = true in your config. Each changed file opens in its own tab for review. Reopening the terminal window resets the changes. See :help diff-mode for vim's diff commands like :diffput and :diffget to manipulate changes. Alternatively, use diff_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

πŸ“ File Management

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 /add or /read-only commands with automatic submission)
    • Other terminals: Uses fallback @file1 @file2 format 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.

🧼 Format On External Change

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).

πŸ‘€ Directory Watch Mode (Optional)

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 :badd and bufload), 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.
  • 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
})

Ignore Patterns (Globs)

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.

Deprecated Functions

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).

πŸ”— Dependencies

πŸ”§ Configuration

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.

πŸ–₯️ Tmux Backend (Preferred)

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 focus
  • Escape - 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.

🎯 Path Header Templates

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

πŸ”Œ Snacks Picker Integration

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) use auto_terminal_keymaps.prefix (default: <leader>a).
  • Picker actions use a separate auto_terminal_keymaps.picker_prefix and default to <localleader> so they don’t conflict with toggles.

What you get

  • Actions for every configured terminal: <term>_add is generated for all entries in Config.config.terminals; <term>_read_only is added only when that terminal defines file_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_keymaps entries:
    • {picker_prefix}a{key} β†’ {terminal}_add
    • {picker_prefix}A{key} β†’ {terminal}_read_only (only when supported) Where {picker_prefix} is auto_terminal_keymaps.picker_prefix (defaults to <localleader>). Example: with picker_prefix = "<localleader>" and { name = "claude", key = "c" }, the picker maps use <localleader>ac for claude_add.
  • If auto_terminal_keymaps is 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.

πŸ“‹ Example Usage

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>ac adds the selected file(s) to Claude. Same for a (Aider), g (Goose), etc.

Single Terminal Setup

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.).

πŸ“¦ Installation

Using lazy.nvim

  1. Add the plugin specification to your lazy.nvim configuration:

    -- 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,
    }
  2. Restart Neovim or run :Lazy sync.

Using packer.nvim

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.

🧩 Integrating with Other Tools or Pickers

ai-terminals.nvim can be easily integrated with other Neovim plugins for advanced workflows. Check the recipes directory for examples.

🀝 Contributing

Contributions, issues, and feature requests are welcome! Please feel free to check the issues page.

πŸ“œ License

This project is licensed under the MIT License - see the LICENSE file for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •