Skip to content
Closed
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
13 changes: 7 additions & 6 deletions vim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

在 Vim 8+ / Neovim 中用右侧常驻分屏运行 `ccagent`(VSCode 式布局:左编辑区、右 chat),并可把选中代码 / 整个缓冲区注入 agent 输入行。

> **发送机制**
> - `AgentSend`:选中代码用 **bracketed paste** 直接灌进 agent 输入框(不写临时文件)
> - `AgentSendBuffer`:已落盘且未修改的 buffer 注入 **`@/abs/path`** 引用,由 agent 自行用 Read 工具读取;未落盘或已修改的 buffer 自动退化为粘贴当前内容

## 安装

Vim 8 原生 package(软链方式,改代码立即生效):
Expand All @@ -26,8 +30,8 @@ ln -s /path/to/bash-agent/vim ~/.local/share/nvim/site/pack/local/start/agent
|------|------|
| `:AgentToggle` | 打开/隐藏右侧 agent 终端;冷启动时使用续聊命令 |
| `:AgentToggle!` | 使用新会话命令;已运行则杀掉当前进程并重启 |
| `:[range]AgentSend` | 把 `[range]`(或可视选择)的代码写入临时文件,向 agent 注入引用 |
| `:AgentSendBuffer` | 整个缓冲区同上 |
| `:[range]AgentSend` | 把 `[range]`(或可视选择)的代码用 bracketed paste 直接注入 agent 输入框 |
| `:AgentSendBuffer` | 已落盘的 buffer 注入 `@/abs/path` 引用;未落盘/已修改则粘贴当前内容 |
| `:AgentAsk <text>` | 直接向 agent 注入任意文本 |

注入的文本不会自动回车提交,你在 agent 终端里确认后再发送。
Expand Down Expand Up @@ -200,9 +204,6 @@ export AGENT_CONTINUE_COMMAND='zsh -ic open_claude'
## 测试

```bash
# headless 单测(context 构建)
vim -Nu NONE -n -es -S vim/test/test_build_context.vim </dev/null

# PTY 冒烟测试(用 cat 冒充 agent,验证终端/注入/退出全流程)
# PTY 冒烟测试(用 cat 冒充 agent,验证终端/注入/@ 引用/退出全流程)
python3 vim/test/smoke.py
```
13 changes: 7 additions & 6 deletions vim/README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

Run `ccagent` in a persistent right-side split in Vim 8+ or Neovim, using a VS Code-style layout with the editor on the left and chat on the right. The plugin can also inject selected code or the entire current buffer into the agent input line.

> **Send mechanism**
> - `AgentSend`: pastes the selected code into the agent input box via **bracketed paste** (no temp file).
> - `AgentSendBuffer`: injects an **`@/abs/path`** reference for saved, unmodified buffers (the agent reads the file via its Read tool); unsaved or modified buffers fall back to pasting the current content.

## Installation

Vim 8 native packages using a symlink, so local changes take effect immediately:
Expand All @@ -26,8 +30,8 @@ ln -s /path/to/bash-agent/vim ~/.local/share/nvim/site/pack/local/start/agent
|---|---|
| `:AgentToggle` | Show or hide the agent terminal. When starting a process, use the continue command. |
| `:AgentToggle!` | Use the new-session command. If a process is running, stop it and restart. |
| `:[range]AgentSend` | Write the range or visual selection to a temporary file and inject a reference into the agent input line. |
| `:AgentSendBuffer` | Send a reference to the entire current buffer. |
| `:[range]AgentSend` | Paste the range or visual selection into the agent input box via bracketed paste. |
| `:AgentSendBuffer` | Inject `@/abs/path` for saved buffers; paste current content for unsaved/modified ones. |
| `:AgentAsk <text>` | Inject arbitrary text and submit it directly to the agent. |

Injected code references are not submitted automatically. Review or extend the input in the agent terminal, then press Enter.
Expand Down Expand Up @@ -200,9 +204,6 @@ Note: the C implementation of `ccagent` supports only `--interactive`; it does n
## Testing

```bash
# Headless unit test for context generation
vim -Nu NONE -n -es -S vim/test/test_build_context.vim </dev/null

# PTY smoke test covering terminal startup, injection, toggling, and shutdown
# PTY smoke test covering terminal startup, injection, @ reference, toggling, and shutdown
python3 vim/test/smoke.py
```
69 changes: 35 additions & 34 deletions vim/autoload/agent.vim
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
" agent.vim — bash-agent 终端集成(路线 A::terminal 常驻分屏)
" 布局:左侧编辑区,右侧 agent chat(VSCode 式)。
" 发送机制:选中内容写入临时 .md 文件,只往 chat 输入行注入一行短引用文本
" (不带回车),用户接着输入问题后回车;agent 用 Read 工具读文件。
" 发送机制:
" - AgentSend 把选中代码用 bracketed paste 直接灌进 agent 输入框
" - AgentSendBuffer 已落盘且未修改的 buffer 注入 @/abs/path 引用(agent
" 自行用 Read 工具读取);否则退化为 AgentSend 粘贴内容
" 注入文本均不带回车,用户确认后自行按 Enter 提交。
" 兼容 vim8(term_start/term_sendkeys)与 neovim(termopen/chansend)。

