@heyhuynhgiabuu/pi-diff 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -181
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -160
- package/dist/index.js.map +1 -1
- package/package.json +5 -51
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -53
- package/dist/cli.js.map +0 -1
- package/dist/review/command.d.ts +0 -3
- package/dist/review/command.d.ts.map +0 -1
- package/dist/review/command.js +0 -492
- package/dist/review/command.js.map +0 -1
- package/dist/review/export.d.ts +0 -7
- package/dist/review/export.d.ts.map +0 -1
- package/dist/review/export.js +0 -74
- package/dist/review/export.js.map +0 -1
- package/dist/review/file-preview.d.ts +0 -9
- package/dist/review/file-preview.d.ts.map +0 -1
- package/dist/review/file-preview.js +0 -225
- package/dist/review/file-preview.js.map +0 -1
- package/dist/review/hunk-bridge.d.ts +0 -54
- package/dist/review/hunk-bridge.d.ts.map +0 -1
- package/dist/review/hunk-bridge.js +0 -105
- package/dist/review/hunk-bridge.js.map +0 -1
- package/dist/review/interactive.d.ts +0 -27
- package/dist/review/interactive.d.ts.map +0 -1
- package/dist/review/interactive.js +0 -137
- package/dist/review/interactive.js.map +0 -1
- package/dist/review/model.d.ts +0 -33
- package/dist/review/model.d.ts.map +0 -1
- package/dist/review/model.js +0 -197
- package/dist/review/model.js.map +0 -1
- package/dist/review/prompt.d.ts +0 -4
- package/dist/review/prompt.d.ts.map +0 -1
- package/dist/review/prompt.js +0 -40
- package/dist/review/prompt.js.map +0 -1
- package/dist/review/session.d.ts +0 -69
- package/dist/review/session.d.ts.map +0 -1
- package/dist/review/session.js +0 -168
- package/dist/review/session.js.map +0 -1
- package/dist/review/tui.d.ts +0 -57
- package/dist/review/tui.d.ts.map +0 -1
- package/dist/review/tui.js +0 -482
- package/dist/review/tui.js.map +0 -1
- package/prompts/review-diff-agent.md +0 -19
package/README.md
CHANGED
|
@@ -25,8 +25,8 @@ A [pi](https://pi.dev) extension that replaces the default `write` and `edit` to
|
|
|
25
25
|
- **Adaptive layout** — auto-detects terminal width; wraps intelligently on wide terminals, truncates on narrow ones
|
|
26
26
|
- **LRU cache** — singleton Shiki highlighter with 192-entry cache for fast re-renders
|
|
27
27
|
- **Large diff fallback** — gracefully degrades (skips highlighting, still shows diff structure) for files > 80k chars
|
|
28
|
-
- **Review context export** — `pi-diff-review` exports Git changes as agent-ready markdown for Warp-style local code review
|
|
29
28
|
- **Fully customizable** — every color and threshold is overridable via environment variables
|
|
29
|
+
- **Edit guard** — blocks `edit` tool calls whose `oldText` is no longer in the target file, forcing the model to re-read before retrying (prevents stale-`oldText` retry loops)
|
|
30
30
|
|
|
31
31
|
## Install
|
|
32
32
|
|
|
@@ -42,170 +42,6 @@ Or load directly for development:
|
|
|
42
42
|
pi -e ./src/index.ts
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
## Git Review in Pi TUI
|
|
46
|
-
|
|
47
|
-
pi-diff provides two review surfaces:
|
|
48
|
-
|
|
49
|
-
1. **`/review-diff`** — the primary interactive local-review command. If [Hunk](https://github.com/modem-dev/hunk) is installed, pi-diff opens Hunk's review-first TUI.
|
|
50
|
-
2. **`review_git_diff`** — a read-only markdown/tool path for agent context and scripted review.
|
|
51
|
-
|
|
52
|
-
### Install Hunk for `/review-diff`
|
|
53
|
-
|
|
54
|
-
`/review-diff` works best with Hunk installed:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
# npm
|
|
58
|
-
npm i -g hunkdiff
|
|
59
|
-
|
|
60
|
-
# or Homebrew
|
|
61
|
-
brew install modem-dev/tap/hunk
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Verify Hunk is available:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
hunk --version
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
If Hunk is not installed, pi-diff falls back to its built-in legacy review overlay and shows a warning.
|
|
71
|
-
|
|
72
|
-
### `/review-diff` — Hunk-backed interactive review
|
|
73
|
-
|
|
74
|
-
Run inside Pi:
|
|
75
|
-
|
|
76
|
-
```text
|
|
77
|
-
/review-diff
|
|
78
|
-
/review-diff --base main
|
|
79
|
-
/review-diff working-tree
|
|
80
|
-
/review-diff feature/base-branch
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
What happens:
|
|
84
|
-
|
|
85
|
-
```text
|
|
86
|
-
/review-diff
|
|
87
|
-
→ pi-diff reads the current Git diff
|
|
88
|
-
→ pipes the patch to `hunk patch -`
|
|
89
|
-
→ Hunk opens its sidebar + diff review TUI
|
|
90
|
-
→ when you exit Hunk, pi-diff returns to Pi
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
You can test the same Hunk view outside Pi:
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
git diff | hunk patch -
|
|
97
|
-
# or
|
|
98
|
-
hunk diff
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Hunk is responsible for the review UI: multi-file sidebar, split/stack layouts, keyboard navigation, mouse support, and agent-note/comment workflows. pi-diff intentionally does **not** maintain a second in-memory comment system.
|
|
102
|
-
|
|
103
|
-
### Hunk configuration
|
|
104
|
-
|
|
105
|
-
Hunk reads config from:
|
|
106
|
-
|
|
107
|
-
```text
|
|
108
|
-
~/.config/hunk/config.toml # global defaults
|
|
109
|
-
.hunk/config.toml # repo-local override
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Recommended global starter config:
|
|
113
|
-
|
|
114
|
-
```toml
|
|
115
|
-
# ~/.config/hunk/config.toml
|
|
116
|
-
|
|
117
|
-
theme = "midnight" # graphite, midnight, paper, ember, catppuccin-latte, catppuccin-mocha, custom
|
|
118
|
-
mode = "auto" # auto, split, stack
|
|
119
|
-
vcs = "git"
|
|
120
|
-
watch = false
|
|
121
|
-
|
|
122
|
-
line_numbers = true
|
|
123
|
-
wrap_lines = false
|
|
124
|
-
agent_notes = true
|
|
125
|
-
exclude_untracked = false
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Example repo-local override:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
mkdir -p .hunk
|
|
132
|
-
$EDITOR .hunk/config.toml
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
```toml
|
|
136
|
-
# .hunk/config.toml
|
|
137
|
-
|
|
138
|
-
theme = "graphite"
|
|
139
|
-
mode = "split"
|
|
140
|
-
line_numbers = true
|
|
141
|
-
wrap_lines = false
|
|
142
|
-
agent_notes = true
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Hunk also supports custom themes:
|
|
146
|
-
|
|
147
|
-
```toml
|
|
148
|
-
theme = "custom"
|
|
149
|
-
|
|
150
|
-
[custom_theme]
|
|
151
|
-
base = "graphite"
|
|
152
|
-
label = "Pi Diff"
|
|
153
|
-
accent = "#7fd1ff"
|
|
154
|
-
panel = "#10161d"
|
|
155
|
-
noteBorder = "#c49bff"
|
|
156
|
-
|
|
157
|
-
[custom_theme.syntax]
|
|
158
|
-
keyword = "#8ed4ff"
|
|
159
|
-
string = "#c7b4ff"
|
|
160
|
-
comment = "#6e85a7"
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
All custom theme colors must be `#rrggbb` hex values.
|
|
164
|
-
|
|
165
|
-
### `review_git_diff` — read-only markdown review tool
|
|
166
|
-
|
|
167
|
-
`review_git_diff` remains available for agent/tool use and for non-destructive markdown exports.
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
// Open the read-only review markdown for working-tree changes, including untracked files
|
|
171
|
-
review_git_diff({});
|
|
172
|
-
|
|
173
|
-
// Review branch changes as main...HEAD
|
|
174
|
-
review_git_diff({ base: "main" });
|
|
175
|
-
|
|
176
|
-
// Focus a file from the changed-file list
|
|
177
|
-
review_git_diff({ file: "src/index.ts" });
|
|
178
|
-
|
|
179
|
-
// Focus a hunk ID shown by the panel
|
|
180
|
-
review_git_diff({ file: "src/index.ts", hunkId: "src/index.ts:10:12" });
|
|
181
|
-
|
|
182
|
-
// Return the older full markdown export with raw unified diff included
|
|
183
|
-
review_git_diff({ base: "main", includeRawDiff: true });
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
- No `base` means working-tree review, including untracked files.
|
|
187
|
-
- `base: "main"` means `main...HEAD` branch review.
|
|
188
|
-
- `includeRawDiff` returns the older full review export with raw unified diff.
|
|
189
|
-
- `maxLinesPerHunk`, `maxFiles`, and `maxHunks` cap large markdown reviews.
|
|
190
|
-
- The tool path is intentionally non-destructive: it never reverts, stages, commits, discards, or edits files.
|
|
191
|
-
|
|
192
|
-
## Review Export CLI
|
|
193
|
-
|
|
194
|
-
`pi-diff-review` exports the same Git review context as markdown for use outside Pi TUI.
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
# Review uncommitted working-tree changes
|
|
198
|
-
pi-diff-review
|
|
199
|
-
|
|
200
|
-
# Review branch changes like a pull request
|
|
201
|
-
pi-diff-review --base main
|
|
202
|
-
|
|
203
|
-
# Include the raw git diff after the structured summary
|
|
204
|
-
pi-diff-review --raw
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
The export includes changed files, hunk headers, changed-line numbers, and review instructions focused on correctness, regressions, security, tests, and maintainability.
|
|
208
|
-
|
|
209
45
|
## How It Works
|
|
210
46
|
|
|
211
47
|
pi-diff wraps the built-in `write` and `edit` tools from the pi SDK, including single-edit and multi-edit `edit` calls. When the agent writes or edits a file:
|
|
@@ -215,6 +51,8 @@ pi-diff wraps the built-in `write` and `edit` tools from the pi SDK, including s
|
|
|
215
51
|
3. **After the write** — computes a structured diff between old and new content
|
|
216
52
|
4. **Renders** the diff with syntax highlighting and word-level emphasis
|
|
217
53
|
|
|
54
|
+
For `edit` calls, a `tool_call` handler runs **before** the tool executes and verifies that every `oldText` in the request is still present in the target file. If any `oldText` is missing, the call is blocked with a clear `VERIFY before EDIT` error, preventing the model from re-trying with stale text from a prior read.
|
|
55
|
+
|
|
218
56
|
The rendering pipeline:
|
|
219
57
|
|
|
220
58
|
```
|
|
@@ -360,21 +198,35 @@ export DIFF_SPLIT_MIN_WIDTH=120
|
|
|
360
198
|
|
|
361
199
|
```
|
|
362
200
|
src/
|
|
363
|
-
|
|
201
|
+
├── index.ts # Extension entry point — wraps write/edit tools with diff rendering
|
|
202
|
+
├── edit-guard.ts # tool_call handler that blocks stale oldText in edit calls
|
|
203
|
+
├── edit-guard.test.ts # Unit tests for the edit guard
|
|
204
|
+
├── core/ # Pure data layer: diff parsing, conflict detection, line resolution
|
|
205
|
+
│ ├── config.ts
|
|
206
|
+
│ ├── conflicts.ts
|
|
207
|
+
│ ├── diff.ts # parseDiff, parsePatchFiles, resolveSepStyle
|
|
208
|
+
│ ├── replace.ts # safe in-place text replacement
|
|
209
|
+
│ ├── resolve-lines.ts
|
|
210
|
+
│ └── *.test.ts
|
|
211
|
+
└── review/ # Shared diff-rendering primitives used by the main extension
|
|
212
|
+
├── git.ts # read git diffs from disk (execFileSync)
|
|
213
|
+
└── hunk-preview.ts # renderSplit, renderUnified, theme helpers (re-exported via __testing)
|
|
364
214
|
```
|
|
365
215
|
|
|
366
216
|
### Key internals
|
|
367
217
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
|
371
|
-
|
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
| `
|
|
218
|
+
All listed symbols are exposed under the `__testing` export for unit tests:
|
|
219
|
+
|
|
220
|
+
| Symbol | Source | Purpose |
|
|
221
|
+
| -------------------------- | ----------------------- | ----------------------------------------------------------------------- |
|
|
222
|
+
| `parseDiff()` | `core/diff.ts` | Convert old/new content to structured `DiffLine[]` (added/removed/chars) |
|
|
223
|
+
| `parsePatchFiles()` | `core/diff.ts` | Parse a full `git apply` patch into per-file diff objects |
|
|
224
|
+
| `resolveSepStyle()` | `core/diff.ts` | Pick the per-side separator style (` │ ` vs `╱`) for split view |
|
|
225
|
+
| `getSepStyle()` | `review/hunk-preview.ts`| Same as above for the shared rendering layer |
|
|
226
|
+
| `computeHunkBlocks()` | `review/hunk-preview.ts`| Build add/remove/modify line blocks with column ranges for split view |
|
|
227
|
+
| `renderSplit()` | `review/hunk-preview.ts`| Side-by-side renderer with diagonal stripe fillers |
|
|
228
|
+
| `renderUnified()` | `review/hunk-preview.ts`| Stacked single-column renderer |
|
|
229
|
+
| `normalizeShikiContrast()` | `review/hunk-preview.ts` | Lighten/darken Shiki output to match the surrounding TUI theme |
|
|
378
230
|
|
|
379
231
|
### Rendering constants
|
|
380
232
|
|
|
@@ -423,12 +275,13 @@ pi install .
|
|
|
423
275
|
pi-diff is a **pi extension** — a TypeScript file that exports a default function receiving the pi API:
|
|
424
276
|
|
|
425
277
|
```typescript
|
|
426
|
-
|
|
427
|
-
|
|
278
|
+
import { registerEditGuard } from "./edit-guard.js";
|
|
279
|
+
|
|
280
|
+
export default function piDiffExtension(pi: ExtensionAPI): void {
|
|
281
|
+
// Wrap the built-in write/edit tools with diff rendering
|
|
428
282
|
const origWrite = createWriteTool(cwd);
|
|
429
283
|
const origEdit = createEditTool(cwd);
|
|
430
284
|
|
|
431
|
-
// Register enhanced versions
|
|
432
285
|
pi.registerTool({
|
|
433
286
|
...origWrite,
|
|
434
287
|
name: "write",
|
|
@@ -436,15 +289,18 @@ export default function piDiffExtension(pi: any): void {
|
|
|
436
289
|
renderCall: (...) => { /* preview */ },
|
|
437
290
|
renderResult: (...) => { /* render diff */ },
|
|
438
291
|
});
|
|
292
|
+
|
|
293
|
+
// Block stale-oldText edit calls before they run
|
|
294
|
+
registerEditGuard(pi);
|
|
439
295
|
}
|
|
440
296
|
```
|
|
441
297
|
|
|
442
298
|
Extensions can:
|
|
443
299
|
|
|
444
300
|
- **Register tools** — `pi.registerTool(definition)`
|
|
445
|
-
- **Listen to events** — `pi.on("session_start" | "input" | "
|
|
301
|
+
- **Listen to events** — `pi.on("session_start" | "input" | "tool_call" | "session_shutdown" | ...)`
|
|
302
|
+
- `tool_call` handlers can `block: true` to prevent the call from running
|
|
446
303
|
- **Register commands** — `pi.registerCommand("/name", handler)`
|
|
447
|
-
- **Register providers** — `pi.registerProvider("name", config)`
|
|
448
304
|
|
|
449
305
|
See the [pi docs](https://pi.dev) for the full extension API.
|
|
450
306
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAEX,YAAY,EAGZ,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EACL,iBAAiB,EAEjB,WAAW,EACX,KAAK,UAAU,EACf,SAAS,EACT,eAAe,EACf,eAAe,EAGhB,MAAM,gBAAgB,CAAC;AAYxB,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAgdxD,oFAAoF;AACpF,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAuKD,iBAAS,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIpD;AAoWD,iBAAe,aAAa,CAC1B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,GAAG,SAAmB,EACtB,EAAE,GAAE,UAAgC,GACnC,OAAO,CAAC,MAAM,CAAC,CAkIjB;AAMD,iBAAe,WAAW,CACxB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,GAAG,SAAoB,EACvB,EAAE,GAAE,UAAgC,GACnC,OAAO,CAAC,MAAM,CAAC,CA2KjB;AAMD,eAAO,MAAM,SAAS;;;;;;;;;CASrB,CAAC;AAEF,wBAA8B,qBAAqB,CAAC,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAipBnF"}
|
package/dist/index.js
CHANGED
|
@@ -27,8 +27,7 @@ import * as Diff from "diff";
|
|
|
27
27
|
import { configIndicatorStyle } from "./core/config.js";
|
|
28
28
|
import { computeHunkBlocks, getSepStyle, parseDiff, parsePatchFiles, resolveSepStyle, sepLabelSplit, sepLabelUnified, } from "./core/diff.js";
|
|
29
29
|
import { replace } from "./core/replace.js";
|
|
30
|
-
import {
|
|
31
|
-
import { registerReviewDiffCommand } from "./review/command.js";
|
|
30
|
+
import { registerEditGuard } from "./edit-guard.js";
|
|
32
31
|
import { applyDiffPalette as applySharedDiffPalette, lang as detectDiffLanguage, renderSplit as renderSharedSplit, resolveDiffColors as resolveSharedDiffColors, themeCacheKey as sharedThemeCacheKey, } from "./review/hunk-preview.js";
|
|
33
32
|
const DIFF_PRESETS = {
|
|
34
33
|
default: {
|
|
@@ -1300,152 +1299,6 @@ export default async function diffRendererExtension(pi) {
|
|
|
1300
1299
|
};
|
|
1301
1300
|
return text;
|
|
1302
1301
|
}
|
|
1303
|
-
registerReviewDiffCommand(pi, cwd);
|
|
1304
|
-
// ── resolve_lines tool ────────────────────────────────────────────────
|
|
1305
|
-
pi.registerTool({
|
|
1306
|
-
name: "resolve_lines",
|
|
1307
|
-
label: "Resolve Lines",
|
|
1308
|
-
description: `Resolve line numbers for an LLM code-review comment by matching its existing_code snippet against the git diff hunks. Uses a three-tier algorithm: new-side hunk match, old-side hunk match, then full file-content scan. Returns \`startLine\` and \`endLine\` when found, or \`unresolved: true\` when the snippet cannot be located.
|
|
1309
|
-
|
|
1310
|
-
Three-tier resolution:
|
|
1311
|
-
1. Match against diff hunks (new-file side) — context + added lines
|
|
1312
|
-
2. Match against diff hunks (old-file side) — context + deleted lines
|
|
1313
|
-
3. Scan full file content for a consecutive line match
|
|
1314
|
-
|
|
1315
|
-
Use after receiving a review comment with \`existing_code\` but zero or drifted line numbers. The tool fetches the current git diff for the file automatically when \`patchText\` is omitted.
|
|
1316
|
-
|
|
1317
|
-
Examples:
|
|
1318
|
-
resolve_lines({ existingCode: "const x = 1;", filePath: "src/index.ts" })
|
|
1319
|
-
resolve_lines({ existingCode: "function foo() {", filePath: "src/utils.ts", patchText: "@@ -1,3 +1,4 @@..." })`,
|
|
1320
|
-
promptSnippet: "Resolve line numbers for an LLM review comment by matching existing_code against git diff hunks.",
|
|
1321
|
-
parameters: {
|
|
1322
|
-
type: "object",
|
|
1323
|
-
properties: {
|
|
1324
|
-
existingCode: {
|
|
1325
|
-
type: "string",
|
|
1326
|
-
description: "The code snippet from the LLM review comment that shows the problematic code. Required.",
|
|
1327
|
-
},
|
|
1328
|
-
filePath: {
|
|
1329
|
-
type: "string",
|
|
1330
|
-
description: "Path to the file this comment is about, relative to repo root. Required.",
|
|
1331
|
-
},
|
|
1332
|
-
patchText: {
|
|
1333
|
-
type: "string",
|
|
1334
|
-
description: "Optional raw unified diff text for the file. When omitted, pi-diff fetches the git diff automatically.",
|
|
1335
|
-
},
|
|
1336
|
-
fileContent: {
|
|
1337
|
-
type: "string",
|
|
1338
|
-
description: "Optional full new-file content for fallback line scan after hunk matching fails.",
|
|
1339
|
-
},
|
|
1340
|
-
},
|
|
1341
|
-
required: ["existingCode", "filePath"],
|
|
1342
|
-
additionalProperties: false,
|
|
1343
|
-
},
|
|
1344
|
-
async execute(_tid, params = {}) {
|
|
1345
|
-
try {
|
|
1346
|
-
const { existingCode, filePath, patchText, fileContent } = params ?? {};
|
|
1347
|
-
if (!existingCode || !filePath) {
|
|
1348
|
-
return {
|
|
1349
|
-
content: [
|
|
1350
|
-
{ type: "text", text: "Error: existingCode and filePath are required" },
|
|
1351
|
-
],
|
|
1352
|
-
};
|
|
1353
|
-
}
|
|
1354
|
-
// Fetch git diff if patchText not provided
|
|
1355
|
-
let patch = patchText;
|
|
1356
|
-
if (!patch) {
|
|
1357
|
-
const { execFileSync } = await import("node:child_process");
|
|
1358
|
-
try {
|
|
1359
|
-
patch = execFileSync("git", ["diff", "--no-ext-diff", "HEAD", "--", filePath], {
|
|
1360
|
-
cwd,
|
|
1361
|
-
encoding: "utf8",
|
|
1362
|
-
maxBuffer: 1024 * 1024,
|
|
1363
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1364
|
-
}).trim();
|
|
1365
|
-
// Also try unstaged diff
|
|
1366
|
-
const unstaged = execFileSync("git", ["diff", "--no-ext-diff", "--", filePath], {
|
|
1367
|
-
cwd,
|
|
1368
|
-
encoding: "utf8",
|
|
1369
|
-
maxBuffer: 1024 * 1024,
|
|
1370
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1371
|
-
}).trim();
|
|
1372
|
-
if (unstaged) {
|
|
1373
|
-
patch = patch ? [patch, unstaged].filter(Boolean).join("\n") : unstaged;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
catch {
|
|
1377
|
-
return {
|
|
1378
|
-
content: [
|
|
1379
|
-
{ type: "text", text: `Error: could not read git diff for ${filePath}` },
|
|
1380
|
-
],
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
if (!patch) {
|
|
1385
|
-
return {
|
|
1386
|
-
content: [
|
|
1387
|
-
{
|
|
1388
|
-
type: "text",
|
|
1389
|
-
text: `No diff found for ${filePath} — file may not have changes`,
|
|
1390
|
-
},
|
|
1391
|
-
],
|
|
1392
|
-
details: { unresolved: true },
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
const result = resolveLinesFromPatch(existingCode, patch, fileContent);
|
|
1396
|
-
if ("unresolved" in result) {
|
|
1397
|
-
return {
|
|
1398
|
-
content: [
|
|
1399
|
-
{
|
|
1400
|
-
type: "text",
|
|
1401
|
-
text: `Could not resolve line numbers for existing_code in ${filePath}`,
|
|
1402
|
-
},
|
|
1403
|
-
],
|
|
1404
|
-
details: { unresolved: true },
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
|
-
return {
|
|
1408
|
-
content: [
|
|
1409
|
-
{
|
|
1410
|
-
type: "text",
|
|
1411
|
-
text: `Resolved ${filePath}:${result.startLine}-${result.endLine}`,
|
|
1412
|
-
},
|
|
1413
|
-
],
|
|
1414
|
-
details: { startLine: result.startLine, endLine: result.endLine },
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
catch (error) {
|
|
1418
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1419
|
-
return {
|
|
1420
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1421
|
-
details: { unresolved: true, error: message },
|
|
1422
|
-
};
|
|
1423
|
-
}
|
|
1424
|
-
},
|
|
1425
|
-
renderCall(args, theme, ctx) {
|
|
1426
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1427
|
-
const fp = typeof args?.filePath === "string" ? args.filePath : "";
|
|
1428
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("resolve_lines"))} ${theme.fg("accent", fp)}`);
|
|
1429
|
-
return text;
|
|
1430
|
-
},
|
|
1431
|
-
renderResult(result, _opt, theme, ctx) {
|
|
1432
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1433
|
-
if (ctx.isError || result.details?.error) {
|
|
1434
|
-
text.setText(`\n${theme.fg("error", result.details?.error ?? "resolve_lines failed")}`);
|
|
1435
|
-
return text;
|
|
1436
|
-
}
|
|
1437
|
-
if (result.details?.unresolved) {
|
|
1438
|
-
text.setText(` ${theme.fg("muted", "unresolved")}`);
|
|
1439
|
-
return text;
|
|
1440
|
-
}
|
|
1441
|
-
if (result.details?.startLine != null) {
|
|
1442
|
-
text.setText(` ${theme.fg("success", `lines ${result.details.startLine}-${result.details.endLine}`)}`);
|
|
1443
|
-
return text;
|
|
1444
|
-
}
|
|
1445
|
-
text.setText(` ${theme.fg("dim", String(result?.content?.[0]?.text ?? "").slice(0, 120))}`);
|
|
1446
|
-
return text;
|
|
1447
|
-
},
|
|
1448
|
-
});
|
|
1449
1302
|
// =======================================================================
|
|
1450
1303
|
// write
|
|
1451
1304
|
// =======================================================================
|
|
@@ -1676,7 +1529,7 @@ Examples:
|
|
|
1676
1529
|
content: [{ type: "text", text: `Edited ${sp(fp)}` }],
|
|
1677
1530
|
details: {
|
|
1678
1531
|
_type: "editInfo",
|
|
1679
|
-
summary,
|
|
1532
|
+
summary: editLine > 0 ? `${summary} at line ${editLine}` : summary,
|
|
1680
1533
|
filePath: fp,
|
|
1681
1534
|
editLine,
|
|
1682
1535
|
diff: diffData,
|
|
@@ -1687,6 +1540,15 @@ Examples:
|
|
|
1687
1540
|
},
|
|
1688
1541
|
};
|
|
1689
1542
|
}
|
|
1543
|
+
// Compute the line of the first edit for the title summary
|
|
1544
|
+
const firstEditLine = (() => {
|
|
1545
|
+
if (!operations[0]?.newText)
|
|
1546
|
+
return 0;
|
|
1547
|
+
const idx = content.indexOf(operations[0].newText);
|
|
1548
|
+
if (idx < 0)
|
|
1549
|
+
return 0;
|
|
1550
|
+
return content.slice(0, idx).split("\n").length;
|
|
1551
|
+
})();
|
|
1690
1552
|
// Merge all diffs into one combined view for rendering
|
|
1691
1553
|
const merged = {
|
|
1692
1554
|
lines: diffs.flatMap((diff, i) => [
|
|
@@ -1710,7 +1572,7 @@ Examples:
|
|
|
1710
1572
|
content: [{ type: "text", text: `Edited ${sp(fp)}` }],
|
|
1711
1573
|
details: {
|
|
1712
1574
|
_type: "multiEditInfo",
|
|
1713
|
-
summary,
|
|
1575
|
+
summary: firstEditLine > 0 ? `${summary} at line ${firstEditLine}` : summary,
|
|
1714
1576
|
filePath: fp,
|
|
1715
1577
|
editCount: operations.length,
|
|
1716
1578
|
diffLineCount: merged.lines.length,
|
|
@@ -1749,7 +1611,7 @@ Examples:
|
|
|
1749
1611
|
: diffs[0];
|
|
1750
1612
|
result.details = {
|
|
1751
1613
|
_type: "editInfo",
|
|
1752
|
-
summary,
|
|
1614
|
+
summary: editLine > 0 ? `${summary} at line ${editLine}` : summary,
|
|
1753
1615
|
filePath: fp,
|
|
1754
1616
|
editLine,
|
|
1755
1617
|
diff: diffData,
|
|
@@ -1779,9 +1641,21 @@ Examples:
|
|
|
1779
1641
|
removed: diffs.reduce((sum, diff) => sum + diff.removed, 0),
|
|
1780
1642
|
chars: diffs.reduce((sum, diff) => sum + diff.chars, 0),
|
|
1781
1643
|
};
|
|
1644
|
+
let firstEditLine = 0;
|
|
1645
|
+
try {
|
|
1646
|
+
if (fp) {
|
|
1647
|
+
const f = readFileSync(fp, "utf8");
|
|
1648
|
+
const idx = f.indexOf(operations[0].newText);
|
|
1649
|
+
if (idx >= 0)
|
|
1650
|
+
firstEditLine = f.slice(0, idx).split("\n").length;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
catch {
|
|
1654
|
+
firstEditLine = 0;
|
|
1655
|
+
}
|
|
1782
1656
|
result.details = {
|
|
1783
1657
|
_type: "multiEditInfo",
|
|
1784
|
-
summary,
|
|
1658
|
+
summary: firstEditLine > 0 ? `${summary} at line ${firstEditLine}` : summary,
|
|
1785
1659
|
filePath: fp,
|
|
1786
1660
|
editCount: operations.length,
|
|
1787
1661
|
diffLineCount: merged.lines.length,
|
|
@@ -1797,7 +1671,24 @@ Examples:
|
|
|
1797
1671
|
resolvePreviewDiffColors(theme);
|
|
1798
1672
|
if (ctx.argsComplete && operations.length > 0) {
|
|
1799
1673
|
const { totalAdded, totalRemoved } = summarizeEditOperations(operations);
|
|
1800
|
-
|
|
1674
|
+
// Compute the line number for the call preview by reading the
|
|
1675
|
+
// file and finding the first oldText. This lets the title show
|
|
1676
|
+
// "at line N" even before the edit executes, matching what the
|
|
1677
|
+
// result title will show.
|
|
1678
|
+
let previewLine = 0;
|
|
1679
|
+
try {
|
|
1680
|
+
if (fp && existsSync(fp)) {
|
|
1681
|
+
const cur = readFileSync(fp, "utf-8");
|
|
1682
|
+
const idx = cur.indexOf(operations[0].oldText);
|
|
1683
|
+
if (idx >= 0)
|
|
1684
|
+
previewLine = cur.slice(0, idx).split("\n").length;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
catch {
|
|
1688
|
+
previewLine = 0;
|
|
1689
|
+
}
|
|
1690
|
+
const loc = previewLine > 0 ? ` ${theme.fg("muted", `at line ${previewLine}`)}` : "";
|
|
1691
|
+
text.setText(formatToolTitle("edit", fp, theme, termW(), ` ${theme.fg("muted", summarize(totalAdded, totalRemoved))}${loc}`));
|
|
1801
1692
|
}
|
|
1802
1693
|
else {
|
|
1803
1694
|
text.setText(formatToolTitle("edit", fp, theme, termW()));
|
|
@@ -1817,19 +1708,16 @@ Examples:
|
|
|
1817
1708
|
}
|
|
1818
1709
|
const d = result.details;
|
|
1819
1710
|
if (d?._type === "editInfo" && d.diff) {
|
|
1820
|
-
|
|
1821
|
-
setDiffPreviewTask(text, "ed", `${d.summary}${loc}`, d.diff, d.language, MAX_PREVIEW_LINES, theme, ctx);
|
|
1711
|
+
setDiffPreviewTask(text, "ed", "", d.diff, d.language, MAX_PREVIEW_LINES, theme, ctx);
|
|
1822
1712
|
return text;
|
|
1823
1713
|
}
|
|
1824
1714
|
if (d?._type === "editInfo") {
|
|
1825
|
-
|
|
1826
|
-
const loc = editLine > 0 ? ` ${theme.fg("muted", `at line ${editLine}`)}` : "";
|
|
1827
|
-
setToolHeaderText(text, `${s}${loc}`, theme);
|
|
1715
|
+
setToolHeaderText(text, "", theme);
|
|
1828
1716
|
return text;
|
|
1829
1717
|
}
|
|
1830
1718
|
if (d?._type === "multiEditInfo") {
|
|
1831
|
-
const {
|
|
1832
|
-
const meta = `${editCount} edits
|
|
1719
|
+
const { editCount, diffLineCount, diff, language } = d;
|
|
1720
|
+
const meta = `${editCount} edits${diffLineCountLabel(diffLineCount, theme)}`;
|
|
1833
1721
|
if (diff) {
|
|
1834
1722
|
setDiffPreviewTask(text, "me", meta, diff, language, MAX_PREVIEW_LINES, theme, ctx);
|
|
1835
1723
|
return text;
|
|
@@ -1842,5 +1730,6 @@ Examples:
|
|
|
1842
1730
|
return text;
|
|
1843
1731
|
},
|
|
1844
1732
|
});
|
|
1733
|
+
registerEditGuard(pi);
|
|
1845
1734
|
}
|
|
1846
1735
|
//# sourceMappingURL=index.js.map
|