AI: Agentic vim - local, cloudy, losely coupled?

3 minute read

AI: Agentic vim - local, cloudy, losely coupled?

I have been playing around with / working productively with AI in vim for a while now. For religious and practical reasons with local llms, but also with AI agents, namely kiro-cli, opencode and gemini cli, integrated into my workflow and environment with sidekick.

Through sidekick, the different AI agents were integrated in a separate buffer but with common and uniform keybindings, context integration (file content, diagnostics, ..) etc.

Still, every AI agent has its own interaction model, UI details, look and feel etc, and environment integration by sidekick, while impressive and mostly surprisingly stable, was rather hacky.

For my personal environment, I’ve been using gemini. My employer only provides kiro-cli and (limited) opencode, and I want to have an as-uniform experience as possible. So, for a while I would have wanted to have an abstraction over the cli agents in my environment.

Theoretically, that’s what ACP is for. However, ACP support in kiro-cli has not been present from the start, and I became rather familiar with the setup I had in the meantime.

Until I had a bit of time at my hands to touch the setup again last week.

The Problem

  • I use nvim
  • I (have to) use different AI agents (kiro-cli, opencode, gemini)
  • I want a uniform look and feel, configuration, capabilities, etc. in my nvim environment, no matter which AI agent powers my AI workflow
  • In most cases, the environment integration is more important to me than the agent specific capabilities and advantages that might be lost with it, especially when it comes to agent autonomy and orchestration

Possible solution

The Agent Client Protocol (ACP) standardizes communication between code editors/IDEs and coding agents.

If all used agents support the ACP, a single ACP client configuration could provide the solution I want.

Sadly, when I started the fun with “my” main agent kiro-cli, it has not supported ACP yet.

Solution

A while ago, kiro-cli added support for ACP 🥳!

No more reason not to try it out.

After a bit of research, I decided to use codecompantion to replace sidekick for the AI integration.

The full configuration can be found here, but I will write about a few details of the configuration.

Keybindings

I had the following sidekick keybindings:

A-a to open/toggle the agent window, including the selection as context, and A-i to toggle with the predefined prompt catalogue.

  keys = {
    {
      "<A-a>",
      function()
        require("sidekick.cli").toggle({ filter = { installed = true } })
      end,
      desc = "Sidekick Toggle",
      mode = { "n", "t", "i" },
    },
    {
      "<A-a>",
      function()
        require("sidekick.cli").send({
          filter = { installed = true },
          msg = "The current file is {file}, this is the current selection: \n\n```\n{selection}\n```",
        })
      end,
      desc = "Sidekick Toggle with context",
      mode = { "x" },
    },
    {
      "<A-i>",
      function()
        require("sidekick.cli").prompt({ filter = { installed = true } })
      end,
      desc = "Sidekick Select Prompt",
      mode = { "n", "t", "i", "x" },
    },
  },

For codecompanion, the same can be achieved with these keybindings:

    keys = {
      {
        "<A-a>",
        "<cmd>CodeCompanionChat Toggle<cr>",
        desc = "CodeCompanion Toggle",
        mode = { "n", "t", "i", "x" },
      },
      {
        "<A-i>",
        "<cmd>CodeCompanionActions<cr>",
        desc = "CodeCompanion Actions",
        mode = { "n", "t", "i", "x" },
      },
    },

Buffer guard

The codecompanion buffer behaves like a normal buffer. Which means that if I jump in the jumplist while being in it, it would be replaced. Or if a custom / weird LSP action is triggered. Or…

So, I wanted to protect the codecompanion buffer, as identified through its virtual filetype.

init = function()
      -- If new buffers are opened or jumps being triggered while focus is on codecompanion, the action should be triggered outside of the codecompanion buffer
      vim.api.nvim_create_autocmd("FileType", {
        pattern = "codecompanion",
        callback = function(args)
          vim.bo[args.buf].buflisted = false
          local win = vim.fn.bufwinid(args.buf)
          if win == -1 then
            return
          end
          -- Guard this window: redirect any non-cc buffer that lands here
          vim.api.nvim_create_autocmd("BufWinEnter", {
            callback = function(ev)
              if not vim.api.nvim_win_is_valid(win) then
                return true
              end
              if vim.api.nvim_get_current_win() ~= win then
                return
              end
              if ev.buf == args.buf then
                return
              end
              -- Restore cc buffer in this window, move new buffer elsewhere
              vim.api.nvim_win_set_buf(win, args.buf)
              for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
                if w ~= win and vim.bo[vim.api.nvim_win_get_buf(w)].filetype ~= "codecompanion" then
                  vim.api.nvim_win_set_buf(w, ev.buf)
                  vim.api.nvim_set_current_win(w)
                  return
                end
              end
              vim.cmd("split | buffer " .. ev.buf)
            end,
          })
        end,
      })
    end

tl;dr

  • ACP is nice for Agent/Client integration
  • setting up an work environment to use agent integration through ACP allows for the definition of common behaviour and user experience, even with different agents underneath it all
  • nvim can be set up to use ACP with plugins, e.g. codecompanion
  • ?
  • profit

Leave a comment