let s:has_nvim = has('nvim')
let s:buf = -1 " agent 终端 buffer 号(-1 = 无)
let s:job = -1 " nvim job id(vim8 由 buffer 反查)
let s:mode = 'continue' " 启动模式:continue(追加 --continue 续聊)/ new(新会话)
let s:seq = 0 " ctx 文件序号

" ---------- 配置 ----------

Expand All @@ -30,19 +32,6 @@ function! s:command(mode) abort
return l:cmd
endfunction

function! s:ctx_dir() abort
let l:tmp = empty($TMPDIR) ? '/tmp' : substitute($TMPDIR, '/$', '', '')
return l:tmp . '/bash-agent-vim'
endfunction

function! s:ensure_ctx_dir() abort
let l:dir = s:ctx_dir()
if !isdirectory(l:dir)
call mkdir(l:dir, 'p', 0700)
endif
return l:dir
endfunction

" ---------- 终端生命周期 ----------

function! s:alive() abort
Expand Down Expand Up @@ -148,6 +137,16 @@ function! s:send_enter() abort
endif
endfunction

" 用 bracketed paste(ESC[200~ ... ESC[201~)包裹后注入。支持该协议的
" REPL(ccagent 的 linenoise / claude / codex 等)会把整段含换行的文本
" 作为一次粘贴并入输入缓冲,不逐行提交;不支持时标记字符会被原样回显,
" 但内容仍可见,agent 侧一般也能识别。vim8 term_sendkeys 把 <...> 解析
" 为特殊键,需把字面量 < 转成 <LT> 记法。
function! s:send_paste(text) abort
let l:body = s:has_nvim ? a:text : substitute(a:text, '<', '<LT>', 'g')
call s:send_raw("\x1b[200~" . l:body . "\x1b[201~")
endfunction

" reenter: 调用本函数前是否已经坐在终端窗口且处于 Terminal-Normal 模式
" (Ctrl-W N 翻历史后)。只有这种情况才需要手动 feedkeys('i') 回到输入模式。
" 刚从别的窗口切进来时 Vim 会在命令结束后自动进入 Terminal-Job 模式,此时
Expand All @@ -170,21 +169,6 @@ function! s:in_term_normal() abort
return !s:has_nvim && s:buf >= 0 && bufnr('%') == s:buf && mode() ==# 'n'
endfunction

" ---------- 上下文构建(公共函数,便于 headless 单测) ----------

function! agent#build_context(first, last) abort
let l:header = printf('%s:%d-%d', expand('%:p'), a:first, a:last)
let l:body = join(['```' . &filetype] + getline(a:first, a:last) + ['```'], "\n")
return l:header . "\n\n" . l:body . "\n"
endfunction

function! s:write_ctx(content) abort
let s:seq += 1
let l:file = printf('%s/ctx-%d-%d.md', s:ensure_ctx_dir(), localtime(), s:seq)
call writefile(split(a:content, "\n", 1), l:file)
return l:file
endfunction

" ---------- 公共 API ----------

" vim 退出前杀掉 agent job 并清掉终端 buffer。
Expand Down Expand Up @@ -218,20 +202,37 @@ function! agent#toggle(mode) abort
call s:focus(l:reenter)
endfunction

