feat(coding-agent): expose edit tool unified patch

closes #4821
This commit is contained in:
Mario Zechner
2026-05-21 12:15:39 +02:00
parent 7dad27e5f2
commit 60a55a2399
5 changed files with 28 additions and 3 deletions

View File

@@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Added
- Added a standard unified patch to edit tool result details for SDK consumers ([#4821](https://github.com/earendil-works/pi/issues/4821)).
### Fixed ### Fixed
- Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). - Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)).

View File

@@ -473,6 +473,8 @@ Specify which built-in tools to enable:
- `noTools: "all"` disables all tools - `noTools: "all"` disables all tools
- `noTools: "builtin"` disables default built-ins while keeping extension and custom tools enabled - `noTools: "builtin"` disables default built-ins while keeping extension and custom tools enabled
The `edit` tool returns `details.diff` for Pi's TUI display and `details.patch` as a standard unified patch for SDK consumers.
```typescript ```typescript
import { createAgentSession } from "@earendil-works/pi-coding-agent"; import { createAgentSession } from "@earendil-works/pi-coding-agent";

View File

@@ -259,8 +259,16 @@ export function applyEditsToNormalizedContent(
return { baseContent, newContent }; return { baseContent, newContent };
} }
/** Generate a standard unified patch. */
export function generateUnifiedPatch(path: string, oldContent: string, newContent: string, contextLines = 4): string {
return Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {
context: contextLines,
headerOptions: Diff.FILE_HEADERS_ONLY,
});
}
/** /**
* Generate a unified diff string with line numbers and context. * Generate a display-oriented diff string with line numbers and context.
* Returns both the diff string and the first changed line number (in the new file). * Returns both the diff string and the first changed line number (in the new file).
*/ */
export function generateDiffString( export function generateDiffString(

View File

@@ -13,6 +13,7 @@ import {
type EditDiffError, type EditDiffError,
type EditDiffResult, type EditDiffResult,
generateDiffString, generateDiffString,
generateUnifiedPatch,
normalizeToLF, normalizeToLF,
restoreLineEndings, restoreLineEndings,
stripBom, stripBom,
@@ -57,8 +58,10 @@ type LegacyEditToolInput = EditToolInput & {
}; };
export interface EditToolDetails { export interface EditToolDetails {
/** Unified diff of the changes made */ /** Display-oriented diff of the changes made */
diff: string; diff: string;
/** Standard unified patch of the changes made */
patch: string;
/** Line number of the first change in the new file (for editor navigation) */ /** Line number of the first change in the new file (for editor navigation) */
firstChangedLine?: number; firstChangedLine?: number;
} }
@@ -394,6 +397,7 @@ export function createEditToolDefinition(
} }
const diffResult = generateDiffString(baseContent, newContent); const diffResult = generateDiffString(baseContent, newContent);
const patch = generateUnifiedPatch(path, baseContent, newContent);
resolve({ resolve({
content: [ content: [
{ {
@@ -401,7 +405,7 @@ export function createEditToolDefinition(
text: `Successfully replaced ${edits.length} block(s) in ${path}.`, text: `Successfully replaced ${edits.length} block(s) in ${path}.`,
}, },
], ],
details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }, details: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
}); });
} catch (error: unknown) { } catch (error: unknown) {
// Clean up abort handler. // Clean up abort handler.

View File

@@ -1,3 +1,4 @@
import { applyPatch } from "diff";
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { join } from "path"; import { join } from "path";
@@ -239,6 +240,12 @@ describe("Coding Agent Tools", () => {
expect(result.details.diff).toBeDefined(); expect(result.details.diff).toBeDefined();
expect(typeof result.details.diff).toBe("string"); expect(typeof result.details.diff).toBe("string");
expect(result.details.diff).toContain("testing"); expect(result.details.diff).toContain("testing");
expect(result.details.patch).toContain("--- ");
expect(result.details.patch).toContain("+++ ");
expect(result.details.patch).toContain("@@");
expect(result.details.patch).toContain("-Hello, world!");
expect(result.details.patch).toContain("+Hello, testing!");
expect(applyPatch(originalContent, result.details.patch)).toBe("Hello, testing!");
}); });
it("should fail if text not found", async () => { it("should fail if text not found", async () => {