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:
@@ -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`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user