@ladbabynpm/picc-edit 0.1.0
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/LICENSE +21 -0
- package/README.md +59 -0
- package/index.ts +342 -0
- package/package.json +60 -0
- package/src/constants.ts +15 -0
- package/src/diff.ts +209 -0
- package/src/edit.ts +283 -0
- package/src/editUtils.ts +203 -0
- package/src/file.ts +162 -0
- package/src/path.ts +62 -0
- package/src/prompt.ts +39 -0
- package/src/readState.ts +52 -0
- package/src/renderDiff.ts +185 -0
- package/src/windowsPaths.ts +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ladbaby
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# picc-edit
|
|
2
|
+
|
|
3
|
+
Claude Code style **Edit** tool for [pi](https://pi.dev) — a faithful port of Claude Code's `Edit` tool, overriding pi's built-in `edit`.
|
|
4
|
+
|
|
5
|
+
Part of [picc](https://github.com/Ladbaby/picc), a pi agent setup mirroring Claude Code's harness.
|
|
6
|
+
|
|
7
|
+
> pi's built-in `edit` is a thin multi-edit wrapper: input `path` (relative or
|
|
8
|
+
> absolute) + `edits: [{oldText, newText}]`, no "must read first" guard, no
|
|
9
|
+
> quote normalization, no `replace_all`. This extension replicates Claude Code's
|
|
10
|
+
> `Edit`: the `file_path` + `old_string` / `new_string` (+ optional
|
|
11
|
+
> `replace_all`) input shape, a session-scoped read-first guard, exact string
|
|
12
|
+
> matching with curly-quote normalization, and a structured diff result.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Install via `pi install npm:@ladbabynpm/picc-edit`.
|
|
17
|
+
|
|
18
|
+
## Tool
|
|
19
|
+
|
|
20
|
+
- **Name:** `edit` (default; overrides pi's built-in `edit`) or `Edit` — configurable (see below).
|
|
21
|
+
- **Parameters:** `file_path` (absolute, required), `old_string` (required), `new_string` (required), `replace_all` (optional, default `false`).
|
|
22
|
+
- **Behavior:**
|
|
23
|
+
- **Existing file** — must have been read this session (any read tool), and must not
|
|
24
|
+
have been modified since that read. `old_string` is matched (with curly-quote
|
|
25
|
+
normalization) and must be unique unless `replace_all` is set. Returns `The file
|
|
26
|
+
<path> has been updated successfully.` (or the `replace_all` variant) with a
|
|
27
|
+
structured diff.
|
|
28
|
+
- **New file** — `old_string` must be `""`; creates the file.
|
|
29
|
+
- Writes with explicit LF handling (the model's sent line endings are respected
|
|
30
|
+
as-is — no repo resampling).
|
|
31
|
+
- **Read-first guard:** reads are observed from pi `tool_result` events for any read
|
|
32
|
+
tool (`read`/`Read`), so it works whether the file was read with pi's built-in
|
|
33
|
+
`read` or picc-read's `Read`. The map is cleared on `session_start`.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
| Setting | Where | Values | Default |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `toolName` | `config.json` | `"edit"` \| `"Edit"` | `"edit"` |
|
|
40
|
+
| `PICC_EDIT_TOOL_NAME` | env | `"edit"` \| `"Edit"` | — |
|
|
41
|
+
| `PICC_EDIT_CONFIG_PATH` | env | absolute path to a config.json | `~/.pi/agent/extensions/picc-edit/config.json` |
|
|
42
|
+
|
|
43
|
+
Precedence for the tool name: `PICC_EDIT_TOOL_NAME` env > `config.json` > `"edit"`.
|
|
44
|
+
|
|
45
|
+
## What is omitted from the live source
|
|
46
|
+
|
|
47
|
+
No pi equivalent, so left out: permission checks (`checkWritePermissionForTool`),
|
|
48
|
+
`checkTeamMemSecrets`, `validateInputForSettingsFileEdit`, team-memory guards,
|
|
49
|
+
skill discovery, `diagnosticTracker`, `fileHistory`, LSP `didChange`/`didSave`,
|
|
50
|
+
`notifyVscodeFileUpdated`, `fetchSingleFileGitDiff`, and analytics.
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install
|
|
56
|
+
npm run lint # biome check
|
|
57
|
+
npm run typecheck # tsc --noEmit
|
|
58
|
+
npm run test # vitest run
|
|
59
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* picc-edit: Claude Code-style Edit tool for pi.
|
|
3
|
+
*
|
|
4
|
+
* A faithful port of Claude Code's `Edit` tool, registering a tool named
|
|
5
|
+
* `edit`/`Edit` that **overrides pi's built-in `edit`** tool (same-name,
|
|
6
|
+
* last-write-wins — see `core/tools/index.js`).
|
|
7
|
+
*
|
|
8
|
+
* Differences from pi's built-in `edit`:
|
|
9
|
+
* - Input is Claude Code's single-edit shape: `file_path` (absolute),
|
|
10
|
+
* `old_string`, `new_string`, optional `replace_all` (not pi's
|
|
11
|
+
* `edits: [{oldText, newText}]` array).
|
|
12
|
+
* - Enforces Claude Code's read-first guard: an existing file must have been
|
|
13
|
+
* read this session, and must not have been modified since that read.
|
|
14
|
+
* Read-state is tracked from `tool_result` events for any file tool that
|
|
15
|
+
* establishes known contents (read/write/edit) — mirroring Claude Code,
|
|
16
|
+
* where a file the agent just wrote or edited is immediately editable.
|
|
17
|
+
* The same mechanism picc-write uses.
|
|
18
|
+
* - Quote normalization: `old_string`/`new_string` may use straight quotes
|
|
19
|
+
* to match a file that contains curly quotes, preserving the file's
|
|
20
|
+
* typography.
|
|
21
|
+
* - Returns a structured patch + `originalFile` in `details`, with faithful
|
|
22
|
+
* success messages (including the `replace_all` variant).
|
|
23
|
+
* - Writes with explicit LF handling (the model's sent line endings are
|
|
24
|
+
* respected as-is — no repo resampling).
|
|
25
|
+
*
|
|
26
|
+
* Omitted from the live source (no pi equivalent):
|
|
27
|
+
* - permission checks (`checkWritePermissionForTool`, `matchingRuleForInput`
|
|
28
|
+
* — pi's permission system handles edits separately)
|
|
29
|
+
* - `checkTeamMemSecrets`, `validateInputForSettingsFileEdit`, team-memory
|
|
30
|
+
* guards, skill discovery, `diagnosticTracker`, `fileHistory`
|
|
31
|
+
* - LSP `didChange`/`didSave`, `notifyVscodeFileUpdated`, `fetchSingleFileGitDiff`
|
|
32
|
+
* - analytics (`logEvent`, `logFileOperation`), GrowthBook, UI.tsx render
|
|
33
|
+
*
|
|
34
|
+
* Tool name configuration:
|
|
35
|
+
* - Default: `"edit"` (lowercase; pi's built-in tool name).
|
|
36
|
+
* - Set `config.json` `toolName` to `"Edit"` (default location
|
|
37
|
+
* `~/.pi/agent/extensions/picc-edit/config.json`), or set
|
|
38
|
+
* `PICC_EDIT_TOOL_NAME=Edit`. Valid values: `"edit"`, `"Edit"`.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
42
|
+
import { homedir } from "node:os";
|
|
43
|
+
import { join } from "node:path";
|
|
44
|
+
import {
|
|
45
|
+
type ExtensionAPI,
|
|
46
|
+
type ExtensionContext,
|
|
47
|
+
} from "@earendil-works/pi-coding-agent";
|
|
48
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
49
|
+
import { Type } from "typebox";
|
|
50
|
+
import { countLinesChanged, generateDisplayDiff } from "./src/diff.js";
|
|
51
|
+
import {
|
|
52
|
+
type EditInput,
|
|
53
|
+
type EditOutcome,
|
|
54
|
+
editOutcome,
|
|
55
|
+
} from "./src/edit.js";
|
|
56
|
+
import {
|
|
57
|
+
convertLeadingTabsToSpaces,
|
|
58
|
+
getFileModificationTime,
|
|
59
|
+
readFileSyncWithMetadata,
|
|
60
|
+
} from "./src/file.js";
|
|
61
|
+
import { expandPath } from "./src/path.js";
|
|
62
|
+
import {
|
|
63
|
+
getEditToolDescription,
|
|
64
|
+
replaceAllMessage,
|
|
65
|
+
singleEditMessage,
|
|
66
|
+
} from "./src/prompt.js";
|
|
67
|
+
import {
|
|
68
|
+
fileStateToolName,
|
|
69
|
+
type ReadEntry,
|
|
70
|
+
readStateClear,
|
|
71
|
+
readStateSet,
|
|
72
|
+
} from "./src/readState.js";
|
|
73
|
+
import { renderDiff } from "./src/renderDiff.js";
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Config (mirrors picc-write)
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
const VALID_TOOL_NAMES = ["edit", "Edit"] as const;
|
|
80
|
+
type ToolName = (typeof VALID_TOOL_NAMES)[number];
|
|
81
|
+
|
|
82
|
+
function resolveConfigPath(): string {
|
|
83
|
+
const env = process.env.PICC_EDIT_CONFIG_PATH;
|
|
84
|
+
if (env) return env;
|
|
85
|
+
return join(
|
|
86
|
+
homedir(),
|
|
87
|
+
".pi",
|
|
88
|
+
"agent",
|
|
89
|
+
"extensions",
|
|
90
|
+
"picc-edit",
|
|
91
|
+
"config.json",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readToolNameFromConfig(): ToolName | undefined {
|
|
96
|
+
const configPath = resolveConfigPath();
|
|
97
|
+
if (!existsSync(configPath)) return undefined;
|
|
98
|
+
try {
|
|
99
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
100
|
+
const parsed = JSON.parse(raw) as { toolName?: unknown };
|
|
101
|
+
const val = parsed?.toolName;
|
|
102
|
+
if (
|
|
103
|
+
typeof val === "string" &&
|
|
104
|
+
(VALID_TOOL_NAMES as readonly string[]).includes(val)
|
|
105
|
+
) {
|
|
106
|
+
return val as ToolName;
|
|
107
|
+
}
|
|
108
|
+
if (val !== undefined) {
|
|
109
|
+
console.warn(
|
|
110
|
+
`[picc-edit] config.json: invalid toolName "${val}" — valid values are ${VALID_TOOL_NAMES.join(", ")}. Falling back to "edit".`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// unreadable / malformed — fall through to default
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function loadToolName(): ToolName {
|
|
120
|
+
const envVal = process.env.PICC_EDIT_TOOL_NAME;
|
|
121
|
+
if (typeof envVal === "string") {
|
|
122
|
+
if ((VALID_TOOL_NAMES as readonly string[]).includes(envVal)) {
|
|
123
|
+
return envVal as ToolName;
|
|
124
|
+
}
|
|
125
|
+
console.warn(
|
|
126
|
+
`[picc-edit] PICC_EDIT_TOOL_NAME="${envVal}" is invalid — valid values are ${VALID_TOOL_NAMES.join(", ")}. Falling back to "edit".`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return readToolNameFromConfig() ?? "edit";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Schema
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
const EDIT_SCHEMA = Type.Object({
|
|
137
|
+
file_path: Type.String({
|
|
138
|
+
description: "The absolute path to the file to modify",
|
|
139
|
+
}),
|
|
140
|
+
new_string: Type.String({
|
|
141
|
+
description:
|
|
142
|
+
"The text to replace it with (must be different from old_string)",
|
|
143
|
+
}),
|
|
144
|
+
old_string: Type.String({
|
|
145
|
+
description: "The text to replace",
|
|
146
|
+
}),
|
|
147
|
+
replace_all: Type.Optional(
|
|
148
|
+
Type.Boolean({ description: "Replace all occurrences of old_string (default false)" }),
|
|
149
|
+
),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Read-state tracking (populates the edit guard across read tools)
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
function recordRead(input: Record<string, unknown>, cwd: string): void {
|
|
157
|
+
// Reads are reported under different key names depending on the active
|
|
158
|
+
// read tool: pi's built-in `read` uses `path`, while picc-read (the
|
|
159
|
+
// Claude Code port) uses `file_path`. Accept both so the read guard is
|
|
160
|
+
// satisfied no matter which read tool the session is using.
|
|
161
|
+
const rawPath =
|
|
162
|
+
(typeof input.file_path === "string" && input.file_path) ||
|
|
163
|
+
(typeof input.path === "string" && input.path);
|
|
164
|
+
if (typeof rawPath !== "string" || !rawPath) return;
|
|
165
|
+
|
|
166
|
+
let fullPath: string;
|
|
167
|
+
try {
|
|
168
|
+
fullPath = expandPath(rawPath, cwd);
|
|
169
|
+
} catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const meta = readFileSyncWithMetadata(fullPath);
|
|
175
|
+
const timestamp = getFileModificationTime(fullPath);
|
|
176
|
+
// Always store as a full read (`offset: undefined`, `limit: undefined`).
|
|
177
|
+
// We cannot reliably distinguish a full read from a partial read from
|
|
178
|
+
// the `tool_result` event alone (no read-output content / truncation
|
|
179
|
+
// info is exposed), and being too strict here causes false positives
|
|
180
|
+
// when models default `offset: 1` or when a read tool injects `path`
|
|
181
|
+
// defaults. The `modified-since-read` check in `editOutcome` still
|
|
182
|
+
// guards against stale edits; sacrificing the partial-read guard is
|
|
183
|
+
// the right trade-off (defense-in-depth, not correctness).
|
|
184
|
+
const entry: ReadEntry = {
|
|
185
|
+
content: meta.content,
|
|
186
|
+
timestamp,
|
|
187
|
+
offset: undefined,
|
|
188
|
+
limit: undefined,
|
|
189
|
+
};
|
|
190
|
+
readStateSet(fullPath, entry);
|
|
191
|
+
} catch {
|
|
192
|
+
// file gone or unreadable — nothing to record
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Extension entry point
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
export default function (pi: ExtensionAPI): void {
|
|
201
|
+
const toolName = loadToolName();
|
|
202
|
+
|
|
203
|
+
// Clear any stale read-state on a fresh/reloaded session.
|
|
204
|
+
pi.on("session_start", () => {
|
|
205
|
+
readStateClear();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Observe successful file tools (read/write/edit) to feed the edit guard.
|
|
209
|
+
// Claude Code refreshes its shared `readFileState` from all three, so a file
|
|
210
|
+
// the agent just wrote or edited is immediately editable without a redundant
|
|
211
|
+
// re-read. `tool_result` events carry no cwd of their own; use the process
|
|
212
|
+
// cwd.
|
|
213
|
+
pi.on("tool_result", (event) => {
|
|
214
|
+
if (!fileStateToolName(event.toolName)) return;
|
|
215
|
+
if (event.isError) return;
|
|
216
|
+
recordRead(event.input, process.cwd());
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
pi.registerTool({
|
|
220
|
+
name: toolName,
|
|
221
|
+
label: toolName,
|
|
222
|
+
description: getEditToolDescription(),
|
|
223
|
+
promptSnippet: "Make precise string replacements in files",
|
|
224
|
+
promptGuidelines: [],
|
|
225
|
+
parameters: EDIT_SCHEMA,
|
|
226
|
+
// Rely on the framework's default background shell (colored Box) rather
|
|
227
|
+
// than self-framing. This overrides the built-in `edit`, whose `renderShell:
|
|
228
|
+
// "self"` we do NOT want to inherit — in "self" mode the framework would
|
|
229
|
+
// skip the background unless we supplied our own Box. "default" gives the
|
|
230
|
+
// standard pending/success/error background for free (see tool-execution).
|
|
231
|
+
renderShell: "default",
|
|
232
|
+
executionMode: "sequential",
|
|
233
|
+
async execute(
|
|
234
|
+
_toolCallId,
|
|
235
|
+
params,
|
|
236
|
+
_signal,
|
|
237
|
+
_onUpdate,
|
|
238
|
+
ctx: ExtensionContext,
|
|
239
|
+
) {
|
|
240
|
+
const input = params as EditInput;
|
|
241
|
+
const cwd = ctx.cwd;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const outcome: EditOutcome = await editOutcome(input, cwd);
|
|
245
|
+
const message = outcome.replaceAll
|
|
246
|
+
? replaceAllMessage(outcome.filePath)
|
|
247
|
+
: singleEditMessage(outcome.filePath);
|
|
248
|
+
// Display-oriented, line-numbered diff in the exact format the
|
|
249
|
+
// built-in diff viewer (`renderDiff`) expects. Both sides are put
|
|
250
|
+
// in the same display space (leading tabs → 2 spaces): `newFile`
|
|
251
|
+
// is already tab-converted by editOutcome, so the old side must
|
|
252
|
+
// be too — otherwise every tab-indented line looks changed and
|
|
253
|
+
// the diff balloons to the whole file. Mirrors how
|
|
254
|
+
// `structuredPatch` is computed (tab-convert both sides). Lives
|
|
255
|
+
// only in `details` (TUI channel) — the model sees just `content`.
|
|
256
|
+
const diff = generateDisplayDiff(
|
|
257
|
+
convertLeadingTabsToSpaces(outcome.originalFile),
|
|
258
|
+
outcome.newFile,
|
|
259
|
+
);
|
|
260
|
+
const { added, removed } = countLinesChanged(
|
|
261
|
+
outcome.structuredPatch,
|
|
262
|
+
outcome.newFile,
|
|
263
|
+
);
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "text", text: message }],
|
|
266
|
+
details: { ...outcome, diff, additions: added, removals: removed },
|
|
267
|
+
};
|
|
268
|
+
} catch (err) {
|
|
269
|
+
// pi's agent loop only flags a tool result as errored when
|
|
270
|
+
// execute() rejects — a resolved `{ isError: true }` is dropped
|
|
271
|
+
// because AgentToolResult has no such field. Throw so the
|
|
272
|
+
// guard / validation failure is surfaced as a real tool error
|
|
273
|
+
// (matching pi's built-in `edit`, which also throws).
|
|
274
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
renderCall(args, theme, context) {
|
|
278
|
+
const path =
|
|
279
|
+
typeof args.file_path === "string" ? args.file_path : "";
|
|
280
|
+
let text = theme.fg("toolTitle", theme.bold(`${toolName} `));
|
|
281
|
+
text += theme.fg("accent", path);
|
|
282
|
+
const t =
|
|
283
|
+
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
284
|
+
t.setText(text);
|
|
285
|
+
return t;
|
|
286
|
+
},
|
|
287
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
288
|
+
if (isPartial) {
|
|
289
|
+
return new Text(theme.fg("warning", "Editing..."), 0, 0);
|
|
290
|
+
}
|
|
291
|
+
const t =
|
|
292
|
+
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
293
|
+
// On error, details is undefined. Show the error message in red.
|
|
294
|
+
if (context.isError) {
|
|
295
|
+
const errorMsg = result.content
|
|
296
|
+
.filter(
|
|
297
|
+
(c): c is { type: "text"; text: string } => c.type === "text",
|
|
298
|
+
)
|
|
299
|
+
.map((c) => c.text)
|
|
300
|
+
.join("\n");
|
|
301
|
+
t.setText(theme.fg("error", errorMsg || "Edit failed"));
|
|
302
|
+
return t;
|
|
303
|
+
}
|
|
304
|
+
const details = result.details as
|
|
305
|
+
| (EditOutcome & {
|
|
306
|
+
diff?: string;
|
|
307
|
+
additions: number;
|
|
308
|
+
removals: number;
|
|
309
|
+
})
|
|
310
|
+
| undefined;
|
|
311
|
+
// Summary line above the diff ("Added N, removed M"), matching
|
|
312
|
+
// Claude Code's FileEditToolUpdatedMessage. Counts come from
|
|
313
|
+
// details (set in execute); the leading blank line separates the
|
|
314
|
+
// summary from the call header. Styled like picc-write's
|
|
315
|
+
// createPreviewText header: muted text with the counts bolded.
|
|
316
|
+
if (details?.diff) {
|
|
317
|
+
const summaryParts: string[] = [];
|
|
318
|
+
if (details.additions > 0) {
|
|
319
|
+
summaryParts.push(
|
|
320
|
+
`Added ${theme.bold(String(details.additions))} line${details.additions === 1 ? "" : "s"}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (details.removals > 0) {
|
|
324
|
+
summaryParts.push(
|
|
325
|
+
`${details.additions > 0 ? "removed" : "Removed"} ${theme.bold(String(details.removals))} line${details.removals === 1 ? "" : "s"}`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const summary =
|
|
329
|
+
summaryParts.length > 0
|
|
330
|
+
? summaryParts.join(", ")
|
|
331
|
+
: "Applied";
|
|
332
|
+
t.setText(
|
|
333
|
+
theme.fg("muted", summary) +
|
|
334
|
+
renderDiff(details.diff, theme),
|
|
335
|
+
);
|
|
336
|
+
} else {
|
|
337
|
+
t.setText(theme.fg("success", "Applied"));
|
|
338
|
+
}
|
|
339
|
+
return t;
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ladbabynpm/picc-edit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code style Edit tool for pi — a faithful port of Claude Code's `Edit` (exact string replacement, quote normalization, read-first guard, replace_all, structured diff).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"edit",
|
|
12
|
+
"file",
|
|
13
|
+
"claude-code"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "ladbabynpm",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/Ladbaby/picc-edit.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/Ladbaby/picc-edit",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Ladbaby/picc-edit/issues"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"index.ts",
|
|
27
|
+
"src",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"pi": {
|
|
32
|
+
"extensions": [
|
|
33
|
+
"./index.ts"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
38
|
+
"@earendil-works/pi-tui": "*",
|
|
39
|
+
"typebox": "*"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"diff": "^5.2.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"lint": "biome check",
|
|
46
|
+
"lint:fix": "biome check --write",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@biomejs/biome": "^2.5.5",
|
|
52
|
+
"@types/diff": "^5.2.1",
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"typescript": "^5.8.0",
|
|
55
|
+
"vitest": "^3.2.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// picc-edit — src/constants.ts
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export const FILE_EDIT_TOOL_NAME = "Edit";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Error strings returned by the edit orchestrator when the read-first /
|
|
9
|
+
* modified-since-read guards fire.
|
|
10
|
+
*/
|
|
11
|
+
export const FILE_NOT_READ_ERROR =
|
|
12
|
+
"File has not been read yet. Read it first before writing to it.";
|
|
13
|
+
|
|
14
|
+
export const FILE_MODIFIED_SINCE_READ_ERROR =
|
|
15
|
+
"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.";
|