pi-readseek 0.4.15 → 0.4.17
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 +2 -4
- package/src/edit.ts +5 -21
- package/src/grep.ts +6 -20
- package/src/read.ts +10 -20
- package/src/readseek-client.ts +20 -20
- package/src/readseek-params.ts +39 -0
- package/src/refs.ts +12 -17
- package/src/register-tool.ts +10 -0
- package/src/sg.ts +12 -18
- package/src/tui-diff-component.ts +23 -1
- package/src/tui-render-utils.ts +20 -0
- package/src/write.ts +10 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.17",
|
|
4
4
|
"description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -39,10 +39,8 @@
|
|
|
39
39
|
"node": ">=20.0.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@jarkkojs/readseek": "^0.4.
|
|
42
|
+
"@jarkkojs/readseek": "^0.4.21",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
|
-
"ignore": "^7.0.5",
|
|
45
|
-
"picomatch": "^4.0.4",
|
|
46
44
|
"xxhash-wasm": "^1.1.0"
|
|
47
45
|
},
|
|
48
46
|
"peerDependencies": {
|
package/src/edit.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { replaceSymbol } from "./replace-symbol.js";
|
|
|
24
24
|
import { buildEditPreviewKey, buildPendingEditPreviewData, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
|
|
25
25
|
import { buildDiffData, type DiffBlockRange } from "./diff-data.js";
|
|
26
26
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
27
|
-
import {
|
|
27
|
+
import { upsertDiffComponent, upsertTextComponent } from "./tui-diff-component.js";
|
|
28
28
|
import type { FreshAnchorsPredicate } from "./tool-types.js";
|
|
29
29
|
import { registerReadSeekTool } from "./register-tool.js";
|
|
30
30
|
|
|
@@ -643,11 +643,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
643
643
|
// expandable diff). Keeping the "↳ pending edit" sub-line and its
|
|
644
644
|
// preview alongside the final result is just duplicate noise.
|
|
645
645
|
if (context.executionStarted) {
|
|
646
|
-
|
|
647
|
-
? context.lastComponent
|
|
648
|
-
: new Text("", 0, 0);
|
|
649
|
-
textComponent.setText(text);
|
|
650
|
-
return textComponent;
|
|
646
|
+
return upsertTextComponent(context.lastComponent, text);
|
|
651
647
|
}
|
|
652
648
|
const previewKey = buildEditPreviewKey(args ?? {});
|
|
653
649
|
const preview = resolvePendingDiffPreview(
|
|
@@ -659,17 +655,9 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
659
655
|
const expanded = !!context.expanded || resolveEditDiffDisplay() === "expanded";
|
|
660
656
|
const preview2 = pendingPreviewLines(text, preview, expanded);
|
|
661
657
|
if (preview2.diffData) {
|
|
662
|
-
|
|
663
|
-
? context.lastComponent
|
|
664
|
-
: new DiffPreviewComponent({ prefixLines: preview2.lines, diffData: preview2.diffData, theme, expanded: true });
|
|
665
|
-
diffComponent.update({ prefixLines: preview2.lines, diffData: preview2.diffData, theme, expanded: true });
|
|
666
|
-
return diffComponent;
|
|
658
|
+
return upsertDiffComponent(context.lastComponent, { prefixLines: preview2.lines, diffData: preview2.diffData, theme, expanded: true });
|
|
667
659
|
}
|
|
668
|
-
|
|
669
|
-
? context.lastComponent
|
|
670
|
-
: new Text("", 0, 0);
|
|
671
|
-
textComponent.setText(clampLinesToWidth(preview2.lines, context.width).join("\n"));
|
|
672
|
-
return textComponent;
|
|
660
|
+
return upsertTextComponent(context.lastComponent, clampLinesToWidth(preview2.lines, context.width).join("\n"));
|
|
673
661
|
},
|
|
674
662
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
675
663
|
const { isPartial, isError, expanded: baseExpanded, width, context } = resolveRenderResultContext(options, rest);
|
|
@@ -719,11 +707,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
719
707
|
if (info.warningsBadge) badges.push(info.warningsBadge);
|
|
720
708
|
text = summaryLine(badges.join(" • "), { hidden: !!diffData && !expanded });
|
|
721
709
|
if (expanded && diffData) {
|
|
722
|
-
|
|
723
|
-
? context.lastComponent
|
|
724
|
-
: new DiffPreviewComponent({ prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
725
|
-
diffComponent.update({ prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
726
|
-
return diffComponent;
|
|
710
|
+
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
727
711
|
}
|
|
728
712
|
}
|
|
729
713
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
package/src/grep.ts
CHANGED
|
@@ -21,7 +21,8 @@ import { formatGrepCallText, formatGrepResultText } from "./grep-render-helpers.
|
|
|
21
21
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
22
22
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
23
23
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
24
|
-
import { registerReadSeekTool } from "./register-tool.js";
|
|
24
|
+
import { optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
25
|
+
import { searchPathParam } from "./readseek-params.js";
|
|
25
26
|
|
|
26
27
|
const GREP_PROMPT_METADATA = defineToolPromptMetadata({
|
|
27
28
|
promptUrl: new URL("../prompts/grep.md", import.meta.url),
|
|
@@ -30,34 +31,19 @@ const GREP_PROMPT_METADATA = defineToolPromptMetadata({
|
|
|
30
31
|
|
|
31
32
|
const grepSchema = Type.Object({
|
|
32
33
|
pattern: Type.String({ description: "Pattern to search" }),
|
|
33
|
-
path:
|
|
34
|
+
path: searchPathParam(),
|
|
34
35
|
glob: Type.Optional(Type.String({ description: "Glob filter" })),
|
|
35
36
|
ignoreCase: Type.Optional(Type.Boolean({ description: "Ignore case" })),
|
|
36
37
|
literal: Type.Optional(Type.Boolean({ description: "Treat pattern literally" })),
|
|
37
|
-
context:
|
|
38
|
-
|
|
39
|
-
Type.Number({ description: "Context lines" }),
|
|
40
|
-
Type.String({ description: "Context lines" }),
|
|
41
|
-
]),
|
|
42
|
-
),
|
|
43
|
-
limit: Type.Optional(
|
|
44
|
-
Type.Union([
|
|
45
|
-
Type.Number({ description: "Max matches" }),
|
|
46
|
-
Type.String({ description: "Max matches" }),
|
|
47
|
-
]),
|
|
48
|
-
),
|
|
38
|
+
context: optionalIntOrString("Context lines"),
|
|
39
|
+
limit: optionalIntOrString("Max matches"),
|
|
49
40
|
summary: Type.Optional(Type.Boolean({ description: "Return per-file counts" })),
|
|
50
41
|
scope: Type.Optional(
|
|
51
42
|
Type.Literal("symbol", {
|
|
52
43
|
description: "Scope matches to symbols",
|
|
53
44
|
}),
|
|
54
45
|
),
|
|
55
|
-
scopeContext:
|
|
56
|
-
Type.Union([
|
|
57
|
-
Type.Number({ description: "Symbol context lines" }),
|
|
58
|
-
Type.String({ description: "Symbol context lines" }),
|
|
59
|
-
]),
|
|
60
|
-
),
|
|
46
|
+
scopeContext: optionalIntOrString("Symbol context lines"),
|
|
61
47
|
});
|
|
62
48
|
|
|
63
49
|
interface GrepParams {
|
package/src/read.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { readseekRead, readseekDetect, type ReadSeekDetection } from "./readseek
|
|
|
29
29
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
30
30
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
31
31
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
32
|
-
import { registerReadSeekTool } from "./register-tool.js";
|
|
32
|
+
import { optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
33
33
|
|
|
34
34
|
const READ_PROMPT_METADATA = defineToolPromptMetadata({
|
|
35
35
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
@@ -145,7 +145,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
145
145
|
} catch (err: any) {
|
|
146
146
|
const code = err?.code;
|
|
147
147
|
if (code === "EISDIR") {
|
|
148
|
-
const message = `Path is a directory: ${rawPath}
|
|
148
|
+
const message = `Path is a directory: ${rawPath}`;
|
|
149
149
|
return buildToolErrorResult("read", "path-is-directory", message, { path: rawParams.path, hint: `Use ls(${JSON.stringify(rawPath)}) to inspect directories.` });
|
|
150
150
|
}
|
|
151
151
|
if (code === "EACCES" || code === "EPERM") {
|
|
@@ -162,23 +162,23 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
162
162
|
|
|
163
163
|
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
164
164
|
if (hasBinaryContent) {
|
|
165
|
-
// Images are always binary; classify and
|
|
165
|
+
// Images are always binary; classify and transcribe in a single readseek detect call.
|
|
166
166
|
let detection: ReadSeekDetection | undefined;
|
|
167
167
|
try {
|
|
168
|
-
detection = await readseekDetect(absolutePath, {
|
|
168
|
+
detection = await readseekDetect(absolutePath, { transcribe: true, signal });
|
|
169
169
|
} catch {
|
|
170
170
|
// detect unavailable — fall through to binary-as-text handling below
|
|
171
171
|
}
|
|
172
172
|
if (detection?.type === "image") {
|
|
173
173
|
const builtinRead = createReadTool(cwd);
|
|
174
174
|
const builtinResult = await builtinRead.execute(toolCallId, p, signal, onUpdate);
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
175
|
+
const transcript = detection.transcribe?.text?.trim();
|
|
176
|
+
if (transcript) {
|
|
177
177
|
return succeed({
|
|
178
178
|
...builtinResult,
|
|
179
179
|
content: [
|
|
180
180
|
...(builtinResult.content ?? []),
|
|
181
|
-
{ type: "text" as const, text: `
|
|
181
|
+
{ type: "text" as const, text: `Transcribed text from image:\n${transcript}` },
|
|
182
182
|
],
|
|
183
183
|
});
|
|
184
184
|
}
|
|
@@ -194,7 +194,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
194
194
|
let startLine = p.offset !== undefined ? p.offset : 1;
|
|
195
195
|
let endIdx = p.limit !== undefined ? Math.min(startLine - 1 + p.limit, total) : total;
|
|
196
196
|
if (p.offset !== undefined && startLine > total) {
|
|
197
|
-
const message = `
|
|
197
|
+
const message = `offset ${p.offset} is past end of file (${total} lines)`;
|
|
198
198
|
return buildToolErrorResult("read", "offset-past-end", message, { path: rawParams.path });
|
|
199
199
|
}
|
|
200
200
|
let symbolMatch: SymbolMatch | undefined;
|
|
@@ -451,18 +451,8 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
451
451
|
promptGuidelines: READ_PROMPT_METADATA.promptGuidelines,
|
|
452
452
|
parameters: Type.Object({
|
|
453
453
|
path: Type.String({ description: "File path" }),
|
|
454
|
-
offset:
|
|
455
|
-
|
|
456
|
-
Type.Number({ description: "Start line (1-indexed)" }),
|
|
457
|
-
Type.String({ description: "Start line (1-indexed)" }),
|
|
458
|
-
]),
|
|
459
|
-
),
|
|
460
|
-
limit: Type.Optional(
|
|
461
|
-
Type.Union([
|
|
462
|
-
Type.Number({ description: "Max lines" }),
|
|
463
|
-
Type.String({ description: "Max lines" }),
|
|
464
|
-
]),
|
|
465
|
-
),
|
|
454
|
+
offset: optionalIntOrString("Start line (1-indexed)"),
|
|
455
|
+
limit: optionalIntOrString("Max lines"),
|
|
466
456
|
symbol: Type.Optional(Type.String({ description: "Symbol name to read" })),
|
|
467
457
|
map: Type.Optional(Type.Boolean({ description: "Append structural map" })),
|
|
468
458
|
bundle: Type.Optional(
|
package/src/readseek-client.ts
CHANGED
|
@@ -108,14 +108,14 @@ export interface ReadSeekCheckOutput {
|
|
|
108
108
|
diagnostics: ReadSeekDiagnostic[];
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export interface
|
|
111
|
+
export interface ReadSeekTranscriptRegion {
|
|
112
112
|
text: string;
|
|
113
|
-
|
|
113
|
+
quad: [number, number, number, number, number, number, number, number];
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export interface
|
|
116
|
+
export interface ReadSeekTranscript {
|
|
117
117
|
text: string;
|
|
118
|
-
|
|
118
|
+
regions: ReadSeekTranscriptRegion[];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export type ReadSeekDetection =
|
|
@@ -136,7 +136,7 @@ export type ReadSeekDetection =
|
|
|
136
136
|
width: number;
|
|
137
137
|
height: number;
|
|
138
138
|
animated: boolean;
|
|
139
|
-
|
|
139
|
+
transcribe?: ReadSeekTranscript;
|
|
140
140
|
}
|
|
141
141
|
| {
|
|
142
142
|
type: "binary";
|
|
@@ -641,21 +641,21 @@ export async function readseekCheck(
|
|
|
641
641
|
);
|
|
642
642
|
}
|
|
643
643
|
|
|
644
|
-
function
|
|
644
|
+
function parseTranscript(value: unknown): ReadSeekTranscript | undefined {
|
|
645
645
|
if (value === undefined || value === null) return undefined;
|
|
646
|
-
if (typeof value !== "object") throw new Error("invalid readseek detect
|
|
647
|
-
const
|
|
648
|
-
if (!Array.isArray(
|
|
646
|
+
if (typeof value !== "object") throw new Error("invalid readseek detect transcribe");
|
|
647
|
+
const transcribe = value as Record<string, unknown>;
|
|
648
|
+
if (!Array.isArray(transcribe.regions)) throw new Error("invalid readseek detect transcribe.regions");
|
|
649
649
|
return {
|
|
650
|
-
text: requireString(
|
|
651
|
-
|
|
652
|
-
if (!
|
|
653
|
-
const item =
|
|
654
|
-
const
|
|
655
|
-
if (!Array.isArray(
|
|
650
|
+
text: requireString(transcribe.text, "transcribe.text"),
|
|
651
|
+
regions: transcribe.regions.map((region) => {
|
|
652
|
+
if (!region || typeof region !== "object") throw new Error("invalid readseek detect transcribe region");
|
|
653
|
+
const item = region as Record<string, unknown>;
|
|
654
|
+
const quad = item.quad;
|
|
655
|
+
if (!Array.isArray(quad) || quad.length !== 8) throw new Error("invalid readseek detect transcribe quad");
|
|
656
656
|
return {
|
|
657
|
-
text: requireString(item.text, "
|
|
658
|
-
|
|
657
|
+
text: requireString(item.text, "transcribe.region.text"),
|
|
658
|
+
quad: quad.map((n, i) => requireNumber(n, `transcribe.region.quad[${i}]`)) as ReadSeekTranscriptRegion["quad"],
|
|
659
659
|
};
|
|
660
660
|
}),
|
|
661
661
|
};
|
|
@@ -687,7 +687,7 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
687
687
|
width: requireNumber(output.width, "width"),
|
|
688
688
|
height: requireNumber(output.height, "height"),
|
|
689
689
|
animated: requireBoolean(output.animated, "animated"),
|
|
690
|
-
|
|
690
|
+
transcribe: parseTranscript(output.transcribe),
|
|
691
691
|
};
|
|
692
692
|
case "binary":
|
|
693
693
|
case "text":
|
|
@@ -699,10 +699,10 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
699
699
|
|
|
700
700
|
export async function readseekDetect(
|
|
701
701
|
filePath: string,
|
|
702
|
-
options: {
|
|
702
|
+
options: { transcribe?: boolean; signal?: AbortSignal } = {},
|
|
703
703
|
): Promise<ReadSeekDetection> {
|
|
704
704
|
const args = ["detect"];
|
|
705
|
-
if (options.
|
|
705
|
+
if (options.transcribe) args.push("--transcribe");
|
|
706
706
|
args.push(filePath);
|
|
707
707
|
return parseDetectOutput(await runReadSeek(args, { signal: options.signal }));
|
|
708
708
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
import { buildToolErrorResult, type ToolErrorResult } from "./readseek-value.js";
|
|
4
|
+
|
|
5
|
+
/** Optional search path shared by the readseek search tools. */
|
|
6
|
+
export function searchPathParam() {
|
|
7
|
+
return Type.Optional(Type.String({ description: "Search path" }));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Optional language hint shared by the readseek search tools. */
|
|
11
|
+
export function langParam() {
|
|
12
|
+
return Type.Optional(Type.String({ description: "Language hint" }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TypeBox fragment for the Git-scope parameters shared by every readseek
|
|
17
|
+
* search tool. Returns fresh schema instances so each tool spreads its own.
|
|
18
|
+
*/
|
|
19
|
+
export function readseekGitSearchParams() {
|
|
20
|
+
return {
|
|
21
|
+
cached: Type.Optional(Type.Boolean({ description: "In a Git repository, search tracked/indexed files" })),
|
|
22
|
+
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
23
|
+
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Enforce that `ignored` is only meaningful alongside `others`. Returns an
|
|
29
|
+
* error envelope tagged with {@link tool} when the rule is violated, else null.
|
|
30
|
+
*/
|
|
31
|
+
export function validateIgnoredRequiresOthers(
|
|
32
|
+
tool: string,
|
|
33
|
+
params: { others?: boolean; ignored?: boolean },
|
|
34
|
+
): ToolErrorResult | null {
|
|
35
|
+
if (params.ignored && !params.others) {
|
|
36
|
+
return buildToolErrorResult(tool, "invalid-parameter", `Error: ${tool} parameter 'ignored' requires 'others'`);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
package/src/refs.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
2
|
import { Type } from "@sinclair/typebox";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
@@ -9,9 +8,10 @@ import { resolveToCwd } from "./path-utils.js";
|
|
|
9
8
|
import { classifyReadSeekFailure, readseekRefs, type ReadSeekReference } from "./readseek-client.js";
|
|
10
9
|
import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
|
|
11
10
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
11
|
+
import { langParam, readseekGitSearchParams, searchPathParam, validateIgnoredRequiresOthers } from "./readseek-params.js";
|
|
12
12
|
import { registerReadSeekTool } from "./register-tool.js";
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { renderAnchoredFilesResult, renderReadSeekSearchCall } from "./tui-render-utils.js";
|
|
15
15
|
|
|
16
16
|
type RefsParams = {
|
|
17
17
|
name: string;
|
|
@@ -83,9 +83,8 @@ function isReadSeekCursorValidationFailure(message: string): boolean {
|
|
|
83
83
|
export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
84
84
|
const { params, signal, cwd, onFileAnchored } = opts;
|
|
85
85
|
const p = params as RefsParams;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
86
|
+
const ignoredError = validateIgnoredRequiresOthers("refs", p);
|
|
87
|
+
if (ignoredError) return ignoredError;
|
|
89
88
|
if (p.scope && p.line === undefined) {
|
|
90
89
|
return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'scope' requires 'line'");
|
|
91
90
|
}
|
|
@@ -136,26 +135,22 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
136
135
|
promptGuidelines: REFS_PROMPT_METADATA.promptGuidelines,
|
|
137
136
|
parameters: Type.Object({
|
|
138
137
|
name: Type.String({ description: "Identifier to find references for" }),
|
|
139
|
-
path:
|
|
140
|
-
lang:
|
|
138
|
+
path: searchPathParam(),
|
|
139
|
+
lang: langParam(),
|
|
141
140
|
scope: Type.Optional(Type.Boolean({ description: "Restrict to the binding under line/column (single file)" })),
|
|
142
141
|
line: Type.Optional(Type.Number({ description: "One-based cursor line, used with scope" })),
|
|
143
142
|
column: Type.Optional(Type.Number({ description: "One-based cursor byte column, used with scope" })),
|
|
144
|
-
|
|
145
|
-
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
146
|
-
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
143
|
+
...readseekGitSearchParams(),
|
|
147
144
|
}),
|
|
148
145
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
149
146
|
return executeRefs({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
150
147
|
},
|
|
151
148
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (flags.length > 0) text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
158
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
149
|
+
return renderReadSeekSearchCall(args, theme, rest, {
|
|
150
|
+
label: "refs",
|
|
151
|
+
accent: args.name,
|
|
152
|
+
flags: [args.scope && "scope", args.cached && "cached", args.others && "others", args.ignored && "ignored"],
|
|
153
|
+
});
|
|
159
154
|
},
|
|
160
155
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
161
156
|
return renderAnchoredFilesResult(result, options, theme, rest, {
|
package/src/register-tool.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optional tool parameter accepting an integer or its string form, since models
|
|
6
|
+
* frequently pass integers as quoted strings. Both union halves share
|
|
7
|
+
* {@link description}.
|
|
8
|
+
*/
|
|
9
|
+
export function optionalIntOrString(description: string) {
|
|
10
|
+
return Type.Optional(Type.Union([Type.Number({ description }), Type.String({ description })]));
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
export type ReadSeekToolPolicy = "read-only" | "mutating";
|
|
4
14
|
|
package/src/sg.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
5
4
|
import { Type } from "@sinclair/typebox";
|
|
6
5
|
|
|
7
6
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
@@ -11,9 +10,10 @@ import { statSearchPathOrError } from "./stat-search-path.js";
|
|
|
11
10
|
import { classifyReadSeekFailure, isReadSeekAvailable, readseekSearch, type ReadSeekHashline, type ReadSeekSearchFileOutput } from "./readseek-client.js";
|
|
12
11
|
import { buildSgOutput } from "./sg-output.js";
|
|
13
12
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
13
|
+
import { langParam, readseekGitSearchParams, searchPathParam, validateIgnoredRequiresOthers } from "./readseek-params.js";
|
|
14
14
|
import { registerReadSeekTool } from "./register-tool.js";
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { renderAnchoredFilesResult, renderReadSeekSearchCall } from "./tui-render-utils.js";
|
|
17
17
|
|
|
18
18
|
type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
|
|
19
19
|
|
|
@@ -107,10 +107,8 @@ function readseekLanguageForPath(language: string | undefined, searchPath: strin
|
|
|
107
107
|
export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
108
108
|
const { params, signal, cwd, onFileAnchored } = opts;
|
|
109
109
|
const p = params as SgParams;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return buildToolErrorResult("search", "invalid-parameter", message);
|
|
113
|
-
}
|
|
110
|
+
const ignoredError = validateIgnoredRequiresOthers("search", p);
|
|
111
|
+
if (ignoredError) return ignoredError;
|
|
114
112
|
const searchPath = resolveToCwd(p.path ?? ".", cwd);
|
|
115
113
|
|
|
116
114
|
const statResult = await statSearchPathOrError("search", p.path, searchPath);
|
|
@@ -201,23 +199,19 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
201
199
|
promptGuidelines: SG_PROMPT_METADATA.promptGuidelines,
|
|
202
200
|
parameters: Type.Object({
|
|
203
201
|
pattern: Type.String({ description: "AST pattern" }),
|
|
204
|
-
lang:
|
|
205
|
-
path:
|
|
206
|
-
|
|
207
|
-
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
208
|
-
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
202
|
+
lang: langParam(),
|
|
203
|
+
path: searchPathParam(),
|
|
204
|
+
...readseekGitSearchParams(),
|
|
209
205
|
}),
|
|
210
206
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
211
207
|
return executeSg({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
212
208
|
},
|
|
213
209
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (flags.length > 0) text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
220
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
210
|
+
return renderReadSeekSearchCall(args, theme, rest, {
|
|
211
|
+
label: "search",
|
|
212
|
+
accent: `/${args.pattern}/`,
|
|
213
|
+
flags: [args.cached && "cached", args.others && "others", args.ignored && "ignored"],
|
|
214
|
+
});
|
|
221
215
|
},
|
|
222
216
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
223
217
|
return renderAnchoredFilesResult(result, options, theme, rest, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { DiffData } from "./diff-data.js";
|
|
4
4
|
import { renderTuiDiff } from "./tui-diff-renderer.js";
|
|
5
5
|
import { clampLineToWidth, normalizeWidth, type RendererTheme } from "./tui-render-utils.js";
|
|
@@ -84,3 +84,25 @@ export class DiffPreviewComponent implements Component {
|
|
|
84
84
|
return clamped;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reuse the prior render tick's {@link DiffPreviewComponent} when present, else
|
|
90
|
+
* construct one, then apply {@link options}. Keeps a streaming diff preview
|
|
91
|
+
* stable across ticks instead of remounting a fresh component each frame.
|
|
92
|
+
*/
|
|
93
|
+
export function upsertDiffComponent(lastComponent: unknown, options: DiffPreviewComponentOptions): DiffPreviewComponent {
|
|
94
|
+
const component = lastComponent instanceof DiffPreviewComponent ? lastComponent : new DiffPreviewComponent(options);
|
|
95
|
+
component.update(options);
|
|
96
|
+
return component;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reuse the prior render tick's text component when it is not a
|
|
101
|
+
* {@link DiffPreviewComponent}, else construct a fresh {@link Text}, then set
|
|
102
|
+
* its content to {@link text}.
|
|
103
|
+
*/
|
|
104
|
+
export function upsertTextComponent(lastComponent: any, text: string): Text {
|
|
105
|
+
const component = lastComponent && !(lastComponent instanceof DiffPreviewComponent) ? lastComponent : new Text("", 0, 0);
|
|
106
|
+
component.setText(text);
|
|
107
|
+
return component;
|
|
108
|
+
}
|
package/src/tui-render-utils.ts
CHANGED
|
@@ -177,6 +177,26 @@ export interface AnchoredFilesLabels {
|
|
|
177
177
|
unitPlural: string;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Render the call line shared by the readseek search tools (search, refs): a
|
|
182
|
+
* tool label, an accent term, the search path, an optional language hint, and
|
|
183
|
+
* the active boolean flags. Falsy {@link opts.flags} entries are dropped.
|
|
184
|
+
*/
|
|
185
|
+
export function renderReadSeekSearchCall(
|
|
186
|
+
args: { path?: string; lang?: string },
|
|
187
|
+
theme: RendererTheme,
|
|
188
|
+
rest: any[],
|
|
189
|
+
opts: { label: string; accent: string; flags: Array<string | false | undefined> },
|
|
190
|
+
): Text {
|
|
191
|
+
const context = rest[0] ?? {};
|
|
192
|
+
let text = `${renderToolLabel(theme, opts.label)} ${theme.fg("accent", opts.accent)}`;
|
|
193
|
+
text += theme.fg("dim", ` in ${args.path ?? "."}`);
|
|
194
|
+
if (args.lang) text += theme.fg("dim", ` (${args.lang})`);
|
|
195
|
+
const flags = opts.flags.filter(Boolean);
|
|
196
|
+
if (flags.length > 0) text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
197
|
+
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
/**
|
|
181
201
|
* Render the result summary shared by the anchored-files search tools (search,
|
|
182
202
|
* refs): a pending line, an error first-line/expanded body, an empty-result
|
package/src/write.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, relative } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { withFileMutationQueue, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { withFileMutationQueue, type ExtensionAPI, type ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
|
|
@@ -16,8 +16,8 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
|
16
16
|
import { buildPendingWritePreviewData, buildWritePreviewKey, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
|
|
17
17
|
import { generateCompactOrFullDiff, normalizeToLF, hasBareCarriageReturn } from "./edit-diff.js";
|
|
18
18
|
import { buildDiffData, type DiffData } from "./diff-data.js";
|
|
19
|
-
import { clampLineToWidth, clampLinesToWidth,
|
|
20
|
-
import {
|
|
19
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
20
|
+
import { upsertDiffComponent, upsertTextComponent } from "./tui-diff-component.js";
|
|
21
21
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
22
22
|
import { registerReadSeekTool } from "./register-tool.js";
|
|
23
23
|
|
|
@@ -399,32 +399,20 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
399
399
|
// its preview alongside the final result is just duplicate noise — the
|
|
400
400
|
// pre-execution state can no longer change in any meaningful way.
|
|
401
401
|
if (context.executionStarted) {
|
|
402
|
-
|
|
403
|
-
? context.lastComponent
|
|
404
|
-
: new Text("", 0, 0);
|
|
405
|
-
textComponent.setText(text);
|
|
406
|
-
return textComponent;
|
|
402
|
+
return upsertTextComponent(context.lastComponent, text);
|
|
407
403
|
}
|
|
408
404
|
const previewKey = buildWritePreviewKey(args ?? {});
|
|
409
405
|
const preview = resolvePendingDiffPreview(context, WRITE_PENDING_PREVIEW_STATE_KEY, previewKey, () => buildPendingWritePreviewData(args ?? {}, context.cwd ?? process.cwd()));
|
|
410
406
|
const expanded = !!context.expanded;
|
|
411
407
|
const parts = pendingWritePreviewParts(text, preview, expanded, theme);
|
|
412
408
|
if (parts.diffData) {
|
|
413
|
-
|
|
414
|
-
? context.lastComponent
|
|
415
|
-
: new DiffPreviewComponent({ prefixLines: parts.lines, diffData: parts.diffData, theme, expanded: true });
|
|
416
|
-
diffComponent.update({ prefixLines: parts.lines, diffData: parts.diffData, theme, expanded: true });
|
|
417
|
-
return diffComponent;
|
|
409
|
+
return upsertDiffComponent(context.lastComponent, { prefixLines: parts.lines, diffData: parts.diffData, theme, expanded: true });
|
|
418
410
|
}
|
|
419
|
-
|
|
420
|
-
? context.lastComponent
|
|
421
|
-
: new Text("", 0, 0);
|
|
422
|
-
textComponent.setText(clampLinesToWidth(parts.lines, context.width).join("\n"));
|
|
423
|
-
return textComponent;
|
|
411
|
+
return upsertTextComponent(context.lastComponent, clampLinesToWidth(parts.lines, context.width).join("\n"));
|
|
424
412
|
},
|
|
425
|
-
renderResult(result: any, options:
|
|
426
|
-
const expanded =
|
|
427
|
-
|
|
413
|
+
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
414
|
+
const { isPartial, expanded, width, context } = resolveRenderResultContext(options, rest);
|
|
415
|
+
if (isPartial) return renderPendingResult("pending write", width);
|
|
428
416
|
const details = result.details ?? {};
|
|
429
417
|
const output = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
430
418
|
if (result.isError || details.readseekValue?.ok === false) {
|
|
@@ -450,11 +438,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
450
438
|
const hasExpandableDiff = !!diffData;
|
|
451
439
|
let text = summaryLine(state, { hidden: hasExpandableDiff && !expanded });
|
|
452
440
|
if (expanded && hasExpandableDiff) {
|
|
453
|
-
|
|
454
|
-
? context.lastComponent
|
|
455
|
-
: new DiffPreviewComponent({ prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
456
|
-
diffComponent.update({ prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
457
|
-
return diffComponent;
|
|
441
|
+
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
458
442
|
}
|
|
459
443
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
460
444
|
},
|