pi-readseek 0.4.30 → 0.5.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/README.md +38 -44
- package/index.ts +45 -7
- package/package.json +2 -2
- package/prompts/def.md +2 -9
- package/prompts/edit.md +13 -13
- package/prompts/grep.md +3 -3
- package/prompts/hover.md +2 -6
- package/prompts/read.md +3 -3
- package/prompts/refs.md +1 -1
- package/prompts/rename.md +3 -7
- package/prompts/sg.md +1 -1
- package/prompts/write.md +5 -5
- package/src/def.ts +12 -19
- package/src/edit-output.ts +3 -3
- package/src/edit-syntax-validate.ts +3 -3
- package/src/edit.ts +18 -25
- package/src/file-map.ts +2 -2
- package/src/grep-budget.ts +7 -51
- package/src/grep-output.ts +4 -4
- package/src/grep.ts +19 -26
- package/src/hover.ts +4 -8
- package/src/read-output.ts +4 -4
- package/src/read.ts +25 -34
- package/src/readseek-client.ts +111 -79
- package/src/readseek-params.ts +1 -1
- package/src/readseek-settings.ts +138 -55
- package/src/readseek-value.ts +4 -4
- package/src/refs-output.ts +3 -3
- package/src/refs.ts +6 -10
- package/src/register-tool.ts +4 -42
- package/src/rename.ts +6 -10
- package/src/replace-symbol.ts +2 -2
- package/src/sg-output.ts +3 -3
- package/src/sg.ts +21 -25
- package/src/syntax-validate-mode.ts +3 -13
- package/src/tool-prompt-metadata.ts +12 -12
- package/src/tui-render-utils.ts +2 -2
- package/src/write.ts +22 -26
package/src/edit.ts
CHANGED
|
@@ -29,8 +29,6 @@ import { upsertDiffComponent, upsertTextComponent } from "./tui-diff-component.j
|
|
|
29
29
|
import type { FreshAnchorsPredicate } from "./tool-types.js";
|
|
30
30
|
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
31
31
|
|
|
32
|
-
import { resolveEditDiffDisplay } from "./readseek-settings.js";
|
|
33
|
-
|
|
34
32
|
const EDIT_PENDING_PREVIEW_STATE_KEY = "hashline-edit-pending-preview";
|
|
35
33
|
|
|
36
34
|
function pendingPreviewLines(summary: string, preview: PendingDiffPreviewResult | undefined, expanded: boolean): { lines: string[]; diffData?: ReturnType<typeof buildDiffData>; headerLabel?: string } {
|
|
@@ -73,7 +71,7 @@ type HashlineParams = Static<typeof hashlineEditSchema>;
|
|
|
73
71
|
|
|
74
72
|
const EDIT_PROMPT_METADATA = defineToolPromptMetadata({
|
|
75
73
|
promptUrl: new URL("../prompts/edit.md", import.meta.url),
|
|
76
|
-
promptSnippet: "Edit files using hash-verified anchors from
|
|
74
|
+
promptSnippet: "Edit files using hash-verified anchors from readSeek_read/readSeek_grep/readSeek_search/readSeek_write",
|
|
77
75
|
});
|
|
78
76
|
|
|
79
77
|
function buildEditError(
|
|
@@ -85,7 +83,7 @@ function buildEditError(
|
|
|
85
83
|
): {
|
|
86
84
|
content: [{ type: "text"; text: string }];
|
|
87
85
|
isError: true;
|
|
88
|
-
details: EditToolDetails & {
|
|
86
|
+
details: EditToolDetails & { readSeekValue: any };
|
|
89
87
|
} {
|
|
90
88
|
return {
|
|
91
89
|
content: [{ type: "text", text: message }],
|
|
@@ -94,13 +92,13 @@ function buildEditError(
|
|
|
94
92
|
diff: "",
|
|
95
93
|
patch: "",
|
|
96
94
|
firstChangedLine: undefined,
|
|
97
|
-
|
|
95
|
+
readSeekValue: {
|
|
98
96
|
tool: "edit",
|
|
99
97
|
ok: false,
|
|
100
98
|
path,
|
|
101
99
|
error: buildReadSeekError(code, message, hint, errorDetails),
|
|
102
100
|
},
|
|
103
|
-
} as EditToolDetails & {
|
|
101
|
+
} as EditToolDetails & { readSeekValue: any },
|
|
104
102
|
};
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -137,14 +135,14 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
137
135
|
if (wasReadInSession && !wasReadInSession(absolutePath)) {
|
|
138
136
|
const message = [
|
|
139
137
|
`You must get fresh anchors for ${absolutePath} before editing it.`,
|
|
140
|
-
`Call
|
|
141
|
-
"
|
|
138
|
+
`Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`,
|
|
139
|
+
"readSeek_edit requires fresh LINE:HASH anchors from readSeek_read, readSeek_grep, readSeek_search, or readSeek_write so the hashes match the current file contents.",
|
|
142
140
|
].join(" ");
|
|
143
141
|
return buildEditError(
|
|
144
142
|
absolutePath,
|
|
145
143
|
"file-not-read",
|
|
146
144
|
message,
|
|
147
|
-
`Call
|
|
145
|
+
`Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`,
|
|
148
146
|
);
|
|
149
147
|
}
|
|
150
148
|
const hasTopLevelReplaceInput =
|
|
@@ -253,7 +251,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
253
251
|
// Error-precedence order: replace_symbol resolution > anchor-overlap > anchored-edit.
|
|
254
252
|
//
|
|
255
253
|
// store successful probe results and reuse them in the apply loop so
|
|
256
|
-
//
|
|
254
|
+
// readSeekMapContent is invoked at most once per replace_symbol edit.
|
|
257
255
|
const replaceSymbolRanges: { start: number; end: number }[] = [];
|
|
258
256
|
const rsProbeResults: { type: "ok"; content: string; replacement: string; warnings: string[]; range: { start: number; end: number } }[] = [];
|
|
259
257
|
try {
|
|
@@ -365,7 +363,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
365
363
|
// Apply pass: reuse all probe results. The probe pass resolved every
|
|
366
364
|
// replace_symbol against originalNormalized; apply those replacements in
|
|
367
365
|
// reverse source order so original line ranges stay valid and no second
|
|
368
|
-
// replaceSymbol/
|
|
366
|
+
// replaceSymbol/readSeekMapContent call is needed.
|
|
369
367
|
const replaceSymbolWarnings: string[] = [];
|
|
370
368
|
if (rsProbeResults.length > 0) {
|
|
371
369
|
const lines = originalNormalized.split("\n");
|
|
@@ -539,10 +537,10 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
539
537
|
patch: builtOutput.patch,
|
|
540
538
|
diffData,
|
|
541
539
|
firstChangedLine: anchorResult.firstChangedLine ?? diffResult.firstChangedLine,
|
|
542
|
-
|
|
540
|
+
readSeekValue: builtOutput.readSeekValue,
|
|
543
541
|
} as EditToolDetails & {
|
|
544
542
|
diffData: typeof diffData;
|
|
545
|
-
|
|
543
|
+
readSeekValue: {
|
|
546
544
|
tool: string;
|
|
547
545
|
ok: boolean;
|
|
548
546
|
path: string;
|
|
@@ -567,11 +565,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
567
565
|
|
|
568
566
|
export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}) {
|
|
569
567
|
const tool = registerReadSeekTool(pi, {
|
|
570
|
-
|
|
571
|
-
pythonName: "edit",
|
|
572
|
-
defaultExposure: "not-safe-by-default",
|
|
573
|
-
}, {
|
|
574
|
-
name: "edit",
|
|
568
|
+
name: "readSeek_edit",
|
|
575
569
|
label: "Edit",
|
|
576
570
|
description: EDIT_PROMPT_METADATA.description,
|
|
577
571
|
promptSnippet: EDIT_PROMPT_METADATA.promptSnippet,
|
|
@@ -617,7 +611,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
617
611
|
previewKey,
|
|
618
612
|
() => buildPendingEditPreviewData(args ?? {}, context.cwd ?? process.cwd()),
|
|
619
613
|
);
|
|
620
|
-
const expanded = !!context.expanded
|
|
614
|
+
const expanded = !!context.expanded;
|
|
621
615
|
const preview2 = pendingPreviewLines(text, preview, expanded);
|
|
622
616
|
if (preview2.diffData) {
|
|
623
617
|
return upsertDiffComponent(context.lastComponent, { prefixLines: preview2.lines, diffData: preview2.diffData, theme, expanded: true });
|
|
@@ -625,7 +619,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
625
619
|
return upsertTextComponent(context.lastComponent, clampLinesToWidth(preview2.lines, context.width).join("\n"));
|
|
626
620
|
},
|
|
627
621
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
628
|
-
const { isPartial, isError, expanded
|
|
622
|
+
const { isPartial, isError, expanded, width, context } = resolveRenderResultContext(options, rest);
|
|
629
623
|
|
|
630
624
|
if (isPartial) {
|
|
631
625
|
return renderPendingResult("pending edit", width);
|
|
@@ -638,13 +632,13 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
638
632
|
.join("\n") ?? "";
|
|
639
633
|
const details = result.details ?? {};
|
|
640
634
|
const diff: string = details.diff ?? "";
|
|
641
|
-
const
|
|
635
|
+
const readSeekValue = details.readSeekValue as {
|
|
642
636
|
warnings?: string[];
|
|
643
637
|
noopEdits?: unknown[];
|
|
644
638
|
} | undefined;
|
|
645
|
-
const warnings =
|
|
646
|
-
const noopEdits =
|
|
647
|
-
const semanticClassification = (
|
|
639
|
+
const warnings = readSeekValue?.warnings ?? [];
|
|
640
|
+
const noopEdits = readSeekValue?.noopEdits ?? [];
|
|
641
|
+
const semanticClassification = (readSeekValue as any)?.semanticSummary?.classification as string | undefined;
|
|
648
642
|
|
|
649
643
|
const info = formatEditResultText({
|
|
650
644
|
isError: isError || !!result.isError,
|
|
@@ -655,7 +649,6 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
655
649
|
semanticClassification: semanticClassification as any,
|
|
656
650
|
});
|
|
657
651
|
|
|
658
|
-
const expanded = baseExpanded || resolveEditDiffDisplay() === "expanded";
|
|
659
652
|
const diffData = (details as any).diffData;
|
|
660
653
|
const stats = diffData?.stats ?? { added: 0, removed: 0 };
|
|
661
654
|
let text = "";
|
package/src/file-map.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
|
|
3
3
|
import type { FileMap } from "./readseek/types.js";
|
|
4
|
-
import {
|
|
4
|
+
import { readSeekMap } from "./readseek-client.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Fetch a structural file map from readseek, which maintains its own on-disk
|
|
@@ -10,7 +10,7 @@ import { readseekMap } from "./readseek-client.js";
|
|
|
10
10
|
export async function getOrGenerateMap(absPath: string): Promise<FileMap | null> {
|
|
11
11
|
try {
|
|
12
12
|
const { size } = await stat(absPath);
|
|
13
|
-
return await
|
|
13
|
+
return await readSeekMap(absPath, size);
|
|
14
14
|
} catch {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
package/src/grep-budget.ts
CHANGED
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { resolveReadSeekJsonSettings } from "./readseek-settings.js";
|
|
3
3
|
|
|
4
|
-
const POSITIVE_BASE10_INT = /^[1-9][0-9]*$/;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Strict positive base-10 integer parser used by readseek env knobs.
|
|
8
|
-
*
|
|
9
|
-
* Accepts: trimmed strings matching /^[1-9][0-9]*$/ that parse to a finite
|
|
10
|
-
* positive integer.
|
|
11
|
-
*
|
|
12
|
-
* Rejects: undefined, empty, whitespace-only, "0", negative, signed, hex
|
|
13
|
-
* ("0x10"), exponent ("1e3"), decimal ("3.14"), separators ("1,000" /
|
|
14
|
-
* "1_000"), embedded whitespace ("5 5").
|
|
15
|
-
*
|
|
16
|
-
* Returns `undefined` on rejection; never throws.
|
|
17
|
-
*/
|
|
18
|
-
function parsePositiveBase10Int(raw: string | undefined | null): number | undefined {
|
|
19
|
-
if (raw === undefined || raw === null) return undefined;
|
|
20
|
-
const trimmed = String(raw).trim();
|
|
21
|
-
if (!POSITIVE_BASE10_INT.test(trimmed)) return undefined;
|
|
22
|
-
const parsed = Number.parseInt(trimmed, 10);
|
|
23
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
24
|
-
return parsed;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
4
|
export interface GrepOutputBudget {
|
|
28
5
|
maxLines: number;
|
|
29
6
|
maxBytes: number;
|
|
@@ -31,7 +8,7 @@ export interface GrepOutputBudget {
|
|
|
31
8
|
|
|
32
9
|
/**
|
|
33
10
|
* Effective grep-output ceilings used as clamp upper bounds and as the
|
|
34
|
-
* fallback defaults when
|
|
11
|
+
* fallback defaults when the settings are unset.
|
|
35
12
|
*
|
|
36
13
|
* The bytes ceiling is the already-tightened 50 KiB used by `buildGrepOutput`
|
|
37
14
|
* today, NOT the unclamped `DEFAULT_MAX_BYTES`.
|
|
@@ -39,41 +16,20 @@ export interface GrepOutputBudget {
|
|
|
39
16
|
export const GREP_OUTPUT_DEFAULT_MAX_LINES = DEFAULT_MAX_LINES;
|
|
40
17
|
export const GREP_OUTPUT_DEFAULT_MAX_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
|
|
41
18
|
|
|
42
|
-
function
|
|
43
|
-
const parsed = parsePositiveBase10Int(rawEnvValue);
|
|
44
|
-
return parsed === undefined ? undefined : Math.min(parsed, ceiling);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function resolveDimension(rawEnvValue: string | undefined, jsonValue: number | undefined, ceiling: number): number {
|
|
48
|
-
if (rawEnvValue !== undefined) {
|
|
49
|
-
const envValue = resolveEnvDimension(rawEnvValue, ceiling);
|
|
50
|
-
if (envValue !== undefined) return envValue;
|
|
51
|
-
}
|
|
19
|
+
function resolveDimension(jsonValue: number | undefined, ceiling: number): number {
|
|
52
20
|
if (jsonValue !== undefined) return Math.min(jsonValue, ceiling);
|
|
53
21
|
return ceiling;
|
|
54
22
|
}
|
|
55
23
|
|
|
56
24
|
/**
|
|
57
|
-
* Resolve the effective grep visible-output budget
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* Invalid / zero / negative env values fall back to the current defaults.
|
|
62
|
-
* Above-default values clamp to the current defaults. Below-default values
|
|
63
|
-
* are used as-is.
|
|
25
|
+
* Resolve the effective grep visible-output budget from the readseek
|
|
26
|
+
* settings. Below-default values are used as-is; above-default values clamp
|
|
27
|
+
* to the current defaults.
|
|
64
28
|
*/
|
|
65
29
|
export function resolveGrepOutputBudget(): GrepOutputBudget {
|
|
66
30
|
const settings = resolveReadSeekJsonSettings().settings.grep;
|
|
67
31
|
return {
|
|
68
|
-
maxLines: resolveDimension(
|
|
69
|
-
|
|
70
|
-
settings?.maxLines,
|
|
71
|
-
GREP_OUTPUT_DEFAULT_MAX_LINES,
|
|
72
|
-
),
|
|
73
|
-
maxBytes: resolveDimension(
|
|
74
|
-
process.env.READSEEK_GREP_MAX_BYTES,
|
|
75
|
-
settings?.maxBytes,
|
|
76
|
-
GREP_OUTPUT_DEFAULT_MAX_BYTES,
|
|
77
|
-
),
|
|
32
|
+
maxLines: resolveDimension(settings?.maxLines, GREP_OUTPUT_DEFAULT_MAX_LINES),
|
|
33
|
+
maxBytes: resolveDimension(settings?.maxBytes, GREP_OUTPUT_DEFAULT_MAX_BYTES),
|
|
78
34
|
};
|
|
79
35
|
}
|
package/src/grep-output.ts
CHANGED
|
@@ -68,7 +68,7 @@ interface BuildGrepOutputInput {
|
|
|
68
68
|
|
|
69
69
|
interface GrepOutputResult {
|
|
70
70
|
text: string;
|
|
71
|
-
|
|
71
|
+
readSeekValue: {
|
|
72
72
|
tool: "grep";
|
|
73
73
|
summary: boolean;
|
|
74
74
|
totalMatches: number;
|
|
@@ -159,7 +159,7 @@ export function buildGrepOutput(input: BuildGrepOutputInput): GrepOutputResult {
|
|
|
159
159
|
if (truncated.truncated) {
|
|
160
160
|
text = `${truncated.content}\n\n[Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${formatSize(truncated.outputBytes)} of ${formatSize(truncated.totalBytes)}). Refine pattern or increase limit.]`;
|
|
161
161
|
}
|
|
162
|
-
const
|
|
162
|
+
const readSeekValue: GrepOutputResult["readSeekValue"] = {
|
|
163
163
|
tool: "grep",
|
|
164
164
|
summary: input.summary,
|
|
165
165
|
totalMatches: input.totalMatches,
|
|
@@ -171,11 +171,11 @@ export function buildGrepOutput(input: BuildGrepOutputInput): GrepOutputResult {
|
|
|
171
171
|
})),
|
|
172
172
|
};
|
|
173
173
|
if (!input.summary && input.scopeMode === "symbol") {
|
|
174
|
-
|
|
174
|
+
readSeekValue.scopes = buildScopeMetadata(input.groups, scopeWarnings);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
return {
|
|
178
178
|
text,
|
|
179
|
-
|
|
179
|
+
readSeekValue,
|
|
180
180
|
};
|
|
181
181
|
}
|
package/src/grep.ts
CHANGED
|
@@ -168,7 +168,6 @@ function escapeForRegex(s: string): string {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
interface GrepToolOptions {
|
|
171
|
-
searchGuideline?: string;
|
|
172
171
|
onFileAnchored?: FileAnchoredCallback;
|
|
173
172
|
}
|
|
174
173
|
|
|
@@ -253,7 +252,7 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
253
252
|
),
|
|
254
253
|
details: {
|
|
255
254
|
...(typeof result.details === "object" && result.details !== null ? result.details : {}),
|
|
256
|
-
|
|
255
|
+
readSeekValue: {
|
|
257
256
|
tool: "grep",
|
|
258
257
|
summary: !!p.summary,
|
|
259
258
|
totalMatches: 0,
|
|
@@ -410,7 +409,7 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
410
409
|
...result,
|
|
411
410
|
details: {
|
|
412
411
|
...passthroughDetails,
|
|
413
|
-
|
|
412
|
+
readSeekValue: {
|
|
414
413
|
tool: "grep",
|
|
415
414
|
summary: true,
|
|
416
415
|
totalMatches: 0,
|
|
@@ -436,7 +435,7 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
436
435
|
...passthroughDetails,
|
|
437
436
|
hashlinePassthrough: true,
|
|
438
437
|
hashlineWarning: warning,
|
|
439
|
-
|
|
438
|
+
readSeekValue: {
|
|
440
439
|
tool: "grep",
|
|
441
440
|
summary: !!p.summary,
|
|
442
441
|
totalMatches: 0,
|
|
@@ -477,20 +476,20 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
477
476
|
renderedGroups = scoped.groups;
|
|
478
477
|
scopeWarnings = scoped.warnings;
|
|
479
478
|
}
|
|
480
|
-
const
|
|
479
|
+
const readSeekRecords = recordsFromGroups(renderedGroups);
|
|
481
480
|
const builtOutput = buildGrepOutput({
|
|
482
481
|
summary: !!summary,
|
|
483
482
|
totalMatches,
|
|
484
483
|
groups: renderedGroups,
|
|
485
484
|
limit: effectiveLimit,
|
|
486
|
-
records:
|
|
485
|
+
records: readSeekRecords,
|
|
487
486
|
scopeMode: p.scope === "symbol" && !summary ? "symbol" : undefined,
|
|
488
487
|
scopeWarnings,
|
|
489
488
|
passthroughLines,
|
|
490
489
|
});
|
|
491
490
|
|
|
492
|
-
if (!summary &&
|
|
493
|
-
const anchoredPaths = new Set(
|
|
491
|
+
if (!summary && readSeekRecords.length > 0) {
|
|
492
|
+
const anchoredPaths = new Set(readSeekRecords.map((record) => record.path));
|
|
494
493
|
for (const absolutePath of anchoredPaths) {
|
|
495
494
|
onFileAnchored?.(absolutePath);
|
|
496
495
|
}
|
|
@@ -508,25 +507,19 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
508
507
|
),
|
|
509
508
|
details: {
|
|
510
509
|
...compactDetails,
|
|
511
|
-
|
|
510
|
+
readSeekValue: builtOutput.readSeekValue,
|
|
512
511
|
},
|
|
513
512
|
};
|
|
514
513
|
}
|
|
515
514
|
|
|
516
515
|
export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}) {
|
|
517
516
|
const tool = registerReadSeekTool(pi, {
|
|
518
|
-
|
|
519
|
-
pythonName: "grep",
|
|
520
|
-
defaultExposure: "safe-by-default",
|
|
521
|
-
}, {
|
|
522
|
-
name: "grep",
|
|
517
|
+
name: "readSeek_grep",
|
|
523
518
|
label: "grep",
|
|
524
519
|
description: GREP_PROMPT_METADATA.description,
|
|
525
520
|
parameters: grepSchema,
|
|
526
521
|
promptSnippet: GREP_PROMPT_METADATA.promptSnippet,
|
|
527
|
-
promptGuidelines:
|
|
528
|
-
? [GREP_PROMPT_METADATA.promptGuidelines[0], options.searchGuideline]
|
|
529
|
-
: GREP_PROMPT_METADATA.promptGuidelines,
|
|
522
|
+
promptGuidelines: GREP_PROMPT_METADATA.promptGuidelines,
|
|
530
523
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
531
524
|
return executeGrep({
|
|
532
525
|
toolCallId,
|
|
@@ -558,14 +551,14 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
|
|
|
558
551
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
559
552
|
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
560
553
|
|
|
561
|
-
if (isPartial) return renderPendingResult("pending
|
|
554
|
+
if (isPartial) return renderPendingResult("pending grep", width);
|
|
562
555
|
|
|
563
556
|
const content = result.content?.[0];
|
|
564
557
|
const textContent = content?.type === "text" ? content.text : "";
|
|
565
558
|
|
|
566
559
|
if (isError || result.isError) return renderErrorResult(textContent, { expanded, width });
|
|
567
560
|
|
|
568
|
-
const
|
|
561
|
+
const readSeekValue = (result.details as any)?.readSeekValue as {
|
|
569
562
|
tool: "grep";
|
|
570
563
|
summary: boolean;
|
|
571
564
|
totalMatches: number;
|
|
@@ -575,26 +568,26 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
|
|
|
575
568
|
const hasBinaryWarning = textContent.includes("appears to be a binary file");
|
|
576
569
|
|
|
577
570
|
const fileSet = new Set<string>();
|
|
578
|
-
for (const r of
|
|
571
|
+
for (const r of readSeekValue?.records ?? []) {
|
|
579
572
|
if (r.path) fileSet.add(r.path);
|
|
580
573
|
}
|
|
581
574
|
|
|
582
575
|
const info = formatGrepResultText({
|
|
583
|
-
totalMatches:
|
|
584
|
-
summary:
|
|
585
|
-
records:
|
|
576
|
+
totalMatches: readSeekValue?.totalMatches ?? 0,
|
|
577
|
+
summary: readSeekValue?.summary ?? false,
|
|
578
|
+
records: readSeekValue?.records ?? [],
|
|
586
579
|
fileCount: fileSet.size,
|
|
587
580
|
hasBinaryWarning,
|
|
588
581
|
});
|
|
589
582
|
|
|
590
583
|
if (info.noMatches && !hasBinaryWarning) return new Text(summaryLine("no matches"), 0, 0);
|
|
591
|
-
const matchCount =
|
|
584
|
+
const matchCount = readSeekValue?.totalMatches ?? 0;
|
|
592
585
|
const matchWord = matchCount === 1 ? "match" : "matches";
|
|
593
586
|
let text = summaryLine(`${matchCount} ${matchWord} returned`, { hidden: !!textContent && !expanded });
|
|
594
587
|
for (const badge of info.badges) text += theme.fg(badge.startsWith("⚠") ? "warning" : "dim", ` ${badge}`);
|
|
595
|
-
if (expanded &&
|
|
588
|
+
if (expanded && readSeekValue?.records) {
|
|
596
589
|
const fileCounts = new Map<string, number>();
|
|
597
|
-
for (const r of
|
|
590
|
+
for (const r of readSeekValue.records) if (r.path && r.kind === "match") fileCounts.set(r.path, (fileCounts.get(r.path) ?? 0) + 1);
|
|
598
591
|
for (const [filePath, count] of [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)) {
|
|
599
592
|
const display = path.relative(cwd, filePath) || filePath;
|
|
600
593
|
text += "\n" + theme.fg("dim", ` ${display} (${count})`);
|
package/src/hover.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
|
8
8
|
import { buildToolErrorResult } from "./readseek-value.js";
|
|
9
9
|
import { resolveToCwd } from "./path-utils.js";
|
|
10
10
|
import { formatFsError } from "./fs-error.js";
|
|
11
|
-
import { classifyReadSeekFailure,
|
|
11
|
+
import { classifyReadSeekFailure, readSeekIdentify } from "./readseek-client.js";
|
|
12
12
|
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
13
13
|
|
|
14
14
|
import { clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
@@ -55,7 +55,7 @@ export async function executeHover(opts: ExecuteHoverOptions): Promise<any> {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
|
-
const output = await
|
|
58
|
+
const output = await readSeekIdentify(filePath, content, {
|
|
59
59
|
line: p.line,
|
|
60
60
|
column: p.column,
|
|
61
61
|
signal,
|
|
@@ -77,7 +77,7 @@ export async function executeHover(opts: ExecuteHoverOptions): Promise<any> {
|
|
|
77
77
|
return {
|
|
78
78
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
79
79
|
details: {
|
|
80
|
-
|
|
80
|
+
readSeekValue: {
|
|
81
81
|
tool: "hover",
|
|
82
82
|
ok: true,
|
|
83
83
|
path: filePath,
|
|
@@ -93,11 +93,7 @@ export async function executeHover(opts: ExecuteHoverOptions): Promise<any> {
|
|
|
93
93
|
|
|
94
94
|
export function registerHoverTool(pi: ExtensionAPI) {
|
|
95
95
|
registerReadSeekTool(pi, {
|
|
96
|
-
|
|
97
|
-
pythonName: "hover",
|
|
98
|
-
defaultExposure: "opt-in",
|
|
99
|
-
}, {
|
|
100
|
-
name: "hover",
|
|
96
|
+
name: "readSeek_hover",
|
|
101
97
|
label: "Hover",
|
|
102
98
|
description: HOVER_PROMPT_METADATA.description,
|
|
103
99
|
promptSnippet: HOVER_PROMPT_METADATA.promptSnippet,
|
package/src/read-output.ts
CHANGED
|
@@ -63,7 +63,7 @@ interface ReadOutputInput {
|
|
|
63
63
|
interface ReadOutputResult {
|
|
64
64
|
text: string;
|
|
65
65
|
lines: ReadSeekLine[];
|
|
66
|
-
|
|
66
|
+
readSeekValue: {
|
|
67
67
|
tool: "read";
|
|
68
68
|
path: string;
|
|
69
69
|
range: {
|
|
@@ -141,7 +141,7 @@ export function buildReadOutput(input: ReadOutputInput): ReadOutputResult {
|
|
|
141
141
|
text = `${warnings.map((warning) => warning.message).join("\n\n")}\n\n${text}`;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
const
|
|
144
|
+
const readSeekValue: ReadOutputResult["readSeekValue"] = {
|
|
145
145
|
tool: "read",
|
|
146
146
|
path: input.path,
|
|
147
147
|
range: {
|
|
@@ -160,7 +160,7 @@ export function buildReadOutput(input: ReadOutputInput): ReadOutputResult {
|
|
|
160
160
|
};
|
|
161
161
|
|
|
162
162
|
if (input.bundle) {
|
|
163
|
-
|
|
163
|
+
readSeekValue.bundle = {
|
|
164
164
|
mode: input.bundle.mode,
|
|
165
165
|
applied: input.bundle.applied,
|
|
166
166
|
localSupport: input.bundle.localSupport.map((item) => {
|
|
@@ -181,6 +181,6 @@ export function buildReadOutput(input: ReadOutputInput): ReadOutputResult {
|
|
|
181
181
|
return {
|
|
182
182
|
text,
|
|
183
183
|
lines,
|
|
184
|
-
|
|
184
|
+
readSeekValue,
|
|
185
185
|
};
|
|
186
186
|
}
|
package/src/read.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { buildReadOutput } from "./read-output.js";
|
|
|
26
26
|
|
|
27
27
|
import { buildLocalBundle } from "./read-local-bundle.js";
|
|
28
28
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
29
|
-
import {
|
|
29
|
+
import { readSeekRead, readSeekDetect, readSeekImage, type ReadSeekDetection } from "./readseek-client.js";
|
|
30
30
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
31
31
|
import { resolveReadSeekOcrMode } from "./readseek-settings.js";
|
|
32
32
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidthCached } from "./tui-render-utils.js";
|
|
@@ -65,9 +65,9 @@ export interface ExecuteReadOptions {
|
|
|
65
65
|
function hasReadAnchors(result: AgentToolResult<any>): boolean {
|
|
66
66
|
const details = (result as { details?: unknown }).details;
|
|
67
67
|
if (!details || typeof details !== "object") return false;
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
70
|
-
const lines = (
|
|
68
|
+
const readSeekValue = (details as { readSeekValue?: unknown }).readSeekValue;
|
|
69
|
+
if (!readSeekValue || typeof readSeekValue !== "object") return false;
|
|
70
|
+
const lines = (readSeekValue as { lines?: unknown }).lines;
|
|
71
71
|
return Array.isArray(lines) && lines.length > 0;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -171,24 +171,19 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
171
171
|
if (hasBinaryContent) {
|
|
172
172
|
let detection: ReadSeekDetection | undefined;
|
|
173
173
|
try {
|
|
174
|
-
detection = await
|
|
174
|
+
detection = await readSeekDetect(absolutePath, { signal });
|
|
175
175
|
} catch {
|
|
176
176
|
}
|
|
177
177
|
if (detection?.kind === "image") {
|
|
178
178
|
const builtinRead = createReadTool(cwd);
|
|
179
179
|
const builtinResult = await builtinRead.execute(toolCallId, p, signal, onUpdate);
|
|
180
180
|
const ocrMode = resolveReadSeekOcrMode();
|
|
181
|
-
const shouldRunVision = ocrMode === "
|
|
181
|
+
const shouldRunVision = ocrMode === "force" || (ocrMode === "auto" && !opts.modelSupportsImages);
|
|
182
182
|
if (!shouldRunVision) return succeed(builtinResult);
|
|
183
183
|
|
|
184
184
|
let ocrFailed = false;
|
|
185
185
|
try {
|
|
186
|
-
const ocrDetection = await
|
|
187
|
-
ocr: true,
|
|
188
|
-
caption: true,
|
|
189
|
-
objects: true,
|
|
190
|
-
signal,
|
|
191
|
-
});
|
|
186
|
+
const ocrDetection = await readSeekImage(absolutePath, ["ocr", "caption", "objects"], { signal });
|
|
192
187
|
const imageAnalysis = formatImageAnalysis(ocrDetection);
|
|
193
188
|
if (imageAnalysis) {
|
|
194
189
|
return succeed({
|
|
@@ -356,33 +351,33 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
356
351
|
}
|
|
357
352
|
|
|
358
353
|
throwIfAborted(signal);
|
|
359
|
-
let
|
|
354
|
+
let readSeekOutput: Awaited<ReturnType<typeof readSeekRead>>;
|
|
360
355
|
try {
|
|
361
|
-
|
|
362
|
-
? await
|
|
363
|
-
: await
|
|
356
|
+
readSeekOutput = total === 0
|
|
357
|
+
? await readSeekRead(absolutePath, undefined, undefined, { signal })
|
|
358
|
+
: await readSeekRead(absolutePath, startLine, endIdx, { signal });
|
|
364
359
|
} catch (err: any) {
|
|
365
360
|
const detail = err?.message ? ` — ${err.message}` : "";
|
|
366
361
|
const message = `readseek failed while reading ${rawPath}${detail}`;
|
|
367
362
|
return buildToolErrorResult("read", "readseek-error", message, { path: rawParams.path, hint: "Ensure @jarkkojs/readseek and its npm platform package are installed.", details: { message: err?.message } });
|
|
368
363
|
}
|
|
369
364
|
const expectedLineCount = Math.max(0, endIdx - startLine + 1);
|
|
370
|
-
const invalidLine =
|
|
371
|
-
if (
|
|
365
|
+
const invalidLine = readSeekOutput.hashlines.find((line, index) => line.line !== startLine + index);
|
|
366
|
+
if (readSeekOutput.hashlines.length !== expectedLineCount || invalidLine) {
|
|
372
367
|
const message = invalidLine
|
|
373
368
|
? `readseek returned non-sequential line ${invalidLine.line} for requested range ${startLine}-${endIdx}`
|
|
374
|
-
: `readseek returned ${
|
|
369
|
+
: `readseek returned ${readSeekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
|
|
375
370
|
return buildToolErrorResult("read", "readseek-output-mismatch", message, { path: rawParams.path });
|
|
376
371
|
}
|
|
377
|
-
const
|
|
372
|
+
const readSeekLines: ReadSeekLine[] = readSeekOutput.hashlines.map((line) => ({
|
|
378
373
|
line: line.line,
|
|
379
374
|
hash: line.hash,
|
|
380
375
|
anchor: `${line.line}:${line.hash}`,
|
|
381
376
|
raw: line.text,
|
|
382
377
|
display: escapeControlCharsForDisplay(line.text),
|
|
383
378
|
}));
|
|
384
|
-
const selected =
|
|
385
|
-
const formatted = renderReadSeekLines(
|
|
379
|
+
const selected = readSeekLines.map((line) => line.raw);
|
|
380
|
+
const formatted = renderReadSeekLines(readSeekLines);
|
|
386
381
|
|
|
387
382
|
const truncation = truncateHead(formatted, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
388
383
|
|
|
@@ -423,7 +418,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
423
418
|
endLine: endIdx,
|
|
424
419
|
totalLines: total,
|
|
425
420
|
selectedLines: selected,
|
|
426
|
-
lines:
|
|
421
|
+
lines: readSeekLines,
|
|
427
422
|
warnings: structuredWarnings,
|
|
428
423
|
truncation: truncation.truncated
|
|
429
424
|
? {
|
|
@@ -456,7 +451,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
456
451
|
content: [{ type: "text", text: readOutput.text }],
|
|
457
452
|
details: {
|
|
458
453
|
truncation: truncation.truncated ? truncation : undefined,
|
|
459
|
-
|
|
454
|
+
readSeekValue: readOutput.readSeekValue,
|
|
460
455
|
},
|
|
461
456
|
});
|
|
462
457
|
}
|
|
@@ -469,11 +464,7 @@ function splitReadSeekLines(text: string): string[] {
|
|
|
469
464
|
|
|
470
465
|
export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}) {
|
|
471
466
|
const tool = registerReadSeekTool(pi, {
|
|
472
|
-
|
|
473
|
-
pythonName: "read",
|
|
474
|
-
defaultExposure: "safe-by-default",
|
|
475
|
-
}, {
|
|
476
|
-
name: "read",
|
|
467
|
+
name: "readSeek_read",
|
|
477
468
|
label: "Read",
|
|
478
469
|
description: READ_PROMPT_METADATA.description,
|
|
479
470
|
promptSnippet: READ_PROMPT_METADATA.promptSnippet,
|
|
@@ -529,16 +520,16 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
529
520
|
return new Text(clampLinesToWidth([summaryLine(errorText)], width).join("\n"), 0, 0);
|
|
530
521
|
}
|
|
531
522
|
|
|
532
|
-
const
|
|
533
|
-
if (!
|
|
523
|
+
const readSeekValue = (result.details as any)?.readSeekValue as { range: { startLine: number; endLine: number; totalLines: number }; truncation: any; symbol: any; map: any; warnings: ReadSeekWarning[] } | undefined;
|
|
524
|
+
if (!readSeekValue) {
|
|
534
525
|
const lines = textContent.split("\n").filter(Boolean).length || textContent.split("\n").length;
|
|
535
526
|
return new Text(summaryLine(`loaded ${lines} ${lines === 1 ? "line" : "lines"}`, { hidden: !!textContent && !expanded }), 0, 0);
|
|
536
527
|
}
|
|
537
528
|
|
|
538
|
-
const info = formatReadResultText({ range:
|
|
539
|
-
const visibleLines = info.truncated &&
|
|
529
|
+
const info = formatReadResultText({ range: readSeekValue.range, truncation: readSeekValue.truncation, symbol: readSeekValue.symbol, map: readSeekValue.map, warnings: readSeekValue.warnings });
|
|
530
|
+
const visibleLines = info.truncated && readSeekValue.truncation ? readSeekValue.truncation.outputLines : readSeekValue.range.endLine - readSeekValue.range.startLine + 1;
|
|
540
531
|
const loadedWord = visibleLines === 1 ? "line" : "lines";
|
|
541
|
-
const summaryParts: string[] = [info.truncated ? `loaded ${visibleLines} of ${
|
|
532
|
+
const summaryParts: string[] = [info.truncated ? `loaded ${visibleLines} of ${readSeekValue.truncation?.totalLines ?? readSeekValue.range.totalLines} ${loadedWord} (truncated)` : `loaded ${visibleLines} ${loadedWord}`];
|
|
542
533
|
if (info.symbolBadge) summaryParts.push(info.symbolBadge);
|
|
543
534
|
for (const badge of info.badges) summaryParts.push(badge);
|
|
544
535
|
const summary = summaryParts.join(" • ");
|