docs(coding-agent/prompts): updated Vim tool prompt docs with strict kbd and insert guidance

- Clarified the Vim tool prompt to require `file` and clearly separate key commands from insert text.
- Added updated usage examples and best-practice guidance for whole-file replacement, line edits, search/replace, and undo.
- Documented key/mode constraints, including insert-entry requirements and required `<Esc>` handling between non-final commands.
This commit is contained in:
can1357
2026-04-13 17:02:58 +02:00
parent 51251ed7c5
commit b3537b0e3d
4 changed files with 92 additions and 35 deletions
+57 -23
View File
@@ -1,30 +1,64 @@
Stateful Vim-style editor with multi-buffer support.
Stateful Vim editor. Every call requires `file` — the buffer loads automatically on first use.
- `{"file": "path"}` — view file
- `{"file": "path", "kbd": ["…"], "insert": "…"}` — edit file
Every call requires `file` — the path to edit. The buffer is loaded automatically on first use.
## kbd vs insert — get this right
- `{"file": "path/to/file.py"}` — view file (loads buffer if needed)
- `{"file": "path/to/file.py", "kbd": ["…"], "insert": "…"}` — edit file
`kbd` = Vim commands only (`dd`, `G`, `o`, `cc`, `gg`, etc.).
`insert` = text content to type into the buffer.
How `kbd` + `insert` work together:
- `kbd` runs Vim key sequences (motions, commands, operators)
- `insert` is **raw text** (with real `\n` newlines in JSON) that gets typed into the buffer
- For `insert` to work, the last `kbd` entry **MUST** leave the buffer in INSERT mode (via `i`, `o`, `O`, `a`, `A`, `cc`, `C`, `s`, `S`, etc.)
- After the call, the tool auto-exits INSERT mode and auto-saves to disk
- Set `pause: true` to skip auto-save and stay in the current mode
**Never put text content in kbd.** Only Vim keystrokes go there.
- BAD: `{"kbd": ["1Gohello world<Esc>"]}` — text mixed into kbd
- GOOD: `{"kbd": ["1Go"], "insert": "hello world"}` — command in kbd, text in insert
Rules:
- Each non-final `kbd` entry must end in NORMAL mode — use `<Esc>` or merge into one string
- To recover from mistakes: `{"file": "f.py", "kbd": ["u"]}` to undo, or `{"file": "f.py", "kbd": [":e!<CR>"]}` to reload from disk
For `insert` to work, the last `kbd` entry must leave INSERT mode active (`o`, `O`, `i`, `a`, `A`, `cc`, `C`, `s`, `S`). The tool auto-exits INSERT and auto-saves after each call.
Special keys: `<Esc>`, `<CR>`, `<BS>`, `<Tab>`, `<C-d>`, `<C-u>`, `<C-r>`, `<C-w>`, `<C-o>`.
Each non-final `kbd` entry must end in NORMAL mode (add `<Esc>`).
Supported: motions (`h/j/k/l`, `w/b/e`, `0/$`, `gg/G`, `{/}`, `f/t`), counts, `.` repeat, insert (`i/a/o/O/I/A/cc/C/s/S`), visual (`v/V`), operators (`d/c/y/p`), text objects (`iw/aw/i"/a"/i(/a(`), undo/redo (`u`/`<C-r>`), search (`/pattern<CR>`, `n/N`), ex (`:s`, `:%s`, `:e`, `:e!`, ranged `:d`).
## Best pattern: replace entire file
Examples:
- `{"file": "src/app.ts"}` — view file
- `{"file": "src/app.ts", "kbd": ["3G", "ciwnewName<Esc>"]}` — rename word on line 3
- `{"file": "src/app.ts", "kbd": ["5G", "cc"], "insert": " if b == 0:\n return None"}` — replace line 5
- `{"file": "src/app.ts", "kbd": ["3G", "o"], "insert": "def multiply(a, b):\n return a * b"}` — insert after line 3
- `{"file": "src/app.ts", "kbd": [":%s/oldName/newName/g<CR>"]}` — find and replace
- `{"file": "src/app.ts", "kbd": ["/TODO<CR>", "dd"]}` — search and delete
- `{"file": "src/app.ts", "kbd": [":3,5d<CR>"]}` — delete line range
For any edit touching multiple locations, replace the whole file — it is the most reliable approach:
```json
{"file": "f.py", "kbd": ["ggdGi"], "insert": "entire new file content"}
```
`ggdGi` = go to top, delete all, enter INSERT. Then `insert` provides the complete new content.
## Surgical edits
**Insert after line N** (include indentation in insert — `o` does NOT auto-indent):
```json
{"file": "f.py", "kbd": ["3Go"], "insert": " new line here"}
```
**Replace line N** (`cc` clears the line and enters INSERT):
```json
{"file": "f.py", "kbd": ["5Gcc"], "insert": " replacement content"}
```
**Find and replace**:
```json
{"file": "f.py", "kbd": [":%s/old/new/g<CR>"]}
```
**Delete line range**:
```json
{"file": "f.py", "kbd": [":3,5d<CR>"]}
```
## Undo mistakes
- `{"file": "f.py", "kbd": ["u"]}` — undo last change
- `{"file": "f.py", "kbd": ["3u"]}` — undo last 3 changes
`:e!` reloads from disk, but since auto-save commits after each call, it will reload your last (possibly wrong) result. Use `u` to revert instead.
## Supported
Keys: `<Esc>` `<CR>` `<BS>` `<Tab>` `<C-d>` `<C-u>` `<C-r>` `<C-w>` `<C-o>`
Motions: `h j k l w b e 0 $ gg G { } f t` with counts
Operators: `d c y p` with motions and text objects (`iw aw i" a" i( a(`)
Insert: `i a o O I A cc C s S` — these all enter INSERT mode; do NOT add another `i` after them
Visual: `v V`
Other: `.` repeat, `u`/`<C-r>` undo/redo, `/pattern<CR>` search, `n N`
Ex: `:w` `:q` `:wq` `:e` `:e!` `:N` `:s///` `:%s///` `:N,Md` `:%d`
+5 -1
View File
@@ -352,7 +352,11 @@ export class VimTool implements AgentTool<typeof vimSchema, VimToolDetails> {
if (fp) engine.buffer.baseFingerprint = fp;
}
const sequences = Array.isArray(params.kbd) ? params.kbd : typeof params.kbd === "string" ? [params.kbd] : undefined;
const sequences = Array.isArray(params.kbd)
? params.kbd
: typeof params.kbd === "string"
? [params.kbd]
: undefined;
if (!sequences) {
// No kbd — just show the file viewport
if (isNewBuffer) {
+10 -2
View File
@@ -1284,7 +1284,11 @@ export class VimEngine {
if (!next || next.value !== "g") {
throw new VimError("Unsupported g motion", token);
}
return { nextIndex: index + 2, target: { line: hasCount ? Math.max(0, count - 1) : 0, col: 0 }, linewise: true };
return {
nextIndex: index + 2,
target: { line: hasCount ? Math.max(0, count - 1) : 0, col: 0 },
linewise: true,
};
}
case "G":
return {
@@ -1708,6 +1712,10 @@ export class VimEngine {
digits += value;
cursor += 1;
}
return { count: digits.length > 0 ? Number.parseInt(digits, 10) : 1, hasCount: digits.length > 0, nextIndex: cursor };
return {
count: digits.length > 0 ? Number.parseInt(digits, 10) : 1,
hasCount: digits.length > 0,
nextIndex: cursor,
};
}
}
+20 -9
View File
@@ -240,7 +240,9 @@ describe("vim tool", () => {
const tool = new VimTool(createSession(tmpDir));
await tool.execute("open", { file: "ambiguous.ts" });
await expect(tool.execute("bad", { file: "ambiguous.ts", kbd: ["o", "o"] })).rejects.toThrow(/left Vim in INSERT mode/i);
await expect(tool.execute("bad", { file: "ambiguous.ts", kbd: ["o", "o"] })).rejects.toThrow(
/left Vim in INSERT mode/i,
);
});
it("rejects additional kbd entries after entering insert mode", async () => {
@@ -249,7 +251,9 @@ describe("vim tool", () => {
const tool = new VimTool(createSession(tmpDir));
await tool.execute("open", { file: "insert-boundary.ts" });
await expect(tool.execute("edit", { file: "insert-boundary.ts", kbd: ["2G", "o", "o"] })).rejects.toThrow(/insert field|<Esc>/i);
await expect(tool.execute("edit", { file: "insert-boundary.ts", kbd: ["2G", "o", "o"] })).rejects.toThrow(
/insert field|<Esc>/i,
);
const saved = await Bun.file(filePath).text();
expect(saved).toBe("alpha\nbeta\n");
});
@@ -311,12 +315,17 @@ describe("vim tool", () => {
const pendingInputs: string[] = [];
await tool.execute("open", { file: "command.ts" });
const result = await tool.execute("command", { file: "command.ts", kbd: [":%s/foo/bar/g<CR>"] }, undefined, update => {
const pending = update.details?.pendingInput;
if (pending?.kind === "command") {
pendingInputs.push(pending.text);
}
});
const result = await tool.execute(
"command",
{ file: "command.ts", kbd: [":%s/foo/bar/g<CR>"] },
undefined,
update => {
const pending = update.details?.pendingInput;
if (pending?.kind === "command") {
pendingInputs.push(pending.text);
}
},
);
expect(pendingInputs).toContain("");
expect(pendingInputs).toContain("%");
@@ -340,7 +349,9 @@ describe("vim tool", () => {
const moved = await tool.execute("move", { file: "plan.ts", kbd: ["2G"] });
expect(textResult(moved)).toContain("L2:1");
await expect(tool.execute("edit", { file: "plan.ts", kbd: ["dd"] })).rejects.toThrow(/Plan mode/i);
await expect(tool.execute("insert", { file: "plan.ts", kbd: ["cc"], insert: "blocked" })).rejects.toThrow(/Plan mode/i);
await expect(tool.execute("insert", { file: "plan.ts", kbd: ["cc"], insert: "blocked" })).rejects.toThrow(
/Plan mode/i,
);
});
});