" 把 [first, last] 范围的代码直接粘贴进 agent 输入框(不自动提交)。
" 注入格式:
" /abs/path/to/file:first-last
" <代码原文>
function! agent#send_range(first, last) abort
if s:buf >= 0 && bufnr('%') == s:buf
echoerr 'agent: 请在代码窗口执行 :AgentSend(当前在 agent 终端窗口)'
return
endif
let l:file = s:write_ctx(agent#build_context(a:first, a:last))
let l:ref = printf('看 %s(%s:%d-%d 的代码片段)', l:file, expand('%:t'), a:first, a:last)
let l:path = expand('%:p')
let l:header = empty(l:path)
\ ? printf('[未命名 buffer]:%d-%d', a:first, a:last)
\ : printf('%s:%d-%d', l:path, a:first, a:last)
let l:text = l:header . "\n" . join(getline(a:first, a:last), "\n") . "\n"
let l:reenter = s:in_term_normal()
call s:ensure_running()
call s:send_raw(l:ref)
call s:send_paste(l:text)
call s:focus(l:reenter)
endfunction

" 优先用 @ 引用整个文件路径(agent 会自动用 Read 工具读取);
" buffer 未落盘或已修改时退化为 send_range(粘贴当前内容,避免读到旧版本)。
function! agent#send_buffer() abort
let l:path = expand('%:p')
if !empty(l:path) && filereadable(l:path) && !&modified
let l:reenter = s:in_term_normal()
call s:ensure_running()
call s:send_raw('@' . l:path)
call s:focus(l:reenter)
return
endif
call agent#send_range(1, line('$'))
endfunction

Expand Down
14 changes: 3 additions & 11 deletions vim/test/smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
1. :AgentToggle! 使用 AGENT_NEW_COMMAND 在右侧启动 terminal
2. 冷启动 :AgentToggle 使用 AGENT_CONTINUE_COMMAND
3. :AgentAsk hello 后 cat 回显 hello(输入行回显 + cat 输出,至少 2 次)
4. :AgentSendBuffer 注入的引用文本不含尖括号、不带回车(cat 不会立刻回显第二行)
4. :AgentSendBuffer 注入 @/abs/path 引用,不含尖括号、不带回车

用法: python3 vim/test/smoke.py
"""
Expand Down Expand Up @@ -78,17 +78,9 @@

# 终端宽度仅 40 列,注入文本会被折行,先去掉空白再匹配
flat = re.sub(r"\s+", "", data)
m = re.search(r"看(\S+ctx-\d+-\d+\.md)(agent_vim_smoke_buf\.sh:1-2的代码片段)", flat)
m = re.search(r"(@/tmp/agent_vim_smoke_buf\.sh)", flat)
if not m:
fails.append("AgentSendBuffer 注入的引用文本未出现在终端")
else:
ctx = m.group(1)
if not os.path.exists(ctx):
fails.append(f"ctx 文件不存在: {ctx}")
else:
body = open(ctx).read()
if not body.startswith("/tmp/agent_vim_smoke_buf.sh:1-2\n\n```sh\necho one\necho two\n```"):
fails.append(f"ctx 文件内容不对: {body!r}")
fails.append("AgentSendBuffer 注入的 @ 引用未出现在终端")

# AgentToggle 应能关闭窗口,且重开时复用同一 buffer
if not os.path.exists(OUT_TOGGLE):
Expand Down
12 changes: 6 additions & 6 deletions vim/test/smoke.vim
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ let g:agent_command = 'agent-command-must-not-run'
let g:agent_width = 40

" 准备一个已知内容的 buffer 用于 AgentSendBuffer
edit! /tmp/agent_vim_smoke_buf.sh
call setline(1, ['echo one', 'echo two'])
" 先写文件落盘并 :edit,确保走 @/abs/path 引用分支(未落盘会退化为粘贴)
call writefile(['echo one', 'echo two'], '/tmp/agent_vim_smoke_buf.sh')
edit /tmp/agent_vim_smoke_buf.sh
set filetype=sh

AgentToggle!
Expand All @@ -32,12 +33,11 @@ call writefile([s:win_closed && s:win_reopen ? 'toggle-ok' : 'toggle-fail'], '/t
let s:lines = getbufline('bash-agent', 1, '$')
call writefile(s:lines, '/tmp/agent_vim_smoke.out')

" 验证:注入文本首尾无泄漏按键(s:focus 的 feedkeys('i') 竞态会在末尾多一个 i,
" 冷启动 AgentToggle 同理会在开头多一个 i)。长引用行会被 40 列终端折行,
" 拼接所有行再检查,避免折行把 )和 i 分到两行漏检。
" 验证:AgentSendBuffer 注入的 @ 引用不含尖括号、不带回车(cat 不会立刻回显第二行)。
" 长引用行会被 40 列终端折行,拼接所有行再检查;@path 之后紧跟 i 才算 focus 竞态泄漏。
let s:joined = join(s:lines, '')
let s:echo_ok = index(s:lines, 'hello-vim-agent') >= 0
let s:ref_ok = stridx(s:joined, 'ctx-') >= 0 && stridx(s:joined, '代码片段)i') < 0
let s:ref_ok = stridx(s:joined, '@/tmp/agent_vim_smoke_buf.sh') >= 0 && stridx(s:joined, '@/tmp/agent_vim_smoke_buf.shi') < 0
call writefile([s:echo_ok && s:ref_ok ? 'noleak-ok' : 'noleak-fail'], '/tmp/agent_vim_smoke_noleak.out')

" 验证:进程退出后窗口自动关闭
Expand Down
21 changes: 0 additions & 21 deletions vim/test/test_build_context.vim

This file was deleted.

Loading