@xynogen/pix-pretty 1.2.0 → 1.3.1

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.
@@ -0,0 +1,159 @@
1
+ import type {
2
+ ExtensionContext,
3
+ FindToolInput,
4
+ ToolRenderResultOptions,
5
+ } from "@earendil-works/pi-coding-agent";
6
+
7
+ import { FG_DIM, RST } from "../ansi.js";
8
+ import { renderFindResults } from "../renderers.js";
9
+ import type {
10
+ FindParams,
11
+ FindResultDetails,
12
+ PiPrettyApi,
13
+ RenderContextLike,
14
+ ThemeLike,
15
+ ToolFactory,
16
+ ToolResultLike,
17
+ } from "../types.js";
18
+ import {
19
+ appendNotices,
20
+ fillToolBackground,
21
+ getTextContent,
22
+ isTextContent,
23
+ makeTextResult,
24
+ renderToolError,
25
+ setResultDetails,
26
+ } from "../utils.js";
27
+ import type { ToolContext } from "./context.js";
28
+
29
+ export function registerFindTool(
30
+ pi: PiPrettyApi,
31
+ createFindTool: ToolFactory<FindToolInput>,
32
+ ctx: ToolContext,
33
+ ): void {
34
+ const { cwd, sp, TextComponent, fffState } = ctx;
35
+ const origFind = createFindTool(cwd);
36
+
37
+ pi.registerTool({
38
+ ...origFind,
39
+ name: "find",
40
+ renderShell: "self",
41
+
42
+ async execute(
43
+ tid: string,
44
+ params: FindParams,
45
+ sig: AbortSignal | undefined,
46
+ upd: unknown,
47
+ toolCtx: ExtensionContext,
48
+ ) {
49
+ // Try FFF first (frecency-ranked, SIMD-accelerated)
50
+ if (fffState.finder && !fffState.finder.isDestroyed) {
51
+ try {
52
+ const effectiveLimit = Math.max(1, params.limit ?? 200);
53
+ let query = params.pattern;
54
+ if (params.path) query = `${params.path} ${query}`;
55
+
56
+ const searchResult = fffState.finder.fileSearch(query, {
57
+ pageSize: effectiveLimit,
58
+ });
59
+ if (searchResult.ok) {
60
+ const { items, totalMatched } = searchResult.value;
61
+ const trimmed = items.slice(0, effectiveLimit);
62
+ const notices: string[] = [];
63
+ if (fffState.partialIndex)
64
+ notices.push("Warning: partial file index");
65
+ if (trimmed.length >= effectiveLimit)
66
+ notices.push(`${effectiveLimit} limit reached`);
67
+ if (totalMatched > trimmed.length)
68
+ notices.push(`${totalMatched} total matches`);
69
+
70
+ const textContent = appendNotices(
71
+ trimmed.map((item) => item.relativePath).join("\n"),
72
+ notices,
73
+ );
74
+ return makeTextResult<FindResultDetails>(textContent, {
75
+ _type: "findResult",
76
+ text: textContent,
77
+ pattern: params.pattern,
78
+ matchCount: trimmed.length,
79
+ });
80
+ }
81
+ } catch {
82
+ /* fall through to SDK */
83
+ }
84
+ }
85
+
86
+ // SDK fallback
87
+ const result = await origFind.execute(
88
+ tid,
89
+ params,
90
+ sig,
91
+ upd as never,
92
+ toolCtx,
93
+ );
94
+ const textContent = getTextContent(result);
95
+ const matchCount = textContent
96
+ ? textContent.trim().split("\n").filter(Boolean).length
97
+ : 0;
98
+
99
+ setResultDetails<FindResultDetails>(result, {
100
+ _type: "findResult",
101
+ text: textContent,
102
+ pattern: params.pattern,
103
+ matchCount,
104
+ });
105
+
106
+ return result;
107
+ },
108
+
109
+ renderCall(
110
+ args: FindParams,
111
+ theme: ThemeLike,
112
+ renderCtx: RenderContextLike,
113
+ ) {
114
+ const pattern = args.pattern ?? "";
115
+ const path = args.path
116
+ ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
117
+ : "";
118
+ const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
119
+ text.setText(
120
+ fillToolBackground(
121
+ `${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`,
122
+ ),
123
+ );
124
+ return text;
125
+ },
126
+
127
+ renderResult(
128
+ result: ToolResultLike<FindResultDetails>,
129
+ _opt: ToolRenderResultOptions,
130
+ theme: ThemeLike,
131
+ renderCtx: RenderContextLike,
132
+ ) {
133
+ const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
134
+
135
+ if (renderCtx.isError) {
136
+ text.setText(renderToolError(getTextContent(result) || "Error", theme));
137
+ return text;
138
+ }
139
+
140
+ const d = result.details;
141
+ if (d?._type === "findResult" && d.text) {
142
+ const rendered = renderFindResults(d.text);
143
+ const info = `${FG_DIM}${d.matchCount} files${RST}`;
144
+ text.setText(fillToolBackground(` ${info}\n${rendered}`));
145
+ return text;
146
+ }
147
+
148
+ const fallback = result.content?.[0];
149
+ const fallbackText =
150
+ fallback && isTextContent(fallback) ? fallback.text : "found";
151
+ text.setText(
152
+ fillToolBackground(
153
+ ` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
154
+ ),
155
+ );
156
+ return text;
157
+ },
158
+ });
159
+ }
@@ -0,0 +1,203 @@
1
+ import type {
2
+ ExtensionContext,
3
+ GrepToolInput,
4
+ ToolRenderResultOptions,
5
+ } from "@earendil-works/pi-coding-agent";
6
+
7
+ import { FG_DIM, RST } from "../ansi.js";
8
+ import { fffFormatGrepText } from "../fff.js";
9
+ import { renderGrepResults } from "../renderers.js";
10
+ import type {
11
+ GrepParams,
12
+ GrepRenderState,
13
+ GrepResultDetails,
14
+ PiPrettyApi,
15
+ RenderContextLike,
16
+ ThemeLike,
17
+ ToolFactory,
18
+ ToolResultLike,
19
+ } from "../types.js";
20
+ import {
21
+ appendNotices,
22
+ countRipgrepMatches,
23
+ fillToolBackground,
24
+ getTextContent,
25
+ isTextContent,
26
+ makeTextResult,
27
+ normalizeLineEndings,
28
+ renderToolError,
29
+ setResultDetails,
30
+ termW,
31
+ } from "../utils.js";
32
+ import type { ToolContext } from "./context.js";
33
+
34
+ export function registerGrepTool(
35
+ pi: PiPrettyApi,
36
+ createGrepTool: ToolFactory<GrepToolInput>,
37
+ ctx: ToolContext,
38
+ ): void {
39
+ const { cwd, sp, TextComponent, fffState, cursorStore } = ctx;
40
+ const origGrep = createGrepTool(cwd);
41
+
42
+ pi.registerTool({
43
+ ...origGrep,
44
+ name: "grep",
45
+ renderShell: "self",
46
+
47
+ async execute(
48
+ tid: string,
49
+ params: GrepParams,
50
+ sig: AbortSignal | undefined,
51
+ upd: unknown,
52
+ toolCtx: ExtensionContext,
53
+ ) {
54
+ // Try FFF first (SIMD-accelerated).
55
+ // Constrained searches (path/glob) fall through to SDK — FFF 0.5.2
56
+ // can abort the process on constrained searches with Unicode filenames.
57
+ if (
58
+ fffState.finder &&
59
+ !fffState.finder.isDestroyed &&
60
+ !params.path &&
61
+ !params.glob
62
+ ) {
63
+ try {
64
+ const effectiveLimit = Math.max(1, params.limit ?? 100);
65
+ const grepResult = fffState.finder.grep(params.pattern, {
66
+ mode: params.literal ? "plain" : "regex",
67
+ smartCase: !params.ignoreCase,
68
+ maxMatchesPerFile: Math.min(effectiveLimit, 50),
69
+ cursor: null,
70
+ beforeContext: params.context ?? 0,
71
+ afterContext: params.context ?? 0,
72
+ });
73
+
74
+ if (grepResult.ok) {
75
+ const grep = grepResult.value;
76
+ const notices: string[] = [];
77
+ if (fffState.partialIndex)
78
+ notices.push("Warning: partial file index");
79
+ if (grep.items.length >= effectiveLimit)
80
+ notices.push(`${effectiveLimit} limit reached`);
81
+ if (grep.regexFallbackError)
82
+ notices.push(
83
+ `Regex failed: ${grep.regexFallbackError}, used literal match`,
84
+ );
85
+ if (grep.nextCursor) {
86
+ const cursorId = cursorStore.store(grep.nextCursor);
87
+ notices.push(
88
+ `More results available. Use cursor="${cursorId}" to continue`,
89
+ );
90
+ }
91
+
92
+ const textContent = appendNotices(
93
+ fffFormatGrepText(grep.items, effectiveLimit),
94
+ notices,
95
+ );
96
+ return makeTextResult<GrepResultDetails>(textContent, {
97
+ _type: "grepResult",
98
+ text: textContent,
99
+ pattern: params.pattern,
100
+ matchCount: Math.min(grep.items.length, effectiveLimit),
101
+ });
102
+ }
103
+ } catch {
104
+ /* fall through to SDK */
105
+ }
106
+ }
107
+
108
+ // SDK fallback
109
+ const result = await origGrep.execute(
110
+ tid,
111
+ params,
112
+ sig,
113
+ upd as never,
114
+ toolCtx,
115
+ );
116
+ const textContent = normalizeLineEndings(getTextContent(result));
117
+ if (result.content) {
118
+ for (const content of result.content) {
119
+ if (isTextContent(content))
120
+ content.text = normalizeLineEndings(content.text || "");
121
+ }
122
+ }
123
+ const matchCount = textContent ? countRipgrepMatches(textContent) : 0;
124
+
125
+ setResultDetails<GrepResultDetails>(result, {
126
+ _type: "grepResult",
127
+ text: textContent,
128
+ pattern: params.pattern,
129
+ matchCount,
130
+ });
131
+
132
+ return result;
133
+ },
134
+
135
+ renderCall(
136
+ args: GrepParams,
137
+ theme: ThemeLike,
138
+ renderCtx: RenderContextLike,
139
+ ) {
140
+ const pattern = args.pattern ?? "";
141
+ const path = args.path
142
+ ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
143
+ : "";
144
+ const glob = args.glob ? ` ${theme.fg("muted", `(${args.glob})`)}` : "";
145
+ const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
146
+ text.setText(
147
+ fillToolBackground(
148
+ `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`,
149
+ ),
150
+ );
151
+ return text;
152
+ },
153
+
154
+ renderResult(
155
+ result: ToolResultLike<GrepResultDetails>,
156
+ _opt: ToolRenderResultOptions,
157
+ theme: ThemeLike,
158
+ renderCtx: RenderContextLike<GrepRenderState>,
159
+ ) {
160
+ const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
161
+
162
+ if (renderCtx.isError) {
163
+ text.setText(renderToolError(getTextContent(result) || "Error", theme));
164
+ return text;
165
+ }
166
+
167
+ const d = result.details;
168
+ if (d?._type === "grepResult" && d.text) {
169
+ const key = `grep:${d.pattern}:${d.matchCount}:${termW()}`;
170
+ if (renderCtx.state._gk !== key) {
171
+ renderCtx.state._gk = key;
172
+ const info = `${FG_DIM}${d.matchCount} matches${RST}`;
173
+ renderCtx.state._gt = fillToolBackground(` ${info}`);
174
+
175
+ renderGrepResults(d.text, d.pattern)
176
+ .then((rendered: string) => {
177
+ if (renderCtx.state._gk !== key) return;
178
+ renderCtx.state._gt = fillToolBackground(
179
+ ` ${info}\n${rendered}`,
180
+ );
181
+ renderCtx.invalidate();
182
+ })
183
+ .catch(() => {});
184
+ }
185
+ text.setText(
186
+ renderCtx.state._gt ??
187
+ fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}`),
188
+ );
189
+ return text;
190
+ }
191
+
192
+ const fallback = result.content?.[0];
193
+ const fallbackText =
194
+ fallback && isTextContent(fallback) ? fallback.text : "searched";
195
+ text.setText(
196
+ fillToolBackground(
197
+ ` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
198
+ ),
199
+ );
200
+ return text;
201
+ },
202
+ });
203
+ }
@@ -0,0 +1,112 @@
1
+ import type {
2
+ AgentToolUpdateCallback,
3
+ ExtensionContext,
4
+ LsToolInput,
5
+ } from "@earendil-works/pi-coding-agent";
6
+
7
+ import { FG_DIM, RST } from "../ansi.js";
8
+ import { renderTree } from "../renderers.js";
9
+ import type {
10
+ LsParams,
11
+ PiPrettyApi,
12
+ RenderContextLike,
13
+ ThemeLike,
14
+ ToolFactory,
15
+ ToolResultLike,
16
+ } from "../types.js";
17
+ import {
18
+ fillToolBackground,
19
+ getTextContent,
20
+ isTextContent,
21
+ renderToolError,
22
+ setResultDetails,
23
+ } from "../utils.js";
24
+ import type { ToolContext } from "./context.js";
25
+
26
+ export function registerLsTool(
27
+ pi: PiPrettyApi,
28
+ createLsTool: ToolFactory<LsToolInput>,
29
+ ctx: ToolContext,
30
+ ): void {
31
+ const { cwd, sp, TextComponent } = ctx;
32
+ const origLs = createLsTool(cwd);
33
+
34
+ pi.registerTool({
35
+ ...origLs,
36
+ name: "ls",
37
+ renderShell: "self",
38
+
39
+ async execute(
40
+ tid: string,
41
+ params: LsParams,
42
+ sig: AbortSignal | undefined,
43
+ upd: AgentToolUpdateCallback<unknown> | undefined,
44
+ toolCtx: ExtensionContext,
45
+ ) {
46
+ const result = (await origLs.execute(
47
+ tid,
48
+ params,
49
+ sig,
50
+ upd,
51
+ toolCtx,
52
+ )) as ToolResultLike;
53
+ const textContent = getTextContent(result);
54
+ const fp = params.path ?? cwd;
55
+ const entryCount = textContent
56
+ ? textContent.trim().split("\n").filter(Boolean).length
57
+ : 0;
58
+
59
+ setResultDetails(result, {
60
+ _type: "lsResult",
61
+ text: textContent ?? "",
62
+ path: fp,
63
+ entryCount,
64
+ });
65
+
66
+ return result;
67
+ },
68
+
69
+ renderCall(args: LsParams, theme: ThemeLike, renderCtx: RenderContextLike) {
70
+ const fp = args.path ?? ".";
71
+ const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
72
+ text.setText(
73
+ fillToolBackground(
74
+ `${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`,
75
+ ),
76
+ );
77
+ return text;
78
+ },
79
+
80
+ renderResult(
81
+ result: ToolResultLike,
82
+ _opt: unknown,
83
+ theme: ThemeLike,
84
+ renderCtx: RenderContextLike,
85
+ ) {
86
+ const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
87
+
88
+ if (renderCtx.isError) {
89
+ text.setText(renderToolError(getTextContent(result) || "Error", theme));
90
+ return text;
91
+ }
92
+
93
+ const d = result.details as Record<string, unknown> | undefined;
94
+ if (d?._type === "lsResult" && d.text) {
95
+ const tree = renderTree(d.text as string, d.path as string);
96
+ const info = `${FG_DIM}${d.entryCount} entries${RST}`;
97
+ text.setText(fillToolBackground(` ${info}\n${tree}`));
98
+ return text;
99
+ }
100
+
101
+ const fallback = result.content?.[0];
102
+ const fallbackText =
103
+ fallback && isTextContent(fallback) ? fallback.text : "listed";
104
+ text.setText(
105
+ fillToolBackground(
106
+ ` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
107
+ ),
108
+ );
109
+ return text;
110
+ },
111
+ });
112
+ }