fix(coding-agent): queue file mutations across edit and write
closes #2327
This commit is contained in:
@@ -1354,6 +1354,36 @@ Use `promptGuidelines` to add tool-specific bullets to the default system prompt
|
||||
|
||||
Note: Some models are idiots and include the @ prefix in tool path arguments. Built-in tools strip a leading @ before resolving paths. If your custom tool accepts a path, normalize a leading @ as well.
|
||||
|
||||
If your custom tool mutates files, use `withFileMutationQueue()` so it participates in the same per-file queue as built-in `edit` and `write`. This matters because tool calls run in parallel by default. Without the queue, two tools can read the same old file contents, compute different updates, and then whichever write lands last overwrites the other.
|
||||
|
||||
Example failure case: your custom tool edits `foo.ts` while built-in `edit` also changes `foo.ts` in the same assistant turn. If your tool does not participate in the queue, both can read the original `foo.ts`, apply separate changes, and one of those changes is lost.
|
||||
|
||||
Pass the real target file path to `withFileMutationQueue()`, not the raw user argument. Resolve it to an absolute path first, relative to `ctx.cwd` or your tool's working directory. For existing files, the helper canonicalizes through `realpath()`, so symlink aliases for the same file share one queue. For new files, it falls back to the resolved absolute path because there is nothing to `realpath()` yet.
|
||||
|
||||
Queue the entire mutation window on that target path. That includes read-modify-write logic, not just the final write.
|
||||
|
||||
```typescript
|
||||
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const absolutePath = resolve(ctx.cwd, params.path);
|
||||
|
||||
return withFileMutationQueue(absolutePath, async () => {
|
||||
await mkdir(dirname(absolutePath), { recursive: true });
|
||||
const current = await readFile(absolutePath, "utf8");
|
||||
const next = current.replace(params.oldText, params.newText);
|
||||
await writeFile(absolutePath, next, "utf8");
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Updated ${params.path}` }],
|
||||
details: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Definition
|
||||
|
||||
```typescript
|
||||
@@ -1617,7 +1647,7 @@ renderResult(result, { expanded, isPartial }, theme) {
|
||||
|
||||
#### Keybinding Hints
|
||||
|
||||
Use `keyHint()` to display keybinding hints that respect user's keybinding configuration:
|
||||
Use `keyHint()` to display keybinding hints that respect the active keybinding configuration:
|
||||
|
||||
```typescript
|
||||
import { keyHint } from "@mariozechner/pi-coding-agent";
|
||||
@@ -1625,18 +1655,23 @@ import { keyHint } from "@mariozechner/pi-coding-agent";
|
||||
renderResult(result, { expanded }, theme) {
|
||||
let text = theme.fg("success", "✓ Done");
|
||||
if (!expanded) {
|
||||
text += ` (${keyHint("expandTools", "to expand")})`;
|
||||
text += ` (${keyHint("app.tools.expand", "to expand")})`;
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
}
|
||||
```
|
||||
|
||||
Available functions:
|
||||
- `keyHint(action, description)` - Editor actions (e.g., `"expandTools"`, `"selectConfirm"`)
|
||||
- `appKeyHint(keybindings, action, description)` - App actions (requires `KeybindingsManager`)
|
||||
- `editorKey(action)` - Get raw key string for editor action
|
||||
- `keyHint(keybinding, description)` - Formats a configured keybinding id such as `"app.tools.expand"` or `"tui.select.confirm"`
|
||||
- `keyText(keybinding)` - Returns the raw configured key text for a keybinding id
|
||||
- `rawKeyHint(key, description)` - Format a raw key string
|
||||
|
||||
Use namespaced keybinding ids:
|
||||
- Coding-agent ids use the `app.*` namespace, for example `app.tools.expand`, `app.editor.external`, `app.session.rename`
|
||||
- Shared TUI ids use the `tui.*` namespace, for example `tui.select.confirm`, `tui.select.cancel`, `tui.input.tab`
|
||||
|
||||
Custom editors and `ctx.ui.custom()` components receive `keybindings: KeybindingsManager` as an injected argument. They should use that injected manager directly instead of calling `getKeybindings()` or `setKeybindings()`.
|
||||
|
||||
#### Best Practices
|
||||
|
||||
- Use `Text` with padding `(0, 0)` - the Box handles padding
|
||||
|
||||
Reference in New Issue
Block a user