pi-readseek 0.3.19 → 0.3.20

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.
@@ -55,8 +55,12 @@ export interface ReadseekEditResult {
55
55
  semanticSummary?: SemanticSummary;
56
56
  }
57
57
 
58
- export function buildReadseekLine(line: number, raw: string): ReadseekLine {
59
- const hash = computeLineHash(line, raw);
58
+ /**
59
+ * Build a {@link ReadseekLine} from an already-known hash. Use this when the
60
+ * hash is supplied by readseek (search, refs) rather than computed from `raw`;
61
+ * {@link buildReadseekLine} delegates here after hashing.
62
+ */
63
+ export function buildReadseekLineWithHash(line: number, hash: string, raw: string): ReadseekLine {
60
64
  return {
61
65
  line,
62
66
  hash,
@@ -66,6 +70,10 @@ export function buildReadseekLine(line: number, raw: string): ReadseekLine {
66
70
  };
67
71
  }
68
72
 
73
+ export function buildReadseekLine(line: number, raw: string): ReadseekLine {
74
+ return buildReadseekLineWithHash(line, computeLineHash(line, raw), raw);
75
+ }
76
+
69
77
  export function buildReadseekLines(startLine: number, rawLines: string[]): ReadseekLine[] {
70
78
  return rawLines.map((raw, index) => buildReadseekLine(startLine + index, raw));
71
79
  }
package/src/refs.ts CHANGED
@@ -4,13 +4,12 @@ import { Type } from "@sinclair/typebox";
4
4
  import path from "node:path";
5
5
  import { stat as fsStat } from "node:fs/promises";
6
6
  import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
7
- import { escapeControlCharsForDisplay } from "./hashline.js";
8
- import { buildToolErrorResult } from "./readseek-value.js";
7
+ import { buildReadseekLineWithHash, buildToolErrorResult } from "./readseek-value.js";
9
8
  import { resolveToCwd } from "./path-utils.js";
10
9
  import { isReadseekAvailable, readseekRefs, type ReadseekReference } from "./readseek-client.js";
11
10
  import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
12
11
 
13
- import { clampLineToWidth, clampLinesToWidth, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
12
+ import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
14
13
 
15
14
  type RefsParams = {
16
15
  name: string;
@@ -39,11 +38,7 @@ interface RefsToolOptions {
39
38
 
40
39
  function refsLine(reference: ReadseekReference): RefsOutputLine {
41
40
  return {
42
- line: reference.line,
43
- hash: reference.line_hash,
44
- anchor: `${reference.line}:${reference.line_hash}`,
45
- raw: reference.text,
46
- display: escapeControlCharsForDisplay(reference.text),
41
+ ...buildReadseekLineWithHash(reference.line, reference.line_hash, reference.text),
47
42
  enclosingSymbol: reference.enclosingSymbol,
48
43
  };
49
44
  }
@@ -165,35 +160,12 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
165
160
  return new Text(clampLineToWidth(text, context.width), 0, 0);
166
161
  },
167
162
  renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
168
- const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
169
-
170
- if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending refs")], width).join("\n"), 0, 0);
171
-
172
- const content = result.content?.[0];
173
- const textContent = content?.type === "text" ? content.text : "";
174
- if (isError || result.isError) {
175
- const firstLine = textContent.split("\n")[0] || "Error";
176
- const body = expanded && textContent ? textContent : firstLine;
177
- return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
178
- }
179
- const readseekValue = (result.details as any)?.readseekValue as
180
- | { tool: "refs"; files: Array<{ path: string; lines: any[] }> }
181
- | undefined;
182
- const files = readseekValue?.files ?? [];
183
- if (files.length === 0) return new Text(summaryLine("no references"), 0, 0);
184
- const fileCount = files.length;
185
- const totalRefs = files.reduce((sum: number, f: any) => sum + f.lines.length, 0);
186
- const refWord = totalRefs === 1 ? "reference" : "references";
187
- const fileWord = fileCount === 1 ? "file" : "files";
188
- let text = summaryLine(`${totalRefs} ${refWord} in ${fileCount} ${fileWord}`, { hidden: !expanded });
189
- if (expanded) {
190
- for (const file of files.slice(0, 20)) {
191
- const display = path.relative(cwd, file.path) || file.path;
192
- text += "\n" + theme.fg("dim", ` ${display} (${file.lines.length})`);
193
- }
194
- if (files.length > 20) text += "\n" + theme.fg("muted", ` … and ${files.length - 20} more files`);
195
- }
196
- return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
163
+ return renderAnchoredFilesResult(result, options, theme, rest, {
164
+ pendingLabel: "pending refs",
165
+ emptyLabel: "no references",
166
+ unitSingular: "reference",
167
+ unitPlural: "references",
168
+ });
197
169
  },
198
170
  } satisfies Parameters<ExtensionAPI["registerTool"]>[0] & { ptc: typeof toolConfig };
199
171
 
package/src/sg.ts CHANGED
@@ -4,13 +4,12 @@ import { Type } from "@sinclair/typebox";
4
4
  import path from "node:path";
5
5
  import { stat as fsStat } from "node:fs/promises";
6
6
  import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
7
- import { escapeControlCharsForDisplay } from "./hashline.js";
8
- import { buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
7
+ import { buildReadseekLineWithHash, buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
9
8
  import { resolveToCwd } from "./path-utils.js";
10
9
  import { isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
11
10
  import { buildSgOutput } from "./sg-output.js";
12
11
 
13
- import { clampLineToWidth, clampLinesToWidth, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
12
+ import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
14
13
 
15
14
  type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
16
15
 
@@ -58,13 +57,7 @@ interface SgToolOptions {
58
57
  }
59
58
 
60
59
  function readseekLineFromSearch(line: ReadseekHashline): ReadseekLine {
61
- return {
62
- line: line.line,
63
- hash: line.hash,
64
- anchor: `${line.line}:${line.hash}`,
65
- raw: line.text,
66
- display: escapeControlCharsForDisplay(line.text),
67
- };
60
+ return buildReadseekLineWithHash(line.line, line.hash, line.text);
68
61
  }
69
62
 
70
63
  function linesFromSearchResult(result: ReadseekSearchFileOutput, ranges: SgRange[]): ReadseekLine[] {
@@ -230,35 +223,12 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
230
223
  return new Text(clampLineToWidth(text, context.width), 0, 0);
231
224
  },
232
225
  renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
233
- const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
234
-
235
- if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending search")], width).join("\n"), 0, 0);
236
-
237
- const content = result.content?.[0];
238
- const textContent = content?.type === "text" ? content.text : "";
239
- if (isError || result.isError) {
240
- const firstLine = textContent.split("\n")[0] || "Error";
241
- const body = expanded && textContent ? textContent : firstLine;
242
- return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
243
- }
244
- const readseekValue = (result.details as any)?.readseekValue as
245
- | { tool: "search"; files: Array<{ path: string; lines: any[] }> }
246
- | undefined;
247
- const files = readseekValue?.files ?? [];
248
- if (files.length === 0) return new Text(summaryLine("no matches"), 0, 0);
249
- const fileCount = files.length;
250
- const totalMatches = files.reduce((sum: number, f: any) => sum + f.lines.length, 0);
251
- const matchWord = totalMatches === 1 ? "match" : "matches";
252
- const fileWord = fileCount === 1 ? "file" : "files";
253
- let text = summaryLine(`${totalMatches} ${matchWord} in ${fileCount} ${fileWord}`, { hidden: files.length > 0 && !expanded });
254
- if (expanded) {
255
- for (const file of files.slice(0, 20)) {
256
- const display = path.relative(cwd, file.path) || file.path;
257
- text += "\n" + theme.fg("dim", ` ${display} (${file.lines.length})`);
258
- }
259
- if (files.length > 20) text += "\n" + theme.fg("muted", ` … and ${files.length - 20} more files`);
260
- }
261
- return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
226
+ return renderAnchoredFilesResult(result, options, theme, rest, {
227
+ pendingLabel: "pending search",
228
+ emptyLabel: "no matches",
229
+ unitSingular: "match",
230
+ unitPlural: "matches",
231
+ });
262
232
  },
263
233
  } satisfies Parameters<ExtensionAPI["registerTool"]>[0] & { ptc: typeof toolConfig };
264
234
 
@@ -1,4 +1,5 @@
1
- import { getCapabilities, hyperlink, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
1
+ import { getCapabilities, hyperlink, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { relative } from "node:path";
2
3
  import { pathToFileURL } from "node:url";
3
4
  import { resolveToCwd } from "./path-utils.js";
4
5
 
@@ -154,3 +155,56 @@ export function wrapReadHashlinesForWidth(text: string, width: number | undefine
154
155
  }
155
156
  return output.join("\n");
156
157
  }
158
+
159
+ export interface AnchoredFilesLabels {
160
+ pendingLabel: string;
161
+ emptyLabel: string;
162
+ unitSingular: string;
163
+ unitPlural: string;
164
+ }
165
+
166
+ /**
167
+ * Render the result summary shared by the anchored-files search tools (search,
168
+ * refs): a pending line, an error first-line/expanded body, an empty-result
169
+ * line, or a `<count> <unit> in <n> files` summary with an expandable per-file
170
+ * list. The four call-site differences are supplied via {@link labels}.
171
+ */
172
+ export function renderAnchoredFilesResult(
173
+ result: any,
174
+ options: any,
175
+ theme: RendererTheme,
176
+ rest: any[],
177
+ labels: AnchoredFilesLabels,
178
+ ): Text {
179
+ const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
180
+
181
+ if (isPartial) return new Text(clampLinesToWidth([summaryLine(labels.pendingLabel)], width).join("\n"), 0, 0);
182
+
183
+ const content = result.content?.[0];
184
+ const textContent = content?.type === "text" ? content.text : "";
185
+ if (isError || result.isError) {
186
+ const firstLine = textContent.split("\n")[0] || "Error";
187
+ const body = expanded && textContent ? textContent : firstLine;
188
+ return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
189
+ }
190
+
191
+ const readseekValue = (result.details as any)?.readseekValue as
192
+ | { files: Array<{ path: string; lines: any[] }> }
193
+ | undefined;
194
+ const files = readseekValue?.files ?? [];
195
+ if (files.length === 0) return new Text(summaryLine(labels.emptyLabel), 0, 0);
196
+
197
+ const fileCount = files.length;
198
+ const total = files.reduce((sum: number, f: any) => sum + f.lines.length, 0);
199
+ const unitWord = total === 1 ? labels.unitSingular : labels.unitPlural;
200
+ const fileWord = fileCount === 1 ? "file" : "files";
201
+ let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, { hidden: !expanded });
202
+ if (expanded) {
203
+ for (const file of files.slice(0, 20)) {
204
+ const display = relative(cwd, file.path) || file.path;
205
+ text += "\n" + theme.fg("dim", ` ${display} (${file.lines.length})`);
206
+ }
207
+ if (files.length > 20) text += "\n" + theme.fg("muted", ` … and ${files.length - 20} more files`);
208
+ }
209
+ return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
210
+ }