pi-readseek 0.6.12 → 0.7.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 +2 -0
- package/index.ts +3 -0
- package/package.json +2 -2
- package/prompts/read.md +2 -1
- package/prompts/view.md +14 -0
- package/src/read.ts +35 -5
- package/src/readseek-client.ts +52 -5
- package/src/view.ts +158 -0
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ binary automatically on supported platforms.
|
|
|
18
18
|
|
|
19
19
|
- `readSeek_read`: reads text with `LINE:HASH` anchors; when image modes are
|
|
20
20
|
enabled, images and PDFs can be returned as base64 or analyzed locally.
|
|
21
|
+
PDF reads return one selected page at a time by default.
|
|
21
22
|
- `readSeek_edit`: edits existing text files using fresh `LINE:HASH` anchors.
|
|
22
23
|
- `readSeek_grep`: searches text and returns edit-ready anchors.
|
|
23
24
|
- `readSeek_search`: searches code by structural AST pattern.
|
|
@@ -27,6 +28,7 @@ binary automatically on supported platforms.
|
|
|
27
28
|
- `readSeek_hover`: identifies the cursor token and enclosing symbol.
|
|
28
29
|
- `readSeek_def`: finds structural symbol definitions.
|
|
29
30
|
- `readSeek_check`: checks a source file for parser errors and missing syntax.
|
|
31
|
+
- `readSeek_view`: views an indexed PDF overview or narrows it by page, node, kind, or depth.
|
|
30
32
|
- `readSeek_write`: creates or overwrites whole files and returns anchors.
|
|
31
33
|
|
|
32
34
|
## Settings
|
package/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { registerHoverTool } from "./src/hover.js";
|
|
|
9
9
|
import { registerWriteTool } from "./src/write.js";
|
|
10
10
|
import { registerDefTool } from "./src/def.js";
|
|
11
11
|
import { registerCheckTool } from "./src/check.js";
|
|
12
|
+
import { registerViewTool } from "./src/view.js";
|
|
12
13
|
import { SessionAnchors } from "./src/session-anchors.js";
|
|
13
14
|
import { readSeekBinaryAvailability } from "./src/readseek-client.js";
|
|
14
15
|
import { resolveReadSeekJsonSettings, type ReadSeekSettingsWarning } from "./src/readseek-settings.js";
|
|
@@ -39,6 +40,7 @@ const READSEEK_TOOL_ENTRIES: ReadonlyArray<{ builtIn: ReplacedBuiltIn | null; re
|
|
|
39
40
|
{ builtIn: "write", readSeekName: "readSeek_write" },
|
|
40
41
|
{ builtIn: null, readSeekName: "readSeek_def" },
|
|
41
42
|
{ builtIn: null, readSeekName: "readSeek_check" },
|
|
43
|
+
{ builtIn: null, readSeekName: "readSeek_view" },
|
|
42
44
|
];
|
|
43
45
|
|
|
44
46
|
function formatSettingsWarning(warning: ReadSeekSettingsWarning): string {
|
|
@@ -87,6 +89,7 @@ export default function piReadSeekExtension(pi: ExtensionAPI): void {
|
|
|
87
89
|
registerHoverTool(pi);
|
|
88
90
|
registerDefTool(pi, { onFileAnchored: markAnchored });
|
|
89
91
|
registerCheckTool(pi);
|
|
92
|
+
registerViewTool(pi);
|
|
90
93
|
registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
|
|
91
94
|
|
|
92
95
|
pi.on("session_start", (_event, ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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,7 +39,7 @@
|
|
|
39
39
|
"node": ">=20.0.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@jarkkojs/readseek": "^0.
|
|
42
|
+
"@jarkkojs/readseek": "^0.7.0",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"xxhash-wasm": "^1.1.0"
|
|
45
45
|
},
|
package/prompts/read.md
CHANGED
|
@@ -15,8 +15,9 @@ Read anchored text by range, map, or symbol, and process images or PDFs with an
|
|
|
15
15
|
- `symbol` — `Name`, `Class.method`, or `Name@<line>`; incompatible with `offset` / `limit`.
|
|
16
16
|
- `bundle` — only `"local"`; requires `symbol` and excludes `map`.
|
|
17
17
|
- `image` — an exposed image/PDF mode; unavailable when `imageMode` is `"off"`.
|
|
18
|
+
- `page` — one-based PDF page; defaults to page 1 and cannot be combined with `offset` / `limit`.
|
|
18
19
|
|
|
19
|
-
Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}. Omitting `image` skips images and PDFs; when `imageMode` is `"off"`, visual files are always skipped.
|
|
20
|
+
Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}. PDF reads return one page by default. Omitting `image` skips images and PDFs; when `imageMode` is `"off"`, visual files are always skipped.
|
|
20
21
|
|
|
21
22
|
Truncated full-file reads append a map when available. Use its ranges for follow-up reads.
|
|
22
23
|
|
package/prompts/view.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
View the structure or selected content of an indexed PDF. Start with the default overview, then narrow by page or node instead of reading the whole document.
|
|
2
|
+
|
|
3
|
+
## Parameters
|
|
4
|
+
|
|
5
|
+
- `path` — PDF document to view.
|
|
6
|
+
- `node` — optional node ID to use as the view root.
|
|
7
|
+
- `page` — optional one-based source page.
|
|
8
|
+
- `kind` — optional node kind filter.
|
|
9
|
+
- `depth` — optional maximum depth below selected roots.
|
|
10
|
+
- `outline` — return outline nodes only.
|
|
11
|
+
|
|
12
|
+
## Output
|
|
13
|
+
|
|
14
|
+
Returns a bounded text projection with stable node IDs and source page references.
|
package/src/read.ts
CHANGED
|
@@ -32,11 +32,11 @@ import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult,
|
|
|
32
32
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
33
33
|
import { filePathParam, mapParam, optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
34
34
|
|
|
35
|
-
|
|
36
35
|
interface ReadParams {
|
|
37
36
|
path: string;
|
|
38
37
|
offset?: number | string;
|
|
39
38
|
limit?: number | string;
|
|
39
|
+
page?: number | string;
|
|
40
40
|
symbol?: string;
|
|
41
41
|
map?: boolean;
|
|
42
42
|
bundle?: string;
|
|
@@ -101,6 +101,12 @@ function formatPdfAnalysis(pdf: ReadSeekPdfOutput): string {
|
|
|
101
101
|
return sections.filter(Boolean).join("\n\n");
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function truncateDocumentText(text: string): string {
|
|
105
|
+
const truncation = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
106
|
+
if (!truncation.truncated) return truncation.content;
|
|
107
|
+
return `${truncation.content}\n[… document output truncated; use readSeek_view with page or node selectors]`;
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
function skippedVisualFile(path: string): AgentToolResult<any> {
|
|
105
111
|
return {
|
|
106
112
|
content: [{ type: "text", text: `[Skipped image/PDF: ${path}; no image mode selected]` }],
|
|
@@ -128,6 +134,15 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
128
134
|
const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
|
|
129
135
|
return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
|
|
130
136
|
}
|
|
137
|
+
|
|
138
|
+
const page = coerceObviousBase10Int(rawParams.page, "page");
|
|
139
|
+
if (!page.ok) {
|
|
140
|
+
return buildToolErrorResult("read", "invalid-page", page.message, { path: rawParams.path });
|
|
141
|
+
}
|
|
142
|
+
if (page.value !== undefined && page.value < 1) {
|
|
143
|
+
const message = `Invalid page: expected a positive integer, received ${page.value}.`;
|
|
144
|
+
return buildToolErrorResult("read", "invalid-page", message, { path: rawParams.path });
|
|
145
|
+
}
|
|
131
146
|
const rawBundle = typeof rawParams.bundle === "string" ? rawParams.bundle.trim() : undefined;
|
|
132
147
|
const requestedMapViaBundle =
|
|
133
148
|
rawBundle === "map" ||
|
|
@@ -136,6 +151,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
136
151
|
...rawParams,
|
|
137
152
|
offset: offset.value,
|
|
138
153
|
limit: limit.value,
|
|
154
|
+
page: page.value,
|
|
139
155
|
map: rawParams.map === true || requestedMapViaBundle,
|
|
140
156
|
bundle: requestedMapViaBundle ? undefined : rawBundle,
|
|
141
157
|
};
|
|
@@ -207,6 +223,15 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
207
223
|
});
|
|
208
224
|
}
|
|
209
225
|
if (detection.kind === "image" || detection.type === "application/pdf") {
|
|
226
|
+
if (p.offset !== undefined || p.limit !== undefined) {
|
|
227
|
+
const message = "Cannot combine offset/limit with image or PDF reads.";
|
|
228
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
229
|
+
}
|
|
230
|
+
if (detection.kind !== "pdf" && p.page !== undefined) {
|
|
231
|
+
const message = "The page parameter applies to PDFs only.";
|
|
232
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
233
|
+
}
|
|
234
|
+
const pdfPage = detection.kind === "pdf" ? (p.page ?? 1) : undefined;
|
|
210
235
|
const imageMode = resolveReadSeekImageMode();
|
|
211
236
|
if (imageMode === "off" || p.image === undefined) {
|
|
212
237
|
return skippedVisualFile(rawParams.path);
|
|
@@ -219,8 +244,8 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
219
244
|
if (p.image === "none") {
|
|
220
245
|
try {
|
|
221
246
|
if (detection.kind === "pdf") {
|
|
222
|
-
const pdf = await readSeekPdf(absolutePath, p.image, { signal });
|
|
223
|
-
const content: AgentToolResult<any>["content"] = [{ type: "text", text: pdf.markdown }];
|
|
247
|
+
const pdf = await readSeekPdf(absolutePath, p.image, { page: pdfPage, signal });
|
|
248
|
+
const content: AgentToolResult<any>["content"] = [{ type: "text", text: truncateDocumentText(pdf.markdown) }];
|
|
224
249
|
for (const image of pdf.images) {
|
|
225
250
|
if (image.encoding !== "base64" || image.data === undefined) continue;
|
|
226
251
|
content.push({ type: "text", text: `[PDF page ${image.page} image]` });
|
|
@@ -243,9 +268,9 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
243
268
|
|
|
244
269
|
try {
|
|
245
270
|
if (detection.kind === "pdf") {
|
|
246
|
-
const pdf = await readSeekPdf(absolutePath, p.image, { signal });
|
|
271
|
+
const pdf = await readSeekPdf(absolutePath, p.image, { page: pdfPage, signal });
|
|
247
272
|
return succeed({
|
|
248
|
-
content: [{ type: "text" as const, text: formatPdfAnalysis(pdf) }],
|
|
273
|
+
content: [{ type: "text" as const, text: truncateDocumentText(formatPdfAnalysis(pdf)) }],
|
|
249
274
|
details: {},
|
|
250
275
|
});
|
|
251
276
|
}
|
|
@@ -269,6 +294,10 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
269
294
|
});
|
|
270
295
|
}
|
|
271
296
|
}
|
|
297
|
+
if (p.page !== undefined) {
|
|
298
|
+
const message = "The page parameter applies to PDFs only.";
|
|
299
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
300
|
+
}
|
|
272
301
|
throwIfAborted(signal);
|
|
273
302
|
const rawText = rawBuffer.toString("utf-8");
|
|
274
303
|
const normalized = normalizeToLF(stripBom(rawText).text);
|
|
@@ -548,6 +577,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
548
577
|
path: filePathParam(),
|
|
549
578
|
offset: optionalIntOrString("Start line (1-indexed)"),
|
|
550
579
|
limit: optionalIntOrString("Maximum lines to return"),
|
|
580
|
+
page: optionalIntOrString("One-based PDF page; defaults to 1"),
|
|
551
581
|
symbol: Type.Optional(Type.String({ description: "Symbol name to read" })),
|
|
552
582
|
map: mapParam(),
|
|
553
583
|
bundle: Type.Optional(
|
package/src/readseek-client.ts
CHANGED
|
@@ -358,6 +358,7 @@ function defaultReadSeekDir(): string | null {
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
const DEFAULT_READSEEK_TIMEOUT_MS = 120_000;
|
|
361
|
+
const DEFAULT_READSEEK_VISION_TIMEOUT_MS = 30 * 60_000;
|
|
361
362
|
|
|
362
363
|
function readSeekTimeoutMs(): number {
|
|
363
364
|
return resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_TIMEOUT_MS;
|
|
@@ -480,6 +481,25 @@ async function runReadSeek(args: string[], options: RunReadSeekOptions = {}): Pr
|
|
|
480
481
|
return JSON.parse(stdout) as unknown;
|
|
481
482
|
}
|
|
482
483
|
|
|
484
|
+
export interface ReadSeekViewOptions {
|
|
485
|
+
node?: string;
|
|
486
|
+
page?: number;
|
|
487
|
+
kind?: string;
|
|
488
|
+
depth?: number;
|
|
489
|
+
outline?: boolean;
|
|
490
|
+
signal?: AbortSignal;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function readSeekView(filePath: string, options: ReadSeekViewOptions = {}): Promise<string> {
|
|
494
|
+
const args = ["view", filePath];
|
|
495
|
+
if (options.node !== undefined) args.push("--node", options.node);
|
|
496
|
+
if (options.page !== undefined) args.push("--page", String(options.page));
|
|
497
|
+
if (options.kind !== undefined) args.push("--kind", options.kind);
|
|
498
|
+
if (options.depth !== undefined) args.push("--depth", String(options.depth));
|
|
499
|
+
if (options.outline) args.push("--outline");
|
|
500
|
+
return runReadSeekRaw(args, { signal: options.signal });
|
|
501
|
+
}
|
|
502
|
+
|
|
483
503
|
let visionInvocationTail = Promise.resolve();
|
|
484
504
|
|
|
485
505
|
/**
|
|
@@ -493,11 +513,36 @@ async function runReadSeekVision(args: string[], options: RunReadSeekOptions = {
|
|
|
493
513
|
});
|
|
494
514
|
const predecessor = visionInvocationTail;
|
|
495
515
|
visionInvocationTail = predecessor.then(() => gate);
|
|
516
|
+
const signal = options.signal;
|
|
496
517
|
|
|
497
|
-
await predecessor;
|
|
498
518
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
519
|
+
if (signal) {
|
|
520
|
+
signal.throwIfAborted();
|
|
521
|
+
await new Promise<void>((resolve, reject) => {
|
|
522
|
+
const onAbort = (): void => {
|
|
523
|
+
signal.removeEventListener("abort", onAbort);
|
|
524
|
+
reject(signal.reason);
|
|
525
|
+
};
|
|
526
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
527
|
+
predecessor.then(
|
|
528
|
+
() => {
|
|
529
|
+
signal.removeEventListener("abort", onAbort);
|
|
530
|
+
resolve();
|
|
531
|
+
},
|
|
532
|
+
(error) => {
|
|
533
|
+
signal.removeEventListener("abort", onAbort);
|
|
534
|
+
reject(error);
|
|
535
|
+
},
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
} else {
|
|
539
|
+
await predecessor;
|
|
540
|
+
}
|
|
541
|
+
signal?.throwIfAborted();
|
|
542
|
+
return await runReadSeek(args, {
|
|
543
|
+
...options,
|
|
544
|
+
timeoutMs: options.timeoutMs ?? resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_VISION_TIMEOUT_MS,
|
|
545
|
+
});
|
|
501
546
|
} finally {
|
|
502
547
|
release();
|
|
503
548
|
}
|
|
@@ -925,10 +970,12 @@ function parsePdfOutput(value: unknown): ReadSeekPdfOutput {
|
|
|
925
970
|
export async function readSeekPdf(
|
|
926
971
|
filePath: string,
|
|
927
972
|
mode: "none" | ReadSeekImageMode,
|
|
928
|
-
options: { signal?: AbortSignal } = {},
|
|
973
|
+
options: { page?: number; signal?: AbortSignal } = {},
|
|
929
974
|
): Promise<ReadSeekPdfOutput> {
|
|
930
975
|
const run = mode === "none" ? runReadSeek : runReadSeekVision;
|
|
931
|
-
|
|
976
|
+
const args = ["read", "--image", mode, filePath];
|
|
977
|
+
if (options.page !== undefined) args.push("--page", String(options.page));
|
|
978
|
+
return parsePdfOutput(await run(args, { signal: options.signal }));
|
|
932
979
|
}
|
|
933
980
|
|
|
934
981
|
// --- Rename ---
|
package/src/view.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_MAX_BYTES,
|
|
4
|
+
DEFAULT_MAX_LINES,
|
|
5
|
+
truncateHead,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
|
|
10
|
+
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
11
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
12
|
+
import { classifyReadSeekFailure, readSeekView } from "./readseek-client.js";
|
|
13
|
+
import { buildToolErrorResult } from "./readseek-value.js";
|
|
14
|
+
import { filePathParam, optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
15
|
+
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
16
|
+
import {
|
|
17
|
+
clampLineToWidth,
|
|
18
|
+
clampLinesToWidth,
|
|
19
|
+
linkToolPath,
|
|
20
|
+
renderErrorResult,
|
|
21
|
+
renderPendingResult,
|
|
22
|
+
resolveRenderResultContext,
|
|
23
|
+
summaryLine,
|
|
24
|
+
} from "./tui-render-utils.js";
|
|
25
|
+
|
|
26
|
+
const NODE_KINDS = [
|
|
27
|
+
"artifact",
|
|
28
|
+
"footer",
|
|
29
|
+
"header",
|
|
30
|
+
"heading",
|
|
31
|
+
"marginal_label",
|
|
32
|
+
"page",
|
|
33
|
+
"page_number",
|
|
34
|
+
"paragraph",
|
|
35
|
+
"section",
|
|
36
|
+
"structural_section",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
const VIEW_PROMPT_METADATA = defineToolPromptMetadata({
|
|
40
|
+
promptUrl: new URL("../prompts/view.md", import.meta.url),
|
|
41
|
+
promptSnippet: "View the structure or selected content of an indexed PDF",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
interface ViewParams {
|
|
45
|
+
path: string;
|
|
46
|
+
node?: string;
|
|
47
|
+
page?: number | string;
|
|
48
|
+
kind?: (typeof NODE_KINDS)[number];
|
|
49
|
+
depth?: number | string;
|
|
50
|
+
outline?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function executeView(opts: {
|
|
54
|
+
params: unknown;
|
|
55
|
+
signal: AbortSignal | undefined;
|
|
56
|
+
cwd: string;
|
|
57
|
+
}): Promise<any> {
|
|
58
|
+
const { params, signal, cwd } = opts;
|
|
59
|
+
const input = params as ViewParams;
|
|
60
|
+
const page = coerceObviousBase10Int(input.page, "page");
|
|
61
|
+
if (!page.ok || (page.value !== undefined && page.value < 1)) {
|
|
62
|
+
const message = page.ok
|
|
63
|
+
? `Invalid page: expected a positive integer, received ${page.value}.`
|
|
64
|
+
: page.message;
|
|
65
|
+
return buildToolErrorResult("view", "invalid-page", message, { path: input.path });
|
|
66
|
+
}
|
|
67
|
+
const depth = coerceObviousBase10Int(input.depth, "depth");
|
|
68
|
+
if (!depth.ok || (depth.value !== undefined && depth.value < 0)) {
|
|
69
|
+
const message = depth.ok
|
|
70
|
+
? `Invalid depth: expected a non-negative integer, received ${depth.value}.`
|
|
71
|
+
: depth.message;
|
|
72
|
+
return buildToolErrorResult("view", "invalid-depth", message, { path: input.path });
|
|
73
|
+
}
|
|
74
|
+
const node = input.node?.trim();
|
|
75
|
+
if (input.node !== undefined && !node) {
|
|
76
|
+
return buildToolErrorResult("view", "invalid-node", "Invalid node: expected a non-empty ID.", {
|
|
77
|
+
path: input.path,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const filePath = resolveToCwd(input.path, cwd);
|
|
82
|
+
try {
|
|
83
|
+
const output = await readSeekView(filePath, {
|
|
84
|
+
node,
|
|
85
|
+
page: page.value,
|
|
86
|
+
kind: input.kind,
|
|
87
|
+
depth: depth.value,
|
|
88
|
+
outline: input.outline,
|
|
89
|
+
signal,
|
|
90
|
+
});
|
|
91
|
+
const truncation = truncateHead(output, {
|
|
92
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
93
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
94
|
+
});
|
|
95
|
+
const notice = truncation.truncated
|
|
96
|
+
? "\n[… document view truncated; narrow it with page, node, kind, or depth]"
|
|
97
|
+
: "";
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: `${truncation.content}${notice}` }],
|
|
100
|
+
details: {
|
|
101
|
+
readSeekValue: {
|
|
102
|
+
tool: "view",
|
|
103
|
+
path: filePath,
|
|
104
|
+
truncated: truncation.truncated,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const failure = classifyReadSeekFailure(error);
|
|
110
|
+
return buildToolErrorResult("view", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function registerViewTool(pi: ExtensionAPI) {
|
|
115
|
+
return registerReadSeekTool(pi, {
|
|
116
|
+
name: "readSeek_view",
|
|
117
|
+
label: "View",
|
|
118
|
+
description: VIEW_PROMPT_METADATA.description,
|
|
119
|
+
promptSnippet: VIEW_PROMPT_METADATA.promptSnippet,
|
|
120
|
+
promptGuidelines: VIEW_PROMPT_METADATA.promptGuidelines,
|
|
121
|
+
parameters: Type.Object({
|
|
122
|
+
path: filePathParam(),
|
|
123
|
+
node: Type.Optional(Type.String({ description: "Node ID to use as the view root" })),
|
|
124
|
+
page: optionalIntOrString("One-based source page"),
|
|
125
|
+
kind: Type.Optional(Type.Union(NODE_KINDS.map((kind) => Type.Literal(kind)), {
|
|
126
|
+
description: "Node kind filter",
|
|
127
|
+
})),
|
|
128
|
+
depth: optionalIntOrString("Maximum depth below the selected roots"),
|
|
129
|
+
outline: Type.Optional(Type.Boolean({ description: "Return outline nodes only" })),
|
|
130
|
+
}),
|
|
131
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
132
|
+
return executeView({ params, signal, cwd: ctx.cwd });
|
|
133
|
+
},
|
|
134
|
+
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
135
|
+
const context = rest[0] ?? {};
|
|
136
|
+
const cwd = context.cwd ?? process.cwd();
|
|
137
|
+
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
138
|
+
const text = `${theme.fg("toolTitle", theme.bold("view"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
139
|
+
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
140
|
+
},
|
|
141
|
+
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
142
|
+
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
143
|
+
if (isPartial) return renderPendingResult("pending view", width, theme);
|
|
144
|
+
const textContent = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
145
|
+
if (isError || result.isError) {
|
|
146
|
+
return renderErrorResult(textContent, { expanded, width, fallback: "view failed", theme });
|
|
147
|
+
}
|
|
148
|
+
const lines = textContent.split("\n").filter(Boolean).length;
|
|
149
|
+
let text = summaryLine(`loaded ${lines} document ${lines === 1 ? "line" : "lines"}`, {
|
|
150
|
+
hidden: !!textContent && !expanded,
|
|
151
|
+
theme,
|
|
152
|
+
style: "success",
|
|
153
|
+
});
|
|
154
|
+
if (expanded && textContent) text += `\n${textContent}`;
|
|
155
|
+
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|