@xynogen/pix-pretty 1.5.0 → 1.6.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/package.json +17 -1
- package/src/diff-render.ts +7 -7
- package/src/index.ts +20 -216
- package/src/paste-chips.test.ts +29 -1
- package/src/paste-chips.ts +33 -2
- package/src/resize.ts +18 -0
- package/src/types-diff.d.ts +3 -4
- package/src/image.ts +0 -163
- package/src/tools/bash.test.ts +0 -81
- package/src/tools/bash.ts +0 -173
- package/src/tools/edit.ts +0 -291
- package/src/tools/find.ts +0 -143
- package/src/tools/grep.ts +0 -179
- package/src/tools/ls.ts +0 -106
- package/src/tools/read.ts +0 -180
- package/src/tools/write.ts +0 -232
- package/src/types-fff.d.ts +0 -80
package/src/tools/bash.test.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { registerBashTool } from "./bash";
|
|
4
|
-
|
|
5
|
-
class MockTextComponent {
|
|
6
|
-
private text: string;
|
|
7
|
-
|
|
8
|
-
constructor(text = "") {
|
|
9
|
-
this.text = text;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
setText(value: string): void {
|
|
13
|
-
this.text = value;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
getText(): string {
|
|
17
|
-
return this.text;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("registerBashTool", () => {
|
|
22
|
-
it("clamps renderCall to small terminal widths", () => {
|
|
23
|
-
const registered: { renderCall?: (...args: any[]) => MockTextComponent } =
|
|
24
|
-
{};
|
|
25
|
-
const origColumns = process.env.COLUMNS;
|
|
26
|
-
process.env.COLUMNS = "24";
|
|
27
|
-
process.stdout.emit("resize");
|
|
28
|
-
process.stdin.emit("resize");
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
registerBashTool(
|
|
32
|
-
{
|
|
33
|
-
registerTool(tool: unknown) {
|
|
34
|
-
Object.assign(registered, tool);
|
|
35
|
-
},
|
|
36
|
-
} as any,
|
|
37
|
-
() => ({
|
|
38
|
-
execute: async () => ({
|
|
39
|
-
content: [{ type: "text", text: "ok" }],
|
|
40
|
-
details: undefined,
|
|
41
|
-
}),
|
|
42
|
-
}),
|
|
43
|
-
{
|
|
44
|
-
cwd: process.cwd(),
|
|
45
|
-
sp: (p: string) => p,
|
|
46
|
-
TextComponent: MockTextComponent as any,
|
|
47
|
-
fffState: {} as any,
|
|
48
|
-
cursorStore: {} as any,
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const text = registered.renderCall?.(
|
|
53
|
-
{
|
|
54
|
-
command: 'printf "very very very long line"\necho second\necho third',
|
|
55
|
-
timeout: 30,
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
fg: (_key: string, value: string) => value,
|
|
59
|
-
bold: (value: string) => value,
|
|
60
|
-
} as any,
|
|
61
|
-
{
|
|
62
|
-
expanded: false,
|
|
63
|
-
isError: false,
|
|
64
|
-
invalidate: () => {},
|
|
65
|
-
state: {},
|
|
66
|
-
} as any,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
expect(text).toBeDefined();
|
|
70
|
-
const rendered = text?.getText() ?? "";
|
|
71
|
-
for (const line of rendered.split("\n")) {
|
|
72
|
-
expect(visibleWidth(line)).toBeLessThanOrEqual(24);
|
|
73
|
-
}
|
|
74
|
-
} finally {
|
|
75
|
-
if (origColumns === undefined) delete process.env.COLUMNS;
|
|
76
|
-
else process.env.COLUMNS = origColumns;
|
|
77
|
-
process.stdout.emit("resize");
|
|
78
|
-
process.stdin.emit("resize");
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
});
|
package/src/tools/bash.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentToolUpdateCallback,
|
|
3
|
-
BashToolInput,
|
|
4
|
-
ExtensionContext,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
|
|
7
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
8
|
-
import { FG_DIM, RST } from "../ansi.js";
|
|
9
|
-
import { MAX_PREVIEW_LINES } from "../config.js";
|
|
10
|
-
import { renderBashOutput } from "../renderers.js";
|
|
11
|
-
import type {
|
|
12
|
-
BashParams,
|
|
13
|
-
PiPrettyApi,
|
|
14
|
-
RenderContextLike,
|
|
15
|
-
ThemeLike,
|
|
16
|
-
ToolFactory,
|
|
17
|
-
ToolResultLike,
|
|
18
|
-
} from "../types.js";
|
|
19
|
-
import {
|
|
20
|
-
fillToolBackground,
|
|
21
|
-
getTextContent,
|
|
22
|
-
isTextContent,
|
|
23
|
-
normalizeLineEndings,
|
|
24
|
-
renderToolError,
|
|
25
|
-
rule,
|
|
26
|
-
setResultDetails,
|
|
27
|
-
termW,
|
|
28
|
-
} from "../utils.js";
|
|
29
|
-
import type { ToolContext } from "./context.js";
|
|
30
|
-
|
|
31
|
-
export function registerBashTool(
|
|
32
|
-
pi: PiPrettyApi,
|
|
33
|
-
createBashTool: ToolFactory<BashToolInput>,
|
|
34
|
-
ctx: ToolContext,
|
|
35
|
-
): void {
|
|
36
|
-
const { cwd, TextComponent } = ctx;
|
|
37
|
-
const origBash = createBashTool(cwd);
|
|
38
|
-
|
|
39
|
-
pi.registerTool({
|
|
40
|
-
...origBash,
|
|
41
|
-
name: "bash",
|
|
42
|
-
// Full-width framing (rules + bg fill) baked at termW(); the default
|
|
43
|
-
// Box shell pads x by 1 and re-wraps at width-2, splitting every line.
|
|
44
|
-
renderShell: "self",
|
|
45
|
-
|
|
46
|
-
async execute(
|
|
47
|
-
tid: string,
|
|
48
|
-
params: BashParams,
|
|
49
|
-
sig: AbortSignal | undefined,
|
|
50
|
-
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
51
|
-
toolCtx: ExtensionContext,
|
|
52
|
-
) {
|
|
53
|
-
const result = (await origBash.execute(
|
|
54
|
-
tid,
|
|
55
|
-
params,
|
|
56
|
-
sig,
|
|
57
|
-
upd,
|
|
58
|
-
toolCtx,
|
|
59
|
-
)) as ToolResultLike;
|
|
60
|
-
const textContent = getTextContent(result);
|
|
61
|
-
|
|
62
|
-
let exitCode: number | null = 0;
|
|
63
|
-
if (textContent) {
|
|
64
|
-
const exitMatch = textContent.match(
|
|
65
|
-
/(?:exit code|exited with|exit status)[:\s]*(\d+)/i,
|
|
66
|
-
);
|
|
67
|
-
if (exitMatch) exitCode = Number(exitMatch[1]);
|
|
68
|
-
if (
|
|
69
|
-
textContent.includes("command not found") ||
|
|
70
|
-
textContent.includes("No such file")
|
|
71
|
-
) {
|
|
72
|
-
exitCode = 1;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
setResultDetails(result, {
|
|
77
|
-
_type: "bashResult",
|
|
78
|
-
text: textContent ?? "",
|
|
79
|
-
exitCode,
|
|
80
|
-
command: params.command ?? "",
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
return result;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
renderCall(
|
|
87
|
-
args: BashParams,
|
|
88
|
-
theme: ThemeLike,
|
|
89
|
-
renderCtx: RenderContextLike,
|
|
90
|
-
) {
|
|
91
|
-
const cmd = args.command ?? "";
|
|
92
|
-
const displayCmdRaw = cmd.trim();
|
|
93
|
-
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
94
|
-
const label = theme.fg("toolTitle", theme.bold("bash"));
|
|
95
|
-
const timeout = args.timeout
|
|
96
|
-
? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}`
|
|
97
|
-
: "";
|
|
98
|
-
const cmdLines = displayCmdRaw.split("\n");
|
|
99
|
-
const firstLine = cmdLines[0] ?? "";
|
|
100
|
-
const compactCmd =
|
|
101
|
-
cmdLines.length > 1
|
|
102
|
-
? `${firstLine} ${theme.fg("muted", `… (+${cmdLines.length - 1} lines)`)}`
|
|
103
|
-
: firstLine;
|
|
104
|
-
const baseCmd = renderCtx.expanded ? displayCmdRaw : compactCmd;
|
|
105
|
-
const availableWidth = Math.max(1, termW() - 1);
|
|
106
|
-
const prefix = `${label} `;
|
|
107
|
-
const reserve = Math.max(0, availableWidth - timeout.length);
|
|
108
|
-
const displayCmd = truncateToWidth(
|
|
109
|
-
theme.fg("accent", baseCmd),
|
|
110
|
-
Math.max(1, reserve - prefix.length),
|
|
111
|
-
"…",
|
|
112
|
-
);
|
|
113
|
-
text.setText(fillToolBackground(`${prefix}${displayCmd}${timeout}`));
|
|
114
|
-
return text;
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
renderResult(
|
|
118
|
-
result: ToolResultLike,
|
|
119
|
-
_opt: unknown,
|
|
120
|
-
theme: ThemeLike,
|
|
121
|
-
renderCtx: RenderContextLike,
|
|
122
|
-
) {
|
|
123
|
-
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
124
|
-
|
|
125
|
-
if (renderCtx.isError) {
|
|
126
|
-
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
127
|
-
return text;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const d = result.details as Record<string, unknown> | undefined;
|
|
131
|
-
if (d?._type === "bashResult") {
|
|
132
|
-
const normalizedText = normalizeLineEndings(d.text as string)
|
|
133
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
134
|
-
.replace(/^\n+|\n+$/g, "");
|
|
135
|
-
const { summary } = renderBashOutput(
|
|
136
|
-
normalizedText,
|
|
137
|
-
d.exitCode as number | null,
|
|
138
|
-
);
|
|
139
|
-
const lines = normalizedText ? normalizedText.split("\n") : [];
|
|
140
|
-
const lineCount = lines.length;
|
|
141
|
-
const lineInfo =
|
|
142
|
-
lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST}` : "";
|
|
143
|
-
const header = ` ${summary}${lineInfo}`;
|
|
144
|
-
|
|
145
|
-
if (normalizedText) {
|
|
146
|
-
const maxShow = renderCtx.expanded ? lineCount : MAX_PREVIEW_LINES;
|
|
147
|
-
const show = lines.slice(0, maxShow);
|
|
148
|
-
const tw = termW();
|
|
149
|
-
const out: string[] = [header, rule(tw)];
|
|
150
|
-
for (const line of show) out.push(` ${line}`);
|
|
151
|
-
out.push(rule(tw));
|
|
152
|
-
if (lineCount > maxShow) {
|
|
153
|
-
out.push(`${FG_DIM} … ${lineCount - maxShow} more lines${RST}`);
|
|
154
|
-
}
|
|
155
|
-
text.setText(fillToolBackground(out.join("\n")));
|
|
156
|
-
} else {
|
|
157
|
-
text.setText(fillToolBackground(header));
|
|
158
|
-
}
|
|
159
|
-
return text;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const fallback = result.content?.[0];
|
|
163
|
-
const fallbackText =
|
|
164
|
-
fallback && isTextContent(fallback) ? fallback.text : "done";
|
|
165
|
-
text.setText(
|
|
166
|
-
fillToolBackground(
|
|
167
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
168
|
-
),
|
|
169
|
-
);
|
|
170
|
-
return text;
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
}
|
package/src/tools/edit.ts
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import type {
|
|
3
|
-
AgentToolUpdateCallback,
|
|
4
|
-
EditToolInput,
|
|
5
|
-
ExtensionContext,
|
|
6
|
-
ToolRenderResultOptions,
|
|
7
|
-
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
|
|
9
|
-
import { MAX_RENDER_LINES } from "../config.js";
|
|
10
|
-
import { parseDiff } from "../diff.js";
|
|
11
|
-
import {
|
|
12
|
-
diffThemeCacheKey,
|
|
13
|
-
renderSplit,
|
|
14
|
-
resolveDiffColors,
|
|
15
|
-
summarize,
|
|
16
|
-
} from "../diff-render.js";
|
|
17
|
-
import { lang } from "../lang.js";
|
|
18
|
-
import type {
|
|
19
|
-
EditOperation,
|
|
20
|
-
EditParams,
|
|
21
|
-
EditRenderState,
|
|
22
|
-
PiPrettyApi,
|
|
23
|
-
RenderContextLike,
|
|
24
|
-
ThemeLike,
|
|
25
|
-
ToolFactory,
|
|
26
|
-
ToolResultLike,
|
|
27
|
-
} from "../types.js";
|
|
28
|
-
import {
|
|
29
|
-
fillToolBackground,
|
|
30
|
-
getTextContent,
|
|
31
|
-
isTextContent,
|
|
32
|
-
renderToolError,
|
|
33
|
-
setResultDetails,
|
|
34
|
-
termW,
|
|
35
|
-
} from "../utils.js";
|
|
36
|
-
import type { ToolContext } from "./context.js";
|
|
37
|
-
|
|
38
|
-
// ── Helpers ────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
export function getEditOperations(input: EditParams): EditOperation[] {
|
|
41
|
-
if (Array.isArray(input?.edits)) {
|
|
42
|
-
return input.edits
|
|
43
|
-
.map((e) => ({
|
|
44
|
-
oldText:
|
|
45
|
-
typeof e?.oldText === "string"
|
|
46
|
-
? e.oldText
|
|
47
|
-
: typeof e?.old_text === "string"
|
|
48
|
-
? e.old_text
|
|
49
|
-
: "",
|
|
50
|
-
newText:
|
|
51
|
-
typeof e?.newText === "string"
|
|
52
|
-
? e.newText
|
|
53
|
-
: typeof e?.new_text === "string"
|
|
54
|
-
? e.new_text
|
|
55
|
-
: "",
|
|
56
|
-
}))
|
|
57
|
-
.filter((e) => e.oldText && e.oldText !== e.newText);
|
|
58
|
-
}
|
|
59
|
-
const oldText =
|
|
60
|
-
typeof input?.oldText === "string"
|
|
61
|
-
? input.oldText
|
|
62
|
-
: typeof input?.old_text === "string"
|
|
63
|
-
? input.old_text
|
|
64
|
-
: "";
|
|
65
|
-
const newText =
|
|
66
|
-
typeof input?.newText === "string"
|
|
67
|
-
? input.newText
|
|
68
|
-
: typeof input?.new_text === "string"
|
|
69
|
-
? input.new_text
|
|
70
|
-
: "";
|
|
71
|
-
return oldText && oldText !== newText ? [{ oldText, newText }] : [];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function summarizeEditOperations(operations: EditOperation[]) {
|
|
75
|
-
const diffs = operations.map((e) => parseDiff(e.oldText, e.newText));
|
|
76
|
-
const totalAdded = diffs.reduce((sum, d) => sum + d.added, 0);
|
|
77
|
-
const totalRemoved = diffs.reduce((sum, d) => sum + d.removed, 0);
|
|
78
|
-
return {
|
|
79
|
-
diffs,
|
|
80
|
-
totalAdded,
|
|
81
|
-
totalRemoved,
|
|
82
|
-
summary: summarize(totalAdded, totalRemoved),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── Tool ───────────────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
export function registerEditTool(
|
|
89
|
-
pi: PiPrettyApi,
|
|
90
|
-
createEditTool: ToolFactory<EditToolInput>,
|
|
91
|
-
ctx: ToolContext,
|
|
92
|
-
trackInvalidator: (id: string, inv: () => void) => void,
|
|
93
|
-
): void {
|
|
94
|
-
const { cwd, sp, TextComponent } = ctx;
|
|
95
|
-
const origEdit = createEditTool(cwd);
|
|
96
|
-
|
|
97
|
-
pi.registerTool({
|
|
98
|
-
...origEdit,
|
|
99
|
-
name: "edit",
|
|
100
|
-
|
|
101
|
-
async execute(
|
|
102
|
-
tid: string,
|
|
103
|
-
params: EditParams,
|
|
104
|
-
sig: AbortSignal | undefined,
|
|
105
|
-
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
106
|
-
toolCtx: ExtensionContext,
|
|
107
|
-
) {
|
|
108
|
-
const fp = params.path ?? params.file_path ?? "";
|
|
109
|
-
const operations = getEditOperations(params);
|
|
110
|
-
const fileLang = lang(fp);
|
|
111
|
-
|
|
112
|
-
const result = (await origEdit.execute(
|
|
113
|
-
tid,
|
|
114
|
-
params as unknown as Parameters<typeof origEdit.execute>[1],
|
|
115
|
-
sig,
|
|
116
|
-
upd,
|
|
117
|
-
toolCtx,
|
|
118
|
-
)) as ToolResultLike;
|
|
119
|
-
|
|
120
|
-
if (operations.length === 0) return result;
|
|
121
|
-
|
|
122
|
-
const { diffs, summary } = summarizeEditOperations(operations);
|
|
123
|
-
|
|
124
|
-
if (operations.length === 1) {
|
|
125
|
-
let editLine = 0;
|
|
126
|
-
try {
|
|
127
|
-
if (fp && existsSync(fp)) {
|
|
128
|
-
const f = readFileSync(fp, "utf-8");
|
|
129
|
-
const idx = f.indexOf(operations[0].newText);
|
|
130
|
-
if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
editLine = 0;
|
|
134
|
-
}
|
|
135
|
-
setResultDetails(result, {
|
|
136
|
-
_type: "editInfo",
|
|
137
|
-
summary,
|
|
138
|
-
editLine,
|
|
139
|
-
oldContent: operations[0].oldText,
|
|
140
|
-
newContent: operations[0].newText,
|
|
141
|
-
language: fileLang,
|
|
142
|
-
filePath: fp,
|
|
143
|
-
});
|
|
144
|
-
return result;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
setResultDetails(result, {
|
|
148
|
-
_type: "multiEditInfo",
|
|
149
|
-
summary,
|
|
150
|
-
editCount: operations.length,
|
|
151
|
-
diffLineCount: diffs.reduce((sum, d) => sum + d.lines.length, 0),
|
|
152
|
-
ops: operations.map((op) => ({
|
|
153
|
-
oldContent: op.oldText,
|
|
154
|
-
newContent: op.newText,
|
|
155
|
-
language: fileLang,
|
|
156
|
-
filePath: fp,
|
|
157
|
-
})),
|
|
158
|
-
});
|
|
159
|
-
return result;
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
renderCall(
|
|
163
|
-
args: EditParams,
|
|
164
|
-
theme: ThemeLike,
|
|
165
|
-
renderCtx: RenderContextLike<EditRenderState>,
|
|
166
|
-
) {
|
|
167
|
-
const fp = args?.path ?? args?.file_path ?? "";
|
|
168
|
-
const operations = getEditOperations(args);
|
|
169
|
-
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
170
|
-
const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
|
|
171
|
-
|
|
172
|
-
if (operations.length === 0) {
|
|
173
|
-
text.setText(fillToolBackground(hdr));
|
|
174
|
-
return text;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const { summary } = summarizeEditOperations(operations);
|
|
178
|
-
const suffix =
|
|
179
|
-
operations.length === 1
|
|
180
|
-
? summary
|
|
181
|
-
: `${operations.length} edits ${summary}`;
|
|
182
|
-
text.setText(fillToolBackground(`${hdr} ${theme.fg("muted", suffix)}`));
|
|
183
|
-
return text;
|
|
184
|
-
},
|
|
185
|
-
|
|
186
|
-
renderResult(
|
|
187
|
-
result: ToolResultLike,
|
|
188
|
-
_opt: ToolRenderResultOptions,
|
|
189
|
-
theme: ThemeLike,
|
|
190
|
-
renderCtx: RenderContextLike<EditRenderState>,
|
|
191
|
-
) {
|
|
192
|
-
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
193
|
-
if (renderCtx.isError) {
|
|
194
|
-
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
195
|
-
return text;
|
|
196
|
-
}
|
|
197
|
-
const d = result.details as Record<string, unknown> | undefined;
|
|
198
|
-
|
|
199
|
-
// Single edit — full split diff
|
|
200
|
-
if (d?._type === "editInfo") {
|
|
201
|
-
const key = `ed:${diffThemeCacheKey(theme)}:${termW()}:${d.summary}:${(d.oldContent as string).length}:${(d.newContent as string).length}:${d.language ?? ""}`;
|
|
202
|
-
if (renderCtx.toolCallId)
|
|
203
|
-
trackInvalidator(renderCtx.toolCallId, renderCtx.invalidate);
|
|
204
|
-
if (renderCtx.state._edk !== key) {
|
|
205
|
-
renderCtx.state._edk = key;
|
|
206
|
-
const loc =
|
|
207
|
-
(d.editLine as number) > 0
|
|
208
|
-
? ` ${theme.fg("muted", `at line ${d.editLine}`)}`
|
|
209
|
-
: "";
|
|
210
|
-
renderCtx.state._edt = ` ${d.summary}${loc}\n${theme.fg("muted", " rendering diff…")}`;
|
|
211
|
-
const dc = resolveDiffColors(theme);
|
|
212
|
-
const diff = parseDiff(
|
|
213
|
-
d.oldContent as string,
|
|
214
|
-
d.newContent as string,
|
|
215
|
-
);
|
|
216
|
-
renderSplit(
|
|
217
|
-
diff,
|
|
218
|
-
d.language as string | undefined,
|
|
219
|
-
MAX_RENDER_LINES,
|
|
220
|
-
dc,
|
|
221
|
-
)
|
|
222
|
-
.then((rendered) => {
|
|
223
|
-
if (renderCtx.state._edk !== key) return;
|
|
224
|
-
const loc2 =
|
|
225
|
-
(d.editLine as number) > 0
|
|
226
|
-
? ` ${theme.fg("muted", `at line ${d.editLine}`)}`
|
|
227
|
-
: "";
|
|
228
|
-
renderCtx.state._edt = ` ${d.summary}${loc2}\n${rendered}`;
|
|
229
|
-
renderCtx.invalidate();
|
|
230
|
-
})
|
|
231
|
-
.catch(() => {
|
|
232
|
-
if (renderCtx.state._edk !== key) return;
|
|
233
|
-
renderCtx.state._edt = ` ${d.summary}`;
|
|
234
|
-
renderCtx.invalidate();
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
text.setText(renderCtx.state._edt ?? ` ${d.summary}`);
|
|
238
|
-
return text;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Multi-edit — stacked diffs
|
|
242
|
-
if (d?._type === "multiEditInfo") {
|
|
243
|
-
const key = `med:${diffThemeCacheKey(theme)}:${termW()}:${d.summary}:${d.editCount}:${d.diffLineCount}`;
|
|
244
|
-
if (renderCtx.toolCallId)
|
|
245
|
-
trackInvalidator(renderCtx.toolCallId, renderCtx.invalidate);
|
|
246
|
-
if (renderCtx.state._edk !== key) {
|
|
247
|
-
renderCtx.state._edk = key;
|
|
248
|
-
renderCtx.state._edt = ` ${d.editCount} edits ${d.summary}\n${theme.fg("muted", " rendering diff…")}`;
|
|
249
|
-
const dc = resolveDiffColors(theme);
|
|
250
|
-
Promise.all(
|
|
251
|
-
(
|
|
252
|
-
d.ops as Array<{
|
|
253
|
-
oldContent: string;
|
|
254
|
-
newContent: string;
|
|
255
|
-
language?: string;
|
|
256
|
-
}>
|
|
257
|
-
).map((op) => {
|
|
258
|
-
const diff = parseDiff(op.oldContent, op.newContent);
|
|
259
|
-
return renderSplit(diff, op.language, MAX_RENDER_LINES, dc);
|
|
260
|
-
}),
|
|
261
|
-
)
|
|
262
|
-
.then((rendered) => {
|
|
263
|
-
if (renderCtx.state._edk !== key) return;
|
|
264
|
-
const body = rendered.join(`\n${theme.fg("muted", " ···")}\n`);
|
|
265
|
-
renderCtx.state._edt = ` ${d.editCount} edits ${d.summary}\n${body}`;
|
|
266
|
-
renderCtx.invalidate();
|
|
267
|
-
})
|
|
268
|
-
.catch(() => {
|
|
269
|
-
if (renderCtx.state._edk !== key) return;
|
|
270
|
-
renderCtx.state._edt = ` ${d.editCount} edits ${d.summary}`;
|
|
271
|
-
renderCtx.invalidate();
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
text.setText(
|
|
275
|
-
renderCtx.state._edt ?? ` ${d.editCount} edits ${d.summary}`,
|
|
276
|
-
);
|
|
277
|
-
return text;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const fallback = result.content?.[0];
|
|
281
|
-
const fallbackText =
|
|
282
|
-
fallback && isTextContent(fallback) ? fallback.text : "edited";
|
|
283
|
-
text.setText(
|
|
284
|
-
fillToolBackground(
|
|
285
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
286
|
-
),
|
|
287
|
-
);
|
|
288
|
-
return text;
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
}
|
package/src/tools/find.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionContext,
|
|
3
|
-
FindToolInput,
|
|
4
|
-
ToolRenderResultOptions,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
FindParams,
|
|
9
|
-
FindResultDetails,
|
|
10
|
-
PiPrettyApi,
|
|
11
|
-
RenderContextLike,
|
|
12
|
-
ThemeLike,
|
|
13
|
-
ToolFactory,
|
|
14
|
-
ToolResultLike,
|
|
15
|
-
} from "../types.js";
|
|
16
|
-
import {
|
|
17
|
-
appendNotices,
|
|
18
|
-
fillToolBackground,
|
|
19
|
-
getTextContent,
|
|
20
|
-
makeTextResult,
|
|
21
|
-
renderDimPreview,
|
|
22
|
-
renderToolError,
|
|
23
|
-
setResultDetails,
|
|
24
|
-
} from "../utils.js";
|
|
25
|
-
import type { ToolContext } from "./context.js";
|
|
26
|
-
|
|
27
|
-
export function registerFindTool(
|
|
28
|
-
pi: PiPrettyApi,
|
|
29
|
-
createFindTool: ToolFactory<FindToolInput>,
|
|
30
|
-
ctx: ToolContext,
|
|
31
|
-
): void {
|
|
32
|
-
const { cwd, sp, TextComponent, fffState } = ctx;
|
|
33
|
-
const origFind = createFindTool(cwd);
|
|
34
|
-
|
|
35
|
-
pi.registerTool({
|
|
36
|
-
...origFind,
|
|
37
|
-
name: "find",
|
|
38
|
-
renderShell: "self",
|
|
39
|
-
|
|
40
|
-
async execute(
|
|
41
|
-
tid: string,
|
|
42
|
-
params: FindParams,
|
|
43
|
-
sig: AbortSignal | undefined,
|
|
44
|
-
upd: unknown,
|
|
45
|
-
toolCtx: ExtensionContext,
|
|
46
|
-
) {
|
|
47
|
-
// Try FFF first (frecency-ranked, SIMD-accelerated)
|
|
48
|
-
if (fffState.finder && !fffState.finder.isDestroyed) {
|
|
49
|
-
try {
|
|
50
|
-
const effectiveLimit = Math.max(1, params.limit ?? 200);
|
|
51
|
-
let query = params.pattern;
|
|
52
|
-
if (params.path) query = `${params.path} ${query}`;
|
|
53
|
-
|
|
54
|
-
const searchResult = fffState.finder.fileSearch(query, {
|
|
55
|
-
pageSize: effectiveLimit,
|
|
56
|
-
});
|
|
57
|
-
if (searchResult.ok) {
|
|
58
|
-
const { items, totalMatched } = searchResult.value;
|
|
59
|
-
const trimmed = items.slice(0, effectiveLimit);
|
|
60
|
-
const notices: string[] = [];
|
|
61
|
-
if (fffState.partialIndex)
|
|
62
|
-
notices.push("Warning: partial file index");
|
|
63
|
-
if (trimmed.length >= effectiveLimit)
|
|
64
|
-
notices.push(`${effectiveLimit} limit reached`);
|
|
65
|
-
if (totalMatched > trimmed.length)
|
|
66
|
-
notices.push(`${totalMatched} total matches`);
|
|
67
|
-
|
|
68
|
-
const textContent = appendNotices(
|
|
69
|
-
trimmed.map((item) => item.relativePath).join("\n"),
|
|
70
|
-
notices,
|
|
71
|
-
);
|
|
72
|
-
return makeTextResult<FindResultDetails>(textContent, {
|
|
73
|
-
_type: "findResult",
|
|
74
|
-
text: textContent,
|
|
75
|
-
pattern: params.pattern,
|
|
76
|
-
matchCount: trimmed.length,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
/* fall through to SDK */
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// SDK fallback
|
|
85
|
-
const result = await origFind.execute(
|
|
86
|
-
tid,
|
|
87
|
-
params,
|
|
88
|
-
sig,
|
|
89
|
-
upd as never,
|
|
90
|
-
toolCtx,
|
|
91
|
-
);
|
|
92
|
-
const textContent = getTextContent(result);
|
|
93
|
-
const matchCount = textContent
|
|
94
|
-
? textContent.trim().split("\n").filter(Boolean).length
|
|
95
|
-
: 0;
|
|
96
|
-
|
|
97
|
-
setResultDetails<FindResultDetails>(result, {
|
|
98
|
-
_type: "findResult",
|
|
99
|
-
text: textContent,
|
|
100
|
-
pattern: params.pattern,
|
|
101
|
-
matchCount,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
return result;
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
renderCall(
|
|
108
|
-
args: FindParams,
|
|
109
|
-
theme: ThemeLike,
|
|
110
|
-
renderCtx: RenderContextLike,
|
|
111
|
-
) {
|
|
112
|
-
const pattern = args.pattern ?? "";
|
|
113
|
-
const path = args.path
|
|
114
|
-
? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
|
|
115
|
-
: "";
|
|
116
|
-
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
117
|
-
text.setText(
|
|
118
|
-
fillToolBackground(
|
|
119
|
-
`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`,
|
|
120
|
-
),
|
|
121
|
-
);
|
|
122
|
-
return text;
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
renderResult(
|
|
126
|
-
result: ToolResultLike<FindResultDetails>,
|
|
127
|
-
_opt: ToolRenderResultOptions,
|
|
128
|
-
theme: ThemeLike,
|
|
129
|
-
renderCtx: RenderContextLike,
|
|
130
|
-
) {
|
|
131
|
-
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
132
|
-
|
|
133
|
-
if (renderCtx.isError) {
|
|
134
|
-
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
135
|
-
return text;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const output = getTextContent(result) || "found";
|
|
139
|
-
text.setText(renderDimPreview(output, theme));
|
|
140
|
-
return text;
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
}
|