A Neovim plugin that brings any CLI AI tool into your editor as a persistent sidebar panel.
aiwaku does not lock you into a specific AI tool. You point it at any command-line AI assistant — copilot, claude, opencode, aider, or anything else — and it runs it in a tmux session attached to a Neovim terminal split. Sessions survive window toggles, editor restarts, and workspace switches. Your conversation context is always one keymap away.
- Tool-agnostic — works with any CLI AI tool you configure
- Persistent sessions — backed by tmux; context survives toggling the panel or restarting Neovim
- Multiple sessions — create, switch, and rename sessions without losing context
- Send visual selection — send selected code (with optional prompt prefix) directly to the AI
- Send entire buffer — send the full current file (with filename and filetype context) to the AI
- LSP code actions — send selections via the standard code action menu when null-ls is active
- Sidebar layout — opens as a left or right vertical split with configurable width
- Async — all tmux operations are non-blocking; the editor stays responsive
| Dependency | Purpose |
|---|---|
| tmux ≥ 3.0 | Session persistence and management |
| Neovim ≥ 0.10 | Required for vim.system and modern API |
Note
aiwaku requires tmux (>= 3.0) to provide persistent sessions. If tmux is not installed or not available on your PATH, session features (create, resume, list) will not work. Install tmux or ensure it is accessible from Neovim before using aiwaku.
| Plugin | Purpose |
|---|---|
| nvim-lua/plenary.nvim | Async job execution |
| stevearc/dressing.nvim (optional) | Floating UI for vim.ui.select; without it Neovim falls back to the built-in command-line prompt. Other providers (e.g. telescope with the ui-select extension) also work. |
| nvimtools/none-ls.nvim (optional) | LSP code actions integration |
Using lazy.nvim:
{
"juhaku/aiwaku.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
"stevearc/dressing.nvim", -- optional: floating UI for vim.ui.select (without it Neovim falls back to the built-in command-line prompt)
},
opts = { cmd = { "opencode" } } -- your CLI AI tool
}The only required option is cmd — the CLI AI command to run.
require("aiwaku").setup({
cmd = { "opencode" },
})cmd can be a list (recommended, avoids shell quoting issues) or a plain string:
cmd = { "/usr/local/bin/claude", "--model", "claude-3-5-sonnet" }
cmd = "aider --model gpt-4o"Full default configuration
require("aiwaku").setup({
-- CLI command to run inside the tmux session.
-- Use a list to avoid shell quoting issues, or a plain string.
cmd = { "copilot" },
-- Sidebar width in columns.
width = 80,
-- Which side to open the sidebar on.
position = "right", -- "right" | "left"
-- Normal/visual mode keymaps.
-- Key: mode list, Value: map of lhs -> { command, description }
keymaps = {
[{ "n" }] = {
["<leader>ai"] = {
command = function() require("aiwaku").toggle() end,
description = "Toggle Aiwaku",
},
["<leader>an"] = {
command = function() require("aiwaku").new_session() end,
description = "Aiwaku: new session",
},
["<leader>as"] = {
command = function() require("aiwaku").select_session() end,
description = "Aiwaku: select session",
},
["<leader>ar"] = {
command = function() require("aiwaku").rename_session() end,
description = "Aiwaku: rename session",
},
["<leader>ab"] = {
command = function() require("aiwaku").send_buffer() end,
description = "Aiwaku: send buffer",
},
},
[{ "v" }] = {
["<leader>ai"] = {
command = "<Esc><Cmd>lua require('aiwaku').send_selection()<CR>",
description = "Aiwaku: send selection",
},
},
},
-- LSP code actions shown via null-ls/none-ls.
-- Each entry needs a title; prompt is optional.
lsp_code_actions = {
{ title = "Send to Aiwaku" },
{ title = "AI: explain this code", prompt = "explain this code:" },
{ title = "AI: refactor this code", prompt = "refactor this code:" },
{ title = "AI: send this file", buffer = true },
{ title = "AI: explain this file", prompt = "explain this file:", buffer = true },
},
-- Keymaps active only inside the terminal buffer.
terminal_keymaps = {
["<C-w>h"] = { command = "<C-\\><C-n><C-w>h", description = "Focus left" },
["<C-w>l"] = { command = "<C-\\><C-n><C-w>l", description = "Focus right" },
["<C-a>r"] = {
command = "<C-\\><C-n><Cmd>lua require('aiwaku').rename_session()<CR>",
description = "Aiwaku: rename session",
},
["<C-a>s"] = {
command = "<C-\\><C-n><Cmd>lua require('aiwaku').select_session()<CR>",
description = "Aiwaku: select session",
},
["<C-a>n"] = {
command = "<C-\\><C-n><Cmd>lua require('aiwaku').new_session()<CR>",
description = "Aiwaku: new session",
},
["<C-a>c"] = {
command = "<C-\\><C-n><Cmd>lua require('aiwaku').clear_context()<CR>",
description = "Aiwaku: clear context",
},
["<C-o>"] = {
command = "<C-\\><C-n><Cmd>lua require('aiwaku').open_cword_in_tab()<CR>",
description = "Aiwaku: open file under cursor in new tab",
},
},
})| Option | Type | Default | Description |
|---|---|---|---|
cmd |
string | string[] |
{ "copilot" } |
CLI command to run in the tmux session |
width |
integer |
80 |
Sidebar panel width in columns |
position |
"right" | "left" |
"right" |
Side of the screen to open the panel |
keymaps |
table |
see above | Normal/visual mode keymaps |
lsp_code_actions |
{ title = string, prompt? = string }[] |
see above | LSP code actions exposed through null-ls/none-ls |
terminal_keymaps |
table |
see above | Keymaps active inside the terminal buffer |
| Key | Action |
|---|---|
<leader>ai |
Toggle the AI sidebar (open/close) |
<leader>an |
Start a new session |
<leader>as |
Select from existing sessions |
<leader>ar |
Rename the current session |
<leader>ab |
Send the current buffer to the AI |
| Key | Action |
|---|---|
<leader>ai |
Send selected text to the AI |
| Key | Action |
|---|---|
<C-w>h |
Move focus to the left window |
<C-w>l |
Move focus to the right window |
<C-a>s |
Select from existing sessions |
<C-a>n |
Start a new session |
<C-a>r |
Rename the current session |
<C-a>c |
Clear context (kill session and start fresh) |
<C-o> |
Open file path under cursor in a new tab |
aiwaku ships a null-ls source that exposes AI actions through the standard LSP code action menu (:lua vim.lsp.buf.code_action()).
Register the source alongside your other null-ls sources:
local null_ls = require("null-ls")
null_ls.setup({
sources = {
require("aiwaku.lsp-code-actions"),
-- your other sources...
},
})The following actions are included by default and appear in the code action menu for any filetype when null-ls is active on the buffer:
| Action | Behaviour |
|---|---|
| Send to Aiwaku | Send selection without a prompt prefix |
| AI: explain this code | Prepend "explain this code:" before the selection |
| AI: refactor this code | Prepend "refactor this code:" before the selection |
| AI: send this file | Send the full buffer without a prompt prefix |
| AI: explain this file | Prepend "explain this file:" before the buffer content |
Actions without buffer = true call send_selection() internally. Actions with buffer = true call send_buffer() instead. The sidebar is opened automatically if it is not already visible.
You can replace the default action list during setup:
require("aiwaku").setup({
cmd = { "opencode" },
lsp_code_actions = {
{ title = "Send to Aiwaku" },
{ title = "AI: write tests", prompt = "write tests for:" },
{ title = "AI: review this code", prompt = "review this code:" },
},
})Each entry requires a title. The prompt field is optional; when omitted, aiwaku sends the current selection without a prefix. Entries with buffer = true send the entire current buffer instead of the visual selection.
Note: null-ls (or its community fork none-ls) must be installed and have an active client attached to the buffer for code actions to appear.
All functions are available on the require("aiwaku") table after calling setup().
| Function | Description |
|---|---|
setup(opts) |
Initialize the plugin with your configuration |
toggle() |
Open or close the sidebar |
new_session(name?) |
Create a new AI session (optional name) |
select_session() |
Open a picker to switch sessions |
rename_session() |
Rename the current session interactively |
clear_context() |
Kill the current session and start a fresh one |
send_selection(prompt?) |
Send the current visual selection to the AI (optional prompt prefix) |
send_buffer(prompt?) |
Send the entire current buffer to the AI (optional prompt prefix) |
open_cword_in_tab() |
Open the file path under cursor (from AI output) in a new tab |
session_name() |
Return the display name of the active session, or nil when none is active |
You can call send_selection with a prefix to give the AI context:
-- From a keymap or command
require("aiwaku").send_selection("Explain this code:")
require("aiwaku").send_selection("Write tests for:")
require("aiwaku").send_selection("Refactor to be more idiomatic:")send_buffer captures all lines of the current buffer and prepends a file-context header (filename and filetype) before sending:
require("aiwaku").send_buffer()
require("aiwaku").send_buffer("Review this file for bugs:")
require("aiwaku").send_buffer("Write a README for this module:")If the sidebar is not visible it is opened automatically before sending.
When the AI references a file (e.g. src/parser.lua:42), place the cursor on that path in the terminal and press <C-o> to open it in a new Neovim tab. Line numbers in the form file.lua:42 or file.lua:42:5 are parsed and the cursor jumps there automatically. If the path is not absolute, aiwaku tries to resolve it relative to the current working directory.
You can also open files from the terminal shell directly — aiwaku propagates $NVIM into the tmux session automatically:
nvim --server "$NVIM" --remote-tab src/parser.luasession_name() returns the display name of the active AI session (the ai- prefix is stripped), or nil when no session is active.
{
function()
return " " .. (require("aiwaku").session_name() or "")
end,
cond = function()
return require("aiwaku").session_name() ~= nil
end,
}_G.AiwakuStatusline = function()
local name = require("aiwaku").session_name()
return name and (" " .. name) or ""
end
vim.o.statusline = "%{%v:lua.AiwakuStatusline()%} %f %=%l:%c"Licensed under MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this plugin by you, shall be licensed as MIT, without any additional terms or conditions.
