pi-readseek 0.3.23 → 0.3.24
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 +1 -1
- package/src/edit.ts +8 -14
- package/src/grep.ts +9 -20
- package/src/read.ts +8 -15
- package/src/readseek-client.ts +22 -0
- package/src/refs.ts +9 -26
- package/src/register-tool.ts +47 -0
- package/src/sg.ts +9 -22
- package/src/tui-render-utils.ts +23 -6
- package/src/write.ts +92 -115
package/package.json
CHANGED
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
|
|
|
@@ -566,22 +567,17 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
566
567
|
|
|
567
568
|
|
|
568
569
|
export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}) {
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
enabled: true,
|
|
572
|
-
policy: "mutating" as const,
|
|
573
|
-
readOnly: false,
|
|
570
|
+
const tool = registerReadseekTool(pi, {
|
|
571
|
+
policy: "mutating",
|
|
574
572
|
pythonName: "edit",
|
|
575
|
-
defaultExposure: "not-safe-by-default"
|
|
576
|
-
}
|
|
577
|
-
const tool = {
|
|
573
|
+
defaultExposure: "not-safe-by-default",
|
|
574
|
+
}, {
|
|
578
575
|
name: "edit",
|
|
579
576
|
label: "Edit",
|
|
580
577
|
description: EDIT_PROMPT_METADATA.description,
|
|
581
578
|
promptSnippet: EDIT_PROMPT_METADATA.promptSnippet,
|
|
582
579
|
promptGuidelines: EDIT_PROMPT_METADATA.promptGuidelines,
|
|
583
580
|
parameters: hashlineEditSchema,
|
|
584
|
-
ptc: toolConfig,
|
|
585
581
|
renderShell: "default" as const,
|
|
586
582
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
587
583
|
return executeEdit({
|
|
@@ -645,7 +641,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
645
641
|
const { isPartial, isError, expanded: baseExpanded, width, context } = resolveRenderResultContext(options, rest);
|
|
646
642
|
|
|
647
643
|
if (isPartial) {
|
|
648
|
-
return
|
|
644
|
+
return renderPendingResult("pending edit", width);
|
|
649
645
|
}
|
|
650
646
|
|
|
651
647
|
// Extract data from result
|
|
@@ -698,8 +694,6 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
698
694
|
}
|
|
699
695
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
700
696
|
},
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
pi.registerTool(tool);
|
|
697
|
+
});
|
|
704
698
|
return tool;
|
|
705
699
|
}
|
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
|
}
|
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();
|
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
|
}
|
|
@@ -127,28 +124,17 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
|
127
124
|
details: { readseekValue: builtOutput.readseekValue },
|
|
128
125
|
};
|
|
129
126
|
} catch (err: any) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
return buildToolErrorResult(
|
|
133
|
-
"refs",
|
|
134
|
-
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
135
|
-
message,
|
|
136
|
-
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
137
|
-
);
|
|
127
|
+
const failure = classifyReadseekFailure(err);
|
|
128
|
+
return buildToolErrorResult("refs", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
138
129
|
}
|
|
139
130
|
}
|
|
140
131
|
|
|
141
132
|
export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
enabled: true,
|
|
145
|
-
policy: "read-only" as const,
|
|
146
|
-
readOnly: true,
|
|
133
|
+
const tool = registerReadseekTool(pi, {
|
|
134
|
+
policy: "read-only",
|
|
147
135
|
pythonName: "refs",
|
|
148
|
-
defaultExposure: "opt-in"
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const tool = {
|
|
136
|
+
defaultExposure: "opt-in",
|
|
137
|
+
}, {
|
|
152
138
|
name: "refs",
|
|
153
139
|
label: "References",
|
|
154
140
|
description: REFS_PROMPT_METADATA.description,
|
|
@@ -165,7 +151,6 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
165
151
|
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
166
152
|
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
167
153
|
}),
|
|
168
|
-
ptc: toolConfig,
|
|
169
154
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
170
155
|
return executeRefs({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
171
156
|
},
|
|
@@ -186,8 +171,6 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
186
171
|
unitPlural: "references",
|
|
187
172
|
});
|
|
188
173
|
},
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
pi.registerTool(tool);
|
|
174
|
+
});
|
|
192
175
|
return tool;
|
|
193
176
|
}
|
|
@@ -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
|
@@ -156,6 +156,27 @@ export function wrapReadHashlinesForWidth(text: string, width: number | undefine
|
|
|
156
156
|
return output.join("\n");
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Render the `isPartial` placeholder line shared by every tool's `renderResult`.
|
|
161
|
+
*/
|
|
162
|
+
export function renderPendingResult(pendingLabel: string, width: number | undefined): Text {
|
|
163
|
+
return new Text(clampLinesToWidth([summaryLine(pendingLabel)], width).join("\n"), 0, 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Render a tool result error: the first line of `textContent`, or its full body
|
|
168
|
+
* when expanded, prefixed and clamped to width. Tools pass their own `fallback`
|
|
169
|
+
* for the empty-content case.
|
|
170
|
+
*/
|
|
171
|
+
export function renderErrorResult(
|
|
172
|
+
textContent: string,
|
|
173
|
+
options: { expanded: boolean; width: number | undefined; fallback?: string },
|
|
174
|
+
): Text {
|
|
175
|
+
const firstLine = textContent.split("\n")[0] || (options.fallback ?? "Error");
|
|
176
|
+
const body = options.expanded && textContent ? textContent : firstLine;
|
|
177
|
+
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), options.width).join("\n"), 0, 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
159
180
|
export interface AnchoredFilesLabels {
|
|
160
181
|
pendingLabel: string;
|
|
161
182
|
emptyLabel: string;
|
|
@@ -178,15 +199,11 @@ export function renderAnchoredFilesResult(
|
|
|
178
199
|
): Text {
|
|
179
200
|
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
180
201
|
|
|
181
|
-
if (isPartial) return
|
|
202
|
+
if (isPartial) return renderPendingResult(labels.pendingLabel, width);
|
|
182
203
|
|
|
183
204
|
const content = result.content?.[0];
|
|
184
205
|
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
|
-
}
|
|
206
|
+
if (isError || result.isError) return renderErrorResult(textContent, { expanded, width });
|
|
190
207
|
|
|
191
208
|
const readseekValue = (result.details as any)?.readseekValue as
|
|
192
209
|
| { 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,72 @@ 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 (see AC 12 — warnings namespace alignment).
|
|
369
|
+
const binaryWarning = result.readseekValue.warnings.find((w) => w.code === "binary-content");
|
|
370
|
+
if (binaryWarning) {
|
|
371
|
+
return {
|
|
372
|
+
content: [{ type: "text" as const, text: result.text }],
|
|
373
|
+
isError: true,
|
|
374
|
+
details: {
|
|
375
|
+
readseekValue: {
|
|
376
|
+
...result.readseekValue,
|
|
377
|
+
ok: false,
|
|
378
|
+
error: buildReadseekError("binary-content", binaryWarning.message),
|
|
379
|
+
},
|
|
380
|
+
warnings: result.warnings,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
384
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
385
|
+
const bareCrWarning = result.readseekValue.warnings.find((w) => w.code === "bare-cr");
|
|
386
|
+
if (bareCrWarning) {
|
|
387
|
+
return {
|
|
388
|
+
content: [{ type: "text" as const, text: result.text }],
|
|
389
|
+
isError: true,
|
|
390
|
+
details: {
|
|
391
|
+
readseekValue: {
|
|
392
|
+
...result.readseekValue,
|
|
393
|
+
ok: false,
|
|
394
|
+
error: buildReadseekError("bare-cr", bareCrWarning.message),
|
|
395
|
+
},
|
|
396
|
+
warnings: result.warnings,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
400
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
},
|
|
410
|
-
};
|
|
411
|
-
});
|
|
412
|
-
} catch (err: any) {
|
|
413
|
-
const mapped = mapFsWriteError(err, absolutePath);
|
|
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
|
-
),
|
|
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,
|
|
430
409
|
},
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
} catch (err: any) {
|
|
413
|
+
return buildWriteFsErrorResult(err, absolutePath);
|
|
434
414
|
}
|
|
435
415
|
},
|
|
436
416
|
renderCall(args: any, theme: any, context: any = {}) {
|
|
@@ -478,9 +458,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
478
458
|
const details = result.details ?? {};
|
|
479
459
|
const output = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
480
460
|
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);
|
|
461
|
+
return renderErrorResult(output, { expanded, width, fallback: "write failed" });
|
|
484
462
|
}
|
|
485
463
|
const diffData = details.diffData;
|
|
486
464
|
const state = details.writeState === "overwritten" ? "overwritten" : "created";
|
|
@@ -510,7 +488,6 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
510
488
|
}
|
|
511
489
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
512
490
|
},
|
|
513
|
-
}
|
|
514
|
-
pi.registerTool(tool);
|
|
491
|
+
});
|
|
515
492
|
return tool;
|
|
516
493
|
}
|