@xynogen/pix-pretty 1.5.1 → 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/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
- }