pi-readseek 0.3.9 → 0.3.11
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 -2
- package/src/edit.ts +3 -13
- package/src/grep.ts +8 -72
- package/src/image-detect.ts +47 -0
- package/src/read.ts +20 -266
- package/src/readseek-value.ts +36 -0
- package/src/sg.ts +13 -74
- package/src/tool-prompt-metadata.ts +1 -2
- package/src/tui-render-utils.ts +27 -0
- package/src/write.ts +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
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.3.
|
|
42
|
+
"@jarkkojs/readseek": "^0.3.8",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"ignore": "^7.0.5",
|
|
45
45
|
"picomatch": "^4.0.4",
|
package/src/edit.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { resolveSyntaxValidateMode, type SyntaxValidateOptions } from "./syntax-
|
|
|
19
19
|
import { replaceSymbol } from "./replace-symbol.js";
|
|
20
20
|
import { buildEditPreviewKey, buildPendingEditPreviewData, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
|
|
21
21
|
import { buildDiffData, type DiffBlockRange } from "./diff-data.js";
|
|
22
|
-
import { clampLineToWidth, clampLinesToWidth,
|
|
22
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
23
23
|
import { DiffPreviewComponent } from "./tui-diff-component.js";
|
|
24
24
|
|
|
25
25
|
import { resolveEditDiffDisplay } from "./readseek-settings.js";
|
|
@@ -86,11 +86,6 @@ type HashlineParams = Static<typeof hashlineEditSchema>;
|
|
|
86
86
|
const EDIT_PROMPT_METADATA = defineToolPromptMetadata({
|
|
87
87
|
promptUrl: new URL("../prompts/edit.md", import.meta.url),
|
|
88
88
|
promptSnippet: "Edit files using hash-verified anchors from read/grep/search/write",
|
|
89
|
-
promptGuidelines: [
|
|
90
|
-
"Use edit for changes to existing files; read or search first and copy fresh LINE:HASH anchors.",
|
|
91
|
-
"Prefer edit anchored set_line, replace_lines, and insert_after over shell rewrites.",
|
|
92
|
-
"Use edit replace only when anchored edits are impractical.",
|
|
93
|
-
],
|
|
94
89
|
});
|
|
95
90
|
|
|
96
91
|
function buildEditError(
|
|
@@ -644,13 +639,9 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
644
639
|
return textComponent;
|
|
645
640
|
},
|
|
646
641
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
647
|
-
const
|
|
648
|
-
rest[0] ?? options ?? {};
|
|
649
|
-
const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
|
|
650
|
-
const isError = context.isError ?? false;
|
|
642
|
+
const { isPartial, isError, expanded: baseExpanded, width, context } = resolveRenderResultContext(options, rest);
|
|
651
643
|
|
|
652
644
|
if (isPartial) {
|
|
653
|
-
const width = (context as any).width ?? (options as any)?.width;
|
|
654
645
|
return new Text(clampLinesToWidth([summaryLine("pending edit")], width).join("\n"), 0, 0);
|
|
655
646
|
}
|
|
656
647
|
|
|
@@ -678,8 +669,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
678
669
|
semanticClassification: semanticClassification as any,
|
|
679
670
|
});
|
|
680
671
|
|
|
681
|
-
const expanded =
|
|
682
|
-
const width = (context as any).width ?? (options as any)?.width;
|
|
672
|
+
const expanded = baseExpanded || resolveEditDiffDisplay() === "expanded";
|
|
683
673
|
const diffData = (details as any).diffData;
|
|
684
674
|
const stats = diffData?.stats ?? { added: 0, removed: 0 };
|
|
685
675
|
let text = "";
|
package/src/grep.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
|
7
7
|
import { normalizeToLF, stripBom, hasBareCarriageReturn } from "./edit-diff.js";
|
|
8
8
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
9
9
|
import { ensureHashInit, formatHashlineDisplay, escapeControlCharsForDisplay } from "./hashline.js";
|
|
10
|
-
import {
|
|
10
|
+
import { buildReadseekLine, buildToolErrorResult } from "./readseek-value.js";
|
|
11
11
|
import { buildGrepOutput } from "./grep-output.js";
|
|
12
12
|
|
|
13
13
|
import { getOrGenerateMap } from "./map-cache.js";
|
|
@@ -17,16 +17,11 @@ import { throwIfAborted } from "./runtime.js";
|
|
|
17
17
|
import { Text } from "@earendil-works/pi-tui";
|
|
18
18
|
import { formatGrepCallText, formatGrepResultText } from "./grep-render-helpers.js";
|
|
19
19
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
20
|
-
import { clampLineToWidth, clampLinesToWidth,
|
|
20
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
21
21
|
|
|
22
22
|
const GREP_PROMPT_METADATA = defineToolPromptMetadata({
|
|
23
23
|
promptUrl: new URL("../prompts/grep.md", import.meta.url),
|
|
24
24
|
promptSnippet: "Search file contents and return edit-ready hashline anchors",
|
|
25
|
-
promptGuidelines: [
|
|
26
|
-
"Use grep for text search across files instead of bash grep or rg.",
|
|
27
|
-
"Use grep summary mode when you only need matching files or counts.",
|
|
28
|
-
"Use search instead of grep when the query depends on code structure.",
|
|
29
|
-
],
|
|
30
25
|
});
|
|
31
26
|
|
|
32
27
|
const grepSchema = Type.Object({
|
|
@@ -314,76 +309,23 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
|
|
|
314
309
|
const rawParams = params as GrepParams;
|
|
315
310
|
const context = coerceObviousBase10Int(rawParams.context, "context");
|
|
316
311
|
if (!context.ok) {
|
|
317
|
-
return
|
|
318
|
-
content: [{ type: "text", text: context.message }],
|
|
319
|
-
isError: true,
|
|
320
|
-
details: {
|
|
321
|
-
readseekValue: {
|
|
322
|
-
tool: "grep",
|
|
323
|
-
ok: false,
|
|
324
|
-
error: buildReadseekError("invalid-params-combo", context.message),
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
};
|
|
312
|
+
return buildToolErrorResult("grep", "invalid-params-combo", context.message);
|
|
328
313
|
}
|
|
329
314
|
const limit = coerceObviousBase10Int(rawParams.limit, "limit");
|
|
330
315
|
if (!limit.ok) {
|
|
331
|
-
return
|
|
332
|
-
content: [{ type: "text", text: limit.message }],
|
|
333
|
-
isError: true,
|
|
334
|
-
details: {
|
|
335
|
-
readseekValue: {
|
|
336
|
-
tool: "grep",
|
|
337
|
-
ok: false,
|
|
338
|
-
error: buildReadseekError("invalid-limit", limit.message),
|
|
339
|
-
},
|
|
340
|
-
},
|
|
341
|
-
};
|
|
316
|
+
return buildToolErrorResult("grep", "invalid-limit", limit.message);
|
|
342
317
|
}
|
|
343
318
|
const scopeContext = coerceObviousBase10Int(rawParams.scopeContext, "scopeContext");
|
|
344
319
|
if (!scopeContext.ok) {
|
|
345
|
-
return
|
|
346
|
-
content: [{ type: "text", text: scopeContext.message }],
|
|
347
|
-
isError: true,
|
|
348
|
-
details: {
|
|
349
|
-
readseekValue: {
|
|
350
|
-
tool: "grep",
|
|
351
|
-
ok: false,
|
|
352
|
-
error: buildReadseekError("invalid-params-combo", scopeContext.message),
|
|
353
|
-
},
|
|
354
|
-
},
|
|
355
|
-
};
|
|
320
|
+
return buildToolErrorResult("grep", "invalid-params-combo", scopeContext.message);
|
|
356
321
|
}
|
|
357
322
|
if (scopeContext.value !== undefined && rawParams.scope !== "symbol") {
|
|
358
323
|
const message = 'Invalid scopeContext: requires scope: "symbol". For normal surrounding-line context outside symbol scope, use the `context` parameter.';
|
|
359
|
-
return
|
|
360
|
-
content: [{
|
|
361
|
-
type: "text",
|
|
362
|
-
text: message,
|
|
363
|
-
}],
|
|
364
|
-
isError: true,
|
|
365
|
-
details: {
|
|
366
|
-
readseekValue: {
|
|
367
|
-
tool: "grep",
|
|
368
|
-
ok: false,
|
|
369
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
370
|
-
},
|
|
371
|
-
},
|
|
372
|
-
};
|
|
324
|
+
return buildToolErrorResult("grep", "invalid-params-combo", message);
|
|
373
325
|
}
|
|
374
326
|
if (scopeContext.value !== undefined && scopeContext.value < 0) {
|
|
375
327
|
const message = `Invalid scopeContext: expected a non-negative integer, received ${scopeContext.value}.`;
|
|
376
|
-
return
|
|
377
|
-
content: [{ type: "text", text: message }],
|
|
378
|
-
isError: true,
|
|
379
|
-
details: {
|
|
380
|
-
readseekValue: {
|
|
381
|
-
tool: "grep",
|
|
382
|
-
ok: false,
|
|
383
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
};
|
|
328
|
+
return buildToolErrorResult("grep", "invalid-params-combo", message);
|
|
387
329
|
}
|
|
388
330
|
const p: GrepParams = {
|
|
389
331
|
...rawParams,
|
|
@@ -716,13 +658,7 @@ if (p.scope === "symbol" && !summary) {
|
|
|
716
658
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
717
659
|
},
|
|
718
660
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
719
|
-
const
|
|
720
|
-
rest[0] ?? options ?? {};
|
|
721
|
-
const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
|
|
722
|
-
const isError = context.isError ?? false;
|
|
723
|
-
const expanded = isRendererExpanded(options as any, context as any);
|
|
724
|
-
const cwd = context.cwd ?? process.cwd();
|
|
725
|
-
const width = (context as any).width ?? (options as any)?.width;
|
|
661
|
+
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
726
662
|
|
|
727
663
|
if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending search")], width).join("\n"), 0, 0);
|
|
728
664
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
2
|
+
|
|
3
|
+
function startsWithBytes(buffer: Buffer, bytes: number[]): boolean {
|
|
4
|
+
return buffer.length >= bytes.length && bytes.every((byte, index) => buffer[index] === byte);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function startsWithAscii(buffer: Buffer, offset: number, text: string): boolean {
|
|
8
|
+
if (buffer.length < offset + text.length) return false;
|
|
9
|
+
for (let index = 0; index < text.length; index++) {
|
|
10
|
+
if (buffer[offset + index] !== text.charCodeAt(index)) return false;
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readUint32BE(buffer: Buffer, offset: number): number {
|
|
16
|
+
return (
|
|
17
|
+
((buffer[offset] ?? 0) * 0x1000000) +
|
|
18
|
+
((buffer[offset + 1] ?? 0) << 16) +
|
|
19
|
+
((buffer[offset + 2] ?? 0) << 8) +
|
|
20
|
+
(buffer[offset + 3] ?? 0)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isPng(buffer: Buffer): boolean {
|
|
25
|
+
return buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isAnimatedPng(buffer: Buffer): boolean {
|
|
29
|
+
let offset = PNG_SIGNATURE.length;
|
|
30
|
+
while (offset + 8 <= buffer.length) {
|
|
31
|
+
const chunkLength = readUint32BE(buffer, offset);
|
|
32
|
+
const chunkTypeOffset = offset + 4;
|
|
33
|
+
if (startsWithAscii(buffer, chunkTypeOffset, "acTL")) return true;
|
|
34
|
+
if (startsWithAscii(buffer, chunkTypeOffset, "IDAT")) return false;
|
|
35
|
+
const nextOffset = offset + 8 + chunkLength + 4;
|
|
36
|
+
if (nextOffset <= offset || nextOffset > buffer.length) return false;
|
|
37
|
+
offset = nextOffset;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isSupportedImageBuffer(buffer: Buffer): boolean {
|
|
43
|
+
if (startsWithBytes(buffer, [0xff, 0xd8, 0xff])) return buffer[3] !== 0xf7;
|
|
44
|
+
if (startsWithBytes(buffer, PNG_SIGNATURE)) return isPng(buffer) && !isAnimatedPng(buffer);
|
|
45
|
+
if (startsWithAscii(buffer, 0, "GIF")) return true;
|
|
46
|
+
return startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP");
|
|
47
|
+
}
|
package/src/read.ts
CHANGED
|
@@ -11,8 +11,9 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
|
11
11
|
import { readFile as fsReadFile } from "fs/promises";
|
|
12
12
|
import { normalizeToLF, stripBom, hasBareCarriageReturn } from "./edit-diff.js";
|
|
13
13
|
import { ensureHashInit, escapeControlCharsForDisplay } from "./hashline.js";
|
|
14
|
-
import {
|
|
14
|
+
import { buildReadseekWarning, buildToolErrorResult, renderReadseekLines, type ReadseekLine, type ReadseekWarning } from "./readseek-value.js";
|
|
15
15
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
16
|
+
import { isSupportedImageBuffer } from "./image-detect.js";
|
|
16
17
|
import { resolveToCwd } from "./path-utils.js";
|
|
17
18
|
import { throwIfAborted } from "./runtime.js";
|
|
18
19
|
import { getOrGenerateMap } from "./map-cache.js";
|
|
@@ -26,17 +27,11 @@ import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
|
26
27
|
import { readseekRead } from "./readseek-client.js";
|
|
27
28
|
import { Text } from "@earendil-works/pi-tui";
|
|
28
29
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
29
|
-
import { clampLineToWidth, clampLinesToWidth,
|
|
30
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
30
31
|
|
|
31
32
|
const READ_PROMPT_METADATA = defineToolPromptMetadata({
|
|
32
33
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
33
34
|
promptSnippet: "Read text files or images; text reads include hashline anchors and optional maps/symbol lookup",
|
|
34
|
-
promptGuidelines: [
|
|
35
|
-
"Use read instead of bash cat/head/tail/sed for file inspection.",
|
|
36
|
-
"Use read for images/screenshots; supported images return attachments like stock pi read.",
|
|
37
|
-
"Use read offset/limit, symbol, or map to keep large files focused.",
|
|
38
|
-
"Use read anchors as fresh inputs for edit.",
|
|
39
|
-
],
|
|
40
35
|
});
|
|
41
36
|
|
|
42
37
|
interface ReadParams {
|
|
@@ -58,55 +53,6 @@ function splitReadseekLines(text: string): string[] {
|
|
|
58
53
|
return withoutTrailingTerminator.split("\n");
|
|
59
54
|
}
|
|
60
55
|
|
|
61
|
-
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
62
|
-
|
|
63
|
-
function startsWithBytes(buffer: Buffer, bytes: number[]): boolean {
|
|
64
|
-
return buffer.length >= bytes.length && bytes.every((byte, index) => buffer[index] === byte);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function startsWithAscii(buffer: Buffer, offset: number, text: string): boolean {
|
|
68
|
-
if (buffer.length < offset + text.length) return false;
|
|
69
|
-
for (let index = 0; index < text.length; index++) {
|
|
70
|
-
if (buffer[offset + index] !== text.charCodeAt(index)) return false;
|
|
71
|
-
}
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function readUint32BE(buffer: Buffer, offset: number): number {
|
|
76
|
-
return (
|
|
77
|
-
((buffer[offset] ?? 0) * 0x1000000) +
|
|
78
|
-
((buffer[offset + 1] ?? 0) << 16) +
|
|
79
|
-
((buffer[offset + 2] ?? 0) << 8) +
|
|
80
|
-
(buffer[offset + 3] ?? 0)
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isPng(buffer: Buffer): boolean {
|
|
85
|
-
return buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function isAnimatedPng(buffer: Buffer): boolean {
|
|
89
|
-
let offset = PNG_SIGNATURE.length;
|
|
90
|
-
while (offset + 8 <= buffer.length) {
|
|
91
|
-
const chunkLength = readUint32BE(buffer, offset);
|
|
92
|
-
const chunkTypeOffset = offset + 4;
|
|
93
|
-
if (startsWithAscii(buffer, chunkTypeOffset, "acTL")) return true;
|
|
94
|
-
if (startsWithAscii(buffer, chunkTypeOffset, "IDAT")) return false;
|
|
95
|
-
const nextOffset = offset + 8 + chunkLength + 4;
|
|
96
|
-
if (nextOffset <= offset || nextOffset > buffer.length) return false;
|
|
97
|
-
offset = nextOffset;
|
|
98
|
-
}
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function isSupportedImageBuffer(buffer: Buffer): boolean {
|
|
103
|
-
if (startsWithBytes(buffer, [0xff, 0xd8, 0xff])) return buffer[3] !== 0xf7;
|
|
104
|
-
if (startsWithBytes(buffer, PNG_SIGNATURE)) return isPng(buffer) && !isAnimatedPng(buffer);
|
|
105
|
-
if (startsWithAscii(buffer, 0, "GIF")) return true;
|
|
106
|
-
return startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
56
|
export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}) {
|
|
111
57
|
const toolConfig = {
|
|
112
58
|
callable: true,
|
|
@@ -151,63 +97,19 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
151
97
|
const rawParams = params as ReadParams;
|
|
152
98
|
const offset = coerceObviousBase10Int(rawParams.offset, "offset");
|
|
153
99
|
if (!offset.ok) {
|
|
154
|
-
return {
|
|
155
|
-
content: [{ type: "text", text: offset.message }],
|
|
156
|
-
isError: true,
|
|
157
|
-
details: {
|
|
158
|
-
readseekValue: {
|
|
159
|
-
tool: "read",
|
|
160
|
-
ok: false,
|
|
161
|
-
path: rawParams.path,
|
|
162
|
-
error: buildReadseekError("invalid-offset", offset.message),
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
};
|
|
100
|
+
return buildToolErrorResult("read", "invalid-offset", offset.message, { path: rawParams.path });
|
|
166
101
|
}
|
|
167
102
|
const limit = coerceObviousBase10Int(rawParams.limit, "limit");
|
|
168
103
|
if (!limit.ok) {
|
|
169
|
-
return {
|
|
170
|
-
content: [{ type: "text", text: limit.message }],
|
|
171
|
-
isError: true,
|
|
172
|
-
details: {
|
|
173
|
-
readseekValue: {
|
|
174
|
-
tool: "read",
|
|
175
|
-
ok: false,
|
|
176
|
-
path: rawParams.path,
|
|
177
|
-
error: buildReadseekError("invalid-limit", limit.message),
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
};
|
|
104
|
+
return buildToolErrorResult("read", "invalid-limit", limit.message, { path: rawParams.path });
|
|
181
105
|
}
|
|
182
106
|
if (limit.value !== undefined && limit.value < 1) {
|
|
183
107
|
const message = `Invalid limit: expected a positive integer, received ${limit.value}.`;
|
|
184
|
-
return {
|
|
185
|
-
content: [{ type: "text", text: message }],
|
|
186
|
-
isError: true,
|
|
187
|
-
details: {
|
|
188
|
-
readseekValue: {
|
|
189
|
-
tool: "read",
|
|
190
|
-
ok: false,
|
|
191
|
-
path: rawParams.path,
|
|
192
|
-
error: buildReadseekError("invalid-limit", message),
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
};
|
|
108
|
+
return buildToolErrorResult("read", "invalid-limit", message, { path: rawParams.path });
|
|
196
109
|
}
|
|
197
110
|
if (offset.value !== undefined && offset.value < 1) {
|
|
198
111
|
const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
|
|
199
|
-
return {
|
|
200
|
-
content: [{ type: "text", text: message }],
|
|
201
|
-
isError: true,
|
|
202
|
-
details: {
|
|
203
|
-
readseekValue: {
|
|
204
|
-
tool: "read",
|
|
205
|
-
ok: false,
|
|
206
|
-
path: rawParams.path,
|
|
207
|
-
error: buildReadseekError("invalid-offset", message),
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
};
|
|
112
|
+
return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
|
|
211
113
|
}
|
|
212
114
|
const p = {
|
|
213
115
|
...rawParams,
|
|
@@ -218,18 +120,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
218
120
|
const trimmedSymbol = typeof rawParams.symbol === "string" ? rawParams.symbol.trim() : "";
|
|
219
121
|
if (trimmedSymbol.length === 0) {
|
|
220
122
|
const message = "Invalid symbol: expected a non-empty string.";
|
|
221
|
-
return {
|
|
222
|
-
content: [{ type: "text", text: message }],
|
|
223
|
-
isError: true,
|
|
224
|
-
details: {
|
|
225
|
-
readseekValue: {
|
|
226
|
-
tool: "read",
|
|
227
|
-
ok: false,
|
|
228
|
-
path: rawParams.path,
|
|
229
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
};
|
|
123
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
233
124
|
}
|
|
234
125
|
p.symbol = trimmedSymbol;
|
|
235
126
|
}
|
|
@@ -246,63 +137,19 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
246
137
|
throwIfAborted(signal);
|
|
247
138
|
if (p.symbol && (p.offset !== undefined || p.limit !== undefined)) {
|
|
248
139
|
const message = "Cannot combine symbol with offset/limit. Use one or the other.";
|
|
249
|
-
return {
|
|
250
|
-
content: [{ type: "text", text: message }],
|
|
251
|
-
isError: true,
|
|
252
|
-
details: {
|
|
253
|
-
readseekValue: {
|
|
254
|
-
tool: "read",
|
|
255
|
-
ok: false,
|
|
256
|
-
path: rawParams.path,
|
|
257
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
};
|
|
140
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
261
141
|
}
|
|
262
142
|
if (p.bundle && !p.symbol) {
|
|
263
143
|
const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
|
|
264
|
-
return {
|
|
265
|
-
content: [{ type: "text", text: message }],
|
|
266
|
-
isError: true,
|
|
267
|
-
details: {
|
|
268
|
-
readseekValue: {
|
|
269
|
-
tool: "read",
|
|
270
|
-
ok: false,
|
|
271
|
-
path: rawParams.path,
|
|
272
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
};
|
|
144
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
276
145
|
}
|
|
277
146
|
if (p.bundle && p.map) {
|
|
278
147
|
const message = "Cannot combine bundle with map. Use one or the other.";
|
|
279
|
-
return {
|
|
280
|
-
content: [{ type: "text", text: message }],
|
|
281
|
-
isError: true,
|
|
282
|
-
details: {
|
|
283
|
-
readseekValue: {
|
|
284
|
-
tool: "read",
|
|
285
|
-
ok: false,
|
|
286
|
-
path: rawParams.path,
|
|
287
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
};
|
|
148
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
291
149
|
}
|
|
292
150
|
if (p.map && p.symbol) {
|
|
293
151
|
const message = "Cannot combine map with symbol. Use one or the other.";
|
|
294
|
-
return {
|
|
295
|
-
content: [{ type: "text", text: message }],
|
|
296
|
-
isError: true,
|
|
297
|
-
details: {
|
|
298
|
-
readseekValue: {
|
|
299
|
-
tool: "read",
|
|
300
|
-
ok: false,
|
|
301
|
-
path: rawParams.path,
|
|
302
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
};
|
|
152
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
306
153
|
}
|
|
307
154
|
// Delegate images to the built-in read tool
|
|
308
155
|
throwIfAborted(signal);
|
|
@@ -320,69 +167,18 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
320
167
|
const code = err?.code;
|
|
321
168
|
if (code === "EISDIR") {
|
|
322
169
|
const message = `Path is a directory: ${rawPath}. Use ls to inspect directories.`;
|
|
323
|
-
return {
|
|
324
|
-
content: [{ type: "text", text: message }],
|
|
325
|
-
isError: true,
|
|
326
|
-
details: {
|
|
327
|
-
readseekValue: {
|
|
328
|
-
tool: "read",
|
|
329
|
-
ok: false,
|
|
330
|
-
path: rawParams.path,
|
|
331
|
-
error: buildReadseekError(
|
|
332
|
-
"path-is-directory",
|
|
333
|
-
message,
|
|
334
|
-
`Use ls(${JSON.stringify(rawPath)}) to inspect directories.`,
|
|
335
|
-
),
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
};
|
|
170
|
+
return buildToolErrorResult("read", "path-is-directory", message, { path: rawParams.path, hint: `Use ls(${JSON.stringify(rawPath)}) to inspect directories.` });
|
|
339
171
|
}
|
|
340
172
|
if (code === "EACCES" || code === "EPERM") {
|
|
341
173
|
const message = `Permission denied — cannot access: ${rawPath}`;
|
|
342
|
-
return {
|
|
343
|
-
content: [{ type: "text", text: message }],
|
|
344
|
-
isError: true,
|
|
345
|
-
details: {
|
|
346
|
-
readseekValue: {
|
|
347
|
-
tool: "read",
|
|
348
|
-
ok: false,
|
|
349
|
-
path: rawParams.path,
|
|
350
|
-
error: buildReadseekError("permission-denied", message),
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
};
|
|
174
|
+
return buildToolErrorResult("read", "permission-denied", message, { path: rawParams.path });
|
|
354
175
|
}
|
|
355
176
|
if (code === "ENOENT") {
|
|
356
177
|
const message = `File not found: ${rawPath}`;
|
|
357
|
-
return {
|
|
358
|
-
content: [{ type: "text", text: message }],
|
|
359
|
-
isError: true,
|
|
360
|
-
details: {
|
|
361
|
-
readseekValue: {
|
|
362
|
-
tool: "read",
|
|
363
|
-
ok: false,
|
|
364
|
-
path: rawParams.path,
|
|
365
|
-
error: buildReadseekError("file-not-found", message),
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
};
|
|
178
|
+
return buildToolErrorResult("read", "file-not-found", message, { path: rawParams.path });
|
|
369
179
|
}
|
|
370
180
|
const message = `File not readable: ${rawPath}${err?.message ? ` — ${err.message}` : ""}`;
|
|
371
|
-
return {
|
|
372
|
-
content: [{ type: "text", text: message }],
|
|
373
|
-
isError: true,
|
|
374
|
-
details: {
|
|
375
|
-
readseekValue: {
|
|
376
|
-
tool: "read",
|
|
377
|
-
ok: false,
|
|
378
|
-
path: rawParams.path,
|
|
379
|
-
error: buildReadseekError("fs-error", message, undefined, {
|
|
380
|
-
fsCode: code,
|
|
381
|
-
fsMessage: err?.message,
|
|
382
|
-
}),
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
};
|
|
181
|
+
return buildToolErrorResult("read", "fs-error", message, { path: rawParams.path, details: { fsCode: code, fsMessage: err?.message } });
|
|
386
182
|
}
|
|
387
183
|
|
|
388
184
|
if (isSupportedImageBuffer(rawBuffer)) {
|
|
@@ -399,18 +195,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
399
195
|
let endIdx = p.limit !== undefined ? Math.min(startLine - 1 + p.limit, total) : total;
|
|
400
196
|
if (p.offset !== undefined && startLine > total) {
|
|
401
197
|
const message = `[offset ${p.offset} is past end of file (${total} lines)]`;
|
|
402
|
-
return {
|
|
403
|
-
content: [{ type: "text", text: message }],
|
|
404
|
-
isError: true,
|
|
405
|
-
details: {
|
|
406
|
-
readseekValue: {
|
|
407
|
-
tool: "read",
|
|
408
|
-
ok: false,
|
|
409
|
-
path: rawParams.path,
|
|
410
|
-
error: buildReadseekError("offset-past-end", message),
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
};
|
|
198
|
+
return buildToolErrorResult("read", "offset-past-end", message, { path: rawParams.path });
|
|
414
199
|
}
|
|
415
200
|
let symbolMatch: SymbolMatch | undefined;
|
|
416
201
|
let symbolFileMap: Awaited<ReturnType<typeof getOrGenerateMap>> | null = null;
|
|
@@ -550,23 +335,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
550
335
|
} catch (err: any) {
|
|
551
336
|
const detail = err?.message ? ` — ${err.message}` : "";
|
|
552
337
|
const message = `readseek failed while reading ${rawPath}${detail}`;
|
|
553
|
-
return {
|
|
554
|
-
content: [{ type: "text", text: message }],
|
|
555
|
-
isError: true,
|
|
556
|
-
details: {
|
|
557
|
-
readseekValue: {
|
|
558
|
-
tool: "read",
|
|
559
|
-
ok: false,
|
|
560
|
-
path: rawParams.path,
|
|
561
|
-
error: buildReadseekError(
|
|
562
|
-
"readseek-error",
|
|
563
|
-
message,
|
|
564
|
-
"Ensure @jarkkojs/readseek and its npm platform package are installed.",
|
|
565
|
-
{ message: err?.message },
|
|
566
|
-
),
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
};
|
|
338
|
+
return buildToolErrorResult("read", "readseek-error", message, { path: rawParams.path, hint: "Ensure @jarkkojs/readseek and its npm platform package are installed.", details: { message: err?.message } });
|
|
570
339
|
}
|
|
571
340
|
const expectedLineCount = Math.max(0, endIdx - startLine + 1);
|
|
572
341
|
const invalidLine = readseekOutput.hashlines.find((line, index) => line.line !== startLine + index);
|
|
@@ -574,18 +343,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
574
343
|
const message = invalidLine
|
|
575
344
|
? `readseek returned non-sequential line ${invalidLine.line} for requested range ${startLine}-${endIdx}`
|
|
576
345
|
: `readseek returned ${readseekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
|
|
577
|
-
return {
|
|
578
|
-
content: [{ type: "text", text: message }],
|
|
579
|
-
isError: true,
|
|
580
|
-
details: {
|
|
581
|
-
readseekValue: {
|
|
582
|
-
tool: "read",
|
|
583
|
-
ok: false,
|
|
584
|
-
path: rawParams.path,
|
|
585
|
-
error: buildReadseekError("readseek-output-mismatch", message),
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
};
|
|
346
|
+
return buildToolErrorResult("read", "readseek-output-mismatch", message, { path: rawParams.path });
|
|
589
347
|
}
|
|
590
348
|
const readseekLines: ReadseekLine[] = readseekOutput.hashlines.map((line) => ({
|
|
591
349
|
line: line.line,
|
|
@@ -708,11 +466,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
708
466
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
709
467
|
},
|
|
710
468
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
711
|
-
const
|
|
712
|
-
const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
|
|
713
|
-
const isError = context.isError ?? false;
|
|
714
|
-
const expanded = isRendererExpanded(options as any, context as any);
|
|
715
|
-
const width = context.width ?? (options as any)?.width;
|
|
469
|
+
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
716
470
|
if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending read")], width).join("\n"), 0, 0);
|
|
717
471
|
|
|
718
472
|
const content = result.content?.[0];
|
package/src/readseek-value.ts
CHANGED
|
@@ -100,6 +100,42 @@ export function buildReadseekError(
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
export interface ToolErrorResult {
|
|
104
|
+
content: [{ type: "text"; text: string }];
|
|
105
|
+
isError: true;
|
|
106
|
+
details: { readseekValue: Record<string, unknown> };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the standard failure envelope shared by every read-family tool: a text
|
|
111
|
+
* content block plus a `readseekValue` carrying `ok: false` and a
|
|
112
|
+
* {@link buildReadseekError} payload.
|
|
113
|
+
*
|
|
114
|
+
* `path` is included only when provided, and `extra` is merged into
|
|
115
|
+
* `readseekValue` so callers can attach tool-specific fields (e.g. write's
|
|
116
|
+
* `lines`/`warnings`).
|
|
117
|
+
*/
|
|
118
|
+
export function buildToolErrorResult(
|
|
119
|
+
tool: string,
|
|
120
|
+
code: string,
|
|
121
|
+
message: string,
|
|
122
|
+
opts: { path?: string; hint?: string; details?: unknown; extra?: Record<string, unknown> } = {},
|
|
123
|
+
): ToolErrorResult {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: message }],
|
|
126
|
+
isError: true,
|
|
127
|
+
details: {
|
|
128
|
+
readseekValue: {
|
|
129
|
+
tool,
|
|
130
|
+
...(opts.extra ?? {}),
|
|
131
|
+
ok: false,
|
|
132
|
+
...(opts.path !== undefined ? { path: opts.path } : {}),
|
|
133
|
+
error: buildReadseekError(code, message, opts.hint, opts.details),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
export function buildReadseekEditResult(input: {
|
|
104
140
|
ok?: boolean;
|
|
105
141
|
path: string;
|
package/src/sg.ts
CHANGED
|
@@ -5,12 +5,12 @@ 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
7
|
import { escapeControlCharsForDisplay } from "./hashline.js";
|
|
8
|
-
import {
|
|
8
|
+
import { buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
|
|
9
9
|
import { resolveToCwd } from "./path-utils.js";
|
|
10
10
|
import { isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
|
|
11
11
|
import { buildSgOutput } from "./sg-output.js";
|
|
12
12
|
|
|
13
|
-
import { clampLineToWidth, clampLinesToWidth,
|
|
13
|
+
import { clampLineToWidth, clampLinesToWidth, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
14
14
|
|
|
15
15
|
type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
|
|
16
16
|
|
|
@@ -47,11 +47,6 @@ export function mergeRanges(ranges: SgRange[]): SgRange[] {
|
|
|
47
47
|
const SG_PROMPT_METADATA = defineToolPromptMetadata({
|
|
48
48
|
promptUrl: new URL("../prompts/sg.md", import.meta.url),
|
|
49
49
|
promptSnippet: "Search code structurally with readseek and return edit-ready anchors",
|
|
50
|
-
promptGuidelines: [
|
|
51
|
-
"Use search when text search is too broad or brittle and the query depends on code shape.",
|
|
52
|
-
"Use search for calls, imports, declarations, JSX, and similar syntax patterns.",
|
|
53
|
-
"Use grep instead of search for plain text search.",
|
|
54
|
-
],
|
|
55
50
|
});
|
|
56
51
|
|
|
57
52
|
export function isSgAvailable(): boolean {
|
|
@@ -128,17 +123,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
128
123
|
const p = params as SgParams;
|
|
129
124
|
if (p.ignored && !p.others) {
|
|
130
125
|
const message = "Error: search parameter 'ignored' requires 'others'";
|
|
131
|
-
return
|
|
132
|
-
content: [{ type: "text", text: message }],
|
|
133
|
-
isError: true,
|
|
134
|
-
details: {
|
|
135
|
-
readseekValue: {
|
|
136
|
-
tool: "search",
|
|
137
|
-
ok: false,
|
|
138
|
-
error: buildReadseekError("invalid-parameter", message),
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
};
|
|
126
|
+
return buildToolErrorResult("search", "invalid-parameter", message);
|
|
142
127
|
}
|
|
143
128
|
const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
|
|
144
129
|
let searchPathIsFile = false;
|
|
@@ -149,47 +134,14 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
149
134
|
} catch (err: any) {
|
|
150
135
|
if (err?.code === "ENOENT") {
|
|
151
136
|
const message = `Error: path '${p.path ?? "."}' does not exist`;
|
|
152
|
-
return {
|
|
153
|
-
content: [{ type: "text", text: message }],
|
|
154
|
-
isError: true,
|
|
155
|
-
details: {
|
|
156
|
-
readseekValue: {
|
|
157
|
-
tool: "search",
|
|
158
|
-
ok: false,
|
|
159
|
-
path: p.path ?? searchPath,
|
|
160
|
-
error: buildReadseekError("path-not-found", message),
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
};
|
|
137
|
+
return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
|
|
164
138
|
}
|
|
165
139
|
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
166
140
|
const message = `Error: permission denied for path '${p.path ?? "."}'`;
|
|
167
|
-
return {
|
|
168
|
-
content: [{ type: "text", text: message }],
|
|
169
|
-
isError: true,
|
|
170
|
-
details: {
|
|
171
|
-
readseekValue: {
|
|
172
|
-
tool: "search",
|
|
173
|
-
ok: false,
|
|
174
|
-
path: p.path ?? searchPath,
|
|
175
|
-
error: buildReadseekError("permission-denied", message),
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
};
|
|
141
|
+
return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
|
|
179
142
|
}
|
|
180
143
|
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
181
|
-
return {
|
|
182
|
-
content: [{ type: "text", text: message }],
|
|
183
|
-
isError: true,
|
|
184
|
-
details: {
|
|
185
|
-
readseekValue: {
|
|
186
|
-
tool: "search",
|
|
187
|
-
ok: false,
|
|
188
|
-
path: p.path ?? searchPath,
|
|
189
|
-
error: buildReadseekError("fs-error", message, undefined, { fsCode: err?.code, fsMessage: err?.message }),
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
};
|
|
144
|
+
return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
|
|
193
145
|
}
|
|
194
146
|
|
|
195
147
|
try {
|
|
@@ -260,19 +212,12 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
260
212
|
} catch (err: any) {
|
|
261
213
|
const message = String(err?.message || err);
|
|
262
214
|
const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
|
|
263
|
-
return
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
ok: false,
|
|
270
|
-
error: missingReadseek
|
|
271
|
-
? buildReadseekError("readseek-not-installed", message, "Run npm install to install @jarkkojs/readseek.")
|
|
272
|
-
: buildReadseekError("readseek-execution-error", message),
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
};
|
|
215
|
+
return buildToolErrorResult(
|
|
216
|
+
"search",
|
|
217
|
+
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
218
|
+
message,
|
|
219
|
+
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
220
|
+
);
|
|
276
221
|
}
|
|
277
222
|
},
|
|
278
223
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
@@ -285,13 +230,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
285
230
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
286
231
|
},
|
|
287
232
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
288
|
-
const
|
|
289
|
-
rest[0] ?? options ?? {};
|
|
290
|
-
const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
|
|
291
|
-
const isError = context.isError ?? false;
|
|
292
|
-
const expanded = isRendererExpanded(options as any, context as any);
|
|
293
|
-
const cwd = context.cwd ?? process.cwd();
|
|
294
|
-
const width = (context as any).width ?? (options as any)?.width;
|
|
233
|
+
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
295
234
|
|
|
296
235
|
if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending search")], width).join("\n"), 0, 0);
|
|
297
236
|
|
|
@@ -59,7 +59,6 @@ function promptFileName(promptUrl: URL): string {
|
|
|
59
59
|
export function defineToolPromptMetadata(options: {
|
|
60
60
|
promptUrl: URL;
|
|
61
61
|
promptSnippet: string;
|
|
62
|
-
promptGuidelines: string[];
|
|
63
62
|
}): ToolPromptMetadata {
|
|
64
63
|
const prompt = loadPrompt(options.promptUrl);
|
|
65
64
|
const fileName = promptFileName(options.promptUrl);
|
|
@@ -67,6 +66,6 @@ export function defineToolPromptMetadata(options: {
|
|
|
67
66
|
return {
|
|
68
67
|
description: compactDescription ?? firstPromptParagraph(prompt),
|
|
69
68
|
promptSnippet: options.promptSnippet,
|
|
70
|
-
promptGuidelines: COMPACT_GUIDELINES[fileName] ??
|
|
69
|
+
promptGuidelines: COMPACT_GUIDELINES[fileName] ?? [],
|
|
71
70
|
};
|
|
72
71
|
}
|
package/src/tui-render-utils.ts
CHANGED
|
@@ -37,6 +37,33 @@ export function isRendererExpanded(options?: { expanded?: boolean }, context?: {
|
|
|
37
37
|
return context?.expanded ?? options?.expanded ?? false;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export interface RenderResultContext {
|
|
41
|
+
isPartial: boolean;
|
|
42
|
+
isError: boolean;
|
|
43
|
+
expanded: boolean;
|
|
44
|
+
width: number | undefined;
|
|
45
|
+
cwd: string;
|
|
46
|
+
context: Record<string, any>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the shared render context for a tool's `renderResult`, which may
|
|
51
|
+
* receive its context either as a trailing `rest[0]` argument or folded into
|
|
52
|
+
* `options`. Returns the common flags plus the raw context object for callers
|
|
53
|
+
* that need fields beyond the common set (e.g. `lastComponent`).
|
|
54
|
+
*/
|
|
55
|
+
export function resolveRenderResultContext(options: any, rest: any[]): RenderResultContext {
|
|
56
|
+
const context = rest[0] ?? options ?? {};
|
|
57
|
+
return {
|
|
58
|
+
isPartial: context.isPartial ?? options?.isPartial ?? false,
|
|
59
|
+
isError: context.isError ?? false,
|
|
60
|
+
expanded: isRendererExpanded(options, context),
|
|
61
|
+
width: context.width ?? options?.width,
|
|
62
|
+
cwd: context.cwd ?? process.cwd(),
|
|
63
|
+
context,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
export function normalizeWidth(width: unknown, fallback = 80): number {
|
|
41
68
|
return typeof width === "number" && Number.isFinite(width) && width > 0 ? Math.floor(width) : fallback;
|
|
42
69
|
}
|
package/src/write.ts
CHANGED
|
@@ -66,11 +66,6 @@ const MAX_BYTES = 50 * 1024;
|
|
|
66
66
|
const WRITE_PROMPT_METADATA = defineToolPromptMetadata({
|
|
67
67
|
promptUrl: new URL("../prompts/write.md", import.meta.url),
|
|
68
68
|
promptSnippet: "Create or overwrite a complete file and return edit anchors",
|
|
69
|
-
promptGuidelines: [
|
|
70
|
-
"Use write to create new files or intentionally replace whole files.",
|
|
71
|
-
"Use edit instead of write for small changes or appends to existing files.",
|
|
72
|
-
"Remember write overwrites existing files without confirmation.",
|
|
73
|
-
],
|
|
74
69
|
});
|
|
75
70
|
|
|
76
71
|
type WriteDiffFields = {
|