pi-readseek 0.3.23 → 0.4.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/package.json +2 -2
- package/src/edit.ts +15 -29
- package/src/grep.ts +9 -20
- package/src/read.ts +8 -15
- package/src/readseek/formatter.ts +0 -16
- package/src/readseek/symbol-lookup.ts +1 -1
- package/src/readseek-client.ts +36 -20
- package/src/refs.ts +20 -26
- package/src/register-tool.ts +47 -0
- package/src/sg.ts +9 -22
- package/src/tui-render-utils.ts +23 -15
- package/src/write.ts +78 -115
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.3
|
|
42
|
+
"@jarkkojs/readseek": "^0.4.3",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"ignore": "^7.0.5",
|
|
45
45
|
"picomatch": "^4.0.4",
|
package/src/edit.ts
CHANGED
|
@@ -20,9 +20,10 @@ import { resolveSyntaxValidateMode, type SyntaxValidateOptions } from "./syntax-
|
|
|
20
20
|
import { replaceSymbol } from "./replace-symbol.js";
|
|
21
21
|
import { buildEditPreviewKey, buildPendingEditPreviewData, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
|
|
22
22
|
import { buildDiffData, type DiffBlockRange } from "./diff-data.js";
|
|
23
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
23
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
24
24
|
import { DiffPreviewComponent } from "./tui-diff-component.js";
|
|
25
25
|
import type { FreshAnchorsPredicate } from "./tool-types.js";
|
|
26
|
+
import { registerReadseekTool } from "./register-tool.js";
|
|
26
27
|
|
|
27
28
|
import { resolveEditDiffDisplay } from "./readseek-settings.js";
|
|
28
29
|
|
|
@@ -35,14 +36,6 @@ function pendingPreviewLines(summary: string, preview: PendingDiffPreviewResult
|
|
|
35
36
|
return { lines: [summary, headerLine], diffData: expanded ? diffData : undefined, headerLabel: preview.data.headerLabel };
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export function wrapWriteError(err: any, path: string): Error {
|
|
39
|
-
const code = err?.code;
|
|
40
|
-
if (code === "EACCES" || code === "EPERM") {
|
|
41
|
-
return new Error(`Permission denied: ${path}`);
|
|
42
|
-
}
|
|
43
|
-
return new Error(`Failed to write file: ${path}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
39
|
|
|
47
40
|
const hashlineEditItemSchema = Type.Union([
|
|
48
41
|
Type.Object({ set_line: Type.Object({ anchor: Type.String(), new_text: Type.String() }) }, { additionalProperties: true }),
|
|
@@ -268,16 +261,16 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
268
261
|
}
|
|
269
262
|
}
|
|
270
263
|
let preAnchorContent = originalNormalized;
|
|
271
|
-
//
|
|
264
|
+
// reject anchored edits that target a line inside any replace_symbol
|
|
272
265
|
// pre-replace range. Resolve each target against the ORIGINAL content so the
|
|
273
266
|
// user-provided anchor line numbers (which reference the file as read) are
|
|
274
267
|
// compared against the pre-replace coordinates.
|
|
275
268
|
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
269
|
+
// surface replace_symbol symbol-resolution errors (not-found, ambiguous)
|
|
270
|
+
// before the overlap check and before any write.
|
|
278
271
|
// Error-precedence order: replace_symbol resolution > anchor-overlap > anchored-edit.
|
|
279
272
|
//
|
|
280
|
-
//
|
|
273
|
+
// store successful probe results and reuse them in the apply loop so
|
|
281
274
|
// readseekMapContent is invoked at most once per replace_symbol edit.
|
|
282
275
|
const replaceSymbolRanges: { start: number; end: number }[] = [];
|
|
283
276
|
const rsProbeResults: { type: "ok"; content: string; replacement: string; warnings: string[]; range: { start: number; end: number } }[] = [];
|
|
@@ -289,7 +282,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
289
282
|
newBody: rs.replace_symbol.new_body,
|
|
290
283
|
});
|
|
291
284
|
if (probe.type !== "ok") {
|
|
292
|
-
//
|
|
285
|
+
// symbol-resolution errors surface before the overlap check.
|
|
293
286
|
return buildEditError(absolutePath, "invalid-edit-variant", probe.message);
|
|
294
287
|
}
|
|
295
288
|
rsProbeResults.push(probe);
|
|
@@ -348,7 +341,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
348
341
|
}
|
|
349
342
|
}
|
|
350
343
|
}
|
|
351
|
-
// Apply pass: reuse all probe results
|
|
344
|
+
// Apply pass: reuse all probe results. The probe pass resolved every
|
|
352
345
|
// replace_symbol against originalNormalized; apply those replacements in
|
|
353
346
|
// reverse source order so original line ranges stay valid and no second
|
|
354
347
|
// replaceSymbol/readseekMapContent call is needed.
|
|
@@ -444,7 +437,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
444
437
|
if (regression) {
|
|
445
438
|
const lines = regression.errorLines.join(", ");
|
|
446
439
|
const message = `syntax-regression: lines ${lines}`;
|
|
447
|
-
//
|
|
440
|
+
// block mode aborts with syntax-regression code; file is left untouched.
|
|
448
441
|
if (syntaxMode === "block") {
|
|
449
442
|
return buildEditError(absolutePath, "syntax-regression", message);
|
|
450
443
|
}
|
|
@@ -566,22 +559,17 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
566
559
|
|
|
567
560
|
|
|
568
561
|
export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}) {
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
enabled: true,
|
|
572
|
-
policy: "mutating" as const,
|
|
573
|
-
readOnly: false,
|
|
562
|
+
const tool = registerReadseekTool(pi, {
|
|
563
|
+
policy: "mutating",
|
|
574
564
|
pythonName: "edit",
|
|
575
|
-
defaultExposure: "not-safe-by-default"
|
|
576
|
-
}
|
|
577
|
-
const tool = {
|
|
565
|
+
defaultExposure: "not-safe-by-default",
|
|
566
|
+
}, {
|
|
578
567
|
name: "edit",
|
|
579
568
|
label: "Edit",
|
|
580
569
|
description: EDIT_PROMPT_METADATA.description,
|
|
581
570
|
promptSnippet: EDIT_PROMPT_METADATA.promptSnippet,
|
|
582
571
|
promptGuidelines: EDIT_PROMPT_METADATA.promptGuidelines,
|
|
583
572
|
parameters: hashlineEditSchema,
|
|
584
|
-
ptc: toolConfig,
|
|
585
573
|
renderShell: "default" as const,
|
|
586
574
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
587
575
|
return executeEdit({
|
|
@@ -645,7 +633,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
645
633
|
const { isPartial, isError, expanded: baseExpanded, width, context } = resolveRenderResultContext(options, rest);
|
|
646
634
|
|
|
647
635
|
if (isPartial) {
|
|
648
|
-
return
|
|
636
|
+
return renderPendingResult("pending edit", width);
|
|
649
637
|
}
|
|
650
638
|
|
|
651
639
|
// Extract data from result
|
|
@@ -698,8 +686,6 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
698
686
|
}
|
|
699
687
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
700
688
|
},
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
pi.registerTool(tool);
|
|
689
|
+
});
|
|
704
690
|
return tool;
|
|
705
691
|
}
|
package/src/grep.ts
CHANGED
|
@@ -17,8 +17,9 @@ 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, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
20
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
21
21
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
22
|
+
import { registerReadseekTool } from "./register-tool.js";
|
|
22
23
|
|
|
23
24
|
const GREP_PROMPT_METADATA = defineToolPromptMetadata({
|
|
24
25
|
promptUrl: new URL("../prompts/grep.md", import.meta.url),
|
|
@@ -508,21 +509,15 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}) {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
enabled: true,
|
|
514
|
-
policy: "read-only" as const,
|
|
515
|
-
readOnly: true,
|
|
512
|
+
const tool = registerReadseekTool(pi, {
|
|
513
|
+
policy: "read-only",
|
|
516
514
|
pythonName: "grep",
|
|
517
|
-
defaultExposure: "safe-by-default"
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const tool = {
|
|
515
|
+
defaultExposure: "safe-by-default",
|
|
516
|
+
}, {
|
|
521
517
|
name: "grep",
|
|
522
518
|
label: "grep",
|
|
523
519
|
description: GREP_PROMPT_METADATA.description,
|
|
524
520
|
parameters: grepSchema,
|
|
525
|
-
ptc: toolConfig,
|
|
526
521
|
promptSnippet: GREP_PROMPT_METADATA.promptSnippet,
|
|
527
522
|
promptGuidelines: options.searchGuideline
|
|
528
523
|
? [GREP_PROMPT_METADATA.promptGuidelines[0], options.searchGuideline]
|
|
@@ -558,16 +553,12 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
|
|
|
558
553
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
559
554
|
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
560
555
|
|
|
561
|
-
if (isPartial) return
|
|
556
|
+
if (isPartial) return renderPendingResult("pending search", width);
|
|
562
557
|
|
|
563
558
|
const content = result.content?.[0];
|
|
564
559
|
const textContent = content?.type === "text" ? content.text : "";
|
|
565
560
|
|
|
566
|
-
if (isError || result.isError) {
|
|
567
|
-
const firstLine = textContent.split("\n")[0] || "Error";
|
|
568
|
-
const body = expanded && textContent ? textContent : firstLine;
|
|
569
|
-
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
|
|
570
|
-
}
|
|
561
|
+
if (isError || result.isError) return renderErrorResult(textContent, { expanded, width });
|
|
571
562
|
|
|
572
563
|
const readseekValue = (result.details as any)?.readseekValue as {
|
|
573
564
|
tool: "grep";
|
|
@@ -607,8 +598,6 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
|
|
|
607
598
|
}
|
|
608
599
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
609
600
|
},
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
pi.registerTool(tool);
|
|
601
|
+
});
|
|
613
602
|
return tool;
|
|
614
603
|
}
|
package/src/read.ts
CHANGED
|
@@ -26,8 +26,9 @@ import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
|
26
26
|
import { readseekRead } from "./readseek-client.js";
|
|
27
27
|
import { Text } from "@earendil-works/pi-tui";
|
|
28
28
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
29
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
29
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
30
30
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
31
|
+
import { registerReadseekTool } from "./register-tool.js";
|
|
31
32
|
|
|
32
33
|
const READ_PROMPT_METADATA = defineToolPromptMetadata({
|
|
33
34
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
@@ -423,16 +424,11 @@ function splitReadseekLines(text: string): string[] {
|
|
|
423
424
|
}
|
|
424
425
|
|
|
425
426
|
export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}) {
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
enabled: true,
|
|
429
|
-
policy: "read-only" as const,
|
|
430
|
-
readOnly: true,
|
|
427
|
+
const tool = registerReadseekTool(pi, {
|
|
428
|
+
policy: "read-only",
|
|
431
429
|
pythonName: "read",
|
|
432
|
-
defaultExposure: "safe-by-default"
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const tool = {
|
|
430
|
+
defaultExposure: "safe-by-default",
|
|
431
|
+
}, {
|
|
436
432
|
name: "read",
|
|
437
433
|
label: "Read",
|
|
438
434
|
description: READ_PROMPT_METADATA.description,
|
|
@@ -460,7 +456,6 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
460
456
|
}),
|
|
461
457
|
),
|
|
462
458
|
}),
|
|
463
|
-
ptc: toolConfig,
|
|
464
459
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
465
460
|
return executeRead({
|
|
466
461
|
toolCallId,
|
|
@@ -489,7 +484,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
489
484
|
},
|
|
490
485
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
491
486
|
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
492
|
-
if (isPartial) return
|
|
487
|
+
if (isPartial) return renderPendingResult("pending read", width);
|
|
493
488
|
|
|
494
489
|
const content = result.content?.[0];
|
|
495
490
|
const textContent = content?.type === "text" ? content.text : "";
|
|
@@ -516,8 +511,6 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
516
511
|
if (expanded && textContent) text += "\n" + wrapReadHashlinesForWidth(textContent, width);
|
|
517
512
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
518
513
|
},
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
pi.registerTool(tool);
|
|
514
|
+
});
|
|
522
515
|
return tool;
|
|
523
516
|
}
|
|
@@ -233,22 +233,6 @@ export function formatFileMap(map: FileMap, level?: DetailLevel): string {
|
|
|
233
233
|
return lines.join("\n");
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
/**
|
|
237
|
-
* Get the appropriate detail level for a map based on size.
|
|
238
|
-
*/
|
|
239
|
-
export function getDetailLevelForSize(currentSize: number): DetailLevel {
|
|
240
|
-
if (currentSize <= THRESHOLDS.FULL_TARGET_BYTES) {
|
|
241
|
-
return DetailLevel.Full;
|
|
242
|
-
}
|
|
243
|
-
if (currentSize <= THRESHOLDS.COMPACT_TARGET_BYTES) {
|
|
244
|
-
return DetailLevel.Compact;
|
|
245
|
-
}
|
|
246
|
-
if (currentSize <= THRESHOLDS.MAX_MAP_BYTES) {
|
|
247
|
-
return DetailLevel.Minimal;
|
|
248
|
-
}
|
|
249
|
-
return DetailLevel.Outline;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
236
|
/**
|
|
253
237
|
* Reduce detail level of a file map.
|
|
254
238
|
*/
|
|
@@ -136,7 +136,7 @@ export function findSymbol(map: FileMap, query: string): SymbolLookupResult {
|
|
|
136
136
|
} else {
|
|
137
137
|
pool = allSymbols.filter((c) => c.symbol.name === namePart);
|
|
138
138
|
}
|
|
139
|
-
//
|
|
139
|
+
// drop candidates without usable startLine. If that empties
|
|
140
140
|
// the pool entirely, surface any same-leaf-name decls elsewhere in the file
|
|
141
141
|
// so the user can see real candidate lines.
|
|
142
142
|
pool = pool.filter((c) => Number.isFinite(c.symbol.startLine) && c.symbol.startLine > 0);
|
package/src/readseek-client.ts
CHANGED
|
@@ -203,6 +203,28 @@ export function isReadseekAvailable(): boolean {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
export interface ReadseekFailure {
|
|
207
|
+
code: "readseek-not-installed" | "readseek-execution-error";
|
|
208
|
+
message: string;
|
|
209
|
+
hint?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Classify an error thrown while invoking readseek into the shared failure
|
|
214
|
+
* taxonomy: a missing binary or package (`readseek-not-installed`, with an
|
|
215
|
+
* install hint) versus any other execution error.
|
|
216
|
+
*/
|
|
217
|
+
export function classifyReadseekFailure(err: unknown): ReadseekFailure {
|
|
218
|
+
const message = String((err as { message?: unknown } | null)?.message || err);
|
|
219
|
+
const missing =
|
|
220
|
+
(err as { code?: unknown } | null)?.code === "ENOENT" ||
|
|
221
|
+
/Cannot find package|Cannot find module|no such file/i.test(message);
|
|
222
|
+
if (missing) {
|
|
223
|
+
return { code: "readseek-not-installed", message, hint: "Run npm install to install @jarkkojs/readseek." };
|
|
224
|
+
}
|
|
225
|
+
return { code: "readseek-execution-error", message };
|
|
226
|
+
}
|
|
227
|
+
|
|
206
228
|
function directoryExists(dirPath: string): boolean {
|
|
207
229
|
try {
|
|
208
230
|
return statSync(dirPath).isDirectory();
|
|
@@ -353,15 +375,7 @@ function parseReadOutput(value: unknown): ReadseekReadOutput {
|
|
|
353
375
|
file_hash: requireString(output.file_hash, "file_hash"),
|
|
354
376
|
start_line: requireNumber(output.start_line, "start_line"),
|
|
355
377
|
end_line: requireNumber(output.end_line, "end_line"),
|
|
356
|
-
hashlines: hashlines.map((line) =>
|
|
357
|
-
if (!line || typeof line !== "object") throw new Error("invalid readseek hashline");
|
|
358
|
-
const item = line as Record<string, unknown>;
|
|
359
|
-
return {
|
|
360
|
-
line: requireNumber(item.line, "hashline.line"),
|
|
361
|
-
hash: requireString(item.hash, "hashline.hash"),
|
|
362
|
-
text: requireString(item.text, "hashline.text"),
|
|
363
|
-
};
|
|
364
|
-
}),
|
|
378
|
+
hashlines: hashlines.map((line) => parseHashline(line, "hashline")),
|
|
365
379
|
};
|
|
366
380
|
}
|
|
367
381
|
|
|
@@ -391,17 +405,19 @@ function parseMapOutput(value: unknown): ReadseekMapOutput {
|
|
|
391
405
|
};
|
|
392
406
|
}
|
|
393
407
|
|
|
408
|
+
function parseHashline(value: unknown, field: string): ReadseekHashline {
|
|
409
|
+
if (!value || typeof value !== "object") throw new Error(`invalid readseek ${field}`);
|
|
410
|
+
const item = value as Record<string, unknown>;
|
|
411
|
+
return {
|
|
412
|
+
line: requireNumber(item.line, `${field}.line`),
|
|
413
|
+
hash: requireString(item.hash, `${field}.hash`),
|
|
414
|
+
text: requireString(item.text, `${field}.text`),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
394
418
|
function parseSearchHashlines(value: unknown, field: string): ReadseekHashline[] {
|
|
395
419
|
if (!Array.isArray(value)) throw new Error(`invalid readseek ${field}`);
|
|
396
|
-
return value.map((line) =>
|
|
397
|
-
if (!line || typeof line !== "object") throw new Error(`invalid readseek ${field}`);
|
|
398
|
-
const item = line as Record<string, unknown>;
|
|
399
|
-
return {
|
|
400
|
-
line: requireNumber(item.line, `${field}.line`),
|
|
401
|
-
hash: requireString(item.hash, `${field}.hash`),
|
|
402
|
-
text: requireString(item.text, `${field}.text`),
|
|
403
|
-
};
|
|
404
|
-
});
|
|
420
|
+
return value.map((line) => parseHashline(line, field));
|
|
405
421
|
}
|
|
406
422
|
|
|
407
423
|
function parseSearchOutput(value: unknown): ReadseekSearchOutput {
|
|
@@ -422,7 +438,7 @@ function parseSearchOutput(value: unknown): ReadseekSearchOutput {
|
|
|
422
438
|
const item = match as Record<string, unknown>;
|
|
423
439
|
if (!Array.isArray(item.captures)) throw new Error("invalid readseek search captures");
|
|
424
440
|
return {
|
|
425
|
-
pattern_index: requireNumber(item.pattern_index, "search.match.pattern_index"),
|
|
441
|
+
pattern_index: item.pattern_index === undefined ? 0 : requireNumber(item.pattern_index, "search.match.pattern_index"),
|
|
426
442
|
start_line: requireNumber(item.start_line, "search.match.start_line"),
|
|
427
443
|
end_line: requireNumber(item.end_line, "search.match.end_line"),
|
|
428
444
|
start_hash: requireString(item.start_hash, "search.match.start_hash"),
|
|
@@ -449,7 +465,7 @@ function parseSearchOutput(value: unknown): ReadseekSearchOutput {
|
|
|
449
465
|
|
|
450
466
|
export async function readseekRead(filePath: string, startLine?: number, endLine?: number): Promise<ReadseekReadOutput> {
|
|
451
467
|
const args = ["read", filePath];
|
|
452
|
-
if (startLine !== undefined) args.push("--
|
|
468
|
+
if (startLine !== undefined) args.push("--start", String(startLine));
|
|
453
469
|
if (endLine !== undefined) args.push("--end", String(endLine));
|
|
454
470
|
return parseReadOutput(await runReadseek(args));
|
|
455
471
|
}
|
package/src/refs.ts
CHANGED
|
@@ -6,9 +6,10 @@ import { stat as fsStat } from "node:fs/promises";
|
|
|
6
6
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
7
|
import { buildReadseekLineWithHash, buildToolErrorResult } from "./readseek-value.js";
|
|
8
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
-
import {
|
|
9
|
+
import { classifyReadseekFailure, readseekRefs, type ReadseekReference } from "./readseek-client.js";
|
|
10
10
|
import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
|
|
11
11
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
12
|
+
import { registerReadseekTool } from "./register-tool.js";
|
|
12
13
|
|
|
13
14
|
import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
|
|
14
15
|
|
|
@@ -29,10 +30,6 @@ const REFS_PROMPT_METADATA = defineToolPromptMetadata({
|
|
|
29
30
|
promptSnippet: "Find references to an identifier with readseek and return edit-ready anchors",
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
export function isRefsAvailable(): boolean {
|
|
33
|
-
return isReadseekAvailable();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
33
|
interface RefsToolOptions {
|
|
37
34
|
onFileAnchored?: FileAnchoredCallback;
|
|
38
35
|
}
|
|
@@ -72,6 +69,14 @@ function groupReferences(references: ReadseekReference[], cwd: string): RefsOutp
|
|
|
72
69
|
return [...files.values()];
|
|
73
70
|
}
|
|
74
71
|
|
|
72
|
+
function isReadseekCursorValidationFailure(message: string): boolean {
|
|
73
|
+
return (
|
|
74
|
+
/line and column must be greater than zero/i.test(message) ||
|
|
75
|
+
/line \d+ not found/i.test(message) ||
|
|
76
|
+
/column \d+ exceeds maximum column \d+ for line \d+/i.test(message)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
/**
|
|
76
81
|
* Executes identifier reference lookup and returns readseek-anchored matches.
|
|
77
82
|
*/
|
|
@@ -127,28 +132,20 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
|
127
132
|
details: { readseekValue: builtOutput.readseekValue },
|
|
128
133
|
};
|
|
129
134
|
} catch (err: any) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
message,
|
|
136
|
-
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
137
|
-
);
|
|
135
|
+
const failure = classifyReadseekFailure(err);
|
|
136
|
+
if (p.scope && isReadseekCursorValidationFailure(failure.message)) {
|
|
137
|
+
return buildToolErrorResult("refs", "invalid-parameter", failure.message);
|
|
138
|
+
}
|
|
139
|
+
return buildToolErrorResult("refs", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
enabled: true,
|
|
145
|
-
policy: "read-only" as const,
|
|
146
|
-
readOnly: true,
|
|
144
|
+
const tool = registerReadseekTool(pi, {
|
|
145
|
+
policy: "read-only",
|
|
147
146
|
pythonName: "refs",
|
|
148
|
-
defaultExposure: "opt-in"
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const tool = {
|
|
147
|
+
defaultExposure: "opt-in",
|
|
148
|
+
}, {
|
|
152
149
|
name: "refs",
|
|
153
150
|
label: "References",
|
|
154
151
|
description: REFS_PROMPT_METADATA.description,
|
|
@@ -165,7 +162,6 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
165
162
|
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
166
163
|
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
167
164
|
}),
|
|
168
|
-
ptc: toolConfig,
|
|
169
165
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
170
166
|
return executeRefs({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
171
167
|
},
|
|
@@ -186,8 +182,6 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
186
182
|
unitPlural: "references",
|
|
187
183
|
});
|
|
188
184
|
},
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
pi.registerTool(tool);
|
|
185
|
+
});
|
|
192
186
|
return tool;
|
|
193
187
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type ReadseekToolPolicy = "read-only" | "mutating";
|
|
4
|
+
|
|
5
|
+
export type ReadseekToolExposure = "safe-by-default" | "opt-in" | "not-safe-by-default";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-tool fields of the pi tool config (`ptc`) that genuinely differ between
|
|
9
|
+
* readseek tools. The remaining fields are constant or derived.
|
|
10
|
+
*/
|
|
11
|
+
export interface ReadseekToolConfig {
|
|
12
|
+
policy: ReadseekToolPolicy;
|
|
13
|
+
pythonName: string;
|
|
14
|
+
defaultExposure: ReadseekToolExposure;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ReadseekToolPtc extends ReadseekToolConfig {
|
|
18
|
+
callable: true;
|
|
19
|
+
enabled: true;
|
|
20
|
+
readOnly: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type ToolSpec = Parameters<ExtensionAPI["registerTool"]>[0];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Attach the standard `ptc` envelope to a tool definition and register it.
|
|
27
|
+
*
|
|
28
|
+
* `callable` and `enabled` are always true and `readOnly` is derived from
|
|
29
|
+
* `policy`, so callers supply only the fields that vary between tools.
|
|
30
|
+
*/
|
|
31
|
+
export function registerReadseekTool<T extends ToolSpec>(
|
|
32
|
+
pi: ExtensionAPI,
|
|
33
|
+
config: ReadseekToolConfig,
|
|
34
|
+
tool: T,
|
|
35
|
+
): T & { ptc: ReadseekToolPtc } {
|
|
36
|
+
const ptc: ReadseekToolPtc = {
|
|
37
|
+
callable: true,
|
|
38
|
+
enabled: true,
|
|
39
|
+
policy: config.policy,
|
|
40
|
+
readOnly: config.policy === "read-only",
|
|
41
|
+
pythonName: config.pythonName,
|
|
42
|
+
defaultExposure: config.defaultExposure,
|
|
43
|
+
};
|
|
44
|
+
const registered = { ...tool, ptc };
|
|
45
|
+
pi.registerTool(registered);
|
|
46
|
+
return registered;
|
|
47
|
+
}
|
package/src/sg.ts
CHANGED
|
@@ -6,9 +6,10 @@ import { stat as fsStat } from "node:fs/promises";
|
|
|
6
6
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
7
|
import { buildReadseekLineWithHash, buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
|
|
8
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
-
import { isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
|
|
9
|
+
import { classifyReadseekFailure, isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
|
|
10
10
|
import { buildSgOutput } from "./sg-output.js";
|
|
11
11
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
12
|
+
import { registerReadseekTool } from "./register-tool.js";
|
|
12
13
|
|
|
13
14
|
import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
|
|
14
15
|
|
|
@@ -193,28 +194,17 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
|
193
194
|
},
|
|
194
195
|
};
|
|
195
196
|
} catch (err: any) {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
return buildToolErrorResult(
|
|
199
|
-
"search",
|
|
200
|
-
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
201
|
-
message,
|
|
202
|
-
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
203
|
-
);
|
|
197
|
+
const failure = classifyReadseekFailure(err);
|
|
198
|
+
return buildToolErrorResult("search", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
204
199
|
}
|
|
205
200
|
}
|
|
206
201
|
|
|
207
202
|
export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
enabled: true,
|
|
211
|
-
policy: "read-only" as const,
|
|
212
|
-
readOnly: true,
|
|
203
|
+
const tool = registerReadseekTool(pi, {
|
|
204
|
+
policy: "read-only",
|
|
213
205
|
pythonName: "search",
|
|
214
|
-
defaultExposure: "opt-in"
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const tool = {
|
|
206
|
+
defaultExposure: "opt-in",
|
|
207
|
+
}, {
|
|
218
208
|
name: "search",
|
|
219
209
|
label: "Structural Search",
|
|
220
210
|
description: SG_PROMPT_METADATA.description,
|
|
@@ -228,7 +218,6 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
228
218
|
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
229
219
|
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
230
220
|
}),
|
|
231
|
-
ptc: toolConfig,
|
|
232
221
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
233
222
|
return executeSg({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
234
223
|
},
|
|
@@ -249,8 +238,6 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
249
238
|
unitPlural: "matches",
|
|
250
239
|
});
|
|
251
240
|
},
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
pi.registerTool(tool);
|
|
241
|
+
});
|
|
255
242
|
return tool;
|
|
256
243
|
}
|
package/src/tui-render-utils.ts
CHANGED
|
@@ -80,15 +80,6 @@ export function clampLinesToWidth(lines: string[], width: number | undefined): s
|
|
|
80
80
|
return lines.map((line) => clampLineToWidth(line, width));
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export function wrapLinesToWidth(lines: string[], width: number | undefined): string[] {
|
|
84
|
-
if (width === undefined || width === null) return lines;
|
|
85
|
-
const normalized = normalizeWidth(width);
|
|
86
|
-
return lines.flatMap((line) => {
|
|
87
|
-
if (visibleWidth(line) <= normalized) return [line];
|
|
88
|
-
return wrapTextWithAnsi(line, normalized).map((wrapped) => clampLineToWidth(wrapped, normalized));
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
83
|
export interface WrapWithHangingIndentOptions {
|
|
93
84
|
/** Optional transform applied to each produced line (e.g. theme tinting). */
|
|
94
85
|
tint?: (text: string) => string;
|
|
@@ -156,6 +147,27 @@ export function wrapReadHashlinesForWidth(text: string, width: number | undefine
|
|
|
156
147
|
return output.join("\n");
|
|
157
148
|
}
|
|
158
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Render the `isPartial` placeholder line shared by every tool's `renderResult`.
|
|
152
|
+
*/
|
|
153
|
+
export function renderPendingResult(pendingLabel: string, width: number | undefined): Text {
|
|
154
|
+
return new Text(clampLinesToWidth([summaryLine(pendingLabel)], width).join("\n"), 0, 0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Render a tool result error: the first line of `textContent`, or its full body
|
|
159
|
+
* when expanded, prefixed and clamped to width. Tools pass their own `fallback`
|
|
160
|
+
* for the empty-content case.
|
|
161
|
+
*/
|
|
162
|
+
export function renderErrorResult(
|
|
163
|
+
textContent: string,
|
|
164
|
+
options: { expanded: boolean; width: number | undefined; fallback?: string },
|
|
165
|
+
): Text {
|
|
166
|
+
const firstLine = textContent.split("\n")[0] || (options.fallback ?? "Error");
|
|
167
|
+
const body = options.expanded && textContent ? textContent : firstLine;
|
|
168
|
+
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), options.width).join("\n"), 0, 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
159
171
|
export interface AnchoredFilesLabels {
|
|
160
172
|
pendingLabel: string;
|
|
161
173
|
emptyLabel: string;
|
|
@@ -178,15 +190,11 @@ export function renderAnchoredFilesResult(
|
|
|
178
190
|
): Text {
|
|
179
191
|
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
180
192
|
|
|
181
|
-
if (isPartial) return
|
|
193
|
+
if (isPartial) return renderPendingResult(labels.pendingLabel, width);
|
|
182
194
|
|
|
183
195
|
const content = result.content?.[0];
|
|
184
196
|
const textContent = content?.type === "text" ? content.text : "";
|
|
185
|
-
if (isError || result.isError) {
|
|
186
|
-
const firstLine = textContent.split("\n")[0] || "Error";
|
|
187
|
-
const body = expanded && textContent ? textContent : firstLine;
|
|
188
|
-
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
|
|
189
|
-
}
|
|
197
|
+
if (isError || result.isError) return renderErrorResult(textContent, { expanded, width });
|
|
190
198
|
|
|
191
199
|
const readseekValue = (result.details as any)?.readseekValue as
|
|
192
200
|
| { files: Array<{ path: string; lines: any[] }> }
|
package/src/write.ts
CHANGED
|
@@ -14,9 +14,10 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
|
14
14
|
import { buildPendingWritePreviewData, buildWritePreviewKey, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
|
|
15
15
|
import { generateCompactOrFullDiff, normalizeToLF, hasBareCarriageReturn } from "./edit-diff.js";
|
|
16
16
|
import { buildDiffData, type DiffData } from "./diff-data.js";
|
|
17
|
-
import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, renderToolLabel, summaryLine } from "./tui-render-utils.js";
|
|
17
|
+
import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, renderErrorResult, renderToolLabel, summaryLine } from "./tui-render-utils.js";
|
|
18
18
|
import { DiffPreviewComponent } from "./tui-diff-component.js";
|
|
19
19
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
20
|
+
import { registerReadseekTool } from "./register-tool.js";
|
|
20
21
|
|
|
21
22
|
const WRITE_PENDING_PREVIEW_STATE_KEY = "hashline-write-pending-preview";
|
|
22
23
|
|
|
@@ -169,6 +170,30 @@ function mapFsWriteError(err: any, path: string): MappedFsError {
|
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
function buildWriteFsErrorResult(err: any, absolutePath: string) {
|
|
174
|
+
const mapped = mapFsWriteError(err, absolutePath);
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text" as const, text: mapped.message }],
|
|
177
|
+
isError: true,
|
|
178
|
+
details: {
|
|
179
|
+
readseekValue: {
|
|
180
|
+
tool: "write" as const,
|
|
181
|
+
path: absolutePath,
|
|
182
|
+
lines: [] as ReadseekLine[],
|
|
183
|
+
warnings: [] as ReadseekWarning[],
|
|
184
|
+
ok: false,
|
|
185
|
+
error: buildReadseekError(
|
|
186
|
+
mapped.code,
|
|
187
|
+
mapped.message,
|
|
188
|
+
undefined,
|
|
189
|
+
mapped.includeMeta ? { fsCode: err?.code, fsMessage: err?.message } : undefined,
|
|
190
|
+
),
|
|
191
|
+
},
|
|
192
|
+
warnings: [] as string[],
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
172
197
|
export async function executeWrite(opts: {
|
|
173
198
|
path: string;
|
|
174
199
|
content: string;
|
|
@@ -301,21 +326,16 @@ export async function executeWrite(opts: {
|
|
|
301
326
|
}
|
|
302
327
|
|
|
303
328
|
export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions = {}) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
enabled: true,
|
|
307
|
-
policy: "mutating" as const,
|
|
308
|
-
readOnly: false,
|
|
329
|
+
const tool = registerReadseekTool(pi, {
|
|
330
|
+
policy: "mutating",
|
|
309
331
|
pythonName: "write",
|
|
310
|
-
defaultExposure: "not-safe-by-default"
|
|
311
|
-
}
|
|
312
|
-
const tool = {
|
|
332
|
+
defaultExposure: "not-safe-by-default",
|
|
333
|
+
}, {
|
|
313
334
|
name: "write",
|
|
314
335
|
label: "write",
|
|
315
336
|
description: WRITE_PROMPT_METADATA.description,
|
|
316
337
|
promptSnippet: WRITE_PROMPT_METADATA.promptSnippet,
|
|
317
338
|
promptGuidelines: WRITE_PROMPT_METADATA.promptGuidelines,
|
|
318
|
-
ptc: toolConfig,
|
|
319
339
|
parameters: Type.Object({
|
|
320
340
|
path: Type.String({ description: "File path" }),
|
|
321
341
|
content: Type.String({ description: "File content" }),
|
|
@@ -325,112 +345,58 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
325
345
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
326
346
|
const absolutePath = resolveToCwd(params.path, cwd);
|
|
327
347
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
path: absolutePath,
|
|
333
|
-
content: params.content,
|
|
334
|
-
map: params.map,
|
|
335
|
-
cwd,
|
|
336
|
-
});
|
|
337
|
-
} catch (err: any) {
|
|
338
|
-
const mapped = mapFsWriteError(err, absolutePath);
|
|
339
|
-
return {
|
|
340
|
-
content: [{ type: "text" as const, text: mapped.message }],
|
|
341
|
-
isError: true,
|
|
342
|
-
details: {
|
|
343
|
-
readseekValue: {
|
|
344
|
-
tool: "write" as const,
|
|
348
|
+
return await withFileMutationQueue(absolutePath, async () => {
|
|
349
|
+
let result: WriteResult;
|
|
350
|
+
try {
|
|
351
|
+
result = await executeWrite({
|
|
345
352
|
path: absolutePath,
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
mapped.includeMeta ? { fsCode: err?.code, fsMessage: err?.message } : undefined,
|
|
354
|
-
),
|
|
355
|
-
},
|
|
356
|
-
warnings: [] as string[],
|
|
357
|
-
},
|
|
358
|
-
};
|
|
359
|
-
}
|
|
353
|
+
content: params.content,
|
|
354
|
+
map: params.map,
|
|
355
|
+
cwd,
|
|
356
|
+
});
|
|
357
|
+
} catch (err: any) {
|
|
358
|
+
return buildWriteFsErrorResult(err, absolutePath);
|
|
359
|
+
}
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
361
|
+
if (result.readseekValue.lines.length > 0) {
|
|
362
|
+
options.onFileAnchored?.(absolutePath);
|
|
363
|
+
}
|
|
364
364
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
365
|
+
// Lift binary-content signal into a fatal readseekValue.error envelope so
|
|
366
|
+
// downstream consumers get the same taxonomy shape as every other tool.
|
|
367
|
+
// The existing ReadseekWarning entry is preserved on readseekValue.warnings for
|
|
368
|
+
// backward compatibility.
|
|
369
|
+
for (const code of ["binary-content", "bare-cr"] as const) {
|
|
370
|
+
const warning = result.readseekValue.warnings.find((w) => w.code === code);
|
|
371
|
+
if (warning) {
|
|
372
|
+
return {
|
|
373
|
+
content: [{ type: "text" as const, text: result.text }],
|
|
374
|
+
isError: true,
|
|
375
|
+
details: {
|
|
376
|
+
readseekValue: {
|
|
377
|
+
...result.readseekValue,
|
|
378
|
+
ok: false,
|
|
379
|
+
error: buildReadseekError(code, warning.message),
|
|
380
|
+
},
|
|
381
|
+
warnings: result.warnings,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
384
386
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
ok: false,
|
|
394
|
-
error: buildReadseekError("bare-cr", bareCrWarning.message),
|
|
387
|
+
return {
|
|
388
|
+
content: [{ type: "text" as const, text: result.text }],
|
|
389
|
+
details: {
|
|
390
|
+
...(result.diff !== undefined ? { diff: result.diff } : {}),
|
|
391
|
+
...(result.diffData !== undefined ? { diffData: result.diffData } : {}),
|
|
392
|
+
...(result.writeState ? { writeState: result.writeState } : {}),
|
|
393
|
+
readseekValue: result.readseekValue,
|
|
394
|
+
warnings: result.warnings,
|
|
395
395
|
},
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
content: [{ type: "text" as const, text: result.text }],
|
|
403
|
-
details: {
|
|
404
|
-
...(result.diff !== undefined ? { diff: result.diff } : {}),
|
|
405
|
-
...(result.diffData !== undefined ? { diffData: result.diffData } : {}),
|
|
406
|
-
...(result.writeState ? { writeState: result.writeState } : {}),
|
|
407
|
-
readseekValue: result.readseekValue,
|
|
408
|
-
warnings: result.warnings,
|
|
409
|
-
},
|
|
410
|
-
};
|
|
411
|
-
});
|
|
396
|
+
};
|
|
397
|
+
});
|
|
412
398
|
} catch (err: any) {
|
|
413
|
-
|
|
414
|
-
return {
|
|
415
|
-
content: [{ type: "text" as const, text: mapped.message }],
|
|
416
|
-
isError: true,
|
|
417
|
-
details: {
|
|
418
|
-
readseekValue: {
|
|
419
|
-
tool: "write" as const,
|
|
420
|
-
path: absolutePath,
|
|
421
|
-
lines: [] as ReadseekLine[],
|
|
422
|
-
warnings: [] as ReadseekWarning[],
|
|
423
|
-
ok: false,
|
|
424
|
-
error: buildReadseekError(
|
|
425
|
-
mapped.code,
|
|
426
|
-
mapped.message,
|
|
427
|
-
undefined,
|
|
428
|
-
mapped.includeMeta ? { fsCode: err?.code, fsMessage: err?.message } : undefined,
|
|
429
|
-
),
|
|
430
|
-
},
|
|
431
|
-
warnings: [] as string[],
|
|
432
|
-
},
|
|
433
|
-
};
|
|
399
|
+
return buildWriteFsErrorResult(err, absolutePath);
|
|
434
400
|
}
|
|
435
401
|
},
|
|
436
402
|
renderCall(args: any, theme: any, context: any = {}) {
|
|
@@ -478,9 +444,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
478
444
|
const details = result.details ?? {};
|
|
479
445
|
const output = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
480
446
|
if (result.isError || details.readseekValue?.ok === false) {
|
|
481
|
-
|
|
482
|
-
const body = expanded && output ? output : firstLine;
|
|
483
|
-
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
|
|
447
|
+
return renderErrorResult(output, { expanded, width, fallback: "write failed" });
|
|
484
448
|
}
|
|
485
449
|
const diffData = details.diffData;
|
|
486
450
|
const state = details.writeState === "overwritten" ? "overwritten" : "created";
|
|
@@ -510,7 +474,6 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
510
474
|
}
|
|
511
475
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
512
476
|
},
|
|
513
|
-
}
|
|
514
|
-
pi.registerTool(tool);
|
|
477
|
+
});
|
|
515
478
|
return tool;
|
|
516
479
|
}
|