pi-readseek 0.4.15 → 0.4.16
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 -3
- package/src/edit.ts +5 -21
- package/src/grep.ts +6 -20
- package/src/read.ts +5 -15
- package/src/readseek-params.ts +39 -0
- package/src/refs.ts +12 -17
- package/src/register-tool.ts +10 -0
- package/src/sg.ts +12 -18
- package/src/tui-diff-component.ts +23 -1
- package/src/tui-render-utils.ts +20 -0
- package/src/write.ts +10 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
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": {
|
|
@@ -41,8 +41,6 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@jarkkojs/readseek": "^0.4.20",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
|
-
"ignore": "^7.0.5",
|
|
45
|
-
"picomatch": "^4.0.4",
|
|
46
44
|
"xxhash-wasm": "^1.1.0"
|
|
47
45
|
},
|
|
48
46
|
"peerDependencies": {
|
package/src/edit.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { replaceSymbol } from "./replace-symbol.js";
|
|
|
24
24
|
import { buildEditPreviewKey, buildPendingEditPreviewData, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
|
|
25
25
|
import { buildDiffData, type DiffBlockRange } from "./diff-data.js";
|
|
26
26
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
27
|
-
import {
|
|
27
|
+
import { upsertDiffComponent, upsertTextComponent } from "./tui-diff-component.js";
|
|
28
28
|
import type { FreshAnchorsPredicate } from "./tool-types.js";
|
|
29
29
|
import { registerReadSeekTool } from "./register-tool.js";
|
|
30
30
|
|
|
@@ -643,11 +643,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
643
643
|
// expandable diff). Keeping the "↳ pending edit" sub-line and its
|
|
644
644
|
// preview alongside the final result is just duplicate noise.
|
|
645
645
|
if (context.executionStarted) {
|
|
646
|
-
|
|
647
|
-
? context.lastComponent
|
|
648
|
-
: new Text("", 0, 0);
|
|
649
|
-
textComponent.setText(text);
|
|
650
|
-
return textComponent;
|
|
646
|
+
return upsertTextComponent(context.lastComponent, text);
|
|
651
647
|
}
|
|
652
648
|
const previewKey = buildEditPreviewKey(args ?? {});
|
|
653
649
|
const preview = resolvePendingDiffPreview(
|
|
@@ -659,17 +655,9 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
659
655
|
const expanded = !!context.expanded || resolveEditDiffDisplay() === "expanded";
|
|
660
656
|
const preview2 = pendingPreviewLines(text, preview, expanded);
|
|
661
657
|
if (preview2.diffData) {
|
|
662
|
-
|
|
663
|
-
? context.lastComponent
|
|
664
|
-
: new DiffPreviewComponent({ prefixLines: preview2.lines, diffData: preview2.diffData, theme, expanded: true });
|
|
665
|
-
diffComponent.update({ prefixLines: preview2.lines, diffData: preview2.diffData, theme, expanded: true });
|
|
666
|
-
return diffComponent;
|
|
658
|
+
return upsertDiffComponent(context.lastComponent, { prefixLines: preview2.lines, diffData: preview2.diffData, theme, expanded: true });
|
|
667
659
|
}
|
|
668
|
-
|
|
669
|
-
? context.lastComponent
|
|
670
|
-
: new Text("", 0, 0);
|
|
671
|
-
textComponent.setText(clampLinesToWidth(preview2.lines, context.width).join("\n"));
|
|
672
|
-
return textComponent;
|
|
660
|
+
return upsertTextComponent(context.lastComponent, clampLinesToWidth(preview2.lines, context.width).join("\n"));
|
|
673
661
|
},
|
|
674
662
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
675
663
|
const { isPartial, isError, expanded: baseExpanded, width, context } = resolveRenderResultContext(options, rest);
|
|
@@ -719,11 +707,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
719
707
|
if (info.warningsBadge) badges.push(info.warningsBadge);
|
|
720
708
|
text = summaryLine(badges.join(" • "), { hidden: !!diffData && !expanded });
|
|
721
709
|
if (expanded && diffData) {
|
|
722
|
-
|
|
723
|
-
? context.lastComponent
|
|
724
|
-
: new DiffPreviewComponent({ prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
725
|
-
diffComponent.update({ prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
726
|
-
return diffComponent;
|
|
710
|
+
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
727
711
|
}
|
|
728
712
|
}
|
|
729
713
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
package/src/grep.ts
CHANGED
|
@@ -21,7 +21,8 @@ import { formatGrepCallText, formatGrepResultText } from "./grep-render-helpers.
|
|
|
21
21
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
22
22
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
23
23
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
24
|
-
import { registerReadSeekTool } from "./register-tool.js";
|
|
24
|
+
import { optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
25
|
+
import { searchPathParam } from "./readseek-params.js";
|
|
25
26
|
|
|
26
27
|
const GREP_PROMPT_METADATA = defineToolPromptMetadata({
|
|
27
28
|
promptUrl: new URL("../prompts/grep.md", import.meta.url),
|
|
@@ -30,34 +31,19 @@ const GREP_PROMPT_METADATA = defineToolPromptMetadata({
|
|
|
30
31
|
|
|
31
32
|
const grepSchema = Type.Object({
|
|
32
33
|
pattern: Type.String({ description: "Pattern to search" }),
|
|
33
|
-
path:
|
|
34
|
+
path: searchPathParam(),
|
|
34
35
|
glob: Type.Optional(Type.String({ description: "Glob filter" })),
|
|
35
36
|
ignoreCase: Type.Optional(Type.Boolean({ description: "Ignore case" })),
|
|
36
37
|
literal: Type.Optional(Type.Boolean({ description: "Treat pattern literally" })),
|
|
37
|
-
context:
|
|
38
|
-
|
|
39
|
-
Type.Number({ description: "Context lines" }),
|
|
40
|
-
Type.String({ description: "Context lines" }),
|
|
41
|
-
]),
|
|
42
|
-
),
|
|
43
|
-
limit: Type.Optional(
|
|
44
|
-
Type.Union([
|
|
45
|
-
Type.Number({ description: "Max matches" }),
|
|
46
|
-
Type.String({ description: "Max matches" }),
|
|
47
|
-
]),
|
|
48
|
-
),
|
|
38
|
+
context: optionalIntOrString("Context lines"),
|
|
39
|
+
limit: optionalIntOrString("Max matches"),
|
|
49
40
|
summary: Type.Optional(Type.Boolean({ description: "Return per-file counts" })),
|
|
50
41
|
scope: Type.Optional(
|
|
51
42
|
Type.Literal("symbol", {
|
|
52
43
|
description: "Scope matches to symbols",
|
|
53
44
|
}),
|
|
54
45
|
),
|
|
55
|
-
scopeContext:
|
|
56
|
-
Type.Union([
|
|
57
|
-
Type.Number({ description: "Symbol context lines" }),
|
|
58
|
-
Type.String({ description: "Symbol context lines" }),
|
|
59
|
-
]),
|
|
60
|
-
),
|
|
46
|
+
scopeContext: optionalIntOrString("Symbol context lines"),
|
|
61
47
|
});
|
|
62
48
|
|
|
63
49
|
interface GrepParams {
|
package/src/read.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { readseekRead, readseekDetect, type ReadSeekDetection } from "./readseek
|
|
|
29
29
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
30
30
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
31
31
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
32
|
-
import { registerReadSeekTool } from "./register-tool.js";
|
|
32
|
+
import { optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
33
33
|
|
|
34
34
|
const READ_PROMPT_METADATA = defineToolPromptMetadata({
|
|
35
35
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
@@ -145,7 +145,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
145
145
|
} catch (err: any) {
|
|
146
146
|
const code = err?.code;
|
|
147
147
|
if (code === "EISDIR") {
|
|
148
|
-
const message = `Path is a directory: ${rawPath}
|
|
148
|
+
const message = `Path is a directory: ${rawPath}`;
|
|
149
149
|
return buildToolErrorResult("read", "path-is-directory", message, { path: rawParams.path, hint: `Use ls(${JSON.stringify(rawPath)}) to inspect directories.` });
|
|
150
150
|
}
|
|
151
151
|
if (code === "EACCES" || code === "EPERM") {
|
|
@@ -194,7 +194,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
194
194
|
let startLine = p.offset !== undefined ? p.offset : 1;
|
|
195
195
|
let endIdx = p.limit !== undefined ? Math.min(startLine - 1 + p.limit, total) : total;
|
|
196
196
|
if (p.offset !== undefined && startLine > total) {
|
|
197
|
-
const message = `
|
|
197
|
+
const message = `offset ${p.offset} is past end of file (${total} lines)`;
|
|
198
198
|
return buildToolErrorResult("read", "offset-past-end", message, { path: rawParams.path });
|
|
199
199
|
}
|
|
200
200
|
let symbolMatch: SymbolMatch | undefined;
|
|
@@ -451,18 +451,8 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
451
451
|
promptGuidelines: READ_PROMPT_METADATA.promptGuidelines,
|
|
452
452
|
parameters: Type.Object({
|
|
453
453
|
path: Type.String({ description: "File path" }),
|
|
454
|
-
offset:
|
|
455
|
-
|
|
456
|
-
Type.Number({ description: "Start line (1-indexed)" }),
|
|
457
|
-
Type.String({ description: "Start line (1-indexed)" }),
|
|
458
|
-
]),
|
|
459
|
-
),
|
|
460
|
-
limit: Type.Optional(
|
|
461
|
-
Type.Union([
|
|
462
|
-
Type.Number({ description: "Max lines" }),
|
|
463
|
-
Type.String({ description: "Max lines" }),
|
|
464
|
-
]),
|
|
465
|
-
),
|
|
454
|
+
offset: optionalIntOrString("Start line (1-indexed)"),
|
|
455
|
+
limit: optionalIntOrString("Max lines"),
|
|
466
456
|
symbol: Type.Optional(Type.String({ description: "Symbol name to read" })),
|
|
467
457
|
map: Type.Optional(Type.Boolean({ description: "Append structural map" })),
|
|
468
458
|
bundle: Type.Optional(
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
import { buildToolErrorResult, type ToolErrorResult } from "./readseek-value.js";
|
|
4
|
+
|
|
5
|
+
/** Optional search path shared by the readseek search tools. */
|
|
6
|
+
export function searchPathParam() {
|
|
7
|
+
return Type.Optional(Type.String({ description: "Search path" }));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Optional language hint shared by the readseek search tools. */
|
|
11
|
+
export function langParam() {
|
|
12
|
+
return Type.Optional(Type.String({ description: "Language hint" }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TypeBox fragment for the Git-scope parameters shared by every readseek
|
|
17
|
+
* search tool. Returns fresh schema instances so each tool spreads its own.
|
|
18
|
+
*/
|
|
19
|
+
export function readseekGitSearchParams() {
|
|
20
|
+
return {
|
|
21
|
+
cached: Type.Optional(Type.Boolean({ description: "In a Git repository, search tracked/indexed files" })),
|
|
22
|
+
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
23
|
+
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Enforce that `ignored` is only meaningful alongside `others`. Returns an
|
|
29
|
+
* error envelope tagged with {@link tool} when the rule is violated, else null.
|
|
30
|
+
*/
|
|
31
|
+
export function validateIgnoredRequiresOthers(
|
|
32
|
+
tool: string,
|
|
33
|
+
params: { others?: boolean; ignored?: boolean },
|
|
34
|
+
): ToolErrorResult | null {
|
|
35
|
+
if (params.ignored && !params.others) {
|
|
36
|
+
return buildToolErrorResult(tool, "invalid-parameter", `Error: ${tool} parameter 'ignored' requires 'others'`);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
package/src/refs.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
2
|
import { Type } from "@sinclair/typebox";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
@@ -9,9 +8,10 @@ import { resolveToCwd } from "./path-utils.js";
|
|
|
9
8
|
import { classifyReadSeekFailure, readseekRefs, type ReadSeekReference } from "./readseek-client.js";
|
|
10
9
|
import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
|
|
11
10
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
11
|
+
import { langParam, readseekGitSearchParams, searchPathParam, validateIgnoredRequiresOthers } from "./readseek-params.js";
|
|
12
12
|
import { registerReadSeekTool } from "./register-tool.js";
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { renderAnchoredFilesResult, renderReadSeekSearchCall } from "./tui-render-utils.js";
|
|
15
15
|
|
|
16
16
|
type RefsParams = {
|
|
17
17
|
name: string;
|
|
@@ -83,9 +83,8 @@ function isReadSeekCursorValidationFailure(message: string): boolean {
|
|
|
83
83
|
export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
84
84
|
const { params, signal, cwd, onFileAnchored } = opts;
|
|
85
85
|
const p = params as RefsParams;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
86
|
+
const ignoredError = validateIgnoredRequiresOthers("refs", p);
|
|
87
|
+
if (ignoredError) return ignoredError;
|
|
89
88
|
if (p.scope && p.line === undefined) {
|
|
90
89
|
return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'scope' requires 'line'");
|
|
91
90
|
}
|
|
@@ -136,26 +135,22 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
136
135
|
promptGuidelines: REFS_PROMPT_METADATA.promptGuidelines,
|
|
137
136
|
parameters: Type.Object({
|
|
138
137
|
name: Type.String({ description: "Identifier to find references for" }),
|
|
139
|
-
path:
|
|
140
|
-
lang:
|
|
138
|
+
path: searchPathParam(),
|
|
139
|
+
lang: langParam(),
|
|
141
140
|
scope: Type.Optional(Type.Boolean({ description: "Restrict to the binding under line/column (single file)" })),
|
|
142
141
|
line: Type.Optional(Type.Number({ description: "One-based cursor line, used with scope" })),
|
|
143
142
|
column: Type.Optional(Type.Number({ description: "One-based cursor byte column, used with scope" })),
|
|
144
|
-
|
|
145
|
-
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
146
|
-
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
143
|
+
...readseekGitSearchParams(),
|
|
147
144
|
}),
|
|
148
145
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
149
146
|
return executeRefs({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
150
147
|
},
|
|
151
148
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (flags.length > 0) text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
158
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
149
|
+
return renderReadSeekSearchCall(args, theme, rest, {
|
|
150
|
+
label: "refs",
|
|
151
|
+
accent: args.name,
|
|
152
|
+
flags: [args.scope && "scope", args.cached && "cached", args.others && "others", args.ignored && "ignored"],
|
|
153
|
+
});
|
|
159
154
|
},
|
|
160
155
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
161
156
|
return renderAnchoredFilesResult(result, options, theme, rest, {
|
package/src/register-tool.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optional tool parameter accepting an integer or its string form, since models
|
|
6
|
+
* frequently pass integers as quoted strings. Both union halves share
|
|
7
|
+
* {@link description}.
|
|
8
|
+
*/
|
|
9
|
+
export function optionalIntOrString(description: string) {
|
|
10
|
+
return Type.Optional(Type.Union([Type.Number({ description }), Type.String({ description })]));
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
export type ReadSeekToolPolicy = "read-only" | "mutating";
|
|
4
14
|
|
package/src/sg.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
5
4
|
import { Type } from "@sinclair/typebox";
|
|
6
5
|
|
|
7
6
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
@@ -11,9 +10,10 @@ import { statSearchPathOrError } from "./stat-search-path.js";
|
|
|
11
10
|
import { classifyReadSeekFailure, isReadSeekAvailable, readseekSearch, type ReadSeekHashline, type ReadSeekSearchFileOutput } from "./readseek-client.js";
|
|
12
11
|
import { buildSgOutput } from "./sg-output.js";
|
|
13
12
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
13
|
+
import { langParam, readseekGitSearchParams, searchPathParam, validateIgnoredRequiresOthers } from "./readseek-params.js";
|
|
14
14
|
import { registerReadSeekTool } from "./register-tool.js";
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { renderAnchoredFilesResult, renderReadSeekSearchCall } from "./tui-render-utils.js";
|
|
17
17
|
|
|
18
18
|
type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
|
|
19
19
|
|
|
@@ -107,10 +107,8 @@ function readseekLanguageForPath(language: string | undefined, searchPath: strin
|
|
|
107
107
|
export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
108
108
|
const { params, signal, cwd, onFileAnchored } = opts;
|
|
109
109
|
const p = params as SgParams;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return buildToolErrorResult("search", "invalid-parameter", message);
|
|
113
|
-
}
|
|
110
|
+
const ignoredError = validateIgnoredRequiresOthers("search", p);
|
|
111
|
+
if (ignoredError) return ignoredError;
|
|
114
112
|
const searchPath = resolveToCwd(p.path ?? ".", cwd);
|
|
115
113
|
|
|
116
114
|
const statResult = await statSearchPathOrError("search", p.path, searchPath);
|
|
@@ -201,23 +199,19 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
201
199
|
promptGuidelines: SG_PROMPT_METADATA.promptGuidelines,
|
|
202
200
|
parameters: Type.Object({
|
|
203
201
|
pattern: Type.String({ description: "AST pattern" }),
|
|
204
|
-
lang:
|
|
205
|
-
path:
|
|
206
|
-
|
|
207
|
-
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
208
|
-
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
202
|
+
lang: langParam(),
|
|
203
|
+
path: searchPathParam(),
|
|
204
|
+
...readseekGitSearchParams(),
|
|
209
205
|
}),
|
|
210
206
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
211
207
|
return executeSg({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
212
208
|
},
|
|
213
209
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (flags.length > 0) text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
220
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
210
|
+
return renderReadSeekSearchCall(args, theme, rest, {
|
|
211
|
+
label: "search",
|
|
212
|
+
accent: `/${args.pattern}/`,
|
|
213
|
+
flags: [args.cached && "cached", args.others && "others", args.ignored && "ignored"],
|
|
214
|
+
});
|
|
221
215
|
},
|
|
222
216
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
223
217
|
return renderAnchoredFilesResult(result, options, theme, rest, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { DiffData } from "./diff-data.js";
|
|
4
4
|
import { renderTuiDiff } from "./tui-diff-renderer.js";
|
|
5
5
|
import { clampLineToWidth, normalizeWidth, type RendererTheme } from "./tui-render-utils.js";
|
|
@@ -84,3 +84,25 @@ export class DiffPreviewComponent implements Component {
|
|
|
84
84
|
return clamped;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reuse the prior render tick's {@link DiffPreviewComponent} when present, else
|
|
90
|
+
* construct one, then apply {@link options}. Keeps a streaming diff preview
|
|
91
|
+
* stable across ticks instead of remounting a fresh component each frame.
|
|
92
|
+
*/
|
|
93
|
+
export function upsertDiffComponent(lastComponent: unknown, options: DiffPreviewComponentOptions): DiffPreviewComponent {
|
|
94
|
+
const component = lastComponent instanceof DiffPreviewComponent ? lastComponent : new DiffPreviewComponent(options);
|
|
95
|
+
component.update(options);
|
|
96
|
+
return component;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reuse the prior render tick's text component when it is not a
|
|
101
|
+
* {@link DiffPreviewComponent}, else construct a fresh {@link Text}, then set
|
|
102
|
+
* its content to {@link text}.
|
|
103
|
+
*/
|
|
104
|
+
export function upsertTextComponent(lastComponent: any, text: string): Text {
|
|
105
|
+
const component = lastComponent && !(lastComponent instanceof DiffPreviewComponent) ? lastComponent : new Text("", 0, 0);
|
|
106
|
+
component.setText(text);
|
|
107
|
+
return component;
|
|
108
|
+
}
|
package/src/tui-render-utils.ts
CHANGED
|
@@ -177,6 +177,26 @@ export interface AnchoredFilesLabels {
|
|
|
177
177
|
unitPlural: string;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Render the call line shared by the readseek search tools (search, refs): a
|
|
182
|
+
* tool label, an accent term, the search path, an optional language hint, and
|
|
183
|
+
* the active boolean flags. Falsy {@link opts.flags} entries are dropped.
|
|
184
|
+
*/
|
|
185
|
+
export function renderReadSeekSearchCall(
|
|
186
|
+
args: { path?: string; lang?: string },
|
|
187
|
+
theme: RendererTheme,
|
|
188
|
+
rest: any[],
|
|
189
|
+
opts: { label: string; accent: string; flags: Array<string | false | undefined> },
|
|
190
|
+
): Text {
|
|
191
|
+
const context = rest[0] ?? {};
|
|
192
|
+
let text = `${renderToolLabel(theme, opts.label)} ${theme.fg("accent", opts.accent)}`;
|
|
193
|
+
text += theme.fg("dim", ` in ${args.path ?? "."}`);
|
|
194
|
+
if (args.lang) text += theme.fg("dim", ` (${args.lang})`);
|
|
195
|
+
const flags = opts.flags.filter(Boolean);
|
|
196
|
+
if (flags.length > 0) text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
197
|
+
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
/**
|
|
181
201
|
* Render the result summary shared by the anchored-files search tools (search,
|
|
182
202
|
* refs): a pending line, an error first-line/expanded body, an empty-result
|
package/src/write.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, relative } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { withFileMutationQueue, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { withFileMutationQueue, type ExtensionAPI, type ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
|
|
@@ -16,8 +16,8 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
|
16
16
|
import { buildPendingWritePreviewData, buildWritePreviewKey, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
|
|
17
17
|
import { generateCompactOrFullDiff, normalizeToLF, hasBareCarriageReturn } from "./edit-diff.js";
|
|
18
18
|
import { buildDiffData, type DiffData } from "./diff-data.js";
|
|
19
|
-
import { clampLineToWidth, clampLinesToWidth,
|
|
20
|
-
import {
|
|
19
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
20
|
+
import { upsertDiffComponent, upsertTextComponent } from "./tui-diff-component.js";
|
|
21
21
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
22
22
|
import { registerReadSeekTool } from "./register-tool.js";
|
|
23
23
|
|
|
@@ -399,32 +399,20 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
399
399
|
// its preview alongside the final result is just duplicate noise — the
|
|
400
400
|
// pre-execution state can no longer change in any meaningful way.
|
|
401
401
|
if (context.executionStarted) {
|
|
402
|
-
|
|
403
|
-
? context.lastComponent
|
|
404
|
-
: new Text("", 0, 0);
|
|
405
|
-
textComponent.setText(text);
|
|
406
|
-
return textComponent;
|
|
402
|
+
return upsertTextComponent(context.lastComponent, text);
|
|
407
403
|
}
|
|
408
404
|
const previewKey = buildWritePreviewKey(args ?? {});
|
|
409
405
|
const preview = resolvePendingDiffPreview(context, WRITE_PENDING_PREVIEW_STATE_KEY, previewKey, () => buildPendingWritePreviewData(args ?? {}, context.cwd ?? process.cwd()));
|
|
410
406
|
const expanded = !!context.expanded;
|
|
411
407
|
const parts = pendingWritePreviewParts(text, preview, expanded, theme);
|
|
412
408
|
if (parts.diffData) {
|
|
413
|
-
|
|
414
|
-
? context.lastComponent
|
|
415
|
-
: new DiffPreviewComponent({ prefixLines: parts.lines, diffData: parts.diffData, theme, expanded: true });
|
|
416
|
-
diffComponent.update({ prefixLines: parts.lines, diffData: parts.diffData, theme, expanded: true });
|
|
417
|
-
return diffComponent;
|
|
409
|
+
return upsertDiffComponent(context.lastComponent, { prefixLines: parts.lines, diffData: parts.diffData, theme, expanded: true });
|
|
418
410
|
}
|
|
419
|
-
|
|
420
|
-
? context.lastComponent
|
|
421
|
-
: new Text("", 0, 0);
|
|
422
|
-
textComponent.setText(clampLinesToWidth(parts.lines, context.width).join("\n"));
|
|
423
|
-
return textComponent;
|
|
411
|
+
return upsertTextComponent(context.lastComponent, clampLinesToWidth(parts.lines, context.width).join("\n"));
|
|
424
412
|
},
|
|
425
|
-
renderResult(result: any, options:
|
|
426
|
-
const expanded =
|
|
427
|
-
|
|
413
|
+
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
414
|
+
const { isPartial, expanded, width, context } = resolveRenderResultContext(options, rest);
|
|
415
|
+
if (isPartial) return renderPendingResult("pending write", width);
|
|
428
416
|
const details = result.details ?? {};
|
|
429
417
|
const output = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
430
418
|
if (result.isError || details.readseekValue?.ok === false) {
|
|
@@ -450,11 +438,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
450
438
|
const hasExpandableDiff = !!diffData;
|
|
451
439
|
let text = summaryLine(state, { hidden: hasExpandableDiff && !expanded });
|
|
452
440
|
if (expanded && hasExpandableDiff) {
|
|
453
|
-
|
|
454
|
-
? context.lastComponent
|
|
455
|
-
: new DiffPreviewComponent({ prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
456
|
-
diffComponent.update({ prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
457
|
-
return diffComponent;
|
|
441
|
+
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
458
442
|
}
|
|
459
443
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
460
444
|
},
|