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
82 changes: 75 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Cursor Agent Neovim Plugin

A minimal Neovim plugin to run the Cursor Agent CLI inside a centered floating terminal. Toggle an interactive terminal at your project root, or send the current buffer or a visual selection to Cursor Agent.
A minimal Neovim plugin to run the Cursor Agent CLI inside a terminal window. Toggle an interactive terminal at your project root, or send the current buffer or a visual selection to Cursor Agent. Supports both floating window and sidebar modes.

### Requirements
- **Cursor Agent CLI**: `cursor-agent` available on your `$PATH`
Expand Down Expand Up @@ -42,22 +42,39 @@ Note: The plugin auto-initializes with defaults on load (via `after/plugin/curso

## Quickstart

- Run `:CursorAgent` to toggle an interactive floating terminal in your project root. Type directly into the `cursor-agent` program.
- Run `:CursorAgent` to toggle an interactive terminal in your project root. Type directly into the `cursor-agent` program.
- Visually select code, then use `:CursorAgentSelection` to ask about just that selection.
- Run `:CursorAgentBuffer` to send the entire current buffer (handy for files like `cursor.md`).
- Press `q` in normal mode in the floating terminal to close it or run :CursorAgent `<leader>ca` to toggle it away.
- Press `q` in normal mode to close the terminal or run `:CursorAgent` / `<leader>ca` to toggle it away.

All interactions happen in a centered floating terminal.
By default, interactions happen in a centered floating window. You can configure the plugin to use an attached sidebar instead (see Configuration section).

## Commands

- **:CursorAgent**: Toggle the interactive Cursor Agent terminal (project root).
- **:CursorAgent**: Toggle the interactive Cursor Agent terminal (project root). Uses your configured window mode.
- **:CursorAgentSelection**: Send the current visual selection (writes to a temp file and opens terminal rendering).
- **:CursorAgentBuffer**: Send the full current buffer (writes to a temp file and opens terminal rendering).

### Window Mode Behavior

#### Floating Mode (default)
- Opens a centered floating window
- Title bar shows "Cursor Agent"
- Rounded borders
- Press `q` in normal mode to close

#### Attached Mode (Sidebar)
- Opens as a vertical split
- Can be positioned on left or right side
- Configurable width as fraction of screen (e.g., 0.2 = 20%)
- Press `q` in normal mode to close
- Window integrates with your existing split layout

## Configuration

Only set what you need. For typical usage, `cmd` and `args` are enough.

### Basic Configuration
```lua
require("cursor-agent").setup({
-- Executable or argv table. Example: "cursor-agent" or {"/usr/local/bin/cursor-agent"}
Expand All @@ -67,7 +84,56 @@ require("cursor-agent").setup({
})
```

Advanced (for lower-level CLI helpers present in the codebase but not required for terminal mode):
### Window Mode Configuration

The plugin supports two window modes: floating (default) and attached (sidebar).

#### Default Configuration (Floating Window)
```lua
require("cursor-agent").setup({
-- Default behavior - opens in floating window
window_mode = "floating", -- or omit this line for default
})
```

#### Attached Mode (Sidebar) - Right Side
```lua
require("cursor-agent").setup({
window_mode = "attached",
position = "right", -- Opens on right side
width = 0.2, -- 1/5 of screen width
})
```

#### Attached Mode (Sidebar) - Left Side
```lua
require("cursor-agent").setup({
window_mode = "attached",
position = "left", -- Opens on left side
width = 0.25, -- 1/4 of screen width
})
```

### Complete Configuration Example
```lua
require("cursor-agent").setup({
-- Standard options
cmd = "cursor-agent",
args = {},
use_stdin = true,
multi_instance = false,
timeout_ms = 60000,
auto_scroll = true,

-- Window mode options
window_mode = "attached", -- Use split window instead of floating
position = "right", -- Position on right side
width = 0.2, -- Use 1/5 of screen width (20%)
})
```

### Advanced Options
For lower-level CLI helpers present in the codebase but not required for terminal mode:
```lua
require("cursor-agent").setup({
-- Whether to send content via stdin when using non-terminal helpers
Expand Down Expand Up @@ -118,7 +184,9 @@ This opens a floating terminal using `termopen`, with the working directory set

## How it works

- A floating terminal is created with `termopen`, centered, wrapped, and ready for immediate input.
- A terminal window is created with `termopen`, ready for immediate input. Window mode depends on your configuration:
- **Floating mode**: Creates a centered floating window with rounded borders
- **Attached mode**: Creates a vertical split positioned on the left or right side
- The terminal starts in the detected project root so Cursor Agent has the right context.
- For selection/buffer commands, the text is written to a temporary file and its path is passed to the CLI as a positional argument.

Expand Down
45 changes: 39 additions & 6 deletions doc/cursor-agent.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,53 @@ CONFIGURATION *cursor-agent-config*
Use Lua to configure in your init.lua:
>
require('cursor-agent').setup({
cmd = 'cursor-agent', -- or { 'cursor-agent', 'cli' }
args = {}, -- default extra args
use_stdin = true, -- send content via stdin
multi_instance = true, -- allow concurrent calls
cmd = 'cursor-agent', -- or { 'cursor-agent', 'cli' }
args = {}, -- default extra args
use_stdin = true, -- send content via stdin
multi_instance = true, -- allow concurrent calls
timeout_ms = 60000,
window_mode = 'floating', -- 'floating' or 'attached' (split)
position = 'right', -- 'left' or 'right' (for attached mode)
width = 0.2, -- width fraction (e.g., 0.2 = 1/5 screen)
})
<

WINDOW MODES *cursor-agent-window-modes*

The plugin supports two window modes:

- `floating` (default): Opens in a centered floating window
- `attached`: Opens in a vertical split pane

For attached mode, you can configure:
- `position`: 'left' or 'right' side of screen (default: 'right')
- `width`: fraction of screen width (default: 0.2 for 1/5 of screen)

Examples:
>
-- Open in right split at 1/5 screen width
require('cursor-agent').setup({
window_mode = 'attached',
position = 'right',
width = 0.2,
})

-- Open in left split at 1/4 screen width
require('cursor-agent').setup({
window_mode = 'attached',
position = 'left',
width = 0.25,
})
<

NOTES *cursor-agent-notes*

- The plugin uses a floating terminal to render Cursor Agent's native UI. The
- terminal starts in the project root (detected via markers like `.git`).
- The plugin uses a terminal window (floating or split pane) to render Cursor
Agent's native UI. The terminal starts in the project root (detected via
markers like `.git`).
- Configure `cmd` to point to your Cursor Agent CLI. If the executable is
missing, an error is shown.
- In both window modes, press 'q' in normal mode to close the window.
- For help tags: :helptags ALL or :helptags {plugin-doc-dir}

LICENSE *cursor-agent-license*
Expand Down
6 changes: 6 additions & 0 deletions lua/cursor-agent/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ local default_config = {
timeout_ms = 60000,
-- Auto-scroll output buffer to the end as new content arrives
auto_scroll = true,
-- Window mode: "floating" or "attached" (split window)
window_mode = "floating",
-- Position for attached mode: "left" or "right"
position = "right",
-- Width for attached mode (fraction of screen width, e.g., 0.2 for 1/5 of screen)
width = 0.2,
}

local active_config = vim.deepcopy(default_config)
Expand Down
111 changes: 76 additions & 35 deletions lua/cursor-agent/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,40 @@ function M.ask(opts)
end

local root = util.get_project_root()
termui.open_float_term({
argv = argv,
title = title,
border = 'rounded',
width = 0.6,
height = 0.6,
cwd = root,
on_exit = function(code)
if code ~= 0 then
util.notify(('cursor-agent exited with code %d'):format(code), vim.log.levels.WARN)
end
end,
})

if cfg.window_mode == "attached" then
termui.open_split_term({
argv = argv,
position = cfg.position,
width = cfg.width,
cwd = root,
on_exit = function(code)
if code ~= 0 then
util.notify(('cursor-agent exited with code %d'):format(code), vim.log.levels.WARN)
end
end,
})
else
termui.open_float_term({
argv = argv,
title = title,
border = 'rounded',
width = 0.6,
height = 0.6,
cwd = root,
on_exit = function(code)
if code ~= 0 then
util.notify(('cursor-agent exited with code %d'):format(code), vim.log.levels.WARN)
end
end,
})
end
end

-- Toggle a long-lived cursor-agent terminal at project root
function M.toggle_terminal()
local st = M._term_state
local cfg = config.get()

-- If window is open, close it (toggle off)
if st.win and vim.api.nvim_win_is_valid(st.win) then
Expand All @@ -104,34 +120,59 @@ function M.toggle_terminal()

-- If we have a valid buffer with a live job, just reopen a window for it
if st.bufnr and vim.api.nvim_buf_is_valid(st.bufnr) and job_is_alive(st.job_id) then
st.win = termui.open_float_win_for_buf(st.bufnr, {
title = 'Cursor Agent',
border = 'rounded',
width = 0.6,
height = 0.6,
})
if cfg.window_mode == "attached" then
st.win = termui.open_split_win_for_buf(st.bufnr, {
position = cfg.position,
width = cfg.width,
})
else
st.win = termui.open_float_win_for_buf(st.bufnr, {
title = 'Cursor Agent',
border = 'rounded',
width = 0.6,
height = 0.6,
})
end
return st.bufnr, st.win
end

-- Otherwise spawn a fresh terminal
local cfg = config.get()
local argv = util.concat_argv(util.to_argv(cfg.cmd), cfg.args)
local root = util.get_project_root()
local bufnr, win, job_id = termui.open_float_term({
argv = argv,
title = 'Cursor Agent',
border = 'rounded',
width = 0.6,
height = 0.6,
cwd = root,
on_exit = function(code)
-- Clear stored job id when it exits
if M._term_state then M._term_state.job_id = nil end
if code ~= 0 then
util.notify(('cursor-agent exited with code %d'):format(code), vim.log.levels.WARN)
end
end,
})
local bufnr, win, job_id

if cfg.window_mode == "attached" then
bufnr, win, job_id = termui.open_split_term({
argv = argv,
position = cfg.position,
width = cfg.width,
cwd = root,
on_exit = function(code)
-- Clear stored job id when it exits
if M._term_state then M._term_state.job_id = nil end
if code ~= 0 then
util.notify(('cursor-agent exited with code %d'):format(code), vim.log.levels.WARN)
end
end,
})
else
bufnr, win, job_id = termui.open_float_term({
argv = argv,
title = 'Cursor Agent',
border = 'rounded',
width = 0.6,
height = 0.6,
cwd = root,
on_exit = function(code)
-- Clear stored job id when it exits
if M._term_state then M._term_state.job_id = nil end
if code ~= 0 then
util.notify(('cursor-agent exited with code %d'):format(code), vim.log.levels.WARN)
end
end,
})
end

st.bufnr, st.win, st.job_id = bufnr, win, job_id
return bufnr, win
end
Expand Down
28 changes: 28 additions & 0 deletions lua/cursor-agent/ui/float.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,34 @@ local function resolve_size(value, total)
return math.floor(total * 0.6)
end

---Open a split window for displaying output
---@param opts table
---@field position string|nil "left" or "right" (defaults to "right")
---@field width number|nil Width in columns or 0-1 float for percentage (defaults to 0.2)
---@return integer bufnr, integer win
function M.open_split(opts)
opts = opts or {}
local position = opts.position or "right"
local width = resolve_size(opts.width or 0.2, vim.o.columns)

local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(bufnr, "filetype", "cursor-agent-output")

-- Create the split window
local split_cmd = position == "left" and "leftabove vertical " or "rightbelow vertical "
split_cmd = split_cmd .. width .. "split"

vim.cmd(split_cmd)
local win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(win, bufnr)

vim.wo[win].wrap = true
vim.wo[win].cursorline = false

return bufnr, win
end

function M.open_float(opts)
opts = opts or {}
local width = resolve_size(opts.width or 0.5, vim.o.columns)
Expand Down
Loading