From e506508c81278574b2af9be5d1eb7fc0f4b93cc5 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 22 Jul 2026 16:46:58 +0800 Subject: [PATCH] refactor(vim): inject code via bracketed paste, reference buffers by @path --- vim/README.md | 13 ++++--- vim/README_EN.md | 13 ++++--- vim/autoload/agent.vim | 69 +++++++++++++++++---------------- vim/test/smoke.py | 14 ++----- vim/test/smoke.vim | 12 +++--- vim/test/test_build_context.vim | 21 ---------- 6 files changed, 58 insertions(+), 84 deletions(-) delete mode 100644 vim/test/test_build_context.vim diff --git a/vim/README.md b/vim/README.md index 9fcb229..68eb9f0 100644 --- a/vim/README.md +++ b/vim/README.md @@ -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(软链方式,改代码立即生效): @@ -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 ` | 直接向 agent 注入任意文本 | 注入的文本不会自动回车提交,你在 agent 终端里确认后再发送。 @@ -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 **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: @@ -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 ` | 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. @@ -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 解析 +" 为特殊键,需把字面量 < 转成 记法。 +function! s:send_paste(text) abort + let l:body = s:has_nvim ? a:text : substitute(a:text, '<', '', 'g') + call s:send_raw("\x1b[200~" . l:body . "\x1b[201~") +endfunction + " reenter: 调用本函数前是否已经坐在终端窗口且处于 Terminal-Normal 模式 " (Ctrl-W N 翻历史后)。只有这种情况才需要手动 feedkeys('i') 回到输入模式。 " 刚从别的窗口切进来时 Vim 会在命令结束后自动进入 Terminal-Job 模式,此时 @@ -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。 @@ -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 diff --git a/vim/test/smoke.py b/vim/test/smoke.py index 4f21fe1..2e3779d 100644 --- a/vim/test/smoke.py +++ b/vim/test/smoke.py @@ -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 """ @@ -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): diff --git a/vim/test/smoke.vim b/vim/test/smoke.vim index 0402534..6f56319 100644 --- a/vim/test/smoke.vim +++ b/vim/test/smoke.vim @@ -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! @@ -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') " 验证:进程退出后窗口自动关闭 diff --git a/vim/test/test_build_context.vim b/vim/test/test_build_context.vim deleted file mode 100644 index 2bdaa4a..0000000 --- a/vim/test/test_build_context.vim +++ /dev/null @@ -1,21 +0,0 @@ -" headless 单测:vim -Nu NONE -n -es -S test_build_context.vim -" 结果写入 /tmp/agent_vim_test.out(PASS / FAIL) -set nocompatible -let s:root = expand(':p:h:h') -execute 'set rtp+=' . fnameescape(s:root) - -" 用一个已知路径的 scratch 文件 -edit! /tmp/agent_vim_test_buf.sh -call setline(1, ['line A', 'line B', 'line C']) -set filetype=sh - -let l:ctx = agent#build_context(2, 3) -let l:expect = "/tmp/agent_vim_test_buf.sh:2-3\n\n```sh\nline B\nline C\n```\n" - -if l:ctx !=# l:expect - call writefile(['FAIL', 'expect: ' . string(l:expect), 'got: ' . string(l:ctx)], '/tmp/agent_vim_test.out') - cquit! -endif - -call writefile(['PASS'], '/tmp/agent_vim_test.out') -quit!