From 02dd71d847a2fe9aff2adaa6910581d688204e90 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sat, 28 Feb 2026 21:13:59 +0100 Subject: [PATCH] fix(ast): allow empty replacements and stop trimming patterns/rewrites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the non-empty validation on `out` in both the TypeScript tool layer and the Rust native layer. An empty replacement template now deletes the matched AST node, which is the natural semantics. Also remove .trim() calls on pattern and rewrite strings — trimming silently destroys intentional whitespace and serves no purpose since ast-grep matches on AST structure, not raw text. --- crates/pi-natives/src/ast.rs | 11 ++--------- packages/coding-agent/CHANGELOG.md | 2 ++ packages/coding-agent/src/tools/ast-replace.ts | 14 +++----------- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/crates/pi-natives/src/ast.rs b/crates/pi-natives/src/ast.rs index 8e74dbc9f..1ecb55c76 100644 --- a/crates/pi-natives/src/ast.rs +++ b/crates/pi-natives/src/ast.rs @@ -517,20 +517,13 @@ fn normalize_rewrite_map( rewrites: Option>, ) -> Result> { let mut normalized = Vec::new(); - for (raw_pattern, raw_rewrite) in rewrites.unwrap_or_default() { - let pattern = raw_pattern.trim(); + for (pattern, rewrite) in rewrites.unwrap_or_default() { if pattern.is_empty() { return Err(Error::from_reason( "`rewrites` keys must be non-empty pattern strings".to_string(), )); } - let rewrite = raw_rewrite.trim(); - if rewrite.is_empty() { - return Err(Error::from_reason(format!( - "`rewrites[{pattern}]` must be a non-empty rewrite template" - ))); - } - normalized.push((pattern.to_string(), rewrite.to_string())); + normalized.push((pattern, rewrite)); } if normalized.is_empty() { return Err(Error::from_reason( diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 74649307c..1c3f120b4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -20,6 +20,8 @@ ### Fixed +- `ast_replace` no longer rejects empty `out` values; an empty string now deletes matched nodes +- `ast_replace` no longer trims `pat` and `out` values, preserving intentional whitespace - `gemini_image` tool: corrected `responseModalities` values from `'Image'`/`'Text'` to uppercase `'IMAGE'`/`'TEXT'` matching the API enum ### Breaking Changes diff --git a/packages/coding-agent/src/tools/ast-replace.ts b/packages/coding-agent/src/tools/ast-replace.ts index 0e02f9f9a..1e93e4d1d 100644 --- a/packages/coding-agent/src/tools/ast-replace.ts +++ b/packages/coding-agent/src/tools/ast-replace.ts @@ -69,15 +69,10 @@ export class AstReplaceTool implements AgentTool> { return untilAborted(signal, async () => { const ops = params.ops.map((entry, index) => { - const pat = entry.pat.trim(); - const out = entry.out.trim(); - if (pat.length === 0) { + if (entry.pat.length === 0) { throw new ToolError(`\`ops[${index}].pat\` must be a non-empty pattern`); } - if (out.length === 0) { - throw new ToolError(`\`ops[${index}].out\` must be a non-empty replacement template`); - } - return [pat, out] as const; + return [entry.pat, entry.out] as const; }); if (ops.length === 0) { throw new ToolError("`ops` must include at least one op entry"); @@ -122,10 +117,7 @@ export class AstReplaceTool implements AgentTool { - const relative = path.relative(this.session.cwd, resolvedSearchPath).replace(/\\/g, "/"); - return relative.length === 0 ? "." : relative; - })(); + const scopePath = path.relative(this.session.cwd, resolvedSearchPath).replace(/\\/g, "/") || "."; let isDirectory: boolean; try { const stat = await Bun.file(resolvedSearchPath).stat();