@xynogen/pix-find 0.1.20 → 0.1.24

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 CHANGED
@@ -20,7 +20,7 @@ After a configurable delay (default 10 seconds), completed output collapses to a
20
20
  }
21
21
  ```
22
22
 
23
- Set `collapse.tools.find: false` to disable for this tool only. See `@xynogen/pix-data/collapse` for the full API.
23
+ Set `collapse.tools.find: false` to disable for this tool only. See `@xynogen/pix-runtime/collapse` for the full API.
24
24
 
25
25
  ## Install
26
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-find",
3
- "version": "0.1.20",
3
+ "version": "0.1.24",
4
4
  "description": "Pi tool — glob file search",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,9 +34,9 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@xynogen/pix-data": "^0.4.0",
38
- "@xynogen/pix-pretty": "^1.7.22",
39
- "@xynogen/pix-runtime": "^0.4.0"
37
+ "@xynogen/pix-data": "^0.4.3",
38
+ "@xynogen/pix-pretty": "^1.11.2",
39
+ "@xynogen/pix-runtime": "^0.5.3"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@earendil-works/pi-coding-agent": "*"
package/src/find.test.ts CHANGED
@@ -110,9 +110,75 @@ describe("registerFindTool", () => {
110
110
 
111
111
  expect(result?.getText()).toContain("src/one.ts");
112
112
  expect(result?.getText()).toContain("src/two.ts");
113
+ // No floating count header in the framed view — the collapsed row carries it.
113
114
  expect(result?.getText()).not.toContain("2 files");
114
115
  });
115
116
 
117
+ it("frames single-file output like multi-file (no inline row)", () => {
118
+ const registered: { renderResult?: (...args: unknown[]) => MockTextComponent } = {};
119
+ const mockPi: PiPrettyApi = {
120
+ registerTool(tool: unknown) {
121
+ Object.assign(registered, tool);
122
+ },
123
+ registerCommand() {},
124
+ on() {},
125
+ };
126
+ registerFindTool(
127
+ mockPi,
128
+ () => ({ execute: async () => ({ content: [], details: undefined }) }),
129
+ {
130
+ cwd: process.cwd(),
131
+ sp: (p: string) => p,
132
+ TextComponent: MockTextComponent as unknown as TextComponentCtor,
133
+ fffState: {
134
+ module: null,
135
+ finder: null,
136
+ partialIndex: false,
137
+ dbDir: null,
138
+ } satisfies FffState,
139
+ cursorStore: { store: () => "", get: () => undefined } as unknown as CursorStore,
140
+ },
141
+ );
142
+ const theme: ThemeLike = { fg: (_k: string, v: string) => v, bold: (v: string) => v };
143
+ const strip = (s: string) => s.replace(/\u001b\[[0-9;]*m/g, "");
144
+ const result = {
145
+ content: [{ type: "text", text: "src/a.ts" }],
146
+ details: { _type: "findResult", text: "src/a.ts", pattern: "*.ts", matchCount: 1 } as never,
147
+ };
148
+ const out =
149
+ registered
150
+ .renderResult?.(result, { isPartial: false }, theme, {
151
+ expanded: false,
152
+ isError: false,
153
+ invalidate: () => {},
154
+ state: {},
155
+ } as unknown as RenderContextLike)
156
+ ?.getText() ?? "";
157
+ // Single result is framed just like multi — one shape, no inline row.
158
+ expect(out).toContain("─");
159
+ expect(out).toContain("src/a.ts");
160
+ expect(strip(out)).not.toContain("file");
161
+ const multi = {
162
+ content: [{ type: "text", text: "a.ts\nb.ts\nc.ts" }],
163
+ details: {
164
+ _type: "findResult",
165
+ text: "a.ts\nb.ts\nc.ts",
166
+ pattern: "*.ts",
167
+ matchCount: 3,
168
+ } as never,
169
+ };
170
+ const multiOut =
171
+ registered
172
+ .renderResult?.(multi, { isPartial: false }, theme, {
173
+ expanded: false,
174
+ isError: false,
175
+ invalidate: () => {},
176
+ state: {},
177
+ } as unknown as RenderContextLike)
178
+ ?.getText() ?? "";
179
+ expect(multiOut).toContain("─");
180
+ });
181
+
116
182
  it("collapses structured errors and restores the exact diagnostic on expansion", () => {
117
183
  const registered: { renderResult?: (...args: unknown[]) => MockTextComponent } = {};
118
184
  const mockPi: PiPrettyApi = {
@@ -159,7 +225,7 @@ describe("registerFindTool", () => {
159
225
  ?.getText() ?? "";
160
226
 
161
227
  expect(render({ timer: 1 })).toContain(diagnostic);
162
- expect(render({ collapsed: true })).toContain("✗ find [ in src · failed");
228
+ expect(render({ collapsed: true })).toContain("✗ find [ in src · failed");
163
229
  expect(render({ collapsed: true }, true)).toContain(diagnostic);
164
230
  });
165
231
  });
package/src/find.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  getTextContent,
21
21
  hideCollapsedToolCall,
22
22
  makeTextResult,
23
+ pluralize,
23
24
  renderCollapsedToolRow,
24
25
  renderDimPreview,
25
26
  renderToolError,
@@ -93,19 +94,35 @@ export function registerFindTool(
93
94
  }
94
95
 
95
96
  // SDK fallback
96
- const result = await origFind.execute(tid, effectiveParams, sig, upd as never, toolCtx);
97
- const textContent = getTextContent(result);
98
- const matchCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
99
-
100
- setResultDetails<FindResultDetails>(result, {
101
- _type: "findResult",
102
- text: textContent,
103
- pattern: params.pattern,
104
- path: params.path,
105
- matchCount,
106
- });
107
-
108
- return result;
97
+ try {
98
+ const result = await origFind.execute(tid, effectiveParams, sig, upd as never, toolCtx);
99
+ const textContent = getTextContent(result);
100
+ const matchCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
101
+
102
+ setResultDetails<FindResultDetails>(result, {
103
+ _type: "findResult",
104
+ text: textContent,
105
+ pattern: params.pattern,
106
+ path: params.path,
107
+ matchCount,
108
+ });
109
+
110
+ return result;
111
+ } catch (error) {
112
+ const text = error instanceof Error ? error.message : String(error);
113
+ if (sig?.aborted || /aborted/i.test(text)) throw error;
114
+ return {
115
+ content: [{ type: "text" as const, text }],
116
+ details: {
117
+ _type: "findResult" as const,
118
+ text,
119
+ pattern: params.pattern,
120
+ path: params.path,
121
+ matchCount: 0,
122
+ },
123
+ isError: true,
124
+ };
125
+ }
109
126
  },
110
127
 
111
128
  renderCall(args: FindParams, theme: ThemeLike, renderCtx: RenderContextLike) {
@@ -148,7 +165,9 @@ export function registerFindTool(
148
165
  const cs = renderCtx.state as CollapseState;
149
166
  if (!isPartial && tickCollapse("find", cs, renderCtx.invalidate, renderCtx.expanded)) {
150
167
  const summary =
151
- d?._type === "findResult" && d.matchCount != null ? `${d.matchCount} files` : "found";
168
+ d?._type === "findResult" && d.matchCount != null
169
+ ? pluralize(d.matchCount, "file")
170
+ : "found";
152
171
  const target = d?._type === "findResult" ? d.pattern : "";
153
172
  const scope = d?._type === "findResult" && d.path ? ` in ${sp(d.path)}` : "";
154
173
  text.setText(
@@ -169,7 +188,13 @@ export function registerFindTool(
169
188
  }
170
189
 
171
190
  const output = getTextContent(result) || "found";
172
- text.setText(renderDimPreview(output, theme));
191
+ // One framed shape; rules follow status color. Count lives in the collapsed row.
192
+ text.setText(
193
+ renderDimPreview(output, theme, {
194
+ frame: true,
195
+ paint: (s: string) => theme.fg(renderCtx.isError ? "error" : "success", s),
196
+ }),
197
+ );
173
198
  return text;
174
199
  },
175
200
  });