pi-readseek 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -46
- package/dist/index.js +7333 -0
- package/package.json +8 -7
- package/index.ts +0 -147
- package/src/binary-detect.ts +0 -22
- package/src/check.ts +0 -92
- package/src/coerce-obvious-int.ts +0 -41
- package/src/confusable-hyphens.ts +0 -6
- package/src/def.ts +0 -143
- package/src/diff-data.ts +0 -302
- package/src/edit-classify.ts +0 -58
- package/src/edit-diff.ts +0 -359
- package/src/edit-output.ts +0 -110
- package/src/edit-render-helpers.ts +0 -141
- package/src/edit-syntax-validate.ts +0 -65
- package/src/edit.ts +0 -686
- package/src/file-map.ts +0 -17
- package/src/fs-error.ts +0 -33
- package/src/grep-budget.ts +0 -35
- package/src/grep-output.ts +0 -181
- package/src/grep-render-helpers.ts +0 -77
- package/src/grep-symbol-scope.ts +0 -184
- package/src/grep.ts +0 -608
- package/src/hashline.ts +0 -736
- package/src/hover.ts +0 -140
- package/src/path-utils.ts +0 -24
- package/src/pending-diff-preview.ts +0 -269
- package/src/read-local-bundle.ts +0 -75
- package/src/read-output.ts +0 -186
- package/src/read-render-helpers.ts +0 -104
- package/src/read.ts +0 -661
- package/src/readseek/constants.ts +0 -17
- package/src/readseek/enums.ts +0 -38
- package/src/readseek/formatter.ts +0 -350
- package/src/readseek/symbol-error-format.ts +0 -18
- package/src/readseek/symbol-lookup.ts +0 -240
- package/src/readseek/symbol-tree.ts +0 -21
- package/src/readseek/types.ts +0 -49
- package/src/readseek-client.ts +0 -1250
- package/src/readseek-params.ts +0 -39
- package/src/readseek-settings.ts +0 -263
- package/src/readseek-value.ts +0 -184
- package/src/refs-output.ts +0 -47
- package/src/refs.ts +0 -161
- package/src/register-tool.ts +0 -29
- package/src/rename.ts +0 -182
- package/src/replace-symbol.ts +0 -77
- package/src/runtime.ts +0 -3
- package/src/search-output.ts +0 -50
- package/src/search.ts +0 -214
- package/src/session-anchors.ts +0 -34
- package/src/stat-search-path.ts +0 -29
- package/src/syntax-validate-mode.ts +0 -15
- package/src/tool-prompt-metadata.ts +0 -107
- package/src/tool-types.ts +0 -12
- package/src/tui-diff-component.ts +0 -108
- package/src/tui-diff-renderer.ts +0 -92
- package/src/tui-render-utils.ts +0 -286
- package/src/view.ts +0 -158
- package/src/write.ts +0 -357
package/src/hover.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Type } from "@sinclair/typebox";
|
|
5
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
6
|
-
|
|
7
|
-
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
8
|
-
import { buildToolErrorResult } from "./readseek-value.js";
|
|
9
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
10
|
-
import { formatFsError } from "./fs-error.js";
|
|
11
|
-
import { classifyReadSeekFailure, readSeekIdentify } from "./readseek-client.js";
|
|
12
|
-
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
13
|
-
|
|
14
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
15
|
-
|
|
16
|
-
const HOVER_PROMPT_METADATA = defineToolPromptMetadata({
|
|
17
|
-
promptUrl: new URL("../prompts/hover.md", import.meta.url),
|
|
18
|
-
promptSnippet: "Identify the token and enclosing symbol at a cursor",
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const hoverSchema = Type.Object({
|
|
22
|
-
path: filePathParam(),
|
|
23
|
-
line: Type.Integer({ minimum: 1, description: "One-based cursor line" }),
|
|
24
|
-
column: Type.Optional(Type.Integer({ minimum: 1, description: "One-based cursor byte column" })),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
interface HoverParams {
|
|
28
|
-
path: string;
|
|
29
|
-
line: number;
|
|
30
|
-
column?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ExecuteHoverOptions {
|
|
34
|
-
params: unknown;
|
|
35
|
-
signal: AbortSignal | undefined;
|
|
36
|
-
cwd: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function executeHover(opts: ExecuteHoverOptions): Promise<any> {
|
|
40
|
-
const { params, signal, cwd } = opts;
|
|
41
|
-
const p = params as HoverParams;
|
|
42
|
-
|
|
43
|
-
if (!Number.isSafeInteger(p.line) || p.line < 1) {
|
|
44
|
-
return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'line' must be a positive integer");
|
|
45
|
-
}
|
|
46
|
-
if (p.column !== undefined && (!Number.isSafeInteger(p.column) || p.column < 1)) {
|
|
47
|
-
return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'column' must be a positive integer");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const filePath = resolveToCwd(p.path, cwd);
|
|
51
|
-
|
|
52
|
-
let content: string;
|
|
53
|
-
try {
|
|
54
|
-
content = await readFile(filePath, "utf-8");
|
|
55
|
-
} catch (err: any) {
|
|
56
|
-
const { code, message } = formatFsError(err, "hover-error");
|
|
57
|
-
return buildToolErrorResult("hover", code, message, { path: p.path });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const output = await readSeekIdentify(filePath, content, {
|
|
62
|
-
line: p.line,
|
|
63
|
-
column: p.column,
|
|
64
|
-
signal,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const lines: string[] = [];
|
|
68
|
-
if (output.identifier) {
|
|
69
|
-
lines.push(`identifier: ${output.identifier.text}`);
|
|
70
|
-
}
|
|
71
|
-
if (output.symbol) {
|
|
72
|
-
lines.push(`symbol: ${output.symbol.name}`);
|
|
73
|
-
lines.push(`kind: ${output.symbol.kind}`);
|
|
74
|
-
lines.push(`qualified: ${output.symbol.qualified_name}`);
|
|
75
|
-
}
|
|
76
|
-
lines.push(`file: ${output.file}`);
|
|
77
|
-
lines.push(`language: ${output.language}`);
|
|
78
|
-
lines.push(`location: ${output.line}:${output.column}`);
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
82
|
-
details: {
|
|
83
|
-
readSeekValue: {
|
|
84
|
-
tool: "hover",
|
|
85
|
-
ok: true,
|
|
86
|
-
path: filePath,
|
|
87
|
-
output,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
} catch (err: any) {
|
|
92
|
-
const failure = classifyReadSeekFailure(err);
|
|
93
|
-
return buildToolErrorResult("hover", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function registerHoverTool(pi: ExtensionAPI) {
|
|
98
|
-
registerReadSeekTool(pi, {
|
|
99
|
-
name: "readSeek_hover",
|
|
100
|
-
label: "Hover",
|
|
101
|
-
description: HOVER_PROMPT_METADATA.description,
|
|
102
|
-
promptSnippet: HOVER_PROMPT_METADATA.promptSnippet,
|
|
103
|
-
promptGuidelines: HOVER_PROMPT_METADATA.promptGuidelines,
|
|
104
|
-
parameters: hoverSchema,
|
|
105
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
106
|
-
return executeHover({ params, signal, cwd: ctx.cwd });
|
|
107
|
-
},
|
|
108
|
-
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
109
|
-
const context = rest[0] ?? {};
|
|
110
|
-
const cwd = context.cwd ?? process.cwd();
|
|
111
|
-
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
112
|
-
let text = theme.fg("toolTitle", theme.bold("hover"));
|
|
113
|
-
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
114
|
-
if (args?.line) text += theme.fg("dim", `:${args.line}`);
|
|
115
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
116
|
-
},
|
|
117
|
-
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
118
|
-
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
119
|
-
|
|
120
|
-
if (isPartial) return renderPendingResult("pending hover", width, theme);
|
|
121
|
-
|
|
122
|
-
const content = result.content?.[0];
|
|
123
|
-
const textContent = content?.type === "text" ? content.text : "";
|
|
124
|
-
|
|
125
|
-
if (isError || result.isError) {
|
|
126
|
-
return renderErrorResult(textContent, { expanded, width, fallback: "hover failed", theme });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const output = (result.details as any)?.readSeekValue?.output;
|
|
130
|
-
const label = output?.identifier?.text
|
|
131
|
-
? `identified ${output.identifier.text}`
|
|
132
|
-
: output?.symbol?.name
|
|
133
|
-
? `symbol ${output.symbol.name}`
|
|
134
|
-
: "identified cursor";
|
|
135
|
-
let text = summaryLine(label, { hidden: !!textContent && !expanded, theme, style: "success" });
|
|
136
|
-
if (expanded && textContent) text += `\n${textContent}`;
|
|
137
|
-
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
}
|
package/src/path-utils.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
3
|
-
|
|
4
|
-
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
5
|
-
|
|
6
|
-
function normalizeUnicodeSpaces(str: string): string {
|
|
7
|
-
return str.replace(UNICODE_SPACES, " ");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function normalizeAtPrefix(filePath: string): string {
|
|
11
|
-
return filePath.startsWith("@") ? filePath.slice(1) : filePath;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function expandPath(filePath: string): string {
|
|
15
|
-
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
|
|
16
|
-
if (normalized === "~") return homedir();
|
|
17
|
-
if (normalized.startsWith("~/")) return homedir() + normalized.slice(1);
|
|
18
|
-
return normalized;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function resolveToCwd(filePath: string, cwd: string): string {
|
|
22
|
-
const expanded = expandPath(filePath);
|
|
23
|
-
return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
|
|
24
|
-
}
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { generateDiffString, normalizeToLF, replaceText } from "./edit-diff.js";
|
|
4
|
-
import { applyHashlineEdits, type HashlineEditItem } from "./hashline.js";
|
|
5
|
-
import { replaceSymbol } from "./replace-symbol.js";
|
|
6
|
-
|
|
7
|
-
const PENDING_DIFF_MAX_BYTES = 1024 * 1024;
|
|
8
|
-
|
|
9
|
-
interface PendingDiffPreviewData {
|
|
10
|
-
filePath: string;
|
|
11
|
-
previousContent: string;
|
|
12
|
-
nextContent: string;
|
|
13
|
-
fileExistedBeforeWrite: boolean;
|
|
14
|
-
headerLabel: "pending edit" | "pending overwrite" | "pending create";
|
|
15
|
-
diff: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type PendingDiffPreviewResult =
|
|
19
|
-
| { type: "ok"; data: PendingDiffPreviewData }
|
|
20
|
-
| { type: "skip"; reason: string };
|
|
21
|
-
|
|
22
|
-
function skip(reason: string): PendingDiffPreviewResult {
|
|
23
|
-
return { type: "skip", reason };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isInsidePath(parent: string, child: string): boolean {
|
|
27
|
-
const rel = relative(parent, child);
|
|
28
|
-
return rel === "" || (rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep) && !resolve(rel).startsWith(".." + sep));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function resolveWorkspacePreviewPath(rawPath: unknown, cwd: string, allowMissing: boolean): { type: "ok"; path: string; existed: boolean } | { type: "skip"; reason: string } {
|
|
32
|
-
if (typeof rawPath !== "string" || rawPath.trim() === "") return { type: "skip", reason: "missing path" };
|
|
33
|
-
const workspace = realpathSync(cwd);
|
|
34
|
-
const normalizedPath = rawPath.replace(/^@/, "");
|
|
35
|
-
const explicitAbsolute = isAbsolute(normalizedPath);
|
|
36
|
-
const requested = resolve(workspace, normalizedPath);
|
|
37
|
-
if (existsSync(requested)) {
|
|
38
|
-
const realTarget = realpathSync(requested);
|
|
39
|
-
if (!explicitAbsolute && !isInsidePath(workspace, realTarget)) return { type: "skip", reason: "path outside workspace" };
|
|
40
|
-
return { type: "ok", path: realTarget, existed: true };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!explicitAbsolute && !isInsidePath(workspace, requested)) return { type: "skip", reason: "path outside workspace" };
|
|
44
|
-
|
|
45
|
-
if (!allowMissing) return { type: "skip", reason: "file not found" };
|
|
46
|
-
const parent = dirname(requested);
|
|
47
|
-
if (!existsSync(parent)) return { type: "skip", reason: "parent directory not found" };
|
|
48
|
-
return { type: "ok", path: requested, existed: false };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function readUtf8File(filePath: string): { type: "ok"; content: string } | { type: "skip"; reason: string } {
|
|
52
|
-
const stat = statSync(filePath);
|
|
53
|
-
if (!stat.isFile()) return { type: "skip", reason: "not a file" };
|
|
54
|
-
if (stat.size > PENDING_DIFF_MAX_BYTES) return { type: "skip", reason: "file too large" };
|
|
55
|
-
const content = readFileSync(filePath, "utf-8");
|
|
56
|
-
if (content.includes("\0")) return { type: "skip", reason: "binary file" };
|
|
57
|
-
return { type: "ok", content };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function buildData(
|
|
61
|
-
filePath: string,
|
|
62
|
-
previousContent: string,
|
|
63
|
-
nextContent: string,
|
|
64
|
-
existed: boolean,
|
|
65
|
-
headerLabel: PendingDiffPreviewData["headerLabel"],
|
|
66
|
-
): PendingDiffPreviewResult {
|
|
67
|
-
const diff = generateDiffString(normalizeToLF(previousContent), normalizeToLF(nextContent)).diff;
|
|
68
|
-
return {
|
|
69
|
-
type: "ok",
|
|
70
|
-
data: {
|
|
71
|
-
filePath,
|
|
72
|
-
previousContent,
|
|
73
|
-
nextContent,
|
|
74
|
-
fileExistedBeforeWrite: existed,
|
|
75
|
-
headerLabel,
|
|
76
|
-
diff,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
type ReplaceEdit = { replace: { old_text: string; new_text: string; all?: boolean; fuzzy?: boolean } };
|
|
82
|
-
type PendingEditInput = { path?: unknown; edits?: unknown[] };
|
|
83
|
-
|
|
84
|
-
function getEdits(input: PendingEditInput): unknown[] {
|
|
85
|
-
return Array.isArray(input.edits) ? input.edits : [];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function isReplaceEdit(edit: unknown): edit is ReplaceEdit {
|
|
89
|
-
if (!edit || typeof edit !== "object" || !("replace" in edit)) return false;
|
|
90
|
-
const replace = (edit as { replace?: { old_text?: unknown; new_text?: unknown } }).replace;
|
|
91
|
-
return typeof replace?.old_text === "string" && typeof replace?.new_text === "string";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
type AnchorEdit =
|
|
95
|
-
| { set_line: { anchor: string; new_text: string } }
|
|
96
|
-
| { replace_lines: { start_anchor: string; end_anchor: string; new_text: string } }
|
|
97
|
-
| { insert_after: { anchor: string; new_text: string } };
|
|
98
|
-
|
|
99
|
-
function isAnchorEdit(edit: unknown): edit is AnchorEdit {
|
|
100
|
-
return !!edit && typeof edit === "object" && ("set_line" in edit || "replace_lines" in edit || "insert_after" in edit);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function applyAnchoredPreview(content: string, edits: AnchorEdit[]): { type: "ok"; content: string } | { type: "skip"; reason: string } {
|
|
104
|
-
try {
|
|
105
|
-
return { type: "ok", content: applyHashlineEdits(content, edits as HashlineEditItem[]).content };
|
|
106
|
-
} catch (err: any) {
|
|
107
|
-
return { type: "skip", reason: `anchor projection failed: ${err?.message ?? String(err)}` };
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
type ReplaceSymbolEdit = { replace_symbol: { symbol: string; new_body: string } };
|
|
112
|
-
|
|
113
|
-
function isReplaceSymbolEdit(edit: unknown): edit is ReplaceSymbolEdit {
|
|
114
|
-
if (!edit || typeof edit !== "object" || !("replace_symbol" in edit)) return false;
|
|
115
|
-
const replaceSymbol = (edit as { replace_symbol?: { symbol?: unknown; new_body?: unknown } }).replace_symbol;
|
|
116
|
-
return typeof replaceSymbol?.symbol === "string" && typeof replaceSymbol?.new_body === "string";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function applyReplaceSymbolPreview(filePath: string, content: string, edit: ReplaceSymbolEdit): Promise<{ type: "ok"; content: string } | { type: "skip"; reason: string }> {
|
|
120
|
-
try {
|
|
121
|
-
if (!edit.replace_symbol.new_body.trim()) return { type: "skip", reason: "replace_symbol new_body is empty" };
|
|
122
|
-
const probe = await replaceSymbol({
|
|
123
|
-
filePath,
|
|
124
|
-
content,
|
|
125
|
-
symbol: edit.replace_symbol.symbol,
|
|
126
|
-
newBody: edit.replace_symbol.new_body,
|
|
127
|
-
});
|
|
128
|
-
if (probe.type !== "ok") return { type: "skip", reason: `symbol projection failed: ${probe.message}` };
|
|
129
|
-
return { type: "ok", content: probe.content };
|
|
130
|
-
} catch (err: any) {
|
|
131
|
-
return { type: "skip", reason: `symbol projection failed: ${err?.message ?? String(err)}` };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
function applyReplacePreview(content: string, edit: ReplaceEdit): { type: "ok"; content: string } | { type: "skip"; reason: string } {
|
|
137
|
-
const { old_text, new_text } = edit.replace;
|
|
138
|
-
if (!old_text.length) return { type: "skip", reason: "replace old_text is empty" };
|
|
139
|
-
const replacement = replaceText(content, old_text, new_text, {
|
|
140
|
-
all: edit.replace.all ?? false,
|
|
141
|
-
fuzzy: edit.replace.fuzzy ?? false,
|
|
142
|
-
});
|
|
143
|
-
if (!replacement.count) return { type: "skip", reason: "replace old_text was not found" };
|
|
144
|
-
return { type: "ok", content: replacement.content };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function buildPendingWritePreviewData(input: { path?: unknown; content?: unknown }, cwd: string): PendingDiffPreviewResult {
|
|
148
|
-
if (typeof input.content !== "string") return skip("missing content");
|
|
149
|
-
if (Buffer.byteLength(input.content, "utf8") > PENDING_DIFF_MAX_BYTES) return skip("content too large");
|
|
150
|
-
const resolved = resolveWorkspacePreviewPath(input.path, cwd, true);
|
|
151
|
-
if (resolved.type === "skip") return resolved;
|
|
152
|
-
const previous = resolved.existed ? readUtf8File(resolved.path) : { type: "ok" as const, content: "" };
|
|
153
|
-
if (previous.type === "skip") return previous;
|
|
154
|
-
return buildData(resolved.path, previous.content, input.content, resolved.existed, resolved.existed ? "pending overwrite" : "pending create");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function buildPendingEditPreviewData(input: PendingEditInput, cwd: string): Promise<PendingDiffPreviewResult> {
|
|
158
|
-
const edits = getEdits(input);
|
|
159
|
-
if (edits.length === 0) return skip("missing edits");
|
|
160
|
-
const resolved = resolveWorkspacePreviewPath(input.path, cwd, false);
|
|
161
|
-
if (resolved.type === "skip") return resolved;
|
|
162
|
-
const previous = readUtf8File(resolved.path);
|
|
163
|
-
if (previous.type === "skip") return previous;
|
|
164
|
-
let next = normalizeToLF(previous.content);
|
|
165
|
-
const anchorBatch: AnchorEdit[] = [];
|
|
166
|
-
|
|
167
|
-
for (const edit of edits) {
|
|
168
|
-
if (isReplaceEdit(edit)) {
|
|
169
|
-
if (anchorBatch.length > 0) {
|
|
170
|
-
const anchored = applyAnchoredPreview(next, anchorBatch);
|
|
171
|
-
if (anchored.type === "skip") return anchored;
|
|
172
|
-
next = anchored.content;
|
|
173
|
-
anchorBatch.length = 0;
|
|
174
|
-
}
|
|
175
|
-
const projected = applyReplacePreview(next, edit);
|
|
176
|
-
if (projected.type === "skip") return projected;
|
|
177
|
-
next = projected.content;
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (isAnchorEdit(edit)) {
|
|
181
|
-
anchorBatch.push(edit);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (isReplaceSymbolEdit(edit)) {
|
|
185
|
-
if (anchorBatch.length > 0) {
|
|
186
|
-
const anchored = applyAnchoredPreview(next, anchorBatch);
|
|
187
|
-
if (anchored.type === "skip") return anchored;
|
|
188
|
-
next = anchored.content;
|
|
189
|
-
anchorBatch.length = 0;
|
|
190
|
-
}
|
|
191
|
-
const projected = await applyReplaceSymbolPreview(resolved.path, next, edit);
|
|
192
|
-
if (projected.type === "skip") return projected;
|
|
193
|
-
next = projected.content;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
return skip("unsupported edit variant");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (anchorBatch.length > 0) {
|
|
200
|
-
const anchored = applyAnchoredPreview(next, anchorBatch);
|
|
201
|
-
if (anchored.type === "skip") return anchored;
|
|
202
|
-
next = anchored.content;
|
|
203
|
-
}
|
|
204
|
-
return buildData(resolved.path, previous.content, next, true, "pending edit");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
interface PendingDiffPreviewCacheSlot<T = PendingDiffPreviewResult> {
|
|
208
|
-
key?: string;
|
|
209
|
-
data?: T;
|
|
210
|
-
pending?: boolean;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function buildEditPreviewKey(input: PendingEditInput): string | undefined {
|
|
214
|
-
if (typeof input.path !== "string") return undefined;
|
|
215
|
-
const edits = getEdits(input);
|
|
216
|
-
if (edits.length === 0) return undefined;
|
|
217
|
-
return JSON.stringify({ path: input.path, edits });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function buildWritePreviewKey(input: { path?: unknown; content?: unknown }): string | undefined {
|
|
221
|
-
if (typeof input.path !== "string" || typeof input.content !== "string") return undefined;
|
|
222
|
-
return JSON.stringify({ path: input.path, content: input.content });
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function resolvePendingDiffPreview<T extends PendingDiffPreviewResult>(
|
|
226
|
-
context: { state?: Record<string, PendingDiffPreviewCacheSlot<T>>; invalidate?: () => void } | undefined,
|
|
227
|
-
stateKey: string,
|
|
228
|
-
previewKey: string | undefined,
|
|
229
|
-
compute: () => T | Promise<T>,
|
|
230
|
-
): T | undefined {
|
|
231
|
-
if (!previewKey) return undefined;
|
|
232
|
-
const root = context?.state;
|
|
233
|
-
if (!root) return undefined;
|
|
234
|
-
const slot = (root[stateKey] ??= {} as PendingDiffPreviewCacheSlot<T>);
|
|
235
|
-
if (slot.key !== previewKey) {
|
|
236
|
-
slot.key = previewKey;
|
|
237
|
-
slot.data = undefined;
|
|
238
|
-
slot.pending = false;
|
|
239
|
-
}
|
|
240
|
-
if (slot.data !== undefined) return slot.data;
|
|
241
|
-
if (slot.pending) return undefined;
|
|
242
|
-
|
|
243
|
-
let value: T | Promise<T>;
|
|
244
|
-
try {
|
|
245
|
-
value = compute();
|
|
246
|
-
} catch (err: any) {
|
|
247
|
-
const skipped = { type: "skip", reason: `projection failed: ${err?.message ?? String(err)}` } as T;
|
|
248
|
-
slot.data = skipped;
|
|
249
|
-
return skipped;
|
|
250
|
-
}
|
|
251
|
-
if (value && typeof (value as any).then === "function") {
|
|
252
|
-
slot.pending = true;
|
|
253
|
-
void (value as Promise<T>).then((resolved) => {
|
|
254
|
-
if (slot.key !== previewKey) return;
|
|
255
|
-
slot.data = resolved;
|
|
256
|
-
slot.pending = false;
|
|
257
|
-
context?.invalidate?.();
|
|
258
|
-
}).catch((err: any) => {
|
|
259
|
-
if (slot.key !== previewKey) return;
|
|
260
|
-
slot.data = { type: "skip", reason: `projection failed: ${err?.message ?? String(err)}` } as T;
|
|
261
|
-
slot.pending = false;
|
|
262
|
-
context?.invalidate?.();
|
|
263
|
-
});
|
|
264
|
-
return undefined;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
slot.data = value as T;
|
|
268
|
-
return slot.data;
|
|
269
|
-
}
|
package/src/read-local-bundle.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { SymbolKind } from "./readseek/enums.js";
|
|
2
|
-
import type { SymbolMatch } from "./readseek/symbol-lookup.js";
|
|
3
|
-
import type { FileMap, FileSymbol } from "./readseek/types.js";
|
|
4
|
-
import { traverseSymbolTree } from "./readseek/symbol-tree.js";
|
|
5
|
-
|
|
6
|
-
export interface LocalBundleSupport {
|
|
7
|
-
symbol: SymbolMatch;
|
|
8
|
-
lines: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface LocalBundlePlan {
|
|
12
|
-
requested: SymbolMatch;
|
|
13
|
-
support: LocalBundleSupport[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface FlatSymbol {
|
|
17
|
-
symbol: FileSymbol;
|
|
18
|
-
parentName?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const CONTAINER_KINDS = new Set<SymbolKind>([
|
|
22
|
-
SymbolKind.Class,
|
|
23
|
-
SymbolKind.Module,
|
|
24
|
-
SymbolKind.Namespace,
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
export function buildLocalBundle(
|
|
28
|
-
fileMap: FileMap,
|
|
29
|
-
requested: SymbolMatch,
|
|
30
|
-
allLines: string[],
|
|
31
|
-
): LocalBundlePlan | null {
|
|
32
|
-
const requestedText = allLines.slice(requested.startLine - 1, requested.endLine).join("\n");
|
|
33
|
-
const identifiers = new Set(requestedText.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? []);
|
|
34
|
-
|
|
35
|
-
const candidates = traverseSymbolTree(fileMap.symbols, (symbol, parentName): FlatSymbol => ({ symbol, parentName })).filter(({ symbol }) => {
|
|
36
|
-
if (CONTAINER_KINDS.has(symbol.kind as SymbolKind)) return false;
|
|
37
|
-
return !(symbol.name === requested.name && symbol.startLine === requested.startLine && symbol.endLine === requested.endLine);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const symbolsByName = new Map<string, FlatSymbol[]>();
|
|
41
|
-
for (const candidate of candidates) {
|
|
42
|
-
const bucket = symbolsByName.get(candidate.symbol.name) ?? [];
|
|
43
|
-
bucket.push(candidate);
|
|
44
|
-
symbolsByName.set(candidate.symbol.name, bucket);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const support: LocalBundleSupport[] = [];
|
|
48
|
-
for (const identifier of identifiers) {
|
|
49
|
-
const matches = symbolsByName.get(identifier) ?? [];
|
|
50
|
-
if (matches.length === 0) continue;
|
|
51
|
-
if (matches.length > 1) return null;
|
|
52
|
-
|
|
53
|
-
const match = matches[0];
|
|
54
|
-
support.push({
|
|
55
|
-
symbol: {
|
|
56
|
-
name: match.symbol.name,
|
|
57
|
-
kind: match.symbol.kind,
|
|
58
|
-
startLine: match.symbol.startLine,
|
|
59
|
-
endLine: match.symbol.endLine,
|
|
60
|
-
...(match.parentName ? { parentName: match.parentName } : {}),
|
|
61
|
-
},
|
|
62
|
-
lines: allLines.slice(match.symbol.startLine - 1, match.symbol.endLine),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
support.sort((a, b) => {
|
|
67
|
-
if (a.symbol.startLine !== b.symbol.startLine) return a.symbol.startLine - b.symbol.startLine;
|
|
68
|
-
return a.symbol.name.localeCompare(b.symbol.name);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
requested,
|
|
73
|
-
support,
|
|
74
|
-
};
|
|
75
|
-
}
|
package/src/read-output.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_MAX_BYTES,
|
|
3
|
-
DEFAULT_MAX_LINES,
|
|
4
|
-
formatSize,
|
|
5
|
-
truncateHead,
|
|
6
|
-
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { buildReadSeekLines, renderReadSeekLines, type ReadSeekLine, type ReadSeekWarning } from "./readseek-value.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
interface ReadSymbolMetadata {
|
|
11
|
-
query: string;
|
|
12
|
-
name: string;
|
|
13
|
-
kind: string;
|
|
14
|
-
parentName?: string;
|
|
15
|
-
startLine: number;
|
|
16
|
-
endLine: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface ReadTruncationMetadata {
|
|
20
|
-
outputLines: number;
|
|
21
|
-
totalLines: number;
|
|
22
|
-
outputBytes: number;
|
|
23
|
-
totalBytes: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface ReadMapMetadata {
|
|
27
|
-
requested: boolean;
|
|
28
|
-
appended: boolean;
|
|
29
|
-
text?: string | null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface ReadContinuationMetadata {
|
|
33
|
-
nextOffset: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface ReadBundleSupportItem {
|
|
37
|
-
symbol: ReadSymbolMetadata;
|
|
38
|
-
lines: string[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface ReadBundleMetadata {
|
|
42
|
-
mode: "local";
|
|
43
|
-
applied: boolean;
|
|
44
|
-
localSupport: ReadBundleSupportItem[];
|
|
45
|
-
warnings?: ReadSeekWarning[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface ReadOutputInput {
|
|
49
|
-
path: string;
|
|
50
|
-
startLine: number;
|
|
51
|
-
endLine: number;
|
|
52
|
-
totalLines: number;
|
|
53
|
-
selectedLines?: string[];
|
|
54
|
-
lines?: ReadSeekLine[];
|
|
55
|
-
warnings?: ReadSeekWarning[];
|
|
56
|
-
truncation?: ReadTruncationMetadata | null;
|
|
57
|
-
continuation?: ReadContinuationMetadata | null;
|
|
58
|
-
symbol?: ReadSymbolMetadata | null;
|
|
59
|
-
map?: ReadMapMetadata;
|
|
60
|
-
bundle?: ReadBundleMetadata | null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface ReadOutputResult {
|
|
64
|
-
text: string;
|
|
65
|
-
lines: ReadSeekLine[];
|
|
66
|
-
readSeekValue: {
|
|
67
|
-
tool: "read";
|
|
68
|
-
path: string;
|
|
69
|
-
range: {
|
|
70
|
-
startLine: number;
|
|
71
|
-
endLine: number;
|
|
72
|
-
totalLines: number;
|
|
73
|
-
};
|
|
74
|
-
warnings: ReadSeekWarning[];
|
|
75
|
-
truncation: ReadTruncationMetadata | null;
|
|
76
|
-
symbol: ReadSymbolMetadata | null;
|
|
77
|
-
map: {
|
|
78
|
-
requested: boolean;
|
|
79
|
-
appended: boolean;
|
|
80
|
-
};
|
|
81
|
-
lines: ReadSeekLine[];
|
|
82
|
-
bundle?: {
|
|
83
|
-
mode: "local";
|
|
84
|
-
applied: boolean;
|
|
85
|
-
localSupport: Array<{
|
|
86
|
-
name: string;
|
|
87
|
-
kind: string;
|
|
88
|
-
parentName?: string;
|
|
89
|
-
startLine: number;
|
|
90
|
-
endLine: number;
|
|
91
|
-
lineAnchors: string[];
|
|
92
|
-
}>;
|
|
93
|
-
warnings: ReadSeekWarning[];
|
|
94
|
-
};
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function buildReadOutput(input: ReadOutputInput): ReadOutputResult {
|
|
99
|
-
const selectedLines = input.selectedLines ?? input.lines?.map((line) => line.raw) ?? [];
|
|
100
|
-
const lines = input.lines ?? buildReadSeekLines(input.startLine, selectedLines);
|
|
101
|
-
const warnings = input.warnings ?? [];
|
|
102
|
-
const renderedLines = renderReadSeekLines(lines);
|
|
103
|
-
const truncated = truncateHead(renderedLines, {
|
|
104
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
105
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
let text = input.truncation ? truncated.content : renderedLines;
|
|
109
|
-
|
|
110
|
-
if (input.truncation) {
|
|
111
|
-
text += `\n\n[Output truncated: showing ${input.truncation.outputLines} of ${input.totalLines} lines (${formatSize(input.truncation.outputBytes)} of ${formatSize(input.truncation.totalBytes)}). Use offset=${input.startLine + input.truncation.outputLines} to continue.]`;
|
|
112
|
-
} else if (input.continuation) {
|
|
113
|
-
text += `\n\n[Showing lines ${input.startLine}-${input.endLine} of ${input.totalLines}. Use offset=${input.continuation.nextOffset} to continue.]`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (input.bundle?.applied) {
|
|
117
|
-
const supportBlocks = input.bundle.localSupport.map((item) => {
|
|
118
|
-
const supportLines = buildReadSeekLines(item.symbol.startLine, item.lines);
|
|
119
|
-
return renderReadSeekLines(supportLines);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
text = [
|
|
123
|
-
"## Requested symbol",
|
|
124
|
-
text,
|
|
125
|
-
"",
|
|
126
|
-
"## Local support",
|
|
127
|
-
...supportBlocks,
|
|
128
|
-
].join("\n");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (input.map?.appended && input.map.text) {
|
|
132
|
-
text += `\n\n${input.map.text}`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (input.symbol) {
|
|
136
|
-
const parentInfo = input.symbol.parentName ? ` in ${input.symbol.parentName}` : "";
|
|
137
|
-
text = `[Symbol: ${input.symbol.name} (${input.symbol.kind})${parentInfo}, lines ${input.symbol.startLine}-${input.symbol.endLine} of ${input.totalLines}]\n\n${text}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (warnings.length) {
|
|
141
|
-
text = `${warnings.map((warning) => warning.message).join("\n\n")}\n\n${text}`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const readSeekValue: ReadOutputResult["readSeekValue"] = {
|
|
145
|
-
tool: "read",
|
|
146
|
-
path: input.path,
|
|
147
|
-
range: {
|
|
148
|
-
startLine: input.startLine,
|
|
149
|
-
endLine: input.endLine,
|
|
150
|
-
totalLines: input.totalLines,
|
|
151
|
-
},
|
|
152
|
-
warnings,
|
|
153
|
-
truncation: input.truncation ?? null,
|
|
154
|
-
symbol: input.symbol ?? null,
|
|
155
|
-
map: {
|
|
156
|
-
requested: input.map?.requested ?? false,
|
|
157
|
-
appended: input.map?.appended ?? false,
|
|
158
|
-
},
|
|
159
|
-
lines,
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
if (input.bundle) {
|
|
163
|
-
readSeekValue.bundle = {
|
|
164
|
-
mode: input.bundle.mode,
|
|
165
|
-
applied: input.bundle.applied,
|
|
166
|
-
localSupport: input.bundle.localSupport.map((item) => {
|
|
167
|
-
const supportLines = buildReadSeekLines(item.symbol.startLine, item.lines);
|
|
168
|
-
return {
|
|
169
|
-
name: item.symbol.name,
|
|
170
|
-
kind: item.symbol.kind,
|
|
171
|
-
parentName: item.symbol.parentName,
|
|
172
|
-
startLine: item.symbol.startLine,
|
|
173
|
-
endLine: item.symbol.endLine,
|
|
174
|
-
lineAnchors: supportLines.map((line) => line.anchor),
|
|
175
|
-
};
|
|
176
|
-
}),
|
|
177
|
-
warnings: input.bundle.warnings ?? [],
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
text,
|
|
183
|
-
lines,
|
|
184
|
-
readSeekValue,
|
|
185
|
-
};
|
|
186
|
-
}
|