@xynogen/pix-edit 0.1.13 → 0.1.15
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 +2 -2
- package/package.json +2 -2
- package/src/edit.test.ts +111 -1
- package/src/edit.ts +35 -5
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@ Replaces Pi's default `edit` tool with an enhanced version that renders a side-b
|
|
|
8
8
|
|
|
9
9
|
## Auto-collapse
|
|
10
10
|
|
|
11
|
-
After a configurable delay (default 10 seconds) the diff
|
|
11
|
+
After a configurable delay (default 10 seconds), a completed diff collapses to a one-line status row. Structured failures use a compact `✗` row after the delay. Expanding restores the existing bounded diff or exact diagnostic without restarting the elapsed timer. This is controlled via the `collapse` section of `~/.pi/agent/pix.json`:
|
|
12
12
|
|
|
13
13
|
```jsonc
|
|
14
14
|
{
|
|
15
15
|
"collapse": {
|
|
16
16
|
"enabled": true,
|
|
17
|
-
"
|
|
17
|
+
"delaySec": 10,
|
|
18
18
|
"tools": { "edit": true }
|
|
19
19
|
}
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-edit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Pi tool — precise text replacement edit with diff rendering",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@xynogen/pix-pretty": "^1.7.
|
|
37
|
+
"@xynogen/pix-pretty": "^1.7.20",
|
|
38
38
|
"@xynogen/pix-data": "^0.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
package/src/edit.test.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { CursorStore, FffState } from "@xynogen/pix-pretty/fff";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
PiPrettyApi,
|
|
5
|
+
RenderContextLike,
|
|
6
|
+
TextComponentCtor,
|
|
7
|
+
ThemeLike,
|
|
8
|
+
} from "@xynogen/pix-pretty/types";
|
|
4
9
|
import { getEditOperations, registerEditTool, summarizeEditOperations } from "./edit";
|
|
5
10
|
|
|
6
11
|
class MockTextComponent {
|
|
@@ -46,6 +51,111 @@ describe("registerEditTool", () => {
|
|
|
46
51
|
);
|
|
47
52
|
expect(tools).toEqual(["edit"]);
|
|
48
53
|
});
|
|
54
|
+
|
|
55
|
+
it("restores the bounded diff when an elapsed card is expanded", () => {
|
|
56
|
+
const registered: { renderResult?: (...args: unknown[]) => MockTextComponent } = {};
|
|
57
|
+
const mockPi: PiPrettyApi = {
|
|
58
|
+
registerTool(tool: unknown) {
|
|
59
|
+
Object.assign(registered, tool);
|
|
60
|
+
},
|
|
61
|
+
registerCommand() {},
|
|
62
|
+
on() {},
|
|
63
|
+
};
|
|
64
|
+
registerEditTool(
|
|
65
|
+
mockPi,
|
|
66
|
+
() => ({ execute: async () => ({ content: [], details: undefined }) }),
|
|
67
|
+
{
|
|
68
|
+
cwd: process.cwd(),
|
|
69
|
+
sp: (p: string) => p,
|
|
70
|
+
TextComponent: MockTextComponent as unknown as TextComponentCtor,
|
|
71
|
+
fffState: { module: null, finder: null, partialIndex: false, dbDir: null },
|
|
72
|
+
cursorStore: { store: () => "", get: () => undefined } as unknown as CursorStore,
|
|
73
|
+
},
|
|
74
|
+
() => {},
|
|
75
|
+
);
|
|
76
|
+
const theme: ThemeLike = {
|
|
77
|
+
fg: (_key: string, value: string) => value,
|
|
78
|
+
bold: (value: string) => value,
|
|
79
|
+
};
|
|
80
|
+
const result = registered.renderResult?.(
|
|
81
|
+
{
|
|
82
|
+
content: [{ type: "text", text: "edited" }],
|
|
83
|
+
details: {
|
|
84
|
+
_type: "editInfo",
|
|
85
|
+
filePath: "sample.ts",
|
|
86
|
+
summary: "+1 -1",
|
|
87
|
+
oldContent: "old",
|
|
88
|
+
newContent: "new",
|
|
89
|
+
language: "typescript",
|
|
90
|
+
editLine: 1,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
undefined,
|
|
94
|
+
theme,
|
|
95
|
+
{
|
|
96
|
+
expanded: true,
|
|
97
|
+
isError: false,
|
|
98
|
+
invalidate: () => {},
|
|
99
|
+
state: { collapsed: true },
|
|
100
|
+
} as unknown as RenderContextLike,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result?.getText()).toContain("rendering diff");
|
|
104
|
+
expect(result?.getText()).not.toContain("✓ edit");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("collapses structured errors and restores the exact diagnostic on expansion", () => {
|
|
108
|
+
const registered: { renderResult?: (...args: unknown[]) => MockTextComponent } = {};
|
|
109
|
+
const mockPi: PiPrettyApi = {
|
|
110
|
+
registerTool(tool: unknown) {
|
|
111
|
+
Object.assign(registered, tool);
|
|
112
|
+
},
|
|
113
|
+
registerCommand() {},
|
|
114
|
+
on() {},
|
|
115
|
+
};
|
|
116
|
+
registerEditTool(
|
|
117
|
+
mockPi,
|
|
118
|
+
() => ({ execute: async () => ({ content: [], details: undefined }) }),
|
|
119
|
+
{
|
|
120
|
+
cwd: process.cwd(),
|
|
121
|
+
sp: (p: string) => p,
|
|
122
|
+
TextComponent: MockTextComponent as unknown as TextComponentCtor,
|
|
123
|
+
fffState: { module: null, finder: null, partialIndex: false, dbDir: null },
|
|
124
|
+
cursorStore: { store: () => "", get: () => undefined } as unknown as CursorStore,
|
|
125
|
+
},
|
|
126
|
+
() => {},
|
|
127
|
+
);
|
|
128
|
+
const theme: ThemeLike = {
|
|
129
|
+
fg: (_key: string, value: string) => value,
|
|
130
|
+
bold: (value: string) => value,
|
|
131
|
+
};
|
|
132
|
+
const diagnostic = "oldText was not found in sample.ts";
|
|
133
|
+
const result = {
|
|
134
|
+
content: [{ type: "text", text: diagnostic }],
|
|
135
|
+
details: {
|
|
136
|
+
_type: "editInfo",
|
|
137
|
+
filePath: "sample.ts",
|
|
138
|
+
summary: "+1 -1",
|
|
139
|
+
oldContent: "old",
|
|
140
|
+
newContent: "new",
|
|
141
|
+
language: "typescript",
|
|
142
|
+
editLine: 0,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
const render = (state: Record<string, unknown>, expanded = false) =>
|
|
146
|
+
registered
|
|
147
|
+
.renderResult?.(result, { isPartial: false }, theme, {
|
|
148
|
+
expanded,
|
|
149
|
+
isError: true,
|
|
150
|
+
invalidate: () => {},
|
|
151
|
+
state,
|
|
152
|
+
} as unknown as RenderContextLike)
|
|
153
|
+
?.getText() ?? "";
|
|
154
|
+
|
|
155
|
+
expect(render({ timer: 1 })).toContain(diagnostic);
|
|
156
|
+
expect(render({ collapsed: true })).toContain("✗ edit sample.ts · failed");
|
|
157
|
+
expect(render({ collapsed: true }, true)).toContain(diagnostic);
|
|
158
|
+
});
|
|
49
159
|
});
|
|
50
160
|
|
|
51
161
|
describe("getEditOperations", () => {
|
package/src/edit.ts
CHANGED
|
@@ -30,7 +30,9 @@ import type {
|
|
|
30
30
|
import {
|
|
31
31
|
fillToolBackground,
|
|
32
32
|
getTextContent,
|
|
33
|
+
hideCollapsedToolCall,
|
|
33
34
|
isTextContent,
|
|
35
|
+
renderCollapsedToolRow,
|
|
34
36
|
renderToolError,
|
|
35
37
|
setResultDetails,
|
|
36
38
|
termW,
|
|
@@ -171,6 +173,12 @@ export function registerEditTool(
|
|
|
171
173
|
const fp = args?.path ?? args?.file_path ?? "";
|
|
172
174
|
const operations = getEditOperations(args);
|
|
173
175
|
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
176
|
+
if (
|
|
177
|
+
hideCollapsedToolCall(renderCtx.state as CollapseState, renderCtx.expanded, (value) =>
|
|
178
|
+
text.setText(value),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
return text;
|
|
174
182
|
const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
|
|
175
183
|
|
|
176
184
|
if (operations.length === 0) {
|
|
@@ -191,22 +199,44 @@ export function registerEditTool(
|
|
|
191
199
|
renderCtx: RenderContextLike<EditRenderState>,
|
|
192
200
|
) {
|
|
193
201
|
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
194
|
-
|
|
202
|
+
const d = result.details as Record<string, unknown> | undefined;
|
|
203
|
+
const isPartial = _opt?.isPartial === true;
|
|
204
|
+
const structuredError =
|
|
205
|
+
renderCtx.isError && (d?._type === "editInfo" || d?._type === "multiEditInfo");
|
|
206
|
+
if (renderCtx.isError && (!structuredError || isPartial)) {
|
|
195
207
|
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
196
208
|
return text;
|
|
197
209
|
}
|
|
198
|
-
const d = result.details as Record<string, unknown> | undefined;
|
|
199
210
|
|
|
200
211
|
// Auto-collapse: show summary line after delay
|
|
201
212
|
const cs = renderCtx.state as CollapseState;
|
|
202
|
-
if (tickCollapse("edit", cs, renderCtx.invalidate)) {
|
|
213
|
+
if (!isPartial && tickCollapse("edit", cs, renderCtx.invalidate, renderCtx.expanded)) {
|
|
203
214
|
const summary =
|
|
204
215
|
d?._type === "editInfo"
|
|
205
216
|
? (d.summary as string)
|
|
206
217
|
: d?._type === "multiEditInfo"
|
|
207
|
-
? `${d.editCount} edits ${d.summary}`
|
|
218
|
+
? `${d.editCount} edits · ${d.summary}`
|
|
208
219
|
: "edited";
|
|
209
|
-
|
|
220
|
+
let filePath = "";
|
|
221
|
+
if (d?._type === "editInfo") filePath = String(d.filePath ?? "");
|
|
222
|
+
else if (d?._type === "multiEditInfo") {
|
|
223
|
+
const ops = d.ops as Array<Record<string, unknown>> | undefined;
|
|
224
|
+
filePath = String(ops?.[0]?.filePath ?? "");
|
|
225
|
+
}
|
|
226
|
+
text.setText(
|
|
227
|
+
renderCollapsedToolRow(
|
|
228
|
+
theme,
|
|
229
|
+
"edit",
|
|
230
|
+
sp(filePath),
|
|
231
|
+
renderCtx.isError ? "failed" : summary,
|
|
232
|
+
renderCtx.isError ? "error" : "success",
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (renderCtx.isError) {
|
|
239
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
210
240
|
return text;
|
|
211
241
|
}
|
|
212
242
|
|