pi-readseek 0.9.5 → 0.9.6
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 +0 -1
- package/dist/index.ts +815 -2227
- package/package.json +2 -2
- package/prompts/edit.md +1 -1
- package/prompts/read.md +0 -46
package/dist/index.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/edit.ts
|
|
2
2
|
import { readFile as fsReadFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES2,
|
|
6
|
-
DEFAULT_MAX_LINES as DEFAULT_MAX_LINES2
|
|
7
|
-
} from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { createPatch } from "diff";
|
|
4
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
8
5
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
9
|
-
import { Text as
|
|
6
|
+
import { Text as Text3 } from "@earendil-works/pi-tui";
|
|
10
7
|
|
|
11
8
|
// ../readseek-api/src/contract.ts
|
|
12
9
|
var DIGEST_FACETS = [
|
|
@@ -32,11 +29,10 @@ var DOCUMENT_NODE_KINDS = [
|
|
|
32
29
|
"structural_section"
|
|
33
30
|
];
|
|
34
31
|
var TOOL_NAMES = [
|
|
35
|
-
"
|
|
32
|
+
"digest",
|
|
36
33
|
"edit",
|
|
37
34
|
"write",
|
|
38
35
|
"grep",
|
|
39
|
-
"digest",
|
|
40
36
|
"view",
|
|
41
37
|
"search",
|
|
42
38
|
"def",
|
|
@@ -113,11 +109,10 @@ var PARAM_DESCRIPTIONS = {
|
|
|
113
109
|
scopeContext: "Context lines within each symbol"
|
|
114
110
|
};
|
|
115
111
|
var TOOL_DESCRIPTIONS = {
|
|
116
|
-
|
|
112
|
+
digest: "Digest source content and images into the native CLI envelope. Returns metadata plus requested facets unchanged from `readseek digest`. Use it before edits to obtain fresh `LINE:HASH` anchors; use for structural maps, diagnostics, and cursor identity.",
|
|
117
113
|
edit: 'Edit existing text files with fresh `LINE:HASH` anchors. Read or search first when no fresh anchors are available. Each `edits[]` item must use exactly one nested object: `{"set_line":{"anchor":"LINE:HASH","new_text":"..."}}`, `{"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}`, `{"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}`, `{"replace_symbol":{"symbol":"name","new_body":"..."}}`, or `{"replace":{"old_text":"...","new_text":"..."}}`.',
|
|
118
114
|
write: "Create or replace a complete text file and return `LINE:HASH` anchors. Use `edit` for small changes to an existing file.",
|
|
119
115
|
grep: "Search text or regex in files and return edit-ready `LINE:HASH` anchors. Use for identifiers, strings, configuration, errors, comments, and documentation.",
|
|
120
|
-
digest: "Digest source content and images into the native CLI envelope. Returns metadata plus requested facets unchanged from `readseek digest`; use for structural maps, diagnostics, and cursor identity.",
|
|
121
116
|
view: "Digest a document's structure or selected content. Start with the overview, then narrow by page, node, kind, or depth instead of reading the whole document. PDF is the first supported document format.",
|
|
122
117
|
search: "Search code by AST structure using ast-grep-style patterns. Results include line hashes usable as `LINE:HASH` anchors. Use `grep` for identifiers, strings, comments, configuration, documentation, or other plain text and regex.",
|
|
123
118
|
def: "Go to a symbol definition or declaration by name. Accepts qualified or unqualified names; use this instead of text search for functions, methods, classes, types, and other declarations.",
|
|
@@ -125,7 +120,10 @@ var TOOL_DESCRIPTIONS = {
|
|
|
125
120
|
rename: "Rename a symbol at a source location. Resolvable same-named bindings are preserved; verified edits apply by default. Set `apply: false` to preview the plan before changing files."
|
|
126
121
|
};
|
|
127
122
|
function renderToolDescription(name, aliases = {}) {
|
|
128
|
-
|
|
123
|
+
const desc = TOOL_DESCRIPTIONS[name];
|
|
124
|
+
if (!desc)
|
|
125
|
+
throw new Error(`Missing TOOL_DESCRIPTIONS for ${name}`);
|
|
126
|
+
return TOOL_NAMES.reduce((description, toolName) => description.replaceAll(`\`${toolName}\``, `\`${aliases[toolName] ?? toolName}\``), desc);
|
|
129
127
|
}
|
|
130
128
|
var gitFlagsSchema = {
|
|
131
129
|
cached: {
|
|
@@ -204,25 +202,27 @@ var editItemSchema = {
|
|
|
204
202
|
};
|
|
205
203
|
var TOOL_CONTRACTS = [
|
|
206
204
|
{
|
|
207
|
-
name: "
|
|
208
|
-
title: "
|
|
209
|
-
description: TOOL_DESCRIPTIONS.
|
|
205
|
+
name: "digest",
|
|
206
|
+
title: "Digest source",
|
|
207
|
+
description: TOOL_DESCRIPTIONS.digest,
|
|
210
208
|
inputSchema: {
|
|
211
209
|
type: "object",
|
|
212
210
|
properties: {
|
|
213
211
|
path: { type: "string", description: PARAM_DESCRIPTIONS.path },
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
select: { type: "string", description: PARAM_DESCRIPTIONS.select },
|
|
213
|
+
at: { type: "string", description: PARAM_DESCRIPTIONS.at },
|
|
214
|
+
end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
|
|
215
|
+
limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
|
|
216
|
+
language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
|
|
217
|
+
visionMode: {
|
|
218
218
|
type: "string",
|
|
219
|
-
enum: [
|
|
220
|
-
description: PARAM_DESCRIPTIONS.
|
|
219
|
+
enum: [...VISION_MODES],
|
|
220
|
+
description: PARAM_DESCRIPTIONS.visionModeDigest
|
|
221
221
|
},
|
|
222
|
-
|
|
222
|
+
visionLevel: {
|
|
223
223
|
type: "string",
|
|
224
|
-
enum: [...
|
|
225
|
-
description: PARAM_DESCRIPTIONS.
|
|
224
|
+
enum: [...VISION_LEVELS],
|
|
225
|
+
description: PARAM_DESCRIPTIONS.visionLevel
|
|
226
226
|
}
|
|
227
227
|
},
|
|
228
228
|
required: ["path"],
|
|
@@ -296,34 +296,6 @@ var TOOL_CONTRACTS = [
|
|
|
296
296
|
additionalProperties: false
|
|
297
297
|
}
|
|
298
298
|
},
|
|
299
|
-
{
|
|
300
|
-
name: "digest",
|
|
301
|
-
title: "Digest source",
|
|
302
|
-
description: TOOL_DESCRIPTIONS.digest,
|
|
303
|
-
inputSchema: {
|
|
304
|
-
type: "object",
|
|
305
|
-
properties: {
|
|
306
|
-
path: { type: "string", description: PARAM_DESCRIPTIONS.path },
|
|
307
|
-
select: { type: "string", description: PARAM_DESCRIPTIONS.select },
|
|
308
|
-
at: { type: "string", description: PARAM_DESCRIPTIONS.at },
|
|
309
|
-
end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
|
|
310
|
-
limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
|
|
311
|
-
language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
|
|
312
|
-
visionMode: {
|
|
313
|
-
type: "string",
|
|
314
|
-
enum: [...VISION_MODES],
|
|
315
|
-
description: PARAM_DESCRIPTIONS.visionModeDigest
|
|
316
|
-
},
|
|
317
|
-
visionLevel: {
|
|
318
|
-
type: "string",
|
|
319
|
-
enum: [...VISION_LEVELS],
|
|
320
|
-
description: PARAM_DESCRIPTIONS.visionLevel
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
required: ["path"],
|
|
324
|
-
additionalProperties: false
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
299
|
{
|
|
328
300
|
name: "view",
|
|
329
301
|
title: "View document",
|
|
@@ -797,8 +769,8 @@ async function runReadSeekNodeJson(binaryPath, args, options = {}) {
|
|
|
797
769
|
}
|
|
798
770
|
// src/tool-prompt-metadata.ts
|
|
799
771
|
var REPLACEABLE_TOOL_GUIDELINES = {
|
|
800
|
-
"
|
|
801
|
-
readSeekName: "
|
|
772
|
+
"digest.md": {
|
|
773
|
+
readSeekName: "readSeek_digest",
|
|
802
774
|
builtInName: "read",
|
|
803
775
|
benefit: "it returns LINE:HASH anchors for safe edits."
|
|
804
776
|
},
|
|
@@ -819,9 +791,6 @@ var REPLACEABLE_TOOL_GUIDELINES = {
|
|
|
819
791
|
}
|
|
820
792
|
};
|
|
821
793
|
var COMPACT_GUIDELINES = {
|
|
822
|
-
"read.md": [
|
|
823
|
-
"Use readSeek_read with symbol or offset/limit to inspect large files without reading them in full."
|
|
824
|
-
],
|
|
825
794
|
"digest.md": [
|
|
826
795
|
"Use readSeek_digest for map, diagnostics, and identity facets instead of separate tools."
|
|
827
796
|
],
|
|
@@ -1068,6 +1037,146 @@ function generateCompactOrFullDiff(oldContent, newContent, contextLines = 4) {
|
|
|
1068
1037
|
return generateDiffString(oldContent, newContent, contextLines);
|
|
1069
1038
|
}
|
|
1070
1039
|
|
|
1040
|
+
// src/path-utils.ts
|
|
1041
|
+
import { homedir } from "node:os";
|
|
1042
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
1043
|
+
var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
1044
|
+
function normalizeUnicodeSpaces(str) {
|
|
1045
|
+
return str.replace(UNICODE_SPACES, " ");
|
|
1046
|
+
}
|
|
1047
|
+
function normalizeAtPrefix(filePath) {
|
|
1048
|
+
return filePath.startsWith("@") ? filePath.slice(1) : filePath;
|
|
1049
|
+
}
|
|
1050
|
+
function expandPath(filePath) {
|
|
1051
|
+
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
|
|
1052
|
+
if (normalized === "~")
|
|
1053
|
+
return homedir();
|
|
1054
|
+
if (normalized.startsWith("~/"))
|
|
1055
|
+
return homedir() + normalized.slice(1);
|
|
1056
|
+
return normalized;
|
|
1057
|
+
}
|
|
1058
|
+
function resolveToCwd(filePath, cwd) {
|
|
1059
|
+
const expanded = expandPath(filePath);
|
|
1060
|
+
return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/runtime.ts
|
|
1064
|
+
function throwIfAborted(signal) {
|
|
1065
|
+
if (signal?.aborted)
|
|
1066
|
+
throw new Error("Operation aborted");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/fs-error.ts
|
|
1070
|
+
var SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
|
|
1071
|
+
function strerror(err) {
|
|
1072
|
+
const raw = String(err?.message ?? String(err));
|
|
1073
|
+
return raw.replace(SYSLOG_PATH_SUFFIX, "");
|
|
1074
|
+
}
|
|
1075
|
+
function formatFsError(err, domain) {
|
|
1076
|
+
const errno = typeof err.code === "string" ? err.code : "EIO";
|
|
1077
|
+
return {
|
|
1078
|
+
code: errno,
|
|
1079
|
+
message: `${domain}: ${errno}: ${strerror(err)}`
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/edit-render-helpers.ts
|
|
1084
|
+
var VARIANT_KEYS = ["set_line", "replace_lines", "insert_after", "replace"];
|
|
1085
|
+
function countEditTypes(edits) {
|
|
1086
|
+
const counts = {
|
|
1087
|
+
set_line: 0,
|
|
1088
|
+
replace_lines: 0,
|
|
1089
|
+
insert_after: 0,
|
|
1090
|
+
replace: 0,
|
|
1091
|
+
total: 0
|
|
1092
|
+
};
|
|
1093
|
+
if (!edits)
|
|
1094
|
+
return counts;
|
|
1095
|
+
for (const edit of edits) {
|
|
1096
|
+
counts.total++;
|
|
1097
|
+
if (edit && typeof edit === "object") {
|
|
1098
|
+
for (const key of VARIANT_KEYS) {
|
|
1099
|
+
if (key in edit) {
|
|
1100
|
+
counts[key]++;
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return counts;
|
|
1107
|
+
}
|
|
1108
|
+
function parseDiffStats(diff) {
|
|
1109
|
+
if (!diff)
|
|
1110
|
+
return { added: 0, removed: 0 };
|
|
1111
|
+
let added = 0;
|
|
1112
|
+
let removed = 0;
|
|
1113
|
+
for (const line of diff.split(`
|
|
1114
|
+
`)) {
|
|
1115
|
+
if (line.startsWith("+++") || line.startsWith("---"))
|
|
1116
|
+
continue;
|
|
1117
|
+
if (line.startsWith("+"))
|
|
1118
|
+
added++;
|
|
1119
|
+
else if (line.startsWith("-"))
|
|
1120
|
+
removed++;
|
|
1121
|
+
}
|
|
1122
|
+
return { added, removed };
|
|
1123
|
+
}
|
|
1124
|
+
function formatEditCallText(args, argsComplete) {
|
|
1125
|
+
const rawPath = typeof args?.path === "string" ? args.path : null;
|
|
1126
|
+
if (!argsComplete) {
|
|
1127
|
+
return { path: rawPath, suffix: undefined };
|
|
1128
|
+
}
|
|
1129
|
+
const edits = args?.edits;
|
|
1130
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
1131
|
+
return { path: rawPath, suffix: undefined };
|
|
1132
|
+
}
|
|
1133
|
+
const counts = countEditTypes(edits);
|
|
1134
|
+
const parts = [];
|
|
1135
|
+
for (const key of VARIANT_KEYS) {
|
|
1136
|
+
if (counts[key] > 0) {
|
|
1137
|
+
parts.push(`${counts[key]} ${key}`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const word = counts.total === 1 ? "edit" : "edits";
|
|
1141
|
+
const suffix = `${counts.total} ${word} (${parts.join(", ")})`;
|
|
1142
|
+
return { path: rawPath, suffix };
|
|
1143
|
+
}
|
|
1144
|
+
function formatEditResultText(input) {
|
|
1145
|
+
const { isError, diff, warnings, noopEdits, errorText } = input;
|
|
1146
|
+
const isNoOp = isError && (Array.isArray(noopEdits) && noopEdits.length > 0 || errorText.includes("No changes made"));
|
|
1147
|
+
const stats = parseDiffStats(diff);
|
|
1148
|
+
const hasDiffStats = stats.added > 0 || stats.removed > 0;
|
|
1149
|
+
const diffStats = hasDiffStats ? `+${stats.added} / -${stats.removed}` : undefined;
|
|
1150
|
+
let warningsBadge;
|
|
1151
|
+
if (warnings.length === 1) {
|
|
1152
|
+
warningsBadge = "⚠ 1 warning";
|
|
1153
|
+
} else if (warnings.length > 1) {
|
|
1154
|
+
warningsBadge = `⚠ ${warnings.length} warnings`;
|
|
1155
|
+
}
|
|
1156
|
+
const showErrorText = isError ? errorText : undefined;
|
|
1157
|
+
let semanticBadge;
|
|
1158
|
+
if (!isError && !isNoOp && input.semanticClassification) {
|
|
1159
|
+
switch (input.semanticClassification) {
|
|
1160
|
+
case "whitespace-only":
|
|
1161
|
+
semanticBadge = "ws-only";
|
|
1162
|
+
break;
|
|
1163
|
+
case "semantic":
|
|
1164
|
+
semanticBadge = "✓ semantic";
|
|
1165
|
+
break;
|
|
1166
|
+
case "mixed":
|
|
1167
|
+
semanticBadge = "mixed";
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
diffStats,
|
|
1173
|
+
noOp: isNoOp,
|
|
1174
|
+
warningsBadge,
|
|
1175
|
+
errorText: showErrorText,
|
|
1176
|
+
semanticBadge
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1071
1180
|
// src/readseek-value.ts
|
|
1072
1181
|
function buildReadSeekLineWithHash(line, hash, raw) {
|
|
1073
1182
|
return {
|
|
@@ -1081,13 +1190,6 @@ function buildReadSeekLineWithHash(line, hash, raw) {
|
|
|
1081
1190
|
function buildReadSeekLine(line, raw) {
|
|
1082
1191
|
return buildReadSeekLineWithHash(line, computeLineHash(raw), raw);
|
|
1083
1192
|
}
|
|
1084
|
-
function buildReadSeekLines(startLine, rawLines) {
|
|
1085
|
-
return rawLines.map((raw, index) => buildReadSeekLine(startLine + index, raw));
|
|
1086
|
-
}
|
|
1087
|
-
function renderReadSeekLines(lines) {
|
|
1088
|
-
return lines.map((line) => `${line.anchor}|${line.display}`).join(`
|
|
1089
|
-
`);
|
|
1090
|
-
}
|
|
1091
1193
|
function formatAnchoredFileBlocks(files, lineSuffix) {
|
|
1092
1194
|
const blocks = [];
|
|
1093
1195
|
for (const file of files) {
|
|
@@ -1140,63 +1242,150 @@ function buildReadSeekEditResult(input) {
|
|
|
1140
1242
|
};
|
|
1141
1243
|
}
|
|
1142
1244
|
|
|
1143
|
-
// src/
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if (buf.includes(0))
|
|
1148
|
-
return true;
|
|
1149
|
-
const decoded = buf.toString("utf8");
|
|
1150
|
-
if (!decoded.includes("�"))
|
|
1151
|
-
return false;
|
|
1152
|
-
return !Buffer.from(decoded, "utf8").equals(buf);
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
// src/path-utils.ts
|
|
1156
|
-
import { homedir } from "node:os";
|
|
1157
|
-
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
1158
|
-
var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
1159
|
-
function normalizeUnicodeSpaces(str) {
|
|
1160
|
-
return str.replace(UNICODE_SPACES, " ");
|
|
1161
|
-
}
|
|
1162
|
-
function normalizeAtPrefix(filePath) {
|
|
1163
|
-
return filePath.startsWith("@") ? filePath.slice(1) : filePath;
|
|
1164
|
-
}
|
|
1165
|
-
function expandPath(filePath) {
|
|
1166
|
-
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
|
|
1167
|
-
if (normalized === "~")
|
|
1168
|
-
return homedir();
|
|
1169
|
-
if (normalized.startsWith("~/"))
|
|
1170
|
-
return homedir() + normalized.slice(1);
|
|
1171
|
-
return normalized;
|
|
1245
|
+
// src/edit-output.ts
|
|
1246
|
+
var EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"];
|
|
1247
|
+
function hasNewText(value) {
|
|
1248
|
+
return typeof value === "object" && value !== null && typeof value.new_text === "string";
|
|
1172
1249
|
}
|
|
1173
|
-
function
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1250
|
+
function getVisibleDiffStats(diff) {
|
|
1251
|
+
const stats = parseDiffStats(diff);
|
|
1252
|
+
if (stats.added > 0 || stats.removed > 0)
|
|
1253
|
+
return stats;
|
|
1254
|
+
if (!diff.includes("→"))
|
|
1255
|
+
return stats;
|
|
1256
|
+
if (diff.includes("→ [deleted]"))
|
|
1257
|
+
return { added: 0, removed: 1 };
|
|
1258
|
+
return { added: 1, removed: 1 };
|
|
1176
1259
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1260
|
+
function buildVisibleSummary(displayPath, diff, edits) {
|
|
1261
|
+
let stats = getVisibleDiffStats(diff);
|
|
1262
|
+
const counts = countEditTypes(edits);
|
|
1263
|
+
const editCount = counts.total || 1;
|
|
1264
|
+
if (counts.total > 0 && counts.insert_after === counts.total && stats.removed > 0 && stats.added === stats.removed + counts.insert_after) {
|
|
1265
|
+
stats = { added: counts.insert_after, removed: 0 };
|
|
1266
|
+
}
|
|
1267
|
+
const changeWord = editCount === 1 ? "change" : "changes";
|
|
1268
|
+
const changedLineCount = Math.max(stats.added, stats.removed);
|
|
1269
|
+
const lineWord = changedLineCount === 1 ? "line" : "lines";
|
|
1270
|
+
return `Edited ${displayPath} (${editCount} ${changeWord}, +${stats.added} -${stats.removed} ${lineWord})`;
|
|
1182
1271
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1272
|
+
function extractNewTextValues(edits) {
|
|
1273
|
+
const values = [];
|
|
1274
|
+
for (const edit of edits ?? []) {
|
|
1275
|
+
if (!edit || typeof edit !== "object")
|
|
1276
|
+
continue;
|
|
1277
|
+
const operations = edit;
|
|
1278
|
+
for (const operationName of EDIT_OPERATION_NAMES) {
|
|
1279
|
+
const operation = operations[operationName];
|
|
1280
|
+
if (hasNewText(operation))
|
|
1281
|
+
values.push(operation.new_text);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return values;
|
|
1189
1285
|
}
|
|
1190
|
-
function
|
|
1191
|
-
|
|
1286
|
+
function formatWhitespaceOnlyWarning(semanticSummary, edits) {
|
|
1287
|
+
if (semanticSummary?.classification !== "whitespace-only")
|
|
1288
|
+
return;
|
|
1289
|
+
if (!extractNewTextValues(edits).some((text) => /\S/.test(text)))
|
|
1290
|
+
return;
|
|
1291
|
+
return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
|
|
1292
|
+
}
|
|
1293
|
+
function formatReplaceHint(edits, noopEdits) {
|
|
1294
|
+
if ((noopEdits ?? []).length > 0)
|
|
1295
|
+
return;
|
|
1296
|
+
const counts = countEditTypes(edits);
|
|
1297
|
+
if (counts.replace === 0)
|
|
1298
|
+
return;
|
|
1299
|
+
if (counts.replace !== counts.total)
|
|
1300
|
+
return;
|
|
1301
|
+
return "[info: this edit used replace (unverified). For safer future edits, prefer set_line/replace_lines with an anchor from readSeek_digest/readSeek_grep/readSeek_search.]";
|
|
1302
|
+
}
|
|
1303
|
+
function buildEditOutput(input) {
|
|
1304
|
+
const summary = `Updated ${input.displayPath}`;
|
|
1305
|
+
const visibleSummary = buildVisibleSummary(input.displayPath, input.diff, input.edits);
|
|
1306
|
+
const semanticWarning = formatWhitespaceOnlyWarning(input.semanticSummary, input.edits);
|
|
1307
|
+
const warningText = input.warnings.length ? `
|
|
1308
|
+
|
|
1309
|
+
Warnings:
|
|
1310
|
+
${input.warnings.join(`
|
|
1311
|
+
`)}` : "";
|
|
1312
|
+
const replaceHint = formatReplaceHint(input.edits, input.noopEdits);
|
|
1313
|
+
let text = visibleSummary;
|
|
1314
|
+
if (semanticWarning)
|
|
1315
|
+
text += `
|
|
1316
|
+
${semanticWarning}`;
|
|
1317
|
+
text += warningText;
|
|
1318
|
+
if (replaceHint)
|
|
1319
|
+
text += `
|
|
1320
|
+
${replaceHint}`;
|
|
1192
1321
|
return {
|
|
1193
|
-
|
|
1194
|
-
|
|
1322
|
+
text,
|
|
1323
|
+
patch: input.patch ?? "",
|
|
1324
|
+
readSeekValue: buildReadSeekEditResult({
|
|
1325
|
+
path: input.path,
|
|
1326
|
+
summary,
|
|
1327
|
+
diff: input.diff,
|
|
1328
|
+
...input.diffData ? { diffData: input.diffData } : {},
|
|
1329
|
+
firstChangedLine: input.firstChangedLine,
|
|
1330
|
+
warnings: input.warnings,
|
|
1331
|
+
noopEdits: input.noopEdits,
|
|
1332
|
+
...input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}
|
|
1333
|
+
})
|
|
1195
1334
|
};
|
|
1196
1335
|
}
|
|
1197
1336
|
|
|
1198
|
-
// src/
|
|
1199
|
-
import
|
|
1337
|
+
// src/edit-classify.ts
|
|
1338
|
+
import * as Diff2 from "diff";
|
|
1339
|
+
function classifyEdit(oldContent, newContent) {
|
|
1340
|
+
if (oldContent === newContent) {
|
|
1341
|
+
return { classification: "no-op" };
|
|
1342
|
+
}
|
|
1343
|
+
const oldLines = oldContent.split(`
|
|
1344
|
+
`);
|
|
1345
|
+
const newLines = newContent.split(`
|
|
1346
|
+
`);
|
|
1347
|
+
if (oldLines.length === newLines.length) {
|
|
1348
|
+
let hasWhitespaceChange2 = false;
|
|
1349
|
+
let hasSemanticChange2 = false;
|
|
1350
|
+
for (let i = 0;i < oldLines.length; i++) {
|
|
1351
|
+
if (oldLines[i] === newLines[i])
|
|
1352
|
+
continue;
|
|
1353
|
+
if (oldLines[i].trim() === newLines[i].trim()) {
|
|
1354
|
+
hasWhitespaceChange2 = true;
|
|
1355
|
+
} else {
|
|
1356
|
+
hasSemanticChange2 = true;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (hasSemanticChange2 && hasWhitespaceChange2)
|
|
1360
|
+
return { classification: "mixed" };
|
|
1361
|
+
if (hasWhitespaceChange2)
|
|
1362
|
+
return { classification: "whitespace-only" };
|
|
1363
|
+
return { classification: "semantic" };
|
|
1364
|
+
}
|
|
1365
|
+
const parts = Diff2.diffLines(oldContent, newContent);
|
|
1366
|
+
let hasWhitespaceChange = false;
|
|
1367
|
+
let hasSemanticChange = false;
|
|
1368
|
+
for (const part of parts) {
|
|
1369
|
+
if (!part.added && !part.removed)
|
|
1370
|
+
continue;
|
|
1371
|
+
const lines = part.value.split(`
|
|
1372
|
+
`);
|
|
1373
|
+
if (lines[lines.length - 1] === "")
|
|
1374
|
+
lines.pop();
|
|
1375
|
+
for (const line of lines) {
|
|
1376
|
+
if (line.trim() === "") {
|
|
1377
|
+
hasWhitespaceChange = true;
|
|
1378
|
+
} else {
|
|
1379
|
+
hasSemanticChange = true;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (hasSemanticChange && hasWhitespaceChange)
|
|
1384
|
+
return { classification: "mixed" };
|
|
1385
|
+
if (hasWhitespaceChange)
|
|
1386
|
+
return { classification: "whitespace-only" };
|
|
1387
|
+
return { classification: "semantic" };
|
|
1388
|
+
}
|
|
1200
1389
|
|
|
1201
1390
|
// src/readseek-client.ts
|
|
1202
1391
|
import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
@@ -1236,7 +1425,7 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
1236
1425
|
import { homedir as homedir2 } from "node:os";
|
|
1237
1426
|
import { join } from "node:path";
|
|
1238
1427
|
var READSEEK_TOOL_REPLACEMENTS = {
|
|
1239
|
-
read: "
|
|
1428
|
+
read: "readSeek_digest",
|
|
1240
1429
|
edit: "readSeek_edit",
|
|
1241
1430
|
grep: "readSeek_grep",
|
|
1242
1431
|
write: "readSeek_write"
|
|
@@ -1434,9 +1623,6 @@ function resolveReadSeekJsonSettings() {
|
|
|
1434
1623
|
warnings: [...globalResult.warnings, ...projectResult.warnings]
|
|
1435
1624
|
};
|
|
1436
1625
|
}
|
|
1437
|
-
function resolveReadSeekImageMode() {
|
|
1438
|
-
return resolveReadSeekJsonSettings().settings.imageMode ?? "auto";
|
|
1439
|
-
}
|
|
1440
1626
|
function resolveReadSeekSyntaxValidation() {
|
|
1441
1627
|
return resolveReadSeekJsonSettings().settings.syntaxValidation;
|
|
1442
1628
|
}
|
|
@@ -1665,23 +1851,6 @@ function requireBoolean(value, field) {
|
|
|
1665
1851
|
throw new Error(`invalid readseek ${field}: expected boolean`);
|
|
1666
1852
|
return value;
|
|
1667
1853
|
}
|
|
1668
|
-
function parseReadOutput(value) {
|
|
1669
|
-
if (!value || typeof value !== "object")
|
|
1670
|
-
throw new Error("invalid readseek read output");
|
|
1671
|
-
const output = value;
|
|
1672
|
-
const hashlines = output.hashlines;
|
|
1673
|
-
if (!Array.isArray(hashlines))
|
|
1674
|
-
throw new Error("invalid readseek hashlines");
|
|
1675
|
-
return {
|
|
1676
|
-
file: requireString(output.file, "file"),
|
|
1677
|
-
language: requireString(output.language, "language"),
|
|
1678
|
-
line_count: requireNumber(output.line_count, "line_count"),
|
|
1679
|
-
file_hash: requireString(output.file_hash, "file_hash"),
|
|
1680
|
-
start_line: requireNumber(output.start_line, "start_line"),
|
|
1681
|
-
end_line: requireNumber(output.end_line, "end_line"),
|
|
1682
|
-
hashlines: hashlines.map((line) => parseHashline(line, "hashline"))
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
1854
|
function parseMapOutput(value) {
|
|
1686
1855
|
if (!value || typeof value !== "object")
|
|
1687
1856
|
throw new Error("invalid readseek map output");
|
|
@@ -1948,32 +2117,6 @@ function parseIdentifyOutput(value) {
|
|
|
1948
2117
|
} : undefined
|
|
1949
2118
|
};
|
|
1950
2119
|
}
|
|
1951
|
-
function parseImageContent(value) {
|
|
1952
|
-
const detection = parseDetectOutput(value);
|
|
1953
|
-
if (detection.kind !== "image")
|
|
1954
|
-
throw new Error("invalid readseek image content");
|
|
1955
|
-
return detection;
|
|
1956
|
-
}
|
|
1957
|
-
function parseTextContent(value) {
|
|
1958
|
-
if (!value || typeof value !== "object")
|
|
1959
|
-
throw new Error("invalid readseek content");
|
|
1960
|
-
const output = value;
|
|
1961
|
-
if (output.hashlines !== undefined && output.start_line === undefined) {
|
|
1962
|
-
const hashlines = parseSearchHashlines(output.hashlines, "content.hashlines");
|
|
1963
|
-
const first = hashlines[0];
|
|
1964
|
-
const last = hashlines[hashlines.length - 1];
|
|
1965
|
-
return {
|
|
1966
|
-
file: requireString(output.file, "file"),
|
|
1967
|
-
language: requireString(output.language, "language"),
|
|
1968
|
-
line_count: requireNumber(output.line_count, "line_count"),
|
|
1969
|
-
file_hash: requireString(output.file_hash, "file_hash"),
|
|
1970
|
-
start_line: first?.line ?? 1,
|
|
1971
|
-
end_line: last?.line ?? 0,
|
|
1972
|
-
hashlines
|
|
1973
|
-
};
|
|
1974
|
-
}
|
|
1975
|
-
return parseReadOutput(value);
|
|
1976
|
-
}
|
|
1977
2120
|
function parseDigestEnvelope(value) {
|
|
1978
2121
|
if (!value || typeof value !== "object")
|
|
1979
2122
|
throw new Error("invalid readseek digest output");
|
|
@@ -2023,11 +2166,6 @@ async function readSeekDigestNative(target, options = {}) {
|
|
|
2023
2166
|
async function readSeekDigest(target, options = {}) {
|
|
2024
2167
|
return parseDigestEnvelope(await runDigestRaw(target, options));
|
|
2025
2168
|
}
|
|
2026
|
-
function requireDigestContent(envelope, field) {
|
|
2027
|
-
if (envelope.content === undefined)
|
|
2028
|
-
throw new Error(`invalid readseek ${field}: missing content`);
|
|
2029
|
-
return envelope.content;
|
|
2030
|
-
}
|
|
2031
2169
|
function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
|
|
2032
2170
|
if (output.language === "unknown" && output.symbols.length === 0)
|
|
2033
2171
|
return null;
|
|
@@ -2082,70 +2220,6 @@ async function readSeekCheck(filePath, content, options = {}) {
|
|
|
2082
2220
|
throw new Error("invalid readseek diagnostics: missing diagnostics facet");
|
|
2083
2221
|
return envelope.diagnostics;
|
|
2084
2222
|
}
|
|
2085
|
-
async function readSeekDetect(filePath, options = {}) {
|
|
2086
|
-
const envelope = await readSeekDigest(filePath, { select: "metadata", signal: options.signal });
|
|
2087
|
-
return envelope.metadata;
|
|
2088
|
-
}
|
|
2089
|
-
async function readSeekDigestContent(filePath, options = {}) {
|
|
2090
|
-
const envelope = await readSeekDigest(filePath, {
|
|
2091
|
-
select: "content",
|
|
2092
|
-
at: options.startLine !== undefined ? buildLineLocation(options.startLine) : undefined,
|
|
2093
|
-
end: options.endLine,
|
|
2094
|
-
visionMode: options.visionMode,
|
|
2095
|
-
signal: options.signal,
|
|
2096
|
-
vision: options.vision
|
|
2097
|
-
});
|
|
2098
|
-
if (envelope.metadata.kind === "pdf") {
|
|
2099
|
-
return { metadata: envelope.metadata, cache: envelope.cache };
|
|
2100
|
-
}
|
|
2101
|
-
const content = requireDigestContent(envelope, "content");
|
|
2102
|
-
if (envelope.metadata.kind === "image") {
|
|
2103
|
-
return { metadata: envelope.metadata, image: parseImageContent(content) };
|
|
2104
|
-
}
|
|
2105
|
-
return { metadata: envelope.metadata, text: parseTextContent(content) };
|
|
2106
|
-
}
|
|
2107
|
-
async function readSeekImage(filePath, modes, options = {}) {
|
|
2108
|
-
const requestedModes = modes.length > 1 ? ["all"] : modes;
|
|
2109
|
-
const results = await Promise.allSettled(requestedModes.map(async (mode) => {
|
|
2110
|
-
const digested = await readSeekDigestContent(filePath, {
|
|
2111
|
-
visionMode: mode,
|
|
2112
|
-
signal: options.signal,
|
|
2113
|
-
vision: true
|
|
2114
|
-
});
|
|
2115
|
-
if (!digested.image)
|
|
2116
|
-
throw new Error(`readseek returned no image analysis for ${filePath}`);
|
|
2117
|
-
return digested.image;
|
|
2118
|
-
}));
|
|
2119
|
-
let merged;
|
|
2120
|
-
for (const result of results) {
|
|
2121
|
-
if (result.status !== "fulfilled")
|
|
2122
|
-
continue;
|
|
2123
|
-
const detection = result.value;
|
|
2124
|
-
merged = merged === undefined ? detection : {
|
|
2125
|
-
...merged,
|
|
2126
|
-
ocr: detection.ocr ?? merged.ocr,
|
|
2127
|
-
caption: detection.caption ?? merged.caption,
|
|
2128
|
-
objects: detection.objects ?? merged.objects
|
|
2129
|
-
};
|
|
2130
|
-
}
|
|
2131
|
-
if (merged !== undefined)
|
|
2132
|
-
return merged;
|
|
2133
|
-
const failure = results.find((result) => result.status === "rejected");
|
|
2134
|
-
if (failure?.status === "rejected")
|
|
2135
|
-
throw failure.reason;
|
|
2136
|
-
throw new Error(`readseek returned no image analysis for ${filePath}`);
|
|
2137
|
-
}
|
|
2138
|
-
async function readSeekPreparedImage(filePath, options = {}) {
|
|
2139
|
-
const digested = await readSeekDigestContent(filePath, {
|
|
2140
|
-
visionMode: "none",
|
|
2141
|
-
signal: options.signal
|
|
2142
|
-
});
|
|
2143
|
-
const output = digested.image;
|
|
2144
|
-
if (!output || output.encoding !== "base64" || output.data === undefined || output.mime === undefined) {
|
|
2145
|
-
throw new Error(`readseek returned no prepared image for ${filePath}`);
|
|
2146
|
-
}
|
|
2147
|
-
return { mime: output.mime, encoding: output.encoding, data: output.data };
|
|
2148
|
-
}
|
|
2149
2223
|
function parseEditOutput(value) {
|
|
2150
2224
|
if (!value || typeof value !== "object")
|
|
2151
2225
|
throw new Error("invalid readseek edit output");
|
|
@@ -2296,1831 +2370,102 @@ async function readSeekDef(target, options) {
|
|
|
2296
2370
|
return parseDefCompact(await runReadSeek(args, { signal: options.signal }));
|
|
2297
2371
|
}
|
|
2298
2372
|
|
|
2299
|
-
// src/
|
|
2300
|
-
|
|
2373
|
+
// src/edit-syntax-validate.ts
|
|
2374
|
+
function dedupeSortLines(diagnostics) {
|
|
2375
|
+
const seen = new Set;
|
|
2376
|
+
const out = [];
|
|
2377
|
+
for (const diagnostic of diagnostics) {
|
|
2378
|
+
const key = diagnostic.start_line === diagnostic.end_line ? String(diagnostic.start_line) : `${diagnostic.start_line}-${diagnostic.end_line}`;
|
|
2379
|
+
if (!seen.has(key)) {
|
|
2380
|
+
seen.add(key);
|
|
2381
|
+
out.push({ key, start: diagnostic.start_line });
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
out.sort((a, b) => a.start - b.start);
|
|
2385
|
+
return out.map((o) => o.key);
|
|
2386
|
+
}
|
|
2387
|
+
var EMPTY = { errorCount: 0, missingCount: 0, diagnostics: [] };
|
|
2388
|
+
async function validateSyntaxRegression(input, options = {}) {
|
|
2389
|
+
let before;
|
|
2390
|
+
let after;
|
|
2301
2391
|
try {
|
|
2302
|
-
|
|
2303
|
-
|
|
2392
|
+
before = input.before === undefined ? EMPTY : await readSeekCheck(input.filePath, input.before, { signal: options.signal });
|
|
2393
|
+
after = await readSeekCheck(input.filePath, input.after, { signal: options.signal });
|
|
2304
2394
|
} catch {
|
|
2305
2395
|
return null;
|
|
2306
2396
|
}
|
|
2397
|
+
const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
|
|
2398
|
+
const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
|
|
2399
|
+
if (newErrorCount === 0 && newMissingCount === 0)
|
|
2400
|
+
return null;
|
|
2401
|
+
return {
|
|
2402
|
+
errorLines: dedupeSortLines(after.diagnostics),
|
|
2403
|
+
newErrorCount,
|
|
2404
|
+
newMissingCount
|
|
2405
|
+
};
|
|
2307
2406
|
}
|
|
2308
2407
|
|
|
2309
|
-
// src/
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
MAX_MAP_BYTES: 25 * 1024,
|
|
2315
|
-
FULL_TARGET_BYTES: 10 * 1024,
|
|
2316
|
-
COMPACT_TARGET_BYTES: 20 * 1024,
|
|
2317
|
-
MAX_OUTLINE_BYTES: 50 * 1024,
|
|
2318
|
-
MAX_TRUNCATED_BYTES: 100 * 1024,
|
|
2319
|
-
TRUNCATED_SYMBOLS_EACH: 50
|
|
2320
|
-
};
|
|
2408
|
+
// src/syntax-validate-mode.ts
|
|
2409
|
+
var DEFAULT = "warn";
|
|
2410
|
+
function resolveSyntaxValidateMode(opts) {
|
|
2411
|
+
return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
|
|
2412
|
+
}
|
|
2321
2413
|
|
|
2322
|
-
// src/
|
|
2323
|
-
var
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
const
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2414
|
+
// src/diff-data.ts
|
|
2415
|
+
var MAX_INLINE_DIFF_LINE_LENGTH = 4096;
|
|
2416
|
+
var MAX_INLINE_DIFF_TOKENS = 512;
|
|
2417
|
+
var MAX_INLINE_DIFF_CELLS = 200000;
|
|
2418
|
+
var MAX_INLINE_DIFF_PAIRS = 200;
|
|
2419
|
+
var INLINE_SIMILARITY_THRESHOLD = 0.35;
|
|
2420
|
+
var INLINE_TOKEN_PATTERN = /([A-Za-z_$][\w$]*|\d+|\s+|[^A-Za-z_$\w\s]+)/gu;
|
|
2421
|
+
var LANGUAGE_BY_EXTENSION = new Map([
|
|
2422
|
+
[".ts", "typescript"],
|
|
2423
|
+
[".tsx", "typescript"],
|
|
2424
|
+
[".js", "javascript"],
|
|
2425
|
+
[".jsx", "javascript"],
|
|
2426
|
+
[".py", "python"],
|
|
2427
|
+
[".rs", "rust"],
|
|
2428
|
+
[".java", "java"]
|
|
2429
|
+
]);
|
|
2430
|
+
function inferLanguage(path3) {
|
|
2431
|
+
const extensionMatch = path3.match(/\.[^.\/\\]+$/);
|
|
2432
|
+
if (!extensionMatch)
|
|
2433
|
+
return;
|
|
2434
|
+
return LANGUAGE_BY_EXTENSION.get(extensionMatch[0].toLowerCase());
|
|
2435
|
+
}
|
|
2436
|
+
function parseFullDiffEntries(diff) {
|
|
2437
|
+
const entries = [];
|
|
2438
|
+
let nextOldLine = 1;
|
|
2439
|
+
let nextNewLine = 1;
|
|
2440
|
+
for (const line of diff ? diff.split(`
|
|
2441
|
+
`) : []) {
|
|
2442
|
+
const removeMatch = line.match(/^-\s*(\d+) (.*)$/);
|
|
2443
|
+
if (removeMatch) {
|
|
2444
|
+
const oldLine = Number(removeMatch[1]);
|
|
2445
|
+
entries.push({ kind: "remove", oldLine, text: removeMatch[2] ?? "" });
|
|
2446
|
+
nextOldLine = oldLine + 1;
|
|
2447
|
+
continue;
|
|
2354
2448
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2449
|
+
const addMatch = line.match(/^\+\s*(\d+) (.*)$/);
|
|
2450
|
+
if (addMatch) {
|
|
2451
|
+
const newLine = Number(addMatch[1]);
|
|
2452
|
+
entries.push({ kind: "add", newLine, text: addMatch[2] ?? "" });
|
|
2453
|
+
nextNewLine = newLine + 1;
|
|
2454
|
+
continue;
|
|
2359
2455
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2456
|
+
const contextMatch = line.match(/^ \s*(\d+) (.*)$/);
|
|
2457
|
+
if (contextMatch) {
|
|
2458
|
+
const oldLine = Number(contextMatch[1]);
|
|
2459
|
+
const lineDelta = nextNewLine - nextOldLine;
|
|
2460
|
+
const newLine = oldLine + lineDelta;
|
|
2461
|
+
entries.push({ kind: "context", oldLine, newLine, text: contextMatch[2] ?? "" });
|
|
2462
|
+
nextOldLine = oldLine + 1;
|
|
2463
|
+
nextNewLine = newLine + 1;
|
|
2464
|
+
continue;
|
|
2362
2465
|
}
|
|
2466
|
+
entries.push({ kind: "meta", text: line });
|
|
2363
2467
|
}
|
|
2364
|
-
return
|
|
2365
|
-
}
|
|
2366
|
-
function formatSymbols(symbols, level, indent = 0) {
|
|
2367
|
-
const lines = [];
|
|
2368
|
-
for (const symbol of symbols) {
|
|
2369
|
-
lines.push(formatSymbol(symbol, indent));
|
|
2370
|
-
if (level !== "outline" /* Outline */ && level !== "truncated" /* Truncated */ && symbol.children?.length) {
|
|
2371
|
-
if (level === "minimal" /* Minimal */) {
|
|
2372
|
-
for (const child of symbol.children) {
|
|
2373
|
-
lines.push(formatSymbol(child, indent + 1));
|
|
2374
|
-
}
|
|
2375
|
-
} else {
|
|
2376
|
-
lines.push(...formatSymbols(symbol.children, level, indent + 1));
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
return lines;
|
|
2381
|
-
}
|
|
2382
|
-
function formatFileMap(map, level) {
|
|
2383
|
-
const effectiveLevel = level ?? map.detailLevel;
|
|
2384
|
-
const fileName = basename(map.path);
|
|
2385
|
-
const lines = [
|
|
2386
|
-
"",
|
|
2387
|
-
BOX_LINE,
|
|
2388
|
-
`File Map: ${fileName}`,
|
|
2389
|
-
`${formatNumber(map.totalLines)} lines │ ${formatSize(map.totalBytes)} │ ${map.language}`,
|
|
2390
|
-
BOX_LINE,
|
|
2391
|
-
""
|
|
2392
|
-
];
|
|
2393
|
-
if (map.truncatedInfo) {
|
|
2394
|
-
const { shownSymbols, totalSymbols } = map.truncatedInfo;
|
|
2395
|
-
lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_TRUNCATED_BYTES)} | ${shownSymbols} of ${formatNumber(totalSymbols)} symbols]`);
|
|
2396
|
-
lines.push("");
|
|
2397
|
-
} else if (effectiveLevel === "outline" /* Outline */) {
|
|
2398
|
-
lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_OUTLINE_BYTES)} | outline]`);
|
|
2399
|
-
lines.push("");
|
|
2400
|
-
} else if (effectiveLevel === "minimal" /* Minimal */) {
|
|
2401
|
-
lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_MAP_BYTES)} | minimal]`);
|
|
2402
|
-
lines.push("");
|
|
2403
|
-
} else if (effectiveLevel === "compact" /* Compact */) {
|
|
2404
|
-
lines.push(`[Map ≤${formatSize(THRESHOLDS.COMPACT_TARGET_BYTES)} | compact]`);
|
|
2405
|
-
lines.push("");
|
|
2406
|
-
}
|
|
2407
|
-
if (map.truncatedInfo) {
|
|
2408
|
-
const half = Math.floor(map.symbols.length / 2);
|
|
2409
|
-
const firstSymbols = map.symbols.slice(0, half);
|
|
2410
|
-
const lastSymbols = map.symbols.slice(half);
|
|
2411
|
-
const firstLines = formatSymbols(firstSymbols, effectiveLevel);
|
|
2412
|
-
lines.push(...firstLines);
|
|
2413
|
-
lines.push("");
|
|
2414
|
-
lines.push(` ─ ─ ─ ${formatNumber(map.truncatedInfo.omittedSymbols)} more symbols ─ ─ ─`);
|
|
2415
|
-
lines.push("");
|
|
2416
|
-
const lastLines = formatSymbols(lastSymbols, effectiveLevel);
|
|
2417
|
-
lines.push(...lastLines);
|
|
2418
|
-
} else {
|
|
2419
|
-
const symbolLines = formatSymbols(map.symbols, effectiveLevel);
|
|
2420
|
-
lines.push(...symbolLines);
|
|
2421
|
-
}
|
|
2422
|
-
lines.push("");
|
|
2423
|
-
lines.push(BOX_LINE);
|
|
2424
|
-
if (map.truncatedInfo) {
|
|
2425
|
-
const firstShown = map.symbols.slice(0, Math.floor(map.symbols.length / 2));
|
|
2426
|
-
const lastShown = map.symbols.slice(Math.floor(map.symbols.length / 2));
|
|
2427
|
-
const lastFirst = firstShown.at(-1);
|
|
2428
|
-
const firstLast = lastShown.at(0);
|
|
2429
|
-
if (lastFirst && firstLast) {
|
|
2430
|
-
const omitStart = lastFirst.endLine + 1;
|
|
2431
|
-
const omitEnd = firstLast.startLine - 1;
|
|
2432
|
-
lines.push(`Omitted symbols are in lines ${formatNumber(omitStart)}-${formatNumber(omitEnd)}.`);
|
|
2433
|
-
}
|
|
2434
|
-
lines.push("Use read(path, offset=LINE, limit=N) to view specific sections.");
|
|
2435
|
-
} else {
|
|
2436
|
-
lines.push("Use read(path, offset=LINE, limit=N) for targeted reads.");
|
|
2437
|
-
}
|
|
2438
|
-
lines.push(BOX_LINE);
|
|
2439
|
-
return lines.join(`
|
|
2440
|
-
`);
|
|
2441
|
-
}
|
|
2442
|
-
function reduceToLevel(map, level) {
|
|
2443
|
-
if (level === "outline" /* Outline */) {
|
|
2444
|
-
return {
|
|
2445
|
-
...map,
|
|
2446
|
-
detailLevel: "outline" /* Outline */,
|
|
2447
|
-
symbols: map.symbols.map((s) => ({
|
|
2448
|
-
name: s.name,
|
|
2449
|
-
kind: s.kind,
|
|
2450
|
-
startLine: s.startLine,
|
|
2451
|
-
endLine: s.endLine
|
|
2452
|
-
}))
|
|
2453
|
-
};
|
|
2454
|
-
}
|
|
2455
|
-
if (level === "minimal" /* Minimal */) {
|
|
2456
|
-
return {
|
|
2457
|
-
...map,
|
|
2458
|
-
detailLevel: "minimal" /* Minimal */,
|
|
2459
|
-
symbols: map.symbols.map((s) => ({
|
|
2460
|
-
name: s.name,
|
|
2461
|
-
kind: s.kind,
|
|
2462
|
-
startLine: s.startLine,
|
|
2463
|
-
endLine: s.endLine,
|
|
2464
|
-
children: s.children?.map((c) => ({
|
|
2465
|
-
name: c.name,
|
|
2466
|
-
kind: c.kind,
|
|
2467
|
-
startLine: c.startLine,
|
|
2468
|
-
endLine: c.endLine
|
|
2469
|
-
}))
|
|
2470
|
-
}))
|
|
2471
|
-
};
|
|
2472
|
-
}
|
|
2473
|
-
if (level === "compact" /* Compact */) {
|
|
2474
|
-
return { ...map, detailLevel: "compact" /* Compact */ };
|
|
2475
|
-
}
|
|
2476
|
-
return { ...map, detailLevel: "full" /* Full */ };
|
|
2477
|
-
}
|
|
2478
|
-
function reduceToTruncated(map, symbolsEach = THRESHOLDS.TRUNCATED_SYMBOLS_EACH) {
|
|
2479
|
-
const { symbols } = map;
|
|
2480
|
-
const total = symbols.length;
|
|
2481
|
-
if (total <= symbolsEach * 2) {
|
|
2482
|
-
return reduceToLevel(map, "outline" /* Outline */);
|
|
2483
|
-
}
|
|
2484
|
-
const firstSymbols = symbols.slice(0, symbolsEach).map((s) => ({
|
|
2485
|
-
name: s.name,
|
|
2486
|
-
kind: s.kind,
|
|
2487
|
-
startLine: s.startLine,
|
|
2488
|
-
endLine: s.endLine
|
|
2489
|
-
}));
|
|
2490
|
-
const lastSymbols = symbols.slice(-symbolsEach).map((s) => ({
|
|
2491
|
-
name: s.name,
|
|
2492
|
-
kind: s.kind,
|
|
2493
|
-
startLine: s.startLine,
|
|
2494
|
-
endLine: s.endLine
|
|
2495
|
-
}));
|
|
2496
|
-
return {
|
|
2497
|
-
...map,
|
|
2498
|
-
symbols: [...firstSymbols, ...lastSymbols],
|
|
2499
|
-
detailLevel: "truncated" /* Truncated */,
|
|
2500
|
-
truncatedInfo: {
|
|
2501
|
-
totalSymbols: total,
|
|
2502
|
-
shownSymbols: symbolsEach * 2,
|
|
2503
|
-
omittedSymbols: total - symbolsEach * 2
|
|
2504
|
-
}
|
|
2505
|
-
};
|
|
2506
|
-
}
|
|
2507
|
-
function formatFileMapWithBudget(map, maxBytes = THRESHOLDS.MAX_TRUNCATED_BYTES) {
|
|
2508
|
-
const tiers = [
|
|
2509
|
-
{ level: "full" /* Full */, budget: THRESHOLDS.FULL_TARGET_BYTES },
|
|
2510
|
-
{ level: "compact" /* Compact */, budget: THRESHOLDS.COMPACT_TARGET_BYTES },
|
|
2511
|
-
{ level: "minimal" /* Minimal */, budget: THRESHOLDS.MAX_MAP_BYTES },
|
|
2512
|
-
{ level: "outline" /* Outline */, budget: THRESHOLDS.MAX_OUTLINE_BYTES }
|
|
2513
|
-
];
|
|
2514
|
-
for (const { level, budget } of tiers) {
|
|
2515
|
-
const reduced = reduceToLevel(map, level);
|
|
2516
|
-
const formatted = formatFileMap(reduced, level);
|
|
2517
|
-
const size = Buffer.byteLength(formatted, "utf8");
|
|
2518
|
-
if (size <= budget && size <= maxBytes) {
|
|
2519
|
-
return formatted;
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
const outline = reduceToLevel(map, "outline" /* Outline */);
|
|
2523
|
-
const outlineFormatted = formatFileMap(outline, "outline" /* Outline */);
|
|
2524
|
-
const outlineSize = Buffer.byteLength(outlineFormatted, "utf8");
|
|
2525
|
-
if (outlineSize <= maxBytes) {
|
|
2526
|
-
return outlineFormatted;
|
|
2527
|
-
}
|
|
2528
|
-
const totalSymbols = map.symbols.length;
|
|
2529
|
-
const minSymbols = 10;
|
|
2530
|
-
const maxSymbolsEach = Math.floor(totalSymbols / 2);
|
|
2531
|
-
let low = minSymbols;
|
|
2532
|
-
let high = maxSymbolsEach;
|
|
2533
|
-
let bestResult = null;
|
|
2534
|
-
while (low <= high) {
|
|
2535
|
-
const mid = Math.floor((low + high) / 2);
|
|
2536
|
-
const truncated = reduceToTruncated(map, mid);
|
|
2537
|
-
const formatted = formatFileMap(truncated, "truncated" /* Truncated */);
|
|
2538
|
-
const size = Buffer.byteLength(formatted, "utf8");
|
|
2539
|
-
if (size <= maxBytes) {
|
|
2540
|
-
bestResult = formatted;
|
|
2541
|
-
low = mid + 1;
|
|
2542
|
-
} else {
|
|
2543
|
-
high = mid - 1;
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
if (bestResult) {
|
|
2547
|
-
return bestResult;
|
|
2548
|
-
}
|
|
2549
|
-
const minimal = reduceToTruncated(map, minSymbols);
|
|
2550
|
-
return formatFileMap(minimal, "truncated" /* Truncated */);
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
// src/readseek/symbol-tree.ts
|
|
2554
|
-
function traverseSymbolTree(symbols, visit) {
|
|
2555
|
-
const result = [];
|
|
2556
|
-
const walk = (nodes, parentName) => {
|
|
2557
|
-
for (const symbol of nodes) {
|
|
2558
|
-
result.push(visit(symbol, parentName));
|
|
2559
|
-
if (symbol.children?.length)
|
|
2560
|
-
walk(symbol.children, symbol.name);
|
|
2561
|
-
}
|
|
2562
|
-
};
|
|
2563
|
-
walk(symbols);
|
|
2564
|
-
return result;
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
// src/readseek/symbol-lookup.ts
|
|
2568
|
-
function toMatch(symbol, parentName) {
|
|
2569
|
-
return {
|
|
2570
|
-
name: symbol.name,
|
|
2571
|
-
kind: symbol.kind,
|
|
2572
|
-
startLine: symbol.startLine,
|
|
2573
|
-
endLine: symbol.endLine,
|
|
2574
|
-
...parentName ? { parentName } : {}
|
|
2575
|
-
};
|
|
2576
|
-
}
|
|
2577
|
-
function toMatches(candidates) {
|
|
2578
|
-
return candidates.map((candidate) => toMatch(candidate.symbol, candidate.parentName));
|
|
2579
|
-
}
|
|
2580
|
-
function flattenSymbols(symbols) {
|
|
2581
|
-
return traverseSymbolTree(symbols, (symbol, parentName) => ({ symbol, parentName }));
|
|
2582
|
-
}
|
|
2583
|
-
function resolveDotPath(symbols, parts) {
|
|
2584
|
-
let candidates = symbols.filter((symbol) => symbol.name === parts[0]).map((symbol) => ({ symbol }));
|
|
2585
|
-
for (let i = 1;i < parts.length; i++) {
|
|
2586
|
-
const nextCandidates = [];
|
|
2587
|
-
for (const candidate of candidates) {
|
|
2588
|
-
for (const child of candidate.symbol.children ?? []) {
|
|
2589
|
-
if (child.name === parts[i]) {
|
|
2590
|
-
nextCandidates.push({ symbol: child, parentName: candidate.symbol.name });
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
candidates = nextCandidates;
|
|
2595
|
-
}
|
|
2596
|
-
return candidates;
|
|
2597
|
-
}
|
|
2598
|
-
var JAVA_TOP_LEVEL_TYPE_KINDS = new Set(["class", "interface", "enum", "module"]);
|
|
2599
|
-
function preferJavaTopLevelType(map, candidates) {
|
|
2600
|
-
if (map.language !== "Java")
|
|
2601
|
-
return;
|
|
2602
|
-
const topLevelTypes = candidates.filter((candidate) => !candidate.parentName && JAVA_TOP_LEVEL_TYPE_KINDS.has(candidate.symbol.kind));
|
|
2603
|
-
return topLevelTypes.length === 1 ? topLevelTypes[0] : undefined;
|
|
2604
|
-
}
|
|
2605
|
-
function resolveDotPathSuffix(symbols, parts) {
|
|
2606
|
-
for (let start = 1;start < parts.length; start++) {
|
|
2607
|
-
const candidates = resolveDotPath(symbols, parts.slice(start));
|
|
2608
|
-
if (candidates.length > 0)
|
|
2609
|
-
return candidates;
|
|
2610
|
-
}
|
|
2611
|
-
return [];
|
|
2612
|
-
}
|
|
2613
|
-
function findSymbol(map, query) {
|
|
2614
|
-
const q = query.trim();
|
|
2615
|
-
if (!q)
|
|
2616
|
-
return { type: "not-found" };
|
|
2617
|
-
if (map.symbols.length === 0)
|
|
2618
|
-
return { type: "not-found" };
|
|
2619
|
-
const allSymbols = flattenSymbols(map.symbols);
|
|
2620
|
-
const atLineMatch = /^(.+?)@(\d+)$/.exec(q);
|
|
2621
|
-
if (atLineMatch) {
|
|
2622
|
-
const namePart = atLineMatch[1];
|
|
2623
|
-
const lineNum = Number.parseInt(atLineMatch[2], 10);
|
|
2624
|
-
let pool;
|
|
2625
|
-
if (namePart.includes(".")) {
|
|
2626
|
-
const parts = namePart.split(".").map((p) => p.trim());
|
|
2627
|
-
pool = parts.every((p) => p.length > 0) ? resolveDotPath(map.symbols, parts) : [];
|
|
2628
|
-
if (pool.length === 0) {
|
|
2629
|
-
pool = allSymbols.filter((c) => c.symbol.name === namePart);
|
|
2630
|
-
}
|
|
2631
|
-
} else {
|
|
2632
|
-
pool = allSymbols.filter((c) => c.symbol.name === namePart);
|
|
2633
|
-
}
|
|
2634
|
-
pool = pool.filter((c) => Number.isFinite(c.symbol.startLine) && c.symbol.startLine > 0);
|
|
2635
|
-
if (pool.length === 0) {
|
|
2636
|
-
const leaf = namePart.split(".").pop() ?? namePart;
|
|
2637
|
-
const sameLeaf = allSymbols.filter((c) => c.symbol.name === leaf && Number.isFinite(c.symbol.startLine) && c.symbol.startLine > 0);
|
|
2638
|
-
if (sameLeaf.length > 0) {
|
|
2639
|
-
const list = sameLeaf.slice(0, 5).map((c) => `${c.parentName ? c.parentName + "." : ""}${c.symbol.name}@${c.symbol.startLine}`).join(", ");
|
|
2640
|
-
return {
|
|
2641
|
-
type: "not-found",
|
|
2642
|
-
message: `${q} not found. Candidates: ${list}`,
|
|
2643
|
-
candidates: toMatches(sameLeaf.slice(0, 5))
|
|
2644
|
-
};
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
if (pool.length > 0) {
|
|
2648
|
-
const containing = pool.filter((c) => c.symbol.startLine <= lineNum && c.symbol.endLine >= lineNum);
|
|
2649
|
-
if (containing.length > 0) {
|
|
2650
|
-
return { type: "found", symbol: toMatch(containing[0].symbol, containing[0].parentName) };
|
|
2651
|
-
}
|
|
2652
|
-
const below = pool.filter((c) => c.symbol.startLine >= lineNum).sort((a, b) => a.symbol.startLine - b.symbol.startLine);
|
|
2653
|
-
if (below.length) {
|
|
2654
|
-
return { type: "found", symbol: toMatch(below[0].symbol, below[0].parentName) };
|
|
2655
|
-
}
|
|
2656
|
-
const above = pool.filter((c) => c.symbol.startLine < lineNum).sort((a, b) => b.symbol.startLine - a.symbol.startLine);
|
|
2657
|
-
if (above.length) {
|
|
2658
|
-
return { type: "found", symbol: toMatch(above[0].symbol, above[0].parentName) };
|
|
2659
|
-
}
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
const exact = allSymbols.filter((c) => c.symbol.name === q);
|
|
2663
|
-
if (exact.length === 1)
|
|
2664
|
-
return { type: "found", symbol: toMatch(exact[0].symbol, exact[0].parentName) };
|
|
2665
|
-
if (exact.length > 1) {
|
|
2666
|
-
const preferred = preferJavaTopLevelType(map, exact);
|
|
2667
|
-
if (preferred)
|
|
2668
|
-
return { type: "found", symbol: toMatch(preferred.symbol, preferred.parentName) };
|
|
2669
|
-
return { type: "ambiguous", candidates: toMatches(exact.slice(0, 5)) };
|
|
2670
|
-
}
|
|
2671
|
-
if (q.includes(".")) {
|
|
2672
|
-
const parts = q.split(".").map((p) => p.trim());
|
|
2673
|
-
if (parts.length >= 2 && parts.every((p) => p.length > 0)) {
|
|
2674
|
-
const candidates = resolveDotPath(map.symbols, parts);
|
|
2675
|
-
if (candidates.length === 1) {
|
|
2676
|
-
return { type: "found", symbol: toMatch(candidates[0].symbol, candidates[0].parentName) };
|
|
2677
|
-
}
|
|
2678
|
-
if (candidates.length > 1) {
|
|
2679
|
-
return {
|
|
2680
|
-
type: "ambiguous",
|
|
2681
|
-
candidates: toMatches(candidates.slice(0, 5))
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
const suffixCandidates = resolveDotPathSuffix(map.symbols, parts);
|
|
2685
|
-
if (suffixCandidates.length === 1) {
|
|
2686
|
-
return { type: "found", symbol: toMatch(suffixCandidates[0].symbol, suffixCandidates[0].parentName) };
|
|
2687
|
-
}
|
|
2688
|
-
if (suffixCandidates.length > 1) {
|
|
2689
|
-
return {
|
|
2690
|
-
type: "ambiguous",
|
|
2691
|
-
candidates: toMatches(suffixCandidates.slice(0, 5))
|
|
2692
|
-
};
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
const qLower = q.toLowerCase();
|
|
2697
|
-
const ci = allSymbols.filter((c) => c.symbol.name.toLowerCase() === qLower);
|
|
2698
|
-
if (ci.length === 1)
|
|
2699
|
-
return { type: "found", symbol: toMatch(ci[0].symbol, ci[0].parentName) };
|
|
2700
|
-
if (ci.length > 1)
|
|
2701
|
-
return { type: "ambiguous", candidates: toMatches(ci.slice(0, 5)) };
|
|
2702
|
-
const prefix = allSymbols.filter((c) => c.symbol.name.toLowerCase().startsWith(qLower));
|
|
2703
|
-
if (prefix.length === 1)
|
|
2704
|
-
return { type: "found", symbol: toMatch(prefix[0].symbol, prefix[0].parentName) };
|
|
2705
|
-
if (prefix.length > 1)
|
|
2706
|
-
return { type: "ambiguous", candidates: toMatches(prefix.slice(0, 5)) };
|
|
2707
|
-
const camelCase = allSymbols.filter((c) => {
|
|
2708
|
-
const words = c.symbol.name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s_]+/);
|
|
2709
|
-
return words.some((w) => w === qLower);
|
|
2710
|
-
});
|
|
2711
|
-
const partial = allSymbols.filter((c) => c.symbol.name.toLowerCase().includes(qLower));
|
|
2712
|
-
const buildOtherCandidates = (chosen) => {
|
|
2713
|
-
const pool = [];
|
|
2714
|
-
const seen = new Set;
|
|
2715
|
-
seen.add(chosen.symbol);
|
|
2716
|
-
for (const c of [...camelCase, ...partial]) {
|
|
2717
|
-
if (seen.has(c.symbol))
|
|
2718
|
-
continue;
|
|
2719
|
-
seen.add(c.symbol);
|
|
2720
|
-
pool.push(c);
|
|
2721
|
-
}
|
|
2722
|
-
return toMatches(pool.slice(0, 4));
|
|
2723
|
-
};
|
|
2724
|
-
if (camelCase.length === 1) {
|
|
2725
|
-
return {
|
|
2726
|
-
type: "fuzzy",
|
|
2727
|
-
symbol: toMatch(camelCase[0].symbol, camelCase[0].parentName),
|
|
2728
|
-
tier: "camelCase",
|
|
2729
|
-
otherCandidates: buildOtherCandidates(camelCase[0])
|
|
2730
|
-
};
|
|
2731
|
-
}
|
|
2732
|
-
if (camelCase.length > 1)
|
|
2733
|
-
return { type: "ambiguous", candidates: toMatches(camelCase.slice(0, 5)) };
|
|
2734
|
-
if (partial.length === 1) {
|
|
2735
|
-
return {
|
|
2736
|
-
type: "fuzzy",
|
|
2737
|
-
symbol: toMatch(partial[0].symbol, partial[0].parentName),
|
|
2738
|
-
tier: "substring",
|
|
2739
|
-
otherCandidates: buildOtherCandidates(partial[0])
|
|
2740
|
-
};
|
|
2741
|
-
}
|
|
2742
|
-
if (partial.length > 1)
|
|
2743
|
-
return { type: "ambiguous", candidates: toMatches(partial.slice(0, 5)) };
|
|
2744
|
-
return { type: "not-found" };
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
// src/readseek/symbol-error-format.ts
|
|
2748
|
-
function formatAmbiguous(query, candidates) {
|
|
2749
|
-
const lines = candidates.map((c) => `- ${c.name} (${c.kind}) — lines ${c.startLine}-${c.endLine}`);
|
|
2750
|
-
const hints = candidates.map((c) => `${query}@${c.startLine}`).join(" or ");
|
|
2751
|
-
return [
|
|
2752
|
-
`Symbol '${query}' is ambiguous.`,
|
|
2753
|
-
"Matches:",
|
|
2754
|
-
...lines,
|
|
2755
|
-
`Use ${hints} to select by start line.`
|
|
2756
|
-
].join(`
|
|
2757
|
-
`);
|
|
2758
|
-
}
|
|
2759
|
-
function formatNotFound(query, map) {
|
|
2760
|
-
const available = map.symbols.slice(0, 20).map((s) => s.name).join(", ");
|
|
2761
|
-
return `[Warning: symbol '${query}' not found. Available symbols: ${available}]`;
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
// src/read-output.ts
|
|
2765
|
-
import {
|
|
2766
|
-
DEFAULT_MAX_BYTES,
|
|
2767
|
-
DEFAULT_MAX_LINES,
|
|
2768
|
-
formatSize as formatSize2,
|
|
2769
|
-
truncateHead
|
|
2770
|
-
} from "@earendil-works/pi-coding-agent";
|
|
2771
|
-
function buildReadOutput(input) {
|
|
2772
|
-
const selectedLines = input.selectedLines ?? input.lines?.map((line) => line.raw) ?? [];
|
|
2773
|
-
const lines = input.lines ?? buildReadSeekLines(input.startLine, selectedLines);
|
|
2774
|
-
const warnings = input.warnings ?? [];
|
|
2775
|
-
const renderedLines = renderReadSeekLines(lines);
|
|
2776
|
-
const truncated = truncateHead(renderedLines, {
|
|
2777
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
2778
|
-
maxBytes: DEFAULT_MAX_BYTES
|
|
2779
|
-
});
|
|
2780
|
-
let text = input.truncation ? truncated.content : renderedLines;
|
|
2781
|
-
if (input.truncation) {
|
|
2782
|
-
text += `
|
|
2783
|
-
|
|
2784
|
-
[Output truncated: showing ${input.truncation.outputLines} of ${input.totalLines} lines (${formatSize2(input.truncation.outputBytes)} of ${formatSize2(input.truncation.totalBytes)}). Use offset=${input.startLine + input.truncation.outputLines} to continue.]`;
|
|
2785
|
-
} else if (input.continuation) {
|
|
2786
|
-
text += `
|
|
2787
|
-
|
|
2788
|
-
[Showing lines ${input.startLine}-${input.endLine} of ${input.totalLines}. Use offset=${input.continuation.nextOffset} to continue.]`;
|
|
2789
|
-
}
|
|
2790
|
-
if (input.bundle?.applied) {
|
|
2791
|
-
const supportBlocks = input.bundle.localSupport.map((item) => {
|
|
2792
|
-
const supportLines = buildReadSeekLines(item.symbol.startLine, item.lines);
|
|
2793
|
-
return renderReadSeekLines(supportLines);
|
|
2794
|
-
});
|
|
2795
|
-
text = [
|
|
2796
|
-
"## Requested symbol",
|
|
2797
|
-
text,
|
|
2798
|
-
"",
|
|
2799
|
-
"## Local support",
|
|
2800
|
-
...supportBlocks
|
|
2801
|
-
].join(`
|
|
2802
|
-
`);
|
|
2803
|
-
}
|
|
2804
|
-
if (input.map?.appended && input.map.text) {
|
|
2805
|
-
text += `
|
|
2806
|
-
|
|
2807
|
-
${input.map.text}`;
|
|
2808
|
-
}
|
|
2809
|
-
if (input.symbol) {
|
|
2810
|
-
const parentInfo = input.symbol.parentName ? ` in ${input.symbol.parentName}` : "";
|
|
2811
|
-
text = `[Symbol: ${input.symbol.name} (${input.symbol.kind})${parentInfo}, lines ${input.symbol.startLine}-${input.symbol.endLine} of ${input.totalLines}]
|
|
2812
|
-
|
|
2813
|
-
${text}`;
|
|
2814
|
-
}
|
|
2815
|
-
if (warnings.length) {
|
|
2816
|
-
text = `${warnings.map((warning) => warning.message).join(`
|
|
2817
|
-
|
|
2818
|
-
`)}
|
|
2819
|
-
|
|
2820
|
-
${text}`;
|
|
2821
|
-
}
|
|
2822
|
-
const readSeekValue = {
|
|
2823
|
-
tool: "read",
|
|
2824
|
-
path: input.path,
|
|
2825
|
-
range: {
|
|
2826
|
-
startLine: input.startLine,
|
|
2827
|
-
endLine: input.endLine,
|
|
2828
|
-
totalLines: input.totalLines
|
|
2829
|
-
},
|
|
2830
|
-
warnings,
|
|
2831
|
-
truncation: input.truncation ?? null,
|
|
2832
|
-
symbol: input.symbol ?? null,
|
|
2833
|
-
map: {
|
|
2834
|
-
requested: input.map?.requested ?? false,
|
|
2835
|
-
appended: input.map?.appended ?? false
|
|
2836
|
-
},
|
|
2837
|
-
lines
|
|
2838
|
-
};
|
|
2839
|
-
if (input.bundle) {
|
|
2840
|
-
readSeekValue.bundle = {
|
|
2841
|
-
mode: input.bundle.mode,
|
|
2842
|
-
applied: input.bundle.applied,
|
|
2843
|
-
localSupport: input.bundle.localSupport.map((item) => {
|
|
2844
|
-
const supportLines = buildReadSeekLines(item.symbol.startLine, item.lines);
|
|
2845
|
-
return {
|
|
2846
|
-
name: item.symbol.name,
|
|
2847
|
-
kind: item.symbol.kind,
|
|
2848
|
-
parentName: item.symbol.parentName,
|
|
2849
|
-
startLine: item.symbol.startLine,
|
|
2850
|
-
endLine: item.symbol.endLine,
|
|
2851
|
-
lineAnchors: supportLines.map((line) => line.anchor)
|
|
2852
|
-
};
|
|
2853
|
-
}),
|
|
2854
|
-
warnings: input.bundle.warnings ?? []
|
|
2855
|
-
};
|
|
2856
|
-
}
|
|
2857
|
-
return {
|
|
2858
|
-
text,
|
|
2859
|
-
lines,
|
|
2860
|
-
readSeekValue
|
|
2861
|
-
};
|
|
2862
|
-
}
|
|
2863
|
-
|
|
2864
|
-
// src/read-local-bundle.ts
|
|
2865
|
-
var CONTAINER_KINDS = new Set([
|
|
2866
|
-
"class" /* Class */,
|
|
2867
|
-
"module" /* Module */,
|
|
2868
|
-
"namespace" /* Namespace */
|
|
2869
|
-
]);
|
|
2870
|
-
function buildLocalBundle(fileMap, requested, allLines) {
|
|
2871
|
-
const requestedText = allLines.slice(requested.startLine - 1, requested.endLine).join(`
|
|
2872
|
-
`);
|
|
2873
|
-
const identifiers = new Set(requestedText.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? []);
|
|
2874
|
-
const candidates = traverseSymbolTree(fileMap.symbols, (symbol, parentName) => ({ symbol, parentName })).filter(({ symbol }) => {
|
|
2875
|
-
if (CONTAINER_KINDS.has(symbol.kind))
|
|
2876
|
-
return false;
|
|
2877
|
-
return !(symbol.name === requested.name && symbol.startLine === requested.startLine && symbol.endLine === requested.endLine);
|
|
2878
|
-
});
|
|
2879
|
-
const symbolsByName = new Map;
|
|
2880
|
-
for (const candidate of candidates) {
|
|
2881
|
-
const bucket = symbolsByName.get(candidate.symbol.name) ?? [];
|
|
2882
|
-
bucket.push(candidate);
|
|
2883
|
-
symbolsByName.set(candidate.symbol.name, bucket);
|
|
2884
|
-
}
|
|
2885
|
-
const support = [];
|
|
2886
|
-
for (const identifier of identifiers) {
|
|
2887
|
-
const matches = symbolsByName.get(identifier) ?? [];
|
|
2888
|
-
if (matches.length === 0)
|
|
2889
|
-
continue;
|
|
2890
|
-
if (matches.length > 1)
|
|
2891
|
-
return null;
|
|
2892
|
-
const match = matches[0];
|
|
2893
|
-
support.push({
|
|
2894
|
-
symbol: {
|
|
2895
|
-
name: match.symbol.name,
|
|
2896
|
-
kind: match.symbol.kind,
|
|
2897
|
-
startLine: match.symbol.startLine,
|
|
2898
|
-
endLine: match.symbol.endLine,
|
|
2899
|
-
...match.parentName ? { parentName: match.parentName } : {}
|
|
2900
|
-
},
|
|
2901
|
-
lines: allLines.slice(match.symbol.startLine - 1, match.symbol.endLine)
|
|
2902
|
-
});
|
|
2903
|
-
}
|
|
2904
|
-
support.sort((a, b) => {
|
|
2905
|
-
if (a.symbol.startLine !== b.symbol.startLine)
|
|
2906
|
-
return a.symbol.startLine - b.symbol.startLine;
|
|
2907
|
-
return a.symbol.name.localeCompare(b.symbol.name);
|
|
2908
|
-
});
|
|
2909
|
-
return {
|
|
2910
|
-
requested,
|
|
2911
|
-
support
|
|
2912
|
-
};
|
|
2913
|
-
}
|
|
2914
|
-
|
|
2915
|
-
// src/coerce-obvious-int.ts
|
|
2916
|
-
var BASE10_INT_RE = /^-?\d+$/;
|
|
2917
|
-
var MIN_SAFE_INTEGER = BigInt(Number.MIN_SAFE_INTEGER);
|
|
2918
|
-
var MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
|
2919
|
-
function formatReceived(value) {
|
|
2920
|
-
if (typeof value === "number" || typeof value === "bigint")
|
|
2921
|
-
return String(value);
|
|
2922
|
-
return JSON.stringify(value) ?? String(value);
|
|
2923
|
-
}
|
|
2924
|
-
function invalidInteger(value, name) {
|
|
2925
|
-
return {
|
|
2926
|
-
ok: false,
|
|
2927
|
-
message: `Invalid ${name}: expected a safe base-10 integer, received ${formatReceived(value)}.`
|
|
2928
|
-
};
|
|
2929
|
-
}
|
|
2930
|
-
function coerceObviousBase10Int(value, name) {
|
|
2931
|
-
if (value === undefined || typeof value === "string" && value.trim() === "") {
|
|
2932
|
-
return { ok: true, value: undefined };
|
|
2933
|
-
}
|
|
2934
|
-
if (typeof value === "number") {
|
|
2935
|
-
if (Number.isSafeInteger(value)) {
|
|
2936
|
-
return { ok: true, value };
|
|
2937
|
-
}
|
|
2938
|
-
return invalidInteger(value, name);
|
|
2939
|
-
}
|
|
2940
|
-
if (typeof value === "string" && BASE10_INT_RE.test(value)) {
|
|
2941
|
-
const parsed = BigInt(value);
|
|
2942
|
-
if (parsed >= MIN_SAFE_INTEGER && parsed <= MAX_SAFE_INTEGER) {
|
|
2943
|
-
return { ok: true, value: Number(parsed) };
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
return invalidInteger(value, name);
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
// src/read-render-helpers.ts
|
|
2950
|
-
function formatReadCallText(args) {
|
|
2951
|
-
const rawPath = typeof args?.path === "string" ? args.path : null;
|
|
2952
|
-
if (typeof args?.symbol === "string" && args.symbol) {
|
|
2953
|
-
return { path: rawPath, suffix: `→ symbol ${args.symbol}` };
|
|
2954
|
-
}
|
|
2955
|
-
if (typeof args?.offset === "number") {
|
|
2956
|
-
const offset = args.offset;
|
|
2957
|
-
const limit = typeof args?.limit === "number" ? args.limit : undefined;
|
|
2958
|
-
if (offset < 1) {
|
|
2959
|
-
return { path: rawPath, suffix: undefined };
|
|
2960
|
-
}
|
|
2961
|
-
if (limit !== undefined) {
|
|
2962
|
-
if (limit < 1) {
|
|
2963
|
-
return { path: rawPath, suffix: undefined };
|
|
2964
|
-
}
|
|
2965
|
-
return { path: rawPath, suffix: `lines ${offset}-${offset + limit - 1}` };
|
|
2966
|
-
}
|
|
2967
|
-
return { path: rawPath, suffix: `from line ${offset}` };
|
|
2968
|
-
}
|
|
2969
|
-
return { path: rawPath, suffix: undefined };
|
|
2970
|
-
}
|
|
2971
|
-
function formatReadResultText(input) {
|
|
2972
|
-
const { range, truncation, symbol, map, warnings } = input;
|
|
2973
|
-
if (input.isError && input.errorText) {
|
|
2974
|
-
return {
|
|
2975
|
-
summary: "",
|
|
2976
|
-
symbolBadge: undefined,
|
|
2977
|
-
badges: [],
|
|
2978
|
-
truncated: false,
|
|
2979
|
-
errorText: input.errorText
|
|
2980
|
-
};
|
|
2981
|
-
}
|
|
2982
|
-
const lineCount = range.endLine - range.startLine + 1;
|
|
2983
|
-
const isFullFile = range.startLine === 1 && range.endLine === range.totalLines;
|
|
2984
|
-
const isTruncated = !!truncation;
|
|
2985
|
-
let summary;
|
|
2986
|
-
if (isTruncated) {
|
|
2987
|
-
summary = `✓ ${truncation.outputLines} of ${truncation.totalLines} lines (truncated)`;
|
|
2988
|
-
} else if (isFullFile) {
|
|
2989
|
-
summary = `✓ ${lineCount} lines`;
|
|
2990
|
-
} else {
|
|
2991
|
-
summary = `✓ ${lineCount} lines (${range.startLine}-${range.endLine} of ${range.totalLines})`;
|
|
2992
|
-
}
|
|
2993
|
-
const symbolBadge = symbol ? `${symbol.kind} ${symbol.name}` : undefined;
|
|
2994
|
-
const badges = [];
|
|
2995
|
-
if (map.appended)
|
|
2996
|
-
badges.push("\uD83D\uDCD0 map");
|
|
2997
|
-
for (const w of warnings) {
|
|
2998
|
-
if (w.code === "binary-content")
|
|
2999
|
-
badges.push("⚠ binary");
|
|
3000
|
-
if (w.code === "bare-cr")
|
|
3001
|
-
badges.push("⚠ bare CR");
|
|
3002
|
-
if (w.code === "fuzzy-symbol-match")
|
|
3003
|
-
badges.push("⚠ fuzzy match");
|
|
3004
|
-
}
|
|
3005
|
-
return {
|
|
3006
|
-
summary,
|
|
3007
|
-
symbolBadge,
|
|
3008
|
-
badges,
|
|
3009
|
-
truncated: isTruncated,
|
|
3010
|
-
errorText: undefined
|
|
3011
|
-
};
|
|
3012
|
-
}
|
|
3013
|
-
|
|
3014
|
-
// src/tui-render-utils.ts
|
|
3015
|
-
import { relative } from "node:path";
|
|
3016
|
-
import { pathToFileURL } from "node:url";
|
|
3017
|
-
import { getCapabilities, hyperlink, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3018
|
-
var SUMMARY_PREFIX = "↳";
|
|
3019
|
-
var EXPAND_HINT = " • Ctrl+O to expand";
|
|
3020
|
-
function renderToolLabel(theme, label) {
|
|
3021
|
-
const boldFn = typeof theme.bold === "function" ? theme.bold.bind(theme) : (text) => text;
|
|
3022
|
-
return theme.fg("toolTitle", boldFn(label));
|
|
3023
|
-
}
|
|
3024
|
-
function linkToolPath(styledText, rawPath, cwd) {
|
|
3025
|
-
try {
|
|
3026
|
-
if (!getCapabilities().hyperlinks)
|
|
3027
|
-
return styledText;
|
|
3028
|
-
const absolutePath = resolveToCwd(rawPath, cwd);
|
|
3029
|
-
return hyperlink(styledText, pathToFileURL(absolutePath).href);
|
|
3030
|
-
} catch {
|
|
3031
|
-
return styledText;
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
function linkedPathLine(theme, rawPath, displayPath, cwd, suffix, width) {
|
|
3035
|
-
const pathWidth = width === undefined ? undefined : Math.max(1, width - 2 - visibleWidth(suffix));
|
|
3036
|
-
const visiblePath = pathWidth === undefined ? displayPath : truncateToWidth(displayPath, pathWidth);
|
|
3037
|
-
const linkedPath = linkToolPath(theme.fg("dim", visiblePath), rawPath, cwd);
|
|
3038
|
-
return ` ${linkedPath}${theme.fg("dim", suffix)}`;
|
|
3039
|
-
}
|
|
3040
|
-
function appendExpandHint(text, hidden) {
|
|
3041
|
-
return hidden ? `${text}${EXPAND_HINT}` : text;
|
|
3042
|
-
}
|
|
3043
|
-
function summaryLine(summary, options = {}) {
|
|
3044
|
-
const prefix = options.theme ? options.theme.fg(options.style ?? "dim", SUMMARY_PREFIX) : SUMMARY_PREFIX;
|
|
3045
|
-
return appendExpandHint(`${prefix} ${summary}`, !!options.hidden);
|
|
3046
|
-
}
|
|
3047
|
-
function isRendererExpanded(options, context, defaultExpanded = false) {
|
|
3048
|
-
return context?.expanded ?? options?.expanded ?? defaultExpanded;
|
|
3049
|
-
}
|
|
3050
|
-
function resolveRenderResultContext(options, rest, defaultExpanded = false) {
|
|
3051
|
-
const context = rest[0] ?? options ?? {};
|
|
3052
|
-
return {
|
|
3053
|
-
isPartial: context.isPartial ?? options?.isPartial ?? false,
|
|
3054
|
-
isError: context.isError ?? false,
|
|
3055
|
-
expanded: isRendererExpanded(options, context, defaultExpanded),
|
|
3056
|
-
width: context.width ?? options?.width,
|
|
3057
|
-
cwd: context.cwd ?? process.cwd(),
|
|
3058
|
-
context
|
|
3059
|
-
};
|
|
3060
|
-
}
|
|
3061
|
-
function normalizeWidth(width, fallback = 80) {
|
|
3062
|
-
return typeof width === "number" && Number.isFinite(width) && width > 0 ? Math.floor(width) : fallback;
|
|
3063
|
-
}
|
|
3064
|
-
function clampLineToWidth(line, width) {
|
|
3065
|
-
if (width === undefined || width === null)
|
|
3066
|
-
return line;
|
|
3067
|
-
const normalized = normalizeWidth(width);
|
|
3068
|
-
return visibleWidth(line) <= normalized ? line : truncateToWidth(line, normalized);
|
|
3069
|
-
}
|
|
3070
|
-
function clampLinesToWidth(lines, width) {
|
|
3071
|
-
if (width === undefined || width === null)
|
|
3072
|
-
return lines;
|
|
3073
|
-
return lines.map((line) => clampLineToWidth(line, width));
|
|
3074
|
-
}
|
|
3075
|
-
function wrapWithHangingIndent(prefix, content, width, options = {}) {
|
|
3076
|
-
const tint = options.tint ?? ((text) => text);
|
|
3077
|
-
if (width === undefined || width === null)
|
|
3078
|
-
return [tint(prefix + content)];
|
|
3079
|
-
const normalized = normalizeWidth(width);
|
|
3080
|
-
const combined = prefix + content;
|
|
3081
|
-
if (visibleWidth(combined) <= normalized)
|
|
3082
|
-
return [tint(combined)];
|
|
3083
|
-
const prefixWidth = visibleWidth(prefix);
|
|
3084
|
-
const contentWidth = Math.max(1, normalized - prefixWidth);
|
|
3085
|
-
const wrapped = wrapTextWithAnsi(content, contentWidth);
|
|
3086
|
-
if (wrapped.length === 0)
|
|
3087
|
-
return [tint(clampLineToWidth(prefix, normalized))];
|
|
3088
|
-
const indent = " ".repeat(prefixWidth);
|
|
3089
|
-
return wrapped.map((line, index) => tint(clampLineToWidth(index === 0 ? prefix + line : indent + line, normalized)));
|
|
3090
|
-
}
|
|
3091
|
-
var wrappedHashlinesCache = new WeakMap;
|
|
3092
|
-
function renderPendingResult(pendingLabel, width, theme) {
|
|
3093
|
-
return new Text(clampLinesToWidth([summaryLine(pendingLabel, { theme, style: "muted" })], width).join(`
|
|
3094
|
-
`), 0, 0);
|
|
3095
|
-
}
|
|
3096
|
-
function renderErrorResult(textContent, options) {
|
|
3097
|
-
const firstLine = textContent.split(`
|
|
3098
|
-
`)[0] || (options.fallback ?? "Error");
|
|
3099
|
-
const body = options.expanded && textContent ? textContent : firstLine;
|
|
3100
|
-
return new Text(clampLinesToWidth(summaryLine(body, { theme: options.theme, style: "error" }).split(`
|
|
3101
|
-
`), options.width).join(`
|
|
3102
|
-
`), 0, 0);
|
|
3103
|
-
}
|
|
3104
|
-
function renderReadSeekSearchCall(args, theme, rest, opts) {
|
|
3105
|
-
const context = rest[0] ?? {};
|
|
3106
|
-
let text = `${renderToolLabel(theme, opts.label)} ${theme.fg("accent", opts.accent)}`;
|
|
3107
|
-
text += theme.fg("dim", ` in ${args.path ?? "."}`);
|
|
3108
|
-
const language = args.language ?? args.lang;
|
|
3109
|
-
if (language)
|
|
3110
|
-
text += theme.fg("dim", ` (${language})`);
|
|
3111
|
-
const flags = opts.flags.filter(Boolean);
|
|
3112
|
-
if (flags.length > 0)
|
|
3113
|
-
text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
3114
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
3115
|
-
}
|
|
3116
|
-
function renderAnchoredFilesResult(result, options, theme, rest, labels) {
|
|
3117
|
-
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
3118
|
-
if (isPartial)
|
|
3119
|
-
return renderPendingResult(labels.pendingLabel, width, theme);
|
|
3120
|
-
const content = result.content?.[0];
|
|
3121
|
-
const textContent = content?.type === "text" ? content.text : "";
|
|
3122
|
-
if (isError || result.isError)
|
|
3123
|
-
return renderErrorResult(textContent, { expanded, width, theme });
|
|
3124
|
-
const readSeekValue = result.details?.readSeekValue;
|
|
3125
|
-
const files = readSeekValue?.files ?? [];
|
|
3126
|
-
if (files.length === 0)
|
|
3127
|
-
return new Text(summaryLine(labels.emptyLabel, { theme, style: "dim" }), 0, 0);
|
|
3128
|
-
const fileCount = files.length;
|
|
3129
|
-
const total = files.reduce((sum, f) => sum + f.lines.length, 0);
|
|
3130
|
-
const unitWord = total === 1 ? labels.unitSingular : labels.unitPlural;
|
|
3131
|
-
const fileWord = fileCount === 1 ? "file" : "files";
|
|
3132
|
-
let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, {
|
|
3133
|
-
hidden: !expanded,
|
|
3134
|
-
theme,
|
|
3135
|
-
style: "success"
|
|
3136
|
-
});
|
|
3137
|
-
if (expanded) {
|
|
3138
|
-
for (const file of files.slice(0, 20)) {
|
|
3139
|
-
const display = relative(cwd, file.path) || file.path;
|
|
3140
|
-
text += `
|
|
3141
|
-
${linkedPathLine(theme, file.path, display, cwd, ` (${file.lines.length})`, width)}`;
|
|
3142
|
-
}
|
|
3143
|
-
if (files.length > 20)
|
|
3144
|
-
text += `
|
|
3145
|
-
` + theme.fg("muted", ` … and ${files.length - 20} more files`);
|
|
3146
|
-
}
|
|
3147
|
-
return new Text(clampLinesToWidth(text.split(`
|
|
3148
|
-
`), width).join(`
|
|
3149
|
-
`), 0, 0);
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
// src/tui-source-render.ts
|
|
3153
|
-
import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
3154
|
-
import { wrapTextWithAnsi as wrapTextWithAnsi2 } from "@earendil-works/pi-tui";
|
|
3155
|
-
var READ_ANCHOR_RE = /^(\d+:[0-9a-zA-Z]{1,16})\|(.*)$/;
|
|
3156
|
-
var GREP_ANCHOR_RE = /^(.*?):(>>| )(\d+):([0-9a-zA-Z]{1,16})\|(.*)$/;
|
|
3157
|
-
var SOURCE_THEME_STYLES = [
|
|
3158
|
-
"mdCodeBlock",
|
|
3159
|
-
"muted",
|
|
3160
|
-
"syntaxComment",
|
|
3161
|
-
"syntaxKeyword",
|
|
3162
|
-
"syntaxFunction",
|
|
3163
|
-
"syntaxVariable",
|
|
3164
|
-
"syntaxString",
|
|
3165
|
-
"syntaxNumber",
|
|
3166
|
-
"syntaxType",
|
|
3167
|
-
"syntaxOperator",
|
|
3168
|
-
"syntaxPunctuation",
|
|
3169
|
-
"toolDiffAdded",
|
|
3170
|
-
"toolDiffRemoved"
|
|
3171
|
-
];
|
|
3172
|
-
function wrapDisplayLines(lines, width) {
|
|
3173
|
-
if (width === undefined || width === null)
|
|
3174
|
-
return lines;
|
|
3175
|
-
const normalized = normalizeWidth(width);
|
|
3176
|
-
return lines.flatMap((line) => {
|
|
3177
|
-
const wrapped = wrapTextWithAnsi2(line, normalized);
|
|
3178
|
-
return wrapped.length > 0 ? wrapped : [""];
|
|
3179
|
-
});
|
|
3180
|
-
}
|
|
3181
|
-
function renderSourceBlock(lines, path3) {
|
|
3182
|
-
return highlightCode(lines.join(`
|
|
3183
|
-
`), getLanguageFromPath(path3));
|
|
3184
|
-
}
|
|
3185
|
-
function renderReadSourceForDisplay(text, path3, anchors, width) {
|
|
3186
|
-
const output = [];
|
|
3187
|
-
let sourceLines = [];
|
|
3188
|
-
const flushSourceLines = () => {
|
|
3189
|
-
if (sourceLines.length === 0)
|
|
3190
|
-
return;
|
|
3191
|
-
output.push(...wrapDisplayLines(renderSourceBlock(sourceLines, path3), width));
|
|
3192
|
-
sourceLines = [];
|
|
3193
|
-
};
|
|
3194
|
-
for (const line of text.split(`
|
|
3195
|
-
`)) {
|
|
3196
|
-
const match = line.match(READ_ANCHOR_RE);
|
|
3197
|
-
if (match && anchors.has(match[1])) {
|
|
3198
|
-
sourceLines.push(match[2] ?? "");
|
|
3199
|
-
continue;
|
|
3200
|
-
}
|
|
3201
|
-
flushSourceLines();
|
|
3202
|
-
output.push(...wrapDisplayLines([line], width));
|
|
3203
|
-
}
|
|
3204
|
-
flushSourceLines();
|
|
3205
|
-
return output.join(`
|
|
3206
|
-
`);
|
|
3207
|
-
}
|
|
3208
|
-
var readSourceCache = new WeakMap;
|
|
3209
|
-
function renderReadSourceForDisplayCached(cacheKey, text, path3, anchors, width, theme) {
|
|
3210
|
-
const themeKey = SOURCE_THEME_STYLES.map((style) => theme.fg(style, "")).join("");
|
|
3211
|
-
const anchorKey = [...anchors].join(`
|
|
3212
|
-
`);
|
|
3213
|
-
const cached = readSourceCache.get(cacheKey);
|
|
3214
|
-
if (cached && cached.text === text && cached.path === path3 && cached.width === width && cached.theme === themeKey && cached.anchors === anchorKey) {
|
|
3215
|
-
return cached.rendered;
|
|
3216
|
-
}
|
|
3217
|
-
const rendered = renderReadSourceForDisplay(text, path3, anchors, width);
|
|
3218
|
-
readSourceCache.set(cacheKey, { text, path: path3, width, theme: themeKey, anchors: anchorKey, rendered });
|
|
3219
|
-
return rendered;
|
|
3220
|
-
}
|
|
3221
|
-
function renderGrepSourceForDisplay(text, anchors, renderPath = (path3) => path3) {
|
|
3222
|
-
return text.split(`
|
|
3223
|
-
`).map((line) => {
|
|
3224
|
-
const match = line.match(GREP_ANCHOR_RE);
|
|
3225
|
-
if (!match)
|
|
3226
|
-
return line;
|
|
3227
|
-
const anchor = `${match[3]}:${match[4]}`;
|
|
3228
|
-
if (!anchors.has(anchor))
|
|
3229
|
-
return line;
|
|
3230
|
-
const separator = match[2] === ">>" ? ":" : "-";
|
|
3231
|
-
return `${renderPath(match[1])}${separator}${match[3]}${separator}${match[5] ?? ""}`;
|
|
3232
|
-
}).join(`
|
|
3233
|
-
`);
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
|
-
// src/register-tool.ts
|
|
3237
|
-
import { Type } from "@sinclair/typebox";
|
|
3238
|
-
function optionalIntOrString(description) {
|
|
3239
|
-
return Type.Optional(Type.Union([Type.Number({ description }), Type.String({ description })]));
|
|
3240
|
-
}
|
|
3241
|
-
function filePathParam() {
|
|
3242
|
-
return Type.String({ description: "Absolute or relative file path" });
|
|
3243
|
-
}
|
|
3244
|
-
function registerReadSeekTool(pi, tool) {
|
|
3245
|
-
pi.registerTool(tool);
|
|
3246
|
-
return tool;
|
|
3247
|
-
}
|
|
3248
|
-
|
|
3249
|
-
// src/read.ts
|
|
3250
|
-
function hasReadAnchors(result) {
|
|
3251
|
-
const details = result.details;
|
|
3252
|
-
if (!details || typeof details !== "object")
|
|
3253
|
-
return false;
|
|
3254
|
-
const readSeekValue = details.readSeekValue;
|
|
3255
|
-
if (!readSeekValue || typeof readSeekValue !== "object")
|
|
3256
|
-
return false;
|
|
3257
|
-
const lines = readSeekValue.lines;
|
|
3258
|
-
return Array.isArray(lines) && lines.length > 0;
|
|
3259
|
-
}
|
|
3260
|
-
function formatImageAnalysis(detection, mode) {
|
|
3261
|
-
if (detection.kind !== "image")
|
|
3262
|
-
return;
|
|
3263
|
-
const sections = [];
|
|
3264
|
-
const ocr = detection.ocr?.trim();
|
|
3265
|
-
if (ocr)
|
|
3266
|
-
sections.push(`OCR text from image:
|
|
3267
|
-
${ocr}`);
|
|
3268
|
-
const caption = detection.caption?.trim();
|
|
3269
|
-
if (caption)
|
|
3270
|
-
sections.push(`Image caption:
|
|
3271
|
-
${caption}`);
|
|
3272
|
-
if (detection.objects?.length) {
|
|
3273
|
-
const lines = detection.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
|
|
3274
|
-
sections.push(`Detected objects:
|
|
3275
|
-
${lines.join(`
|
|
3276
|
-
`)}`);
|
|
3277
|
-
}
|
|
3278
|
-
if (sections.length > 0)
|
|
3279
|
-
return sections.join(`
|
|
3280
|
-
|
|
3281
|
-
`);
|
|
3282
|
-
if (mode === "ocr")
|
|
3283
|
-
return "No OCR text detected in image.";
|
|
3284
|
-
if (mode === "objects")
|
|
3285
|
-
return "No objects detected in image.";
|
|
3286
|
-
if (mode === "all")
|
|
3287
|
-
return "No image analysis returned.";
|
|
3288
|
-
return "Image caption returned no text.";
|
|
3289
|
-
}
|
|
3290
|
-
function documentRedirectMessage(path3, cache) {
|
|
3291
|
-
const cacheNote = cache ? ` Index ${cache.state} (hash ${cache.content_hash}).` : "";
|
|
3292
|
-
return `Document content is not available via readSeek_read for ${path3}.${cacheNote} Use readSeek_view to digest structure or selected content.`;
|
|
3293
|
-
}
|
|
3294
|
-
function skippedImageFile(path3) {
|
|
3295
|
-
return {
|
|
3296
|
-
content: [{ type: "text", text: `[Skipped image: ${path3}; no image mode selected]` }],
|
|
3297
|
-
details: {}
|
|
3298
|
-
};
|
|
3299
|
-
}
|
|
3300
|
-
async function executeRead(opts) {
|
|
3301
|
-
const { params, signal, cwd, onSuccessfulRead } = opts;
|
|
3302
|
-
await ensureHashInit();
|
|
3303
|
-
const rawParams = params;
|
|
3304
|
-
const offset = coerceObviousBase10Int(rawParams.offset, "offset");
|
|
3305
|
-
if (!offset.ok) {
|
|
3306
|
-
return buildToolErrorResult("read", "invalid-offset", offset.message, { path: rawParams.path });
|
|
3307
|
-
}
|
|
3308
|
-
const limit = coerceObviousBase10Int(rawParams.limit, "limit");
|
|
3309
|
-
if (!limit.ok) {
|
|
3310
|
-
return buildToolErrorResult("read", "invalid-limit", limit.message, { path: rawParams.path });
|
|
3311
|
-
}
|
|
3312
|
-
if (limit.value !== undefined && limit.value < 1) {
|
|
3313
|
-
const message = `Invalid limit: expected a positive integer, received ${limit.value}.`;
|
|
3314
|
-
return buildToolErrorResult("read", "invalid-limit", message, { path: rawParams.path });
|
|
3315
|
-
}
|
|
3316
|
-
if (offset.value !== undefined && offset.value < 1) {
|
|
3317
|
-
const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
|
|
3318
|
-
return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
|
|
3319
|
-
}
|
|
3320
|
-
const rawSymbol = typeof rawParams.symbol === "string" && rawParams.symbol.trim().length > 0 ? rawParams.symbol.trim() : undefined;
|
|
3321
|
-
const rawBundle = typeof rawParams.bundle === "string" ? rawParams.bundle.trim() : undefined;
|
|
3322
|
-
const p = {
|
|
3323
|
-
...rawParams,
|
|
3324
|
-
offset: offset.value,
|
|
3325
|
-
limit: limit.value,
|
|
3326
|
-
symbol: rawSymbol,
|
|
3327
|
-
bundle: rawBundle
|
|
3328
|
-
};
|
|
3329
|
-
const rawPath = p.path.replace(/^@/, "");
|
|
3330
|
-
const absolutePath = resolveToCwd(rawPath, cwd);
|
|
3331
|
-
const succeed = (result) => {
|
|
3332
|
-
const isError = result.isError;
|
|
3333
|
-
if (!isError && hasReadAnchors(result)) {
|
|
3334
|
-
onSuccessfulRead?.(absolutePath);
|
|
3335
|
-
}
|
|
3336
|
-
return result;
|
|
3337
|
-
};
|
|
3338
|
-
throwIfAborted(signal);
|
|
3339
|
-
if (p.bundle !== undefined && p.bundle !== "local") {
|
|
3340
|
-
const message = `Invalid bundle: expected "local", received ${JSON.stringify(p.bundle)}.`;
|
|
3341
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3342
|
-
}
|
|
3343
|
-
if (p.symbol && (p.offset !== undefined || p.limit !== undefined)) {
|
|
3344
|
-
const message = "Cannot combine symbol with offset/limit. Use one or the other.";
|
|
3345
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3346
|
-
}
|
|
3347
|
-
if (p.bundle && !p.symbol) {
|
|
3348
|
-
const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
|
|
3349
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3350
|
-
}
|
|
3351
|
-
throwIfAborted(signal);
|
|
3352
|
-
const ext = rawPath.split(".").pop()?.toLowerCase() ?? "";
|
|
3353
|
-
let rawBuffer;
|
|
3354
|
-
try {
|
|
3355
|
-
rawBuffer = await fsReadFile(absolutePath);
|
|
3356
|
-
} catch (err) {
|
|
3357
|
-
const { code, message } = formatFsError(err, "read-error");
|
|
3358
|
-
return buildToolErrorResult("read", code, message, {
|
|
3359
|
-
path: rawParams.path
|
|
3360
|
-
});
|
|
3361
|
-
}
|
|
3362
|
-
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
3363
|
-
const hasPdfHeader = rawBuffer.subarray(0, 1024).includes(Buffer.from("%PDF-"));
|
|
3364
|
-
const maybeVisual = hasBinaryContent || ext === "pdf" || hasPdfHeader;
|
|
3365
|
-
let earlyDigest;
|
|
3366
|
-
if (maybeVisual) {
|
|
3367
|
-
let detection;
|
|
3368
|
-
try {
|
|
3369
|
-
earlyDigest = await readSeekDigestContent(absolutePath, {
|
|
3370
|
-
...ext === "pdf" || hasPdfHeader ? {} : { visionMode: "none" },
|
|
3371
|
-
signal
|
|
3372
|
-
});
|
|
3373
|
-
detection = earlyDigest.metadata;
|
|
3374
|
-
} catch {
|
|
3375
|
-
try {
|
|
3376
|
-
detection = await readSeekDetect(absolutePath, { signal });
|
|
3377
|
-
} catch {
|
|
3378
|
-
return buildToolErrorResult("read", "read-error", `Visual file detection failed for ${absolutePath}.`, {
|
|
3379
|
-
path: rawParams.path
|
|
3380
|
-
});
|
|
3381
|
-
}
|
|
3382
|
-
}
|
|
3383
|
-
if (detection.kind === "pdf") {
|
|
3384
|
-
return buildToolErrorResult("read", "document-use-view", documentRedirectMessage(rawParams.path, earlyDigest?.cache), { path: rawParams.path });
|
|
3385
|
-
}
|
|
3386
|
-
if (detection.kind === "image") {
|
|
3387
|
-
if (p.offset !== undefined || p.limit !== undefined) {
|
|
3388
|
-
const message = "Cannot combine offset/limit with image reads.";
|
|
3389
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3390
|
-
}
|
|
3391
|
-
const imageMode = resolveReadSeekImageMode();
|
|
3392
|
-
const useNativeVision = imageMode === "auto" && opts.modelSupportsImages === true;
|
|
3393
|
-
if (imageMode === "on" && p.image === "none") {
|
|
3394
|
-
return buildToolErrorResult("read", "invalid-params-combo", 'image="none" is only available when imageMode is "auto".', {
|
|
3395
|
-
path: rawParams.path
|
|
3396
|
-
});
|
|
3397
|
-
}
|
|
3398
|
-
if (imageMode === "off" || p.image === undefined) {
|
|
3399
|
-
return skippedImageFile(rawParams.path);
|
|
3400
|
-
}
|
|
3401
|
-
if (useNativeVision || p.image === "none") {
|
|
3402
|
-
try {
|
|
3403
|
-
const prepared = earlyDigest?.image && earlyDigest.image.encoding === "base64" && earlyDigest.image.data !== undefined && earlyDigest.image.mime !== undefined ? { mime: earlyDigest.image.mime, encoding: earlyDigest.image.encoding, data: earlyDigest.image.data } : await readSeekPreparedImage(absolutePath, { signal });
|
|
3404
|
-
return succeed({
|
|
3405
|
-
content: [{ type: "image", data: prepared.data, mimeType: prepared.mime }],
|
|
3406
|
-
details: {}
|
|
3407
|
-
});
|
|
3408
|
-
} catch (error) {
|
|
3409
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3410
|
-
return buildToolErrorResult("read", "read-error", `Image preprocessing unavailable: ${message}`, {
|
|
3411
|
-
path: rawParams.path
|
|
3412
|
-
});
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
try {
|
|
3416
|
-
const analysis = await readSeekImage(absolutePath, [p.image], { signal });
|
|
3417
|
-
const imageAnalysis = formatImageAnalysis(analysis, p.image);
|
|
3418
|
-
if (imageAnalysis) {
|
|
3419
|
-
return succeed({
|
|
3420
|
-
content: [{ type: "text", text: imageAnalysis }],
|
|
3421
|
-
details: {}
|
|
3422
|
-
});
|
|
3423
|
-
}
|
|
3424
|
-
} catch (error) {
|
|
3425
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3426
|
-
return buildToolErrorResult("read", "read-error", `Image analysis unavailable: ${message}`, {
|
|
3427
|
-
path: rawParams.path
|
|
3428
|
-
});
|
|
3429
|
-
}
|
|
3430
|
-
return buildToolErrorResult("read", "read-error", "Image analysis returned no content.", {
|
|
3431
|
-
path: rawParams.path
|
|
3432
|
-
});
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
throwIfAborted(signal);
|
|
3436
|
-
const rawText = rawBuffer.toString("utf-8");
|
|
3437
|
-
const normalized = normalizeToLF(stripBom(rawText).text);
|
|
3438
|
-
const allLines = splitReadSeekLines(normalized);
|
|
3439
|
-
const total = allLines.length;
|
|
3440
|
-
const structuredWarnings = [];
|
|
3441
|
-
let startLine = p.offset !== undefined ? p.offset : 1;
|
|
3442
|
-
let endIdx = p.limit !== undefined ? Math.min(startLine - 1 + p.limit, total) : total;
|
|
3443
|
-
if (p.offset !== undefined && startLine > total) {
|
|
3444
|
-
const message = `offset ${p.offset} is past end of file (${total} lines)`;
|
|
3445
|
-
return buildToolErrorResult("read", "offset-past-end", message, { path: rawParams.path });
|
|
3446
|
-
}
|
|
3447
|
-
let symbolMatch;
|
|
3448
|
-
let symbolFileMap = null;
|
|
3449
|
-
let symbolWarning;
|
|
3450
|
-
let bundleMetadata = null;
|
|
3451
|
-
if (p.symbol) {
|
|
3452
|
-
symbolFileMap = await getOrGenerateMap(absolutePath);
|
|
3453
|
-
if (!symbolFileMap) {
|
|
3454
|
-
const extLabel = ext || "unknown";
|
|
3455
|
-
symbolWarning = `[Warning: symbol lookup not available for .${extLabel} files — showing full file]
|
|
3456
|
-
|
|
3457
|
-
`;
|
|
3458
|
-
} else {
|
|
3459
|
-
const lookup = findSymbol(symbolFileMap, p.symbol);
|
|
3460
|
-
if (lookup.type === "ambiguous") {
|
|
3461
|
-
return succeed({
|
|
3462
|
-
content: [
|
|
3463
|
-
{
|
|
3464
|
-
type: "text",
|
|
3465
|
-
text: formatAmbiguous(p.symbol, lookup.candidates)
|
|
3466
|
-
}
|
|
3467
|
-
],
|
|
3468
|
-
isError: false,
|
|
3469
|
-
details: {}
|
|
3470
|
-
});
|
|
3471
|
-
}
|
|
3472
|
-
if (lookup.type === "not-found") {
|
|
3473
|
-
symbolWarning = `${formatNotFound(p.symbol, symbolFileMap)}
|
|
3474
|
-
|
|
3475
|
-
`;
|
|
3476
|
-
}
|
|
3477
|
-
if (lookup.type === "found") {
|
|
3478
|
-
startLine = Math.max(1, lookup.symbol.startLine);
|
|
3479
|
-
endIdx = Math.min(total, lookup.symbol.endLine);
|
|
3480
|
-
symbolMatch = lookup.symbol;
|
|
3481
|
-
}
|
|
3482
|
-
if (lookup.type === "fuzzy") {
|
|
3483
|
-
startLine = Math.max(1, lookup.symbol.startLine);
|
|
3484
|
-
endIdx = Math.min(total, lookup.symbol.endLine);
|
|
3485
|
-
symbolMatch = lookup.symbol;
|
|
3486
|
-
const tierLabel = lookup.tier === "camelCase" ? "camelCase word boundary" : "substring";
|
|
3487
|
-
const otherNames = lookup.otherCandidates.map((c) => `\`${c.name}\``).join(", ");
|
|
3488
|
-
const confirmHint = `read({ symbol: "${lookup.symbol.name}" }) or ${lookup.symbol.name}@${lookup.symbol.startLine} to select by start line`;
|
|
3489
|
-
const lines = [
|
|
3490
|
-
`[Symbol '${p.symbol}' not exact-matched. Closest match: \`${lookup.symbol.name}\` (${lookup.symbol.kind}, lines ${lookup.symbol.startLine}-${lookup.symbol.endLine}) via ${tierLabel}.`
|
|
3491
|
-
];
|
|
3492
|
-
if (otherNames)
|
|
3493
|
-
lines.push(` Other candidates: ${otherNames}.`);
|
|
3494
|
-
lines.push(` To confirm: ${confirmHint}.]`);
|
|
3495
|
-
const bannerText = lines.join(`
|
|
3496
|
-
`);
|
|
3497
|
-
structuredWarnings.push(buildReadSeekWarning("fuzzy-symbol-match", bannerText, {
|
|
3498
|
-
tier: lookup.tier,
|
|
3499
|
-
symbol: lookup.symbol,
|
|
3500
|
-
otherCandidates: lookup.otherCandidates
|
|
3501
|
-
}));
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
if (p.bundle === "local") {
|
|
3506
|
-
if (!symbolFileMap) {
|
|
3507
|
-
const extLabel = ext || "unknown";
|
|
3508
|
-
const warning = buildReadSeekWarning("bundle-unmappable", `[Warning: local bundle unavailable because symbol mapping is not available for .${extLabel} files — showing plain symbol read]`);
|
|
3509
|
-
structuredWarnings.push(warning);
|
|
3510
|
-
bundleMetadata = {
|
|
3511
|
-
mode: "local",
|
|
3512
|
-
applied: false,
|
|
3513
|
-
localSupport: [],
|
|
3514
|
-
warnings: [warning]
|
|
3515
|
-
};
|
|
3516
|
-
} else if (!symbolMatch) {
|
|
3517
|
-
bundleMetadata = {
|
|
3518
|
-
mode: "local",
|
|
3519
|
-
applied: false,
|
|
3520
|
-
localSupport: [],
|
|
3521
|
-
warnings: []
|
|
3522
|
-
};
|
|
3523
|
-
} else {
|
|
3524
|
-
const bundle = buildLocalBundle(symbolFileMap, symbolMatch, allLines);
|
|
3525
|
-
if (!bundle) {
|
|
3526
|
-
const warning = buildReadSeekWarning("bundle-context-unavailable", `[Warning: local bundle context could not be determined for symbol '${symbolMatch.name}' — showing plain symbol read]`);
|
|
3527
|
-
structuredWarnings.push(warning);
|
|
3528
|
-
bundleMetadata = {
|
|
3529
|
-
mode: "local",
|
|
3530
|
-
applied: false,
|
|
3531
|
-
localSupport: [],
|
|
3532
|
-
warnings: [warning]
|
|
3533
|
-
};
|
|
3534
|
-
} else {
|
|
3535
|
-
bundleMetadata = {
|
|
3536
|
-
mode: "local",
|
|
3537
|
-
applied: true,
|
|
3538
|
-
localSupport: bundle.support.map((item) => ({
|
|
3539
|
-
symbol: {
|
|
3540
|
-
query: item.symbol.name,
|
|
3541
|
-
name: item.symbol.name,
|
|
3542
|
-
kind: item.symbol.kind,
|
|
3543
|
-
parentName: item.symbol.parentName,
|
|
3544
|
-
startLine: item.symbol.startLine,
|
|
3545
|
-
endLine: item.symbol.endLine
|
|
3546
|
-
},
|
|
3547
|
-
lines: item.lines
|
|
3548
|
-
})),
|
|
3549
|
-
warnings: []
|
|
3550
|
-
};
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
}
|
|
3554
|
-
throwIfAborted(signal);
|
|
3555
|
-
let readSeekOutput;
|
|
3556
|
-
try {
|
|
3557
|
-
const digested = total === 0 ? await readSeekDigestContent(absolutePath, { signal }) : await readSeekDigestContent(absolutePath, { startLine, endLine: endIdx, signal });
|
|
3558
|
-
if (!digested.text) {
|
|
3559
|
-
throw new Error(`readseek returned no text content for ${absolutePath}`);
|
|
3560
|
-
}
|
|
3561
|
-
readSeekOutput = digested.text;
|
|
3562
|
-
} catch (err) {
|
|
3563
|
-
const detail = err?.message ? ` — ${err.message}` : "";
|
|
3564
|
-
const message = `readseek failed while reading ${rawPath}${detail}`;
|
|
3565
|
-
return buildToolErrorResult("read", "readseek-error", message, { path: rawParams.path, hint: "Ensure @jarkkojs/readseek and its npm platform package are installed.", details: { message: err?.message } });
|
|
3566
|
-
}
|
|
3567
|
-
const expectedLineCount = Math.max(0, endIdx - startLine + 1);
|
|
3568
|
-
const invalidLine = readSeekOutput.hashlines.find((line, index) => line.line !== startLine + index);
|
|
3569
|
-
if (readSeekOutput.hashlines.length !== expectedLineCount || invalidLine) {
|
|
3570
|
-
const message = invalidLine ? `readseek returned non-sequential line ${invalidLine.line} for requested range ${startLine}-${endIdx}` : `readseek returned ${readSeekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
|
|
3571
|
-
return buildToolErrorResult("read", "readseek-output-mismatch", message, { path: rawParams.path });
|
|
3572
|
-
}
|
|
3573
|
-
const readSeekLines = readSeekOutput.hashlines.map((line) => ({
|
|
3574
|
-
line: line.line,
|
|
3575
|
-
hash: line.hash,
|
|
3576
|
-
anchor: `${line.line}:${line.hash}`,
|
|
3577
|
-
raw: line.text,
|
|
3578
|
-
display: escapeControlCharsForDisplay(line.text)
|
|
3579
|
-
}));
|
|
3580
|
-
const selected = readSeekLines.map((line) => line.raw);
|
|
3581
|
-
const formatted = renderReadSeekLines(readSeekLines);
|
|
3582
|
-
const truncation = truncateHead2(formatted, { maxLines: DEFAULT_MAX_LINES2, maxBytes: DEFAULT_MAX_BYTES2 });
|
|
3583
|
-
const shouldAppendMap = !!truncation.truncated && !p.offset && !p.limit && !symbolMatch;
|
|
3584
|
-
let appendedMap = false;
|
|
3585
|
-
let mapText = null;
|
|
3586
|
-
if (shouldAppendMap) {
|
|
3587
|
-
try {
|
|
3588
|
-
const fileMap = await getOrGenerateMap(absolutePath);
|
|
3589
|
-
if (fileMap) {
|
|
3590
|
-
const formattedMap = formatFileMapWithBudget(fileMap);
|
|
3591
|
-
mapText = formattedMap;
|
|
3592
|
-
appendedMap = true;
|
|
3593
|
-
}
|
|
3594
|
-
} catch {}
|
|
3595
|
-
}
|
|
3596
|
-
let currentBuffer;
|
|
3597
|
-
try {
|
|
3598
|
-
currentBuffer = await fsReadFile(absolutePath);
|
|
3599
|
-
} catch (err) {
|
|
3600
|
-
const { code, message } = formatFsError(err, "read-error");
|
|
3601
|
-
return buildToolErrorResult("read", code, message, { path: rawParams.path });
|
|
3602
|
-
}
|
|
3603
|
-
if (!currentBuffer.equals(rawBuffer)) {
|
|
3604
|
-
return buildToolErrorResult("read", "file-changed-during-read", `File changed while it was being read: ${rawPath}. Re-read it to obtain a consistent snapshot.`, { path: rawParams.path });
|
|
3605
|
-
}
|
|
3606
|
-
if (symbolWarning) {
|
|
3607
|
-
structuredWarnings.push(buildReadSeekWarning("symbol-warning", symbolWarning.trim()));
|
|
3608
|
-
}
|
|
3609
|
-
if (hasBinaryContent) {
|
|
3610
|
-
const warning = "[Warning: file appears to be binary — output may be garbled]";
|
|
3611
|
-
structuredWarnings.push(buildReadSeekWarning("binary-content", warning));
|
|
3612
|
-
}
|
|
3613
|
-
if (hasBareCarriageReturn(rawText)) {
|
|
3614
|
-
const warning = "[Warning: file contains bare CR (\\r) line endings — line numbering may be inconsistent with grep and other tools]";
|
|
3615
|
-
structuredWarnings.push(buildReadSeekWarning("bare-cr", warning));
|
|
3616
|
-
}
|
|
3617
|
-
const readOutput = buildReadOutput({
|
|
3618
|
-
path: absolutePath,
|
|
3619
|
-
startLine,
|
|
3620
|
-
endLine: endIdx,
|
|
3621
|
-
totalLines: total,
|
|
3622
|
-
selectedLines: selected,
|
|
3623
|
-
lines: readSeekLines,
|
|
3624
|
-
warnings: structuredWarnings,
|
|
3625
|
-
truncation: truncation.truncated ? {
|
|
3626
|
-
outputLines: truncation.outputLines,
|
|
3627
|
-
totalLines: total,
|
|
3628
|
-
outputBytes: truncation.outputBytes,
|
|
3629
|
-
totalBytes: truncation.totalBytes
|
|
3630
|
-
} : null,
|
|
3631
|
-
continuation: !truncation.truncated && endIdx < total ? { nextOffset: endIdx + 1 } : null,
|
|
3632
|
-
symbol: symbolMatch ? {
|
|
3633
|
-
query: p.symbol ?? symbolMatch.name,
|
|
3634
|
-
name: symbolMatch.name,
|
|
3635
|
-
kind: symbolMatch.kind,
|
|
3636
|
-
parentName: symbolMatch.parentName,
|
|
3637
|
-
startLine: symbolMatch.startLine,
|
|
3638
|
-
endLine: symbolMatch.endLine
|
|
3639
|
-
} : null,
|
|
3640
|
-
map: {
|
|
3641
|
-
requested: false,
|
|
3642
|
-
appended: appendedMap,
|
|
3643
|
-
text: mapText
|
|
3644
|
-
},
|
|
3645
|
-
...bundleMetadata ? { bundle: bundleMetadata } : {}
|
|
3646
|
-
});
|
|
3647
|
-
return succeed({
|
|
3648
|
-
content: [{ type: "text", text: readOutput.text }],
|
|
3649
|
-
details: {
|
|
3650
|
-
truncation: truncation.truncated ? truncation : undefined,
|
|
3651
|
-
readSeekValue: readOutput.readSeekValue
|
|
3652
|
-
}
|
|
3653
|
-
});
|
|
3654
|
-
}
|
|
3655
|
-
function splitReadSeekLines(text) {
|
|
3656
|
-
if (text.length === 0)
|
|
3657
|
-
return [];
|
|
3658
|
-
const withoutTrailingTerminator = text.endsWith(`
|
|
3659
|
-
`) ? text.slice(0, -1) : text;
|
|
3660
|
-
return withoutTrailingTerminator.split(`
|
|
3661
|
-
`);
|
|
3662
|
-
}
|
|
3663
|
-
function registerReadTool(pi, options = {}) {
|
|
3664
|
-
const name = options.name ?? "readSeek_read";
|
|
3665
|
-
const imageMode = resolveReadSeekImageMode();
|
|
3666
|
-
const imageModes = imageMode === "auto" ? ["none", "all", "ocr", "caption", "objects"] : ["all", "ocr", "caption", "objects"];
|
|
3667
|
-
const promptMetadata = defineToolPromptMetadata({
|
|
3668
|
-
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
3669
|
-
promptSnippet: imageMode === "off" ? "Read anchored text or symbols" : "Read anchored text, symbols, or images",
|
|
3670
|
-
registeredName: name
|
|
3671
|
-
});
|
|
3672
|
-
const tool = registerReadSeekTool(pi, {
|
|
3673
|
-
name,
|
|
3674
|
-
label: "Read",
|
|
3675
|
-
description: imageMode === "off" ? "Read anchored text by range or symbol. Images and documents are disabled; use readSeek_view for documents." : promptMetadata.description,
|
|
3676
|
-
promptSnippet: promptMetadata.promptSnippet,
|
|
3677
|
-
promptGuidelines: [
|
|
3678
|
-
...promptMetadata.promptGuidelines,
|
|
3679
|
-
...imageMode === "auto" ? ["Image-capable Pi models receive prepared images directly. For standalone images, image=none skips local analysis; other modes use ReadSeek when native vision is unavailable."] : []
|
|
3680
|
-
],
|
|
3681
|
-
parameters: Type2.Object({
|
|
3682
|
-
path: filePathParam(),
|
|
3683
|
-
offset: optionalIntOrString("One-based starting line"),
|
|
3684
|
-
limit: optionalIntOrString("Maximum lines to return"),
|
|
3685
|
-
symbol: Type2.Optional(Type2.String({ description: "Symbol name to read" })),
|
|
3686
|
-
bundle: Type2.Optional(Type2.Literal("local", {
|
|
3687
|
-
description: "Include related symbols from the same file; requires symbol"
|
|
3688
|
-
})),
|
|
3689
|
-
...imageMode === "off" ? {} : {
|
|
3690
|
-
image: Type2.Optional(Type2.Union(imageModes.map((mode) => Type2.Literal(mode)), {
|
|
3691
|
-
description: `Standalone image mode: ${imageModes.join(", ")}. Required for standalone images.${imageMode === "auto" ? " For standalone images, none skips local analysis." : ""}`
|
|
3692
|
-
}))
|
|
3693
|
-
}
|
|
3694
|
-
}),
|
|
3695
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
3696
|
-
return executeRead({
|
|
3697
|
-
toolCallId,
|
|
3698
|
-
params,
|
|
3699
|
-
signal,
|
|
3700
|
-
onUpdate,
|
|
3701
|
-
cwd: ctx.cwd,
|
|
3702
|
-
onSuccessfulRead: options.onSuccessfulRead,
|
|
3703
|
-
modelSupportsImages: ctx.model?.input.includes("image") ?? false
|
|
3704
|
-
});
|
|
3705
|
-
},
|
|
3706
|
-
renderCall(args, theme, ...rest) {
|
|
3707
|
-
const context = rest[0] ?? {};
|
|
3708
|
-
const cwd = context.cwd ?? process.cwd();
|
|
3709
|
-
const { path: filePath, suffix } = formatReadCallText(args);
|
|
3710
|
-
const rangeSuffix = typeof args?.offset === "number" && typeof args?.limit === "number" && args.offset > 0 && args.limit > 0 ? `:${args.offset}-${args.offset + args.limit - 1}` : "";
|
|
3711
|
-
let text = renderToolLabel(theme, "read");
|
|
3712
|
-
if (filePath) {
|
|
3713
|
-
text += ` ${linkToolPath(theme.fg("accent", `${filePath}${rangeSuffix}`), filePath, cwd)}`;
|
|
3714
|
-
} else {
|
|
3715
|
-
text += ` ${theme.fg("toolOutput", "...")}`;
|
|
3716
|
-
}
|
|
3717
|
-
if (!rangeSuffix && suffix)
|
|
3718
|
-
text += ` ${theme.fg("dim", suffix)}`;
|
|
3719
|
-
return new Text2(clampLineToWidth(text, context.width), 0, 0);
|
|
3720
|
-
},
|
|
3721
|
-
renderResult(result, options2, theme, ...rest) {
|
|
3722
|
-
const { isPartial, isError, expanded, width, context } = resolveRenderResultContext(options2, rest, resolveReadSeekToolDisplayMode("read") === "expanded");
|
|
3723
|
-
if (isPartial)
|
|
3724
|
-
return renderPendingResult("pending read", width, theme);
|
|
3725
|
-
const content = result.content?.[0];
|
|
3726
|
-
const textContent = content?.type === "text" ? content.text : "";
|
|
3727
|
-
if (isError || result.isError) {
|
|
3728
|
-
const firstLine = textContent.split(`
|
|
3729
|
-
`)[0] || "Error";
|
|
3730
|
-
const errorText = expanded ? textContent || firstLine : firstLine;
|
|
3731
|
-
return new Text2(clampLinesToWidth([summaryLine(errorText, { theme, style: "error" })], width).join(`
|
|
3732
|
-
`), 0, 0);
|
|
3733
|
-
}
|
|
3734
|
-
const readSeekValue = result.details?.readSeekValue;
|
|
3735
|
-
if (!readSeekValue) {
|
|
3736
|
-
const lines = textContent.split(`
|
|
3737
|
-
`).filter(Boolean).length;
|
|
3738
|
-
let text2 = summaryLine(`loaded ${lines} ${lines === 1 ? "line" : "lines"}`, {
|
|
3739
|
-
hidden: !!textContent && !expanded,
|
|
3740
|
-
theme,
|
|
3741
|
-
style: "success"
|
|
3742
|
-
});
|
|
3743
|
-
if (expanded && textContent) {
|
|
3744
|
-
text2 += `
|
|
3745
|
-
${textContent}`;
|
|
3746
|
-
}
|
|
3747
|
-
return new Text2(clampLinesToWidth(text2.split(`
|
|
3748
|
-
`), width).join(`
|
|
3749
|
-
`), 0, 0);
|
|
3750
|
-
}
|
|
3751
|
-
const info = formatReadResultText({ range: readSeekValue.range, truncation: readSeekValue.truncation, symbol: readSeekValue.symbol, map: readSeekValue.map, warnings: readSeekValue.warnings });
|
|
3752
|
-
const visibleLines = info.truncated && readSeekValue.truncation ? readSeekValue.truncation.outputLines : readSeekValue.range.endLine - readSeekValue.range.startLine + 1;
|
|
3753
|
-
const loadedWord = visibleLines === 1 ? "line" : "lines";
|
|
3754
|
-
const summaryParts = [info.truncated ? `loaded ${visibleLines} of ${readSeekValue.truncation?.totalLines ?? readSeekValue.range.totalLines} ${loadedWord} (truncated)` : `loaded ${visibleLines} ${loadedWord}`];
|
|
3755
|
-
if (info.symbolBadge)
|
|
3756
|
-
summaryParts.push(info.symbolBadge);
|
|
3757
|
-
for (const badge of info.badges)
|
|
3758
|
-
summaryParts.push(badge);
|
|
3759
|
-
const summary = summaryParts.join(" • ");
|
|
3760
|
-
let text = summaryLine(summary, { hidden: !!textContent && !expanded, theme, style: "success" });
|
|
3761
|
-
if (expanded && textContent) {
|
|
3762
|
-
const anchors = new Set(readSeekValue.lines?.map((line) => line.anchor) ?? []);
|
|
3763
|
-
for (const item of readSeekValue.bundle?.localSupport ?? []) {
|
|
3764
|
-
for (const anchor of item.lineAnchors)
|
|
3765
|
-
anchors.add(anchor);
|
|
3766
|
-
}
|
|
3767
|
-
const sourcePath = readSeekValue.path ?? context.args?.path ?? "";
|
|
3768
|
-
text += `
|
|
3769
|
-
` + renderReadSourceForDisplayCached(content, textContent, sourcePath, anchors, width, theme);
|
|
3770
|
-
}
|
|
3771
|
-
return new Text2(clampLinesToWidth(text.split(`
|
|
3772
|
-
`), width).join(`
|
|
3773
|
-
`), 0, 0);
|
|
3774
|
-
}
|
|
3775
|
-
});
|
|
3776
|
-
return tool;
|
|
3777
|
-
}
|
|
3778
|
-
|
|
3779
|
-
// src/edit.ts
|
|
3780
|
-
import { readFile as fsReadFile2 } from "node:fs/promises";
|
|
3781
|
-
import { createPatch } from "diff";
|
|
3782
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
3783
|
-
import { Type as Type3 } from "@sinclair/typebox";
|
|
3784
|
-
import { Text as Text4 } from "@earendil-works/pi-tui";
|
|
3785
|
-
|
|
3786
|
-
// src/edit-render-helpers.ts
|
|
3787
|
-
var VARIANT_KEYS = ["set_line", "replace_lines", "insert_after", "replace"];
|
|
3788
|
-
function countEditTypes(edits) {
|
|
3789
|
-
const counts = {
|
|
3790
|
-
set_line: 0,
|
|
3791
|
-
replace_lines: 0,
|
|
3792
|
-
insert_after: 0,
|
|
3793
|
-
replace: 0,
|
|
3794
|
-
total: 0
|
|
3795
|
-
};
|
|
3796
|
-
if (!edits)
|
|
3797
|
-
return counts;
|
|
3798
|
-
for (const edit of edits) {
|
|
3799
|
-
counts.total++;
|
|
3800
|
-
if (edit && typeof edit === "object") {
|
|
3801
|
-
for (const key of VARIANT_KEYS) {
|
|
3802
|
-
if (key in edit) {
|
|
3803
|
-
counts[key]++;
|
|
3804
|
-
break;
|
|
3805
|
-
}
|
|
3806
|
-
}
|
|
3807
|
-
}
|
|
3808
|
-
}
|
|
3809
|
-
return counts;
|
|
3810
|
-
}
|
|
3811
|
-
function parseDiffStats(diff) {
|
|
3812
|
-
if (!diff)
|
|
3813
|
-
return { added: 0, removed: 0 };
|
|
3814
|
-
let added = 0;
|
|
3815
|
-
let removed = 0;
|
|
3816
|
-
for (const line of diff.split(`
|
|
3817
|
-
`)) {
|
|
3818
|
-
if (line.startsWith("+++") || line.startsWith("---"))
|
|
3819
|
-
continue;
|
|
3820
|
-
if (line.startsWith("+"))
|
|
3821
|
-
added++;
|
|
3822
|
-
else if (line.startsWith("-"))
|
|
3823
|
-
removed++;
|
|
3824
|
-
}
|
|
3825
|
-
return { added, removed };
|
|
3826
|
-
}
|
|
3827
|
-
function formatEditCallText(args, argsComplete) {
|
|
3828
|
-
const rawPath = typeof args?.path === "string" ? args.path : null;
|
|
3829
|
-
if (!argsComplete) {
|
|
3830
|
-
return { path: rawPath, suffix: undefined };
|
|
3831
|
-
}
|
|
3832
|
-
const edits = args?.edits;
|
|
3833
|
-
if (!Array.isArray(edits) || edits.length === 0) {
|
|
3834
|
-
return { path: rawPath, suffix: undefined };
|
|
3835
|
-
}
|
|
3836
|
-
const counts = countEditTypes(edits);
|
|
3837
|
-
const parts = [];
|
|
3838
|
-
for (const key of VARIANT_KEYS) {
|
|
3839
|
-
if (counts[key] > 0) {
|
|
3840
|
-
parts.push(`${counts[key]} ${key}`);
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
const word = counts.total === 1 ? "edit" : "edits";
|
|
3844
|
-
const suffix = `${counts.total} ${word} (${parts.join(", ")})`;
|
|
3845
|
-
return { path: rawPath, suffix };
|
|
3846
|
-
}
|
|
3847
|
-
function formatEditResultText(input) {
|
|
3848
|
-
const { isError, diff, warnings, noopEdits, errorText } = input;
|
|
3849
|
-
const isNoOp = isError && (Array.isArray(noopEdits) && noopEdits.length > 0 || errorText.includes("No changes made"));
|
|
3850
|
-
const stats = parseDiffStats(diff);
|
|
3851
|
-
const hasDiffStats = stats.added > 0 || stats.removed > 0;
|
|
3852
|
-
const diffStats = hasDiffStats ? `+${stats.added} / -${stats.removed}` : undefined;
|
|
3853
|
-
let warningsBadge;
|
|
3854
|
-
if (warnings.length === 1) {
|
|
3855
|
-
warningsBadge = "⚠ 1 warning";
|
|
3856
|
-
} else if (warnings.length > 1) {
|
|
3857
|
-
warningsBadge = `⚠ ${warnings.length} warnings`;
|
|
3858
|
-
}
|
|
3859
|
-
const showErrorText = isError ? errorText : undefined;
|
|
3860
|
-
let semanticBadge;
|
|
3861
|
-
if (!isError && !isNoOp && input.semanticClassification) {
|
|
3862
|
-
switch (input.semanticClassification) {
|
|
3863
|
-
case "whitespace-only":
|
|
3864
|
-
semanticBadge = "ws-only";
|
|
3865
|
-
break;
|
|
3866
|
-
case "semantic":
|
|
3867
|
-
semanticBadge = "✓ semantic";
|
|
3868
|
-
break;
|
|
3869
|
-
case "mixed":
|
|
3870
|
-
semanticBadge = "mixed";
|
|
3871
|
-
break;
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
|
-
return {
|
|
3875
|
-
diffStats,
|
|
3876
|
-
noOp: isNoOp,
|
|
3877
|
-
warningsBadge,
|
|
3878
|
-
errorText: showErrorText,
|
|
3879
|
-
semanticBadge
|
|
3880
|
-
};
|
|
3881
|
-
}
|
|
3882
|
-
|
|
3883
|
-
// src/edit-output.ts
|
|
3884
|
-
var EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"];
|
|
3885
|
-
function hasNewText(value) {
|
|
3886
|
-
return typeof value === "object" && value !== null && typeof value.new_text === "string";
|
|
3887
|
-
}
|
|
3888
|
-
function getVisibleDiffStats(diff) {
|
|
3889
|
-
const stats = parseDiffStats(diff);
|
|
3890
|
-
if (stats.added > 0 || stats.removed > 0)
|
|
3891
|
-
return stats;
|
|
3892
|
-
if (!diff.includes("→"))
|
|
3893
|
-
return stats;
|
|
3894
|
-
if (diff.includes("→ [deleted]"))
|
|
3895
|
-
return { added: 0, removed: 1 };
|
|
3896
|
-
return { added: 1, removed: 1 };
|
|
3897
|
-
}
|
|
3898
|
-
function buildVisibleSummary(displayPath, diff, edits) {
|
|
3899
|
-
let stats = getVisibleDiffStats(diff);
|
|
3900
|
-
const counts = countEditTypes(edits);
|
|
3901
|
-
const editCount = counts.total || 1;
|
|
3902
|
-
if (counts.total > 0 && counts.insert_after === counts.total && stats.removed > 0 && stats.added === stats.removed + counts.insert_after) {
|
|
3903
|
-
stats = { added: counts.insert_after, removed: 0 };
|
|
3904
|
-
}
|
|
3905
|
-
const changeWord = editCount === 1 ? "change" : "changes";
|
|
3906
|
-
const changedLineCount = Math.max(stats.added, stats.removed);
|
|
3907
|
-
const lineWord = changedLineCount === 1 ? "line" : "lines";
|
|
3908
|
-
return `Edited ${displayPath} (${editCount} ${changeWord}, +${stats.added} -${stats.removed} ${lineWord})`;
|
|
3909
|
-
}
|
|
3910
|
-
function extractNewTextValues(edits) {
|
|
3911
|
-
const values = [];
|
|
3912
|
-
for (const edit of edits ?? []) {
|
|
3913
|
-
if (!edit || typeof edit !== "object")
|
|
3914
|
-
continue;
|
|
3915
|
-
const operations = edit;
|
|
3916
|
-
for (const operationName of EDIT_OPERATION_NAMES) {
|
|
3917
|
-
const operation = operations[operationName];
|
|
3918
|
-
if (hasNewText(operation))
|
|
3919
|
-
values.push(operation.new_text);
|
|
3920
|
-
}
|
|
3921
|
-
}
|
|
3922
|
-
return values;
|
|
3923
|
-
}
|
|
3924
|
-
function formatWhitespaceOnlyWarning(semanticSummary, edits) {
|
|
3925
|
-
if (semanticSummary?.classification !== "whitespace-only")
|
|
3926
|
-
return;
|
|
3927
|
-
if (!extractNewTextValues(edits).some((text) => /\S/.test(text)))
|
|
3928
|
-
return;
|
|
3929
|
-
return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
|
|
3930
|
-
}
|
|
3931
|
-
function formatReplaceHint(edits, noopEdits) {
|
|
3932
|
-
if ((noopEdits ?? []).length > 0)
|
|
3933
|
-
return;
|
|
3934
|
-
const counts = countEditTypes(edits);
|
|
3935
|
-
if (counts.replace === 0)
|
|
3936
|
-
return;
|
|
3937
|
-
if (counts.replace !== counts.total)
|
|
3938
|
-
return;
|
|
3939
|
-
return "[info: this edit used replace (unverified). For safer future edits, prefer set_line/replace_lines with an anchor from readSeek_read/readSeek_grep/readSeek_search.]";
|
|
3940
|
-
}
|
|
3941
|
-
function buildEditOutput(input) {
|
|
3942
|
-
const summary = `Updated ${input.displayPath}`;
|
|
3943
|
-
const visibleSummary = buildVisibleSummary(input.displayPath, input.diff, input.edits);
|
|
3944
|
-
const semanticWarning = formatWhitespaceOnlyWarning(input.semanticSummary, input.edits);
|
|
3945
|
-
const warningText = input.warnings.length ? `
|
|
3946
|
-
|
|
3947
|
-
Warnings:
|
|
3948
|
-
${input.warnings.join(`
|
|
3949
|
-
`)}` : "";
|
|
3950
|
-
const replaceHint = formatReplaceHint(input.edits, input.noopEdits);
|
|
3951
|
-
let text = visibleSummary;
|
|
3952
|
-
if (semanticWarning)
|
|
3953
|
-
text += `
|
|
3954
|
-
${semanticWarning}`;
|
|
3955
|
-
text += warningText;
|
|
3956
|
-
if (replaceHint)
|
|
3957
|
-
text += `
|
|
3958
|
-
${replaceHint}`;
|
|
3959
|
-
return {
|
|
3960
|
-
text,
|
|
3961
|
-
patch: input.patch ?? "",
|
|
3962
|
-
readSeekValue: buildReadSeekEditResult({
|
|
3963
|
-
path: input.path,
|
|
3964
|
-
summary,
|
|
3965
|
-
diff: input.diff,
|
|
3966
|
-
...input.diffData ? { diffData: input.diffData } : {},
|
|
3967
|
-
firstChangedLine: input.firstChangedLine,
|
|
3968
|
-
warnings: input.warnings,
|
|
3969
|
-
noopEdits: input.noopEdits,
|
|
3970
|
-
...input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}
|
|
3971
|
-
})
|
|
3972
|
-
};
|
|
3973
|
-
}
|
|
3974
|
-
|
|
3975
|
-
// src/edit-classify.ts
|
|
3976
|
-
import * as Diff2 from "diff";
|
|
3977
|
-
function classifyEdit(oldContent, newContent) {
|
|
3978
|
-
if (oldContent === newContent) {
|
|
3979
|
-
return { classification: "no-op" };
|
|
3980
|
-
}
|
|
3981
|
-
const oldLines = oldContent.split(`
|
|
3982
|
-
`);
|
|
3983
|
-
const newLines = newContent.split(`
|
|
3984
|
-
`);
|
|
3985
|
-
if (oldLines.length === newLines.length) {
|
|
3986
|
-
let hasWhitespaceChange2 = false;
|
|
3987
|
-
let hasSemanticChange2 = false;
|
|
3988
|
-
for (let i = 0;i < oldLines.length; i++) {
|
|
3989
|
-
if (oldLines[i] === newLines[i])
|
|
3990
|
-
continue;
|
|
3991
|
-
if (oldLines[i].trim() === newLines[i].trim()) {
|
|
3992
|
-
hasWhitespaceChange2 = true;
|
|
3993
|
-
} else {
|
|
3994
|
-
hasSemanticChange2 = true;
|
|
3995
|
-
}
|
|
3996
|
-
}
|
|
3997
|
-
if (hasSemanticChange2 && hasWhitespaceChange2)
|
|
3998
|
-
return { classification: "mixed" };
|
|
3999
|
-
if (hasWhitespaceChange2)
|
|
4000
|
-
return { classification: "whitespace-only" };
|
|
4001
|
-
return { classification: "semantic" };
|
|
4002
|
-
}
|
|
4003
|
-
const parts = Diff2.diffLines(oldContent, newContent);
|
|
4004
|
-
let hasWhitespaceChange = false;
|
|
4005
|
-
let hasSemanticChange = false;
|
|
4006
|
-
for (const part of parts) {
|
|
4007
|
-
if (!part.added && !part.removed)
|
|
4008
|
-
continue;
|
|
4009
|
-
const lines = part.value.split(`
|
|
4010
|
-
`);
|
|
4011
|
-
if (lines[lines.length - 1] === "")
|
|
4012
|
-
lines.pop();
|
|
4013
|
-
for (const line of lines) {
|
|
4014
|
-
if (line.trim() === "") {
|
|
4015
|
-
hasWhitespaceChange = true;
|
|
4016
|
-
} else {
|
|
4017
|
-
hasSemanticChange = true;
|
|
4018
|
-
}
|
|
4019
|
-
}
|
|
4020
|
-
}
|
|
4021
|
-
if (hasSemanticChange && hasWhitespaceChange)
|
|
4022
|
-
return { classification: "mixed" };
|
|
4023
|
-
if (hasWhitespaceChange)
|
|
4024
|
-
return { classification: "whitespace-only" };
|
|
4025
|
-
return { classification: "semantic" };
|
|
4026
|
-
}
|
|
4027
|
-
|
|
4028
|
-
// src/edit-syntax-validate.ts
|
|
4029
|
-
function dedupeSortLines(diagnostics) {
|
|
4030
|
-
const seen = new Set;
|
|
4031
|
-
const out = [];
|
|
4032
|
-
for (const diagnostic of diagnostics) {
|
|
4033
|
-
const key = diagnostic.start_line === diagnostic.end_line ? String(diagnostic.start_line) : `${diagnostic.start_line}-${diagnostic.end_line}`;
|
|
4034
|
-
if (!seen.has(key)) {
|
|
4035
|
-
seen.add(key);
|
|
4036
|
-
out.push({ key, start: diagnostic.start_line });
|
|
4037
|
-
}
|
|
4038
|
-
}
|
|
4039
|
-
out.sort((a, b) => a.start - b.start);
|
|
4040
|
-
return out.map((o) => o.key);
|
|
4041
|
-
}
|
|
4042
|
-
var EMPTY = { errorCount: 0, missingCount: 0, diagnostics: [] };
|
|
4043
|
-
async function validateSyntaxRegression(input, options = {}) {
|
|
4044
|
-
let before;
|
|
4045
|
-
let after;
|
|
4046
|
-
try {
|
|
4047
|
-
before = input.before === undefined ? EMPTY : await readSeekCheck(input.filePath, input.before, { signal: options.signal });
|
|
4048
|
-
after = await readSeekCheck(input.filePath, input.after, { signal: options.signal });
|
|
4049
|
-
} catch {
|
|
4050
|
-
return null;
|
|
4051
|
-
}
|
|
4052
|
-
const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
|
|
4053
|
-
const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
|
|
4054
|
-
if (newErrorCount === 0 && newMissingCount === 0)
|
|
4055
|
-
return null;
|
|
4056
|
-
return {
|
|
4057
|
-
errorLines: dedupeSortLines(after.diagnostics),
|
|
4058
|
-
newErrorCount,
|
|
4059
|
-
newMissingCount
|
|
4060
|
-
};
|
|
4061
|
-
}
|
|
4062
|
-
|
|
4063
|
-
// src/syntax-validate-mode.ts
|
|
4064
|
-
var DEFAULT = "warn";
|
|
4065
|
-
function resolveSyntaxValidateMode(opts) {
|
|
4066
|
-
return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
|
|
4067
|
-
}
|
|
4068
|
-
|
|
4069
|
-
// src/diff-data.ts
|
|
4070
|
-
var MAX_INLINE_DIFF_LINE_LENGTH = 4096;
|
|
4071
|
-
var MAX_INLINE_DIFF_TOKENS = 512;
|
|
4072
|
-
var MAX_INLINE_DIFF_CELLS = 200000;
|
|
4073
|
-
var MAX_INLINE_DIFF_PAIRS = 200;
|
|
4074
|
-
var INLINE_SIMILARITY_THRESHOLD = 0.35;
|
|
4075
|
-
var INLINE_TOKEN_PATTERN = /([A-Za-z_$][\w$]*|\d+|\s+|[^A-Za-z_$\w\s]+)/gu;
|
|
4076
|
-
var LANGUAGE_BY_EXTENSION = new Map([
|
|
4077
|
-
[".ts", "typescript"],
|
|
4078
|
-
[".tsx", "typescript"],
|
|
4079
|
-
[".js", "javascript"],
|
|
4080
|
-
[".jsx", "javascript"],
|
|
4081
|
-
[".py", "python"],
|
|
4082
|
-
[".rs", "rust"],
|
|
4083
|
-
[".java", "java"]
|
|
4084
|
-
]);
|
|
4085
|
-
function inferLanguage(path3) {
|
|
4086
|
-
const extensionMatch = path3.match(/\.[^.\/\\]+$/);
|
|
4087
|
-
if (!extensionMatch)
|
|
4088
|
-
return;
|
|
4089
|
-
return LANGUAGE_BY_EXTENSION.get(extensionMatch[0].toLowerCase());
|
|
4090
|
-
}
|
|
4091
|
-
function parseFullDiffEntries(diff) {
|
|
4092
|
-
const entries = [];
|
|
4093
|
-
let nextOldLine = 1;
|
|
4094
|
-
let nextNewLine = 1;
|
|
4095
|
-
for (const line of diff ? diff.split(`
|
|
4096
|
-
`) : []) {
|
|
4097
|
-
const removeMatch = line.match(/^-\s*(\d+) (.*)$/);
|
|
4098
|
-
if (removeMatch) {
|
|
4099
|
-
const oldLine = Number(removeMatch[1]);
|
|
4100
|
-
entries.push({ kind: "remove", oldLine, text: removeMatch[2] ?? "" });
|
|
4101
|
-
nextOldLine = oldLine + 1;
|
|
4102
|
-
continue;
|
|
4103
|
-
}
|
|
4104
|
-
const addMatch = line.match(/^\+\s*(\d+) (.*)$/);
|
|
4105
|
-
if (addMatch) {
|
|
4106
|
-
const newLine = Number(addMatch[1]);
|
|
4107
|
-
entries.push({ kind: "add", newLine, text: addMatch[2] ?? "" });
|
|
4108
|
-
nextNewLine = newLine + 1;
|
|
4109
|
-
continue;
|
|
4110
|
-
}
|
|
4111
|
-
const contextMatch = line.match(/^ \s*(\d+) (.*)$/);
|
|
4112
|
-
if (contextMatch) {
|
|
4113
|
-
const oldLine = Number(contextMatch[1]);
|
|
4114
|
-
const lineDelta = nextNewLine - nextOldLine;
|
|
4115
|
-
const newLine = oldLine + lineDelta;
|
|
4116
|
-
entries.push({ kind: "context", oldLine, newLine, text: contextMatch[2] ?? "" });
|
|
4117
|
-
nextOldLine = oldLine + 1;
|
|
4118
|
-
nextNewLine = newLine + 1;
|
|
4119
|
-
continue;
|
|
4120
|
-
}
|
|
4121
|
-
entries.push({ kind: "meta", text: line });
|
|
4122
|
-
}
|
|
4123
|
-
return entries;
|
|
2468
|
+
return entries;
|
|
4124
2469
|
}
|
|
4125
2470
|
function parseCompactDiffEntries(diff, oldContent, newContent) {
|
|
4126
2471
|
const oldLines = oldContent.split(`
|
|
@@ -4251,66 +2596,204 @@ function buildInlineSpans(removeText, addText) {
|
|
|
4251
2596
|
return;
|
|
4252
2597
|
return { removeSpans, addSpans };
|
|
4253
2598
|
}
|
|
4254
|
-
function buildInlineDiffs(entries) {
|
|
4255
|
-
const inlineDiffs = [];
|
|
4256
|
-
let remainingPairs = MAX_INLINE_DIFF_PAIRS;
|
|
4257
|
-
for (let index = 0;index < entries.length; ) {
|
|
4258
|
-
if (entries[index]?.kind !== "remove") {
|
|
4259
|
-
index++;
|
|
4260
|
-
continue;
|
|
4261
|
-
}
|
|
4262
|
-
const removeStart = index;
|
|
4263
|
-
while (entries[index]?.kind === "remove")
|
|
4264
|
-
index++;
|
|
4265
|
-
const addStart = index;
|
|
4266
|
-
while (entries[index]?.kind === "add")
|
|
4267
|
-
index++;
|
|
4268
|
-
const removeCount = addStart - removeStart;
|
|
4269
|
-
const addCount = index - addStart;
|
|
4270
|
-
if (removeCount === 0 || addCount === 0 || removeCount !== addCount)
|
|
4271
|
-
continue;
|
|
4272
|
-
for (let offset = 0;offset < removeCount; offset++) {
|
|
4273
|
-
if (remainingPairs <= 0)
|
|
4274
|
-
break;
|
|
4275
|
-
remainingPairs--;
|
|
4276
|
-
const removeIndex = removeStart + offset;
|
|
4277
|
-
const addIndex = addStart + offset;
|
|
4278
|
-
const removeEntry = entries[removeIndex];
|
|
4279
|
-
const addEntry = entries[addIndex];
|
|
4280
|
-
if (removeEntry?.kind !== "remove" || addEntry?.kind !== "add")
|
|
4281
|
-
continue;
|
|
4282
|
-
const spans = buildInlineSpans(removeEntry.text, addEntry.text);
|
|
4283
|
-
if (!spans)
|
|
4284
|
-
continue;
|
|
4285
|
-
inlineDiffs.push({
|
|
4286
|
-
removeLineIndex: removeIndex,
|
|
4287
|
-
addLineIndex: addIndex,
|
|
4288
|
-
removeSpans: spans.removeSpans,
|
|
4289
|
-
addSpans: spans.addSpans
|
|
4290
|
-
});
|
|
2599
|
+
function buildInlineDiffs(entries) {
|
|
2600
|
+
const inlineDiffs = [];
|
|
2601
|
+
let remainingPairs = MAX_INLINE_DIFF_PAIRS;
|
|
2602
|
+
for (let index = 0;index < entries.length; ) {
|
|
2603
|
+
if (entries[index]?.kind !== "remove") {
|
|
2604
|
+
index++;
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const removeStart = index;
|
|
2608
|
+
while (entries[index]?.kind === "remove")
|
|
2609
|
+
index++;
|
|
2610
|
+
const addStart = index;
|
|
2611
|
+
while (entries[index]?.kind === "add")
|
|
2612
|
+
index++;
|
|
2613
|
+
const removeCount = addStart - removeStart;
|
|
2614
|
+
const addCount = index - addStart;
|
|
2615
|
+
if (removeCount === 0 || addCount === 0 || removeCount !== addCount)
|
|
2616
|
+
continue;
|
|
2617
|
+
for (let offset = 0;offset < removeCount; offset++) {
|
|
2618
|
+
if (remainingPairs <= 0)
|
|
2619
|
+
break;
|
|
2620
|
+
remainingPairs--;
|
|
2621
|
+
const removeIndex = removeStart + offset;
|
|
2622
|
+
const addIndex = addStart + offset;
|
|
2623
|
+
const removeEntry = entries[removeIndex];
|
|
2624
|
+
const addEntry = entries[addIndex];
|
|
2625
|
+
if (removeEntry?.kind !== "remove" || addEntry?.kind !== "add")
|
|
2626
|
+
continue;
|
|
2627
|
+
const spans = buildInlineSpans(removeEntry.text, addEntry.text);
|
|
2628
|
+
if (!spans)
|
|
2629
|
+
continue;
|
|
2630
|
+
inlineDiffs.push({
|
|
2631
|
+
removeLineIndex: removeIndex,
|
|
2632
|
+
addLineIndex: addIndex,
|
|
2633
|
+
removeSpans: spans.removeSpans,
|
|
2634
|
+
addSpans: spans.addSpans
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return inlineDiffs.length ? inlineDiffs : undefined;
|
|
2639
|
+
}
|
|
2640
|
+
function finalizeDiffData(path3, entries, blockRanges) {
|
|
2641
|
+
const language = inferLanguage(path3);
|
|
2642
|
+
const inlineDiffs = buildInlineDiffs(entries);
|
|
2643
|
+
return {
|
|
2644
|
+
version: 1,
|
|
2645
|
+
entries,
|
|
2646
|
+
stats: buildStats(entries),
|
|
2647
|
+
...language ? { language } : {},
|
|
2648
|
+
...blockRanges?.length ? { blockRanges: [...blockRanges] } : {},
|
|
2649
|
+
...inlineDiffs ? { inlineDiffs } : {}
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
function buildDiffData(input) {
|
|
2653
|
+
const entries = parseCompactDiffEntries(input.diff, input.oldContent, input.newContent) ?? parseFullDiffEntries(input.diff);
|
|
2654
|
+
return finalizeDiffData(input.path, entries, input.blockRanges);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/tui-render-utils.ts
|
|
2658
|
+
import { relative } from "node:path";
|
|
2659
|
+
import { pathToFileURL } from "node:url";
|
|
2660
|
+
import { getCapabilities, hyperlink, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2661
|
+
var SUMMARY_PREFIX = "↳";
|
|
2662
|
+
var EXPAND_HINT = " • Ctrl+O to expand";
|
|
2663
|
+
function renderToolLabel(theme, label) {
|
|
2664
|
+
const boldFn = typeof theme.bold === "function" ? theme.bold.bind(theme) : (text) => text;
|
|
2665
|
+
return theme.fg("toolTitle", boldFn(label));
|
|
2666
|
+
}
|
|
2667
|
+
function linkToolPath(styledText, rawPath, cwd) {
|
|
2668
|
+
try {
|
|
2669
|
+
if (!getCapabilities().hyperlinks)
|
|
2670
|
+
return styledText;
|
|
2671
|
+
const absolutePath = resolveToCwd(rawPath, cwd);
|
|
2672
|
+
return hyperlink(styledText, pathToFileURL(absolutePath).href);
|
|
2673
|
+
} catch {
|
|
2674
|
+
return styledText;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
function linkedPathLine(theme, rawPath, displayPath, cwd, suffix, width) {
|
|
2678
|
+
const pathWidth = width === undefined ? undefined : Math.max(1, width - 2 - visibleWidth(suffix));
|
|
2679
|
+
const visiblePath = pathWidth === undefined ? displayPath : truncateToWidth(displayPath, pathWidth);
|
|
2680
|
+
const linkedPath = linkToolPath(theme.fg("dim", visiblePath), rawPath, cwd);
|
|
2681
|
+
return ` ${linkedPath}${theme.fg("dim", suffix)}`;
|
|
2682
|
+
}
|
|
2683
|
+
function appendExpandHint(text, hidden) {
|
|
2684
|
+
return hidden ? `${text}${EXPAND_HINT}` : text;
|
|
2685
|
+
}
|
|
2686
|
+
function summaryLine(summary, options = {}) {
|
|
2687
|
+
const prefix = options.theme ? options.theme.fg(options.style ?? "dim", SUMMARY_PREFIX) : SUMMARY_PREFIX;
|
|
2688
|
+
return appendExpandHint(`${prefix} ${summary}`, !!options.hidden);
|
|
2689
|
+
}
|
|
2690
|
+
function isRendererExpanded(options, context, defaultExpanded = false) {
|
|
2691
|
+
return context?.expanded ?? options?.expanded ?? defaultExpanded;
|
|
2692
|
+
}
|
|
2693
|
+
function resolveRenderResultContext(options, rest, defaultExpanded = false) {
|
|
2694
|
+
const context = rest[0] ?? options ?? {};
|
|
2695
|
+
return {
|
|
2696
|
+
isPartial: context.isPartial ?? options?.isPartial ?? false,
|
|
2697
|
+
isError: context.isError ?? false,
|
|
2698
|
+
expanded: isRendererExpanded(options, context, defaultExpanded),
|
|
2699
|
+
width: context.width ?? options?.width,
|
|
2700
|
+
cwd: context.cwd ?? process.cwd(),
|
|
2701
|
+
context
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
function normalizeWidth(width, fallback = 80) {
|
|
2705
|
+
return typeof width === "number" && Number.isFinite(width) && width > 0 ? Math.floor(width) : fallback;
|
|
2706
|
+
}
|
|
2707
|
+
function clampLineToWidth(line, width) {
|
|
2708
|
+
if (width === undefined || width === null)
|
|
2709
|
+
return line;
|
|
2710
|
+
const normalized = normalizeWidth(width);
|
|
2711
|
+
return visibleWidth(line) <= normalized ? line : truncateToWidth(line, normalized);
|
|
2712
|
+
}
|
|
2713
|
+
function clampLinesToWidth(lines, width) {
|
|
2714
|
+
if (width === undefined || width === null)
|
|
2715
|
+
return lines;
|
|
2716
|
+
return lines.map((line) => clampLineToWidth(line, width));
|
|
2717
|
+
}
|
|
2718
|
+
function wrapWithHangingIndent(prefix, content, width, options = {}) {
|
|
2719
|
+
const tint = options.tint ?? ((text) => text);
|
|
2720
|
+
if (width === undefined || width === null)
|
|
2721
|
+
return [tint(prefix + content)];
|
|
2722
|
+
const normalized = normalizeWidth(width);
|
|
2723
|
+
const combined = prefix + content;
|
|
2724
|
+
if (visibleWidth(combined) <= normalized)
|
|
2725
|
+
return [tint(combined)];
|
|
2726
|
+
const prefixWidth = visibleWidth(prefix);
|
|
2727
|
+
const contentWidth = Math.max(1, normalized - prefixWidth);
|
|
2728
|
+
const wrapped = wrapTextWithAnsi(content, contentWidth);
|
|
2729
|
+
if (wrapped.length === 0)
|
|
2730
|
+
return [tint(clampLineToWidth(prefix, normalized))];
|
|
2731
|
+
const indent = " ".repeat(prefixWidth);
|
|
2732
|
+
return wrapped.map((line, index) => tint(clampLineToWidth(index === 0 ? prefix + line : indent + line, normalized)));
|
|
2733
|
+
}
|
|
2734
|
+
var wrappedHashlinesCache = new WeakMap;
|
|
2735
|
+
function renderPendingResult(pendingLabel, width, theme) {
|
|
2736
|
+
return new Text(clampLinesToWidth([summaryLine(pendingLabel, { theme, style: "muted" })], width).join(`
|
|
2737
|
+
`), 0, 0);
|
|
2738
|
+
}
|
|
2739
|
+
function renderErrorResult(textContent, options) {
|
|
2740
|
+
const firstLine = textContent.split(`
|
|
2741
|
+
`)[0] || (options.fallback ?? "Error");
|
|
2742
|
+
const body = options.expanded && textContent ? textContent : firstLine;
|
|
2743
|
+
return new Text(clampLinesToWidth(summaryLine(body, { theme: options.theme, style: "error" }).split(`
|
|
2744
|
+
`), options.width).join(`
|
|
2745
|
+
`), 0, 0);
|
|
2746
|
+
}
|
|
2747
|
+
function renderReadSeekSearchCall(args, theme, rest, opts) {
|
|
2748
|
+
const context = rest[0] ?? {};
|
|
2749
|
+
let text = `${renderToolLabel(theme, opts.label)} ${theme.fg("accent", opts.accent)}`;
|
|
2750
|
+
text += theme.fg("dim", ` in ${args.path ?? "."}`);
|
|
2751
|
+
const language = args.language ?? args.lang;
|
|
2752
|
+
if (language)
|
|
2753
|
+
text += theme.fg("dim", ` (${language})`);
|
|
2754
|
+
const flags = opts.flags.filter(Boolean);
|
|
2755
|
+
if (flags.length > 0)
|
|
2756
|
+
text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
2757
|
+
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
2758
|
+
}
|
|
2759
|
+
function renderAnchoredFilesResult(result, options, theme, rest, labels) {
|
|
2760
|
+
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
2761
|
+
if (isPartial)
|
|
2762
|
+
return renderPendingResult(labels.pendingLabel, width, theme);
|
|
2763
|
+
const content = result.content?.[0];
|
|
2764
|
+
const textContent = content?.type === "text" ? content.text : "";
|
|
2765
|
+
if (isError || result.isError)
|
|
2766
|
+
return renderErrorResult(textContent, { expanded, width, theme });
|
|
2767
|
+
const readSeekValue = result.details?.readSeekValue;
|
|
2768
|
+
const files = readSeekValue?.files ?? [];
|
|
2769
|
+
if (files.length === 0)
|
|
2770
|
+
return new Text(summaryLine(labels.emptyLabel, { theme, style: "dim" }), 0, 0);
|
|
2771
|
+
const fileCount = files.length;
|
|
2772
|
+
const total = files.reduce((sum, f) => sum + f.lines.length, 0);
|
|
2773
|
+
const unitWord = total === 1 ? labels.unitSingular : labels.unitPlural;
|
|
2774
|
+
const fileWord = fileCount === 1 ? "file" : "files";
|
|
2775
|
+
let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, {
|
|
2776
|
+
hidden: !expanded,
|
|
2777
|
+
theme,
|
|
2778
|
+
style: "success"
|
|
2779
|
+
});
|
|
2780
|
+
if (expanded) {
|
|
2781
|
+
for (const file of files.slice(0, 20)) {
|
|
2782
|
+
const display = relative(cwd, file.path) || file.path;
|
|
2783
|
+
text += `
|
|
2784
|
+
${linkedPathLine(theme, file.path, display, cwd, ` (${file.lines.length})`, width)}`;
|
|
4291
2785
|
}
|
|
2786
|
+
if (files.length > 20)
|
|
2787
|
+
text += `
|
|
2788
|
+
` + theme.fg("muted", ` … and ${files.length - 20} more files`);
|
|
4292
2789
|
}
|
|
4293
|
-
return
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
const language = inferLanguage(path3);
|
|
4297
|
-
const inlineDiffs = buildInlineDiffs(entries);
|
|
4298
|
-
return {
|
|
4299
|
-
version: 1,
|
|
4300
|
-
entries,
|
|
4301
|
-
stats: buildStats(entries),
|
|
4302
|
-
...language ? { language } : {},
|
|
4303
|
-
...blockRanges?.length ? { blockRanges: [...blockRanges] } : {},
|
|
4304
|
-
...inlineDiffs ? { inlineDiffs } : {}
|
|
4305
|
-
};
|
|
4306
|
-
}
|
|
4307
|
-
function buildDiffData(input) {
|
|
4308
|
-
const entries = parseCompactDiffEntries(input.diff, input.oldContent, input.newContent) ?? parseFullDiffEntries(input.diff);
|
|
4309
|
-
return finalizeDiffData(input.path, entries, input.blockRanges);
|
|
2790
|
+
return new Text(clampLinesToWidth(text.split(`
|
|
2791
|
+
`), width).join(`
|
|
2792
|
+
`), 0, 0);
|
|
4310
2793
|
}
|
|
4311
2794
|
|
|
4312
2795
|
// src/tui-diff-component.ts
|
|
4313
|
-
import { Text as
|
|
2796
|
+
import { Text as Text2, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
|
|
4314
2797
|
|
|
4315
2798
|
// src/tui-diff-renderer.ts
|
|
4316
2799
|
import { visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
@@ -4478,34 +2961,47 @@ function upsertDiffComponent(lastComponent, options) {
|
|
|
4478
2961
|
return component;
|
|
4479
2962
|
}
|
|
4480
2963
|
function upsertTextComponent(lastComponent, text) {
|
|
4481
|
-
const component = lastComponent && !(lastComponent instanceof DiffPreviewComponent) ? lastComponent : new
|
|
2964
|
+
const component = lastComponent && !(lastComponent instanceof DiffPreviewComponent) ? lastComponent : new Text2("", 0, 0);
|
|
4482
2965
|
component.setText(text);
|
|
4483
2966
|
return component;
|
|
4484
2967
|
}
|
|
4485
2968
|
|
|
2969
|
+
// src/register-tool.ts
|
|
2970
|
+
import { Type } from "@sinclair/typebox";
|
|
2971
|
+
function optionalIntOrString(description) {
|
|
2972
|
+
return Type.Optional(Type.Union([Type.Number({ description }), Type.String({ description })]));
|
|
2973
|
+
}
|
|
2974
|
+
function filePathParam() {
|
|
2975
|
+
return Type.String({ description: "Absolute or relative file path" });
|
|
2976
|
+
}
|
|
2977
|
+
function registerReadSeekTool(pi, tool) {
|
|
2978
|
+
pi.registerTool(tool);
|
|
2979
|
+
return tool;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
4486
2982
|
// src/edit.ts
|
|
4487
|
-
var hashlineEditItemSchema =
|
|
4488
|
-
set_line:
|
|
4489
|
-
anchor:
|
|
4490
|
-
new_text:
|
|
2983
|
+
var hashlineEditItemSchema = Type2.Object({
|
|
2984
|
+
set_line: Type2.Optional(Type2.Object({
|
|
2985
|
+
anchor: Type2.String({ description: "Fresh LINE:HASH anchor for the line to replace" }),
|
|
2986
|
+
new_text: Type2.String({ description: "Replacement text; use an empty string to delete the line" })
|
|
4491
2987
|
}, { additionalProperties: false, description: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4492
|
-
replace_lines:
|
|
4493
|
-
start_anchor:
|
|
4494
|
-
end_anchor:
|
|
4495
|
-
new_text:
|
|
2988
|
+
replace_lines: Type2.Optional(Type2.Object({
|
|
2989
|
+
start_anchor: Type2.String({ description: "Fresh LINE:HASH anchor for the first line in the range" }),
|
|
2990
|
+
end_anchor: Type2.String({ description: "Fresh LINE:HASH anchor for the last line in the range" }),
|
|
2991
|
+
new_text: Type2.String({ description: "Replacement text; use an empty string to delete the range" })
|
|
4496
2992
|
}, { additionalProperties: false, description: 'Replace a range: {"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4497
|
-
insert_after:
|
|
4498
|
-
anchor:
|
|
4499
|
-
new_text:
|
|
2993
|
+
insert_after: Type2.Optional(Type2.Object({
|
|
2994
|
+
anchor: Type2.String({ description: "Fresh LINE:HASH anchor for the line after which to insert" }),
|
|
2995
|
+
new_text: Type2.String({ description: "Text to insert after the anchored line" })
|
|
4500
2996
|
}, { additionalProperties: false, description: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4501
|
-
replace:
|
|
4502
|
-
old_text:
|
|
4503
|
-
new_text:
|
|
4504
|
-
all:
|
|
2997
|
+
replace: Type2.Optional(Type2.Object({
|
|
2998
|
+
old_text: Type2.String({ description: "Exact text to find" }),
|
|
2999
|
+
new_text: Type2.String({ description: "Replacement text" }),
|
|
3000
|
+
all: Type2.Optional(Type2.Boolean({ description: "Replace every exact match" }))
|
|
4505
3001
|
}, { additionalProperties: false, description: 'Replace text: {"replace":{"old_text":"...","new_text":"..."}}' })),
|
|
4506
|
-
replace_symbol:
|
|
4507
|
-
symbol:
|
|
4508
|
-
new_body:
|
|
3002
|
+
replace_symbol: Type2.Optional(Type2.Object({
|
|
3003
|
+
symbol: Type2.String({ description: "Mapped symbol name" }),
|
|
3004
|
+
new_body: Type2.String({ description: "Complete replacement body for the symbol" })
|
|
4509
3005
|
}, { additionalProperties: false, description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
|
|
4510
3006
|
}, {
|
|
4511
3007
|
additionalProperties: false,
|
|
@@ -4513,14 +3009,14 @@ var hashlineEditItemSchema = Type3.Object({
|
|
|
4513
3009
|
maxProperties: 1,
|
|
4514
3010
|
description: "Exactly one nested edit variant"
|
|
4515
3011
|
});
|
|
4516
|
-
var hashlineEditSchema =
|
|
3012
|
+
var hashlineEditSchema = Type2.Object({
|
|
4517
3013
|
path: filePathParam(),
|
|
4518
|
-
language:
|
|
4519
|
-
edits:
|
|
3014
|
+
language: Type2.Optional(Type2.String({ description: "Language override for symbol replacement" })),
|
|
3015
|
+
edits: Type2.Array(hashlineEditItemSchema, {
|
|
4520
3016
|
minItems: 1,
|
|
4521
3017
|
description: "Use set_line, replace_lines, insert_after, replace_symbol, or replace"
|
|
4522
3018
|
}),
|
|
4523
|
-
postEditVerify:
|
|
3019
|
+
postEditVerify: Type2.Optional(Type2.Boolean({
|
|
4524
3020
|
description: "Read back and verify persisted content"
|
|
4525
3021
|
}))
|
|
4526
3022
|
}, { additionalProperties: false });
|
|
@@ -4556,7 +3052,7 @@ async function executeEdit(opts) {
|
|
|
4556
3052
|
return await withFileMutationQueue(absolutePath, async () => {
|
|
4557
3053
|
throwIfAborted(signal);
|
|
4558
3054
|
if (wasReadInSession && !wasReadInSession(absolutePath)) {
|
|
4559
|
-
const hint = `Call
|
|
3055
|
+
const hint = `Call readSeek_digest(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`;
|
|
4560
3056
|
return buildEditError(absolutePath, "file-not-read", `You must get fresh anchors for ${absolutePath} before editing it. ${hint}`, hint);
|
|
4561
3057
|
}
|
|
4562
3058
|
const edits = parsed.edits ?? [];
|
|
@@ -4627,7 +3123,7 @@ async function executeEdit(opts) {
|
|
|
4627
3123
|
if (parsed.postEditVerify === true) {
|
|
4628
3124
|
let verifiedContent;
|
|
4629
3125
|
try {
|
|
4630
|
-
verifiedContent = await
|
|
3126
|
+
verifiedContent = await fsReadFile(absolutePath, "utf-8");
|
|
4631
3127
|
} catch (err) {
|
|
4632
3128
|
return buildEditError(absolutePath, "post-edit-verification-read-failed", `Edit write completed but post-edit verification failed: could not read ${path3} after writing.`, undefined, { fsCode: err?.code, fsMessage: err?.message });
|
|
4633
3129
|
}
|
|
@@ -4766,7 +3262,7 @@ ${theme.fg("error", info.errorText)}`;
|
|
|
4766
3262
|
`), diffData, theme, expanded: true });
|
|
4767
3263
|
}
|
|
4768
3264
|
}
|
|
4769
|
-
return new
|
|
3265
|
+
return new Text3(clampLinesToWidth(text.split(`
|
|
4770
3266
|
`), width).join(`
|
|
4771
3267
|
`), 0, 0);
|
|
4772
3268
|
}
|
|
@@ -4775,22 +3271,34 @@ ${theme.fg("error", info.errorText)}`;
|
|
|
4775
3271
|
}
|
|
4776
3272
|
|
|
4777
3273
|
// src/grep.ts
|
|
4778
|
-
import { readFile as
|
|
3274
|
+
import { readFile as fsReadFile2, stat as fsStat } from "node:fs/promises";
|
|
4779
3275
|
import path3 from "node:path";
|
|
4780
3276
|
import { createGrepTool } from "@earendil-works/pi-coding-agent";
|
|
4781
|
-
import { Type as
|
|
4782
|
-
import { Text as
|
|
3277
|
+
import { Type as Type4 } from "@sinclair/typebox";
|
|
3278
|
+
import { Text as Text4 } from "@earendil-works/pi-tui";
|
|
3279
|
+
|
|
3280
|
+
// src/binary-detect.ts
|
|
3281
|
+
function looksLikeBinary(buf) {
|
|
3282
|
+
if (buf.length === 0)
|
|
3283
|
+
return false;
|
|
3284
|
+
if (buf.includes(0))
|
|
3285
|
+
return true;
|
|
3286
|
+
const decoded = buf.toString("utf8");
|
|
3287
|
+
if (!decoded.includes("�"))
|
|
3288
|
+
return false;
|
|
3289
|
+
return !Buffer.from(decoded, "utf8").equals(buf);
|
|
3290
|
+
}
|
|
4783
3291
|
|
|
4784
3292
|
// src/grep-output.ts
|
|
4785
3293
|
import {
|
|
4786
|
-
formatSize
|
|
4787
|
-
truncateHead
|
|
3294
|
+
formatSize,
|
|
3295
|
+
truncateHead
|
|
4788
3296
|
} from "@earendil-works/pi-coding-agent";
|
|
4789
3297
|
|
|
4790
3298
|
// src/grep-budget.ts
|
|
4791
|
-
import { DEFAULT_MAX_BYTES
|
|
4792
|
-
var GREP_OUTPUT_DEFAULT_MAX_LINES =
|
|
4793
|
-
var GREP_OUTPUT_DEFAULT_MAX_BYTES = Math.min(
|
|
3299
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
3300
|
+
var GREP_OUTPUT_DEFAULT_MAX_LINES = DEFAULT_MAX_LINES;
|
|
3301
|
+
var GREP_OUTPUT_DEFAULT_MAX_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
|
|
4794
3302
|
function resolveDimension(jsonValue, ceiling) {
|
|
4795
3303
|
if (jsonValue !== undefined)
|
|
4796
3304
|
return Math.min(jsonValue, ceiling);
|
|
@@ -4876,14 +3384,14 @@ ${passthroughLines.join(`
|
|
|
4876
3384
|
${text}`;
|
|
4877
3385
|
}
|
|
4878
3386
|
const budget = resolveGrepOutputBudget();
|
|
4879
|
-
const truncated =
|
|
3387
|
+
const truncated = truncateHead(text, {
|
|
4880
3388
|
maxLines: budget.maxLines,
|
|
4881
3389
|
maxBytes: budget.maxBytes
|
|
4882
3390
|
});
|
|
4883
3391
|
if (truncated.truncated) {
|
|
4884
3392
|
text = `${truncated.content}
|
|
4885
3393
|
|
|
4886
|
-
[Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${
|
|
3394
|
+
[Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${formatSize(truncated.outputBytes)} of ${formatSize(truncated.totalBytes)}). Refine pattern or increase limit.]`;
|
|
4887
3395
|
}
|
|
4888
3396
|
const readSeekValue = {
|
|
4889
3397
|
tool: "grep",
|
|
@@ -4905,6 +3413,31 @@ ${text}`;
|
|
|
4905
3413
|
};
|
|
4906
3414
|
}
|
|
4907
3415
|
|
|
3416
|
+
// src/file-map.ts
|
|
3417
|
+
import { stat } from "node:fs/promises";
|
|
3418
|
+
async function getOrGenerateMap(absPath) {
|
|
3419
|
+
try {
|
|
3420
|
+
const { size } = await stat(absPath);
|
|
3421
|
+
return await readSeekMap(absPath, size);
|
|
3422
|
+
} catch {
|
|
3423
|
+
return null;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
// src/readseek/symbol-tree.ts
|
|
3428
|
+
function traverseSymbolTree(symbols, visit) {
|
|
3429
|
+
const result = [];
|
|
3430
|
+
const walk = (nodes, parentName) => {
|
|
3431
|
+
for (const symbol of nodes) {
|
|
3432
|
+
result.push(visit(symbol, parentName));
|
|
3433
|
+
if (symbol.children?.length)
|
|
3434
|
+
walk(symbol.children, symbol.name);
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
walk(symbols);
|
|
3438
|
+
return result;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
4908
3441
|
// src/grep-symbol-scope.ts
|
|
4909
3442
|
function findEnclosingSymbol(map, lineNumber) {
|
|
4910
3443
|
const candidates = traverseSymbolTree(map.symbols, (symbol, parentName) => ({
|
|
@@ -5112,19 +3645,73 @@ function formatGrepResultText(input) {
|
|
|
5112
3645
|
};
|
|
5113
3646
|
}
|
|
5114
3647
|
|
|
3648
|
+
// src/coerce-obvious-int.ts
|
|
3649
|
+
var BASE10_INT_RE = /^-?\d+$/;
|
|
3650
|
+
var MIN_SAFE_INTEGER = BigInt(Number.MIN_SAFE_INTEGER);
|
|
3651
|
+
var MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
|
3652
|
+
function formatReceived(value) {
|
|
3653
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
3654
|
+
return String(value);
|
|
3655
|
+
return JSON.stringify(value) ?? String(value);
|
|
3656
|
+
}
|
|
3657
|
+
function invalidInteger(value, name) {
|
|
3658
|
+
return {
|
|
3659
|
+
ok: false,
|
|
3660
|
+
message: `Invalid ${name}: expected a safe base-10 integer, received ${formatReceived(value)}.`
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
function coerceObviousBase10Int(value, name) {
|
|
3664
|
+
if (value === undefined || typeof value === "string" && value.trim() === "") {
|
|
3665
|
+
return { ok: true, value: undefined };
|
|
3666
|
+
}
|
|
3667
|
+
if (typeof value === "number") {
|
|
3668
|
+
if (Number.isSafeInteger(value)) {
|
|
3669
|
+
return { ok: true, value };
|
|
3670
|
+
}
|
|
3671
|
+
return invalidInteger(value, name);
|
|
3672
|
+
}
|
|
3673
|
+
if (typeof value === "string" && BASE10_INT_RE.test(value)) {
|
|
3674
|
+
const parsed = BigInt(value);
|
|
3675
|
+
if (parsed >= MIN_SAFE_INTEGER && parsed <= MAX_SAFE_INTEGER) {
|
|
3676
|
+
return { ok: true, value: Number(parsed) };
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
return invalidInteger(value, name);
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
// src/tui-source-render.ts
|
|
3683
|
+
import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
3684
|
+
import { wrapTextWithAnsi as wrapTextWithAnsi2 } from "@earendil-works/pi-tui";
|
|
3685
|
+
var GREP_ANCHOR_RE = /^(.*?):(>>| )(\d+):([0-9a-zA-Z]{1,16})\|(.*)$/;
|
|
3686
|
+
var readSourceCache = new WeakMap;
|
|
3687
|
+
function renderGrepSourceForDisplay(text, anchors, renderPath = (path3) => path3) {
|
|
3688
|
+
return text.split(`
|
|
3689
|
+
`).map((line) => {
|
|
3690
|
+
const match = line.match(GREP_ANCHOR_RE);
|
|
3691
|
+
if (!match)
|
|
3692
|
+
return line;
|
|
3693
|
+
const anchor = `${match[3]}:${match[4]}`;
|
|
3694
|
+
if (!anchors.has(anchor))
|
|
3695
|
+
return line;
|
|
3696
|
+
const separator = match[2] === ">>" ? ":" : "-";
|
|
3697
|
+
return `${renderPath(match[1])}${separator}${match[3]}${separator}${match[5] ?? ""}`;
|
|
3698
|
+
}).join(`
|
|
3699
|
+
`);
|
|
3700
|
+
}
|
|
3701
|
+
|
|
5115
3702
|
// src/readseek-params.ts
|
|
5116
|
-
import { Type as
|
|
3703
|
+
import { Type as Type3 } from "@sinclair/typebox";
|
|
5117
3704
|
function searchPathParam() {
|
|
5118
|
-
return
|
|
3705
|
+
return Type3.Optional(Type3.String({ description: PARAM_DESCRIPTIONS.searchPath }));
|
|
5119
3706
|
}
|
|
5120
3707
|
function languageParam() {
|
|
5121
|
-
return
|
|
3708
|
+
return Type3.Optional(Type3.String({ description: PARAM_DESCRIPTIONS.language }));
|
|
5122
3709
|
}
|
|
5123
3710
|
function readSeekGitSearchParams() {
|
|
5124
3711
|
return {
|
|
5125
|
-
cached:
|
|
5126
|
-
others:
|
|
5127
|
-
ignored:
|
|
3712
|
+
cached: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.cached })),
|
|
3713
|
+
others: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.others })),
|
|
3714
|
+
ignored: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.ignored }))
|
|
5128
3715
|
};
|
|
5129
3716
|
}
|
|
5130
3717
|
function validateIgnoredRequiresOthers(tool, params) {
|
|
@@ -5136,16 +3723,16 @@ function validateIgnoredRequiresOthers(tool, params) {
|
|
|
5136
3723
|
}
|
|
5137
3724
|
|
|
5138
3725
|
// src/grep.ts
|
|
5139
|
-
var grepSchema =
|
|
5140
|
-
pattern:
|
|
3726
|
+
var grepSchema = Type4.Object({
|
|
3727
|
+
pattern: Type4.String({ description: "Regex pattern; set literal for exact text" }),
|
|
5141
3728
|
path: searchPathParam(),
|
|
5142
|
-
glob:
|
|
5143
|
-
ignoreCase:
|
|
5144
|
-
literal:
|
|
3729
|
+
glob: Type4.Optional(Type4.String({ description: "File-name glob, such as *.ts" })),
|
|
3730
|
+
ignoreCase: Type4.Optional(Type4.Boolean({ description: "Ignore case" })),
|
|
3731
|
+
literal: Type4.Optional(Type4.Boolean({ description: "Treat pattern literally" })),
|
|
5145
3732
|
context: optionalIntOrString("Surrounding lines for each match"),
|
|
5146
3733
|
limit: optionalIntOrString("Maximum matches to return"),
|
|
5147
|
-
summary:
|
|
5148
|
-
scope:
|
|
3734
|
+
summary: Type4.Optional(Type4.Boolean({ description: "Return per-file counts" })),
|
|
3735
|
+
scope: Type4.Optional(Type4.Literal("symbol", {
|
|
5149
3736
|
description: "Group matches by enclosing symbol"
|
|
5150
3737
|
})),
|
|
5151
3738
|
scopeContext: optionalIntOrString("Context lines within each symbol")
|
|
@@ -5272,7 +3859,7 @@ async function executeGrep(opts) {
|
|
|
5272
3859
|
}
|
|
5273
3860
|
if (!searchPathIsDirectory) {
|
|
5274
3861
|
try {
|
|
5275
|
-
const buf = await
|
|
3862
|
+
const buf = await fsReadFile2(searchPath);
|
|
5276
3863
|
if (looksLikeBinary(buf)) {
|
|
5277
3864
|
const warning = `[Warning: '${p.path ?? searchPath}' appears to be a binary file — grep skips binary files by default. Use a hex tool or the read tool to inspect it.]`;
|
|
5278
3865
|
return {
|
|
@@ -5298,7 +3885,7 @@ async function executeGrep(opts) {
|
|
|
5298
3885
|
if (fileCache.has(absolutePath))
|
|
5299
3886
|
return fileCache.get(absolutePath);
|
|
5300
3887
|
try {
|
|
5301
|
-
const rawBuffer = await
|
|
3888
|
+
const rawBuffer = await fsReadFile2(absolutePath);
|
|
5302
3889
|
if (looksLikeBinary(rawBuffer)) {
|
|
5303
3890
|
fileCache.set(absolutePath, undefined);
|
|
5304
3891
|
return;
|
|
@@ -5545,7 +4132,7 @@ function registerGrepTool(pi, options = {}) {
|
|
|
5545
4132
|
text += theme.fg("dim", ` in ${suffix}`);
|
|
5546
4133
|
}
|
|
5547
4134
|
}
|
|
5548
|
-
return new
|
|
4135
|
+
return new Text4(clampLineToWidth(text, context.width), 0, 0);
|
|
5549
4136
|
},
|
|
5550
4137
|
renderResult(result, options2, theme, ...rest) {
|
|
5551
4138
|
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options2, rest, resolveReadSeekToolDisplayMode("grep") === "expanded");
|
|
@@ -5570,7 +4157,7 @@ function registerGrepTool(pi, options = {}) {
|
|
|
5570
4157
|
hasBinaryWarning
|
|
5571
4158
|
});
|
|
5572
4159
|
if (info.noMatches && !hasBinaryWarning)
|
|
5573
|
-
return new
|
|
4160
|
+
return new Text4(summaryLine("no matches", { theme, style: "dim" }), 0, 0);
|
|
5574
4161
|
const matchCount = readSeekValue?.totalMatches ?? 0;
|
|
5575
4162
|
const matchWord = matchCount === 1 ? "match" : "matches";
|
|
5576
4163
|
let text = summaryLine(`${matchCount} ${matchWord} returned`, {
|
|
@@ -5585,7 +4172,7 @@ function registerGrepTool(pi, options = {}) {
|
|
|
5585
4172
|
text += `
|
|
5586
4173
|
` + renderGrepSourceForDisplay(textContent, anchors, (displayPath) => linkToolPath(theme.fg("dim", displayPath), displayPath, cwd));
|
|
5587
4174
|
}
|
|
5588
|
-
return new
|
|
4175
|
+
return new Text4(clampLinesToWidth(text.split(`
|
|
5589
4176
|
`), width).join(`
|
|
5590
4177
|
`), 0, 0);
|
|
5591
4178
|
}
|
|
@@ -5595,7 +4182,7 @@ function registerGrepTool(pi, options = {}) {
|
|
|
5595
4182
|
|
|
5596
4183
|
// src/search.ts
|
|
5597
4184
|
import path4 from "node:path";
|
|
5598
|
-
import { Type as
|
|
4185
|
+
import { Type as Type5 } from "@sinclair/typebox";
|
|
5599
4186
|
|
|
5600
4187
|
// src/stat-search-path.ts
|
|
5601
4188
|
import { stat as fsStat2 } from "node:fs/promises";
|
|
@@ -5762,8 +4349,8 @@ function registerSearchTool(pi, options = {}) {
|
|
|
5762
4349
|
description: SEARCH_PROMPT_METADATA.description,
|
|
5763
4350
|
promptSnippet: SEARCH_PROMPT_METADATA.promptSnippet,
|
|
5764
4351
|
promptGuidelines: SEARCH_PROMPT_METADATA.promptGuidelines,
|
|
5765
|
-
parameters:
|
|
5766
|
-
pattern:
|
|
4352
|
+
parameters: Type5.Object({
|
|
4353
|
+
pattern: Type5.String({ description: PARAM_DESCRIPTIONS.pattern }),
|
|
5767
4354
|
language: languageParam(),
|
|
5768
4355
|
path: searchPathParam(),
|
|
5769
4356
|
...readSeekGitSearchParams()
|
|
@@ -5791,7 +4378,7 @@ function registerSearchTool(pi, options = {}) {
|
|
|
5791
4378
|
}
|
|
5792
4379
|
|
|
5793
4380
|
// src/refs.ts
|
|
5794
|
-
import { Type as
|
|
4381
|
+
import { Type as Type6 } from "@sinclair/typebox";
|
|
5795
4382
|
import path5 from "node:path";
|
|
5796
4383
|
|
|
5797
4384
|
// src/refs-output.ts
|
|
@@ -5910,13 +4497,13 @@ function registerRefsTool(pi, options = {}) {
|
|
|
5910
4497
|
description: REFS_PROMPT_METADATA.description,
|
|
5911
4498
|
promptSnippet: REFS_PROMPT_METADATA.promptSnippet,
|
|
5912
4499
|
promptGuidelines: REFS_PROMPT_METADATA.promptGuidelines,
|
|
5913
|
-
parameters:
|
|
5914
|
-
name:
|
|
4500
|
+
parameters: Type6.Object({
|
|
4501
|
+
name: Type6.Optional(Type6.String({ description: PARAM_DESCRIPTIONS.refsName })),
|
|
5915
4502
|
path: searchPathParam(),
|
|
5916
4503
|
language: languageParam(),
|
|
5917
|
-
scope:
|
|
5918
|
-
line:
|
|
5919
|
-
column:
|
|
4504
|
+
scope: Type6.Optional(Type6.Boolean({ description: PARAM_DESCRIPTIONS.scope })),
|
|
4505
|
+
line: Type6.Optional(Type6.Number({ description: PARAM_DESCRIPTIONS.line })),
|
|
4506
|
+
column: Type6.Optional(Type6.Number({ description: PARAM_DESCRIPTIONS.column })),
|
|
5920
4507
|
...readSeekGitSearchParams()
|
|
5921
4508
|
}),
|
|
5922
4509
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -5943,20 +4530,20 @@ function registerRefsTool(pi, options = {}) {
|
|
|
5943
4530
|
}
|
|
5944
4531
|
|
|
5945
4532
|
// src/rename.ts
|
|
5946
|
-
import { Type as
|
|
4533
|
+
import { Type as Type7 } from "@sinclair/typebox";
|
|
5947
4534
|
import path6 from "node:path";
|
|
5948
|
-
import { Text as
|
|
4535
|
+
import { Text as Text5 } from "@earendil-works/pi-tui";
|
|
5949
4536
|
var RENAME_PROMPT_METADATA = defineToolPromptMetadata({
|
|
5950
4537
|
promptUrl: new URL("../prompts/rename.md", import.meta.url),
|
|
5951
4538
|
promptSnippet: "Rename a symbol at a source location with verified edits"
|
|
5952
4539
|
});
|
|
5953
|
-
var renameSchema =
|
|
4540
|
+
var renameSchema = Type7.Object({
|
|
5954
4541
|
path: filePathParam(),
|
|
5955
|
-
line:
|
|
5956
|
-
column:
|
|
5957
|
-
to:
|
|
5958
|
-
workspace:
|
|
5959
|
-
apply:
|
|
4542
|
+
line: Type7.Integer({ minimum: 1, description: "One-based cursor line of the symbol to rename" }),
|
|
4543
|
+
column: Type7.Optional(Type7.Integer({ minimum: 1, description: "One-based cursor byte column for disambiguation" })),
|
|
4544
|
+
to: Type7.String({ description: "New symbol name" }),
|
|
4545
|
+
workspace: Type7.Optional(Type7.Boolean({ description: "Rename across the project" })),
|
|
4546
|
+
apply: Type7.Optional(Type7.Boolean({ description: "Apply the verified edits; defaults to true" }))
|
|
5960
4547
|
});
|
|
5961
4548
|
async function executeRename(opts) {
|
|
5962
4549
|
const { params, signal, cwd, onFileMutated } = opts;
|
|
@@ -6046,7 +4633,7 @@ function registerRenameTool(pi, options = {}) {
|
|
|
6046
4633
|
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
6047
4634
|
if (args?.to)
|
|
6048
4635
|
text += theme.fg("dim", ` → ${args.to}`);
|
|
6049
|
-
return new
|
|
4636
|
+
return new Text5(clampLineToWidth(text, context.width), 0, 0);
|
|
6050
4637
|
},
|
|
6051
4638
|
renderResult(result, options2, theme, ...rest) {
|
|
6052
4639
|
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options2, rest);
|
|
@@ -6079,7 +4666,7 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
|
|
|
6079
4666
|
}
|
|
6080
4667
|
const lines = clampLinesToWidth(text.split(`
|
|
6081
4668
|
`), width);
|
|
6082
|
-
return new
|
|
4669
|
+
return new Text5(lines.join(`
|
|
6083
4670
|
`), 0, 0);
|
|
6084
4671
|
}
|
|
6085
4672
|
});
|
|
@@ -6088,9 +4675,9 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
|
|
|
6088
4675
|
// src/write.ts
|
|
6089
4676
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6090
4677
|
import { dirname as dirname2, relative as relative3 } from "node:path";
|
|
6091
|
-
import { withFileMutationQueue as withFileMutationQueue2, truncateHead as
|
|
6092
|
-
import { Text as
|
|
6093
|
-
import { Type as
|
|
4678
|
+
import { withFileMutationQueue as withFileMutationQueue2, truncateHead as truncateHead2 } from "@earendil-works/pi-coding-agent";
|
|
4679
|
+
import { Text as Text6 } from "@earendil-works/pi-tui";
|
|
4680
|
+
import { Type as Type8 } from "@sinclair/typebox";
|
|
6094
4681
|
|
|
6095
4682
|
// src/pending-diff-preview.ts
|
|
6096
4683
|
import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync, statSync as statSync3 } from "node:fs";
|
|
@@ -6295,7 +4882,7 @@ async function executeWrite(opts) {
|
|
|
6295
4882
|
const warnings = [];
|
|
6296
4883
|
const readSeekWarnings = [];
|
|
6297
4884
|
if (hasBareCarriageReturn(content)) {
|
|
6298
|
-
const message = "File content contains bare CR (\\r) line endings; readSeek_write refuses to emit anchors that
|
|
4885
|
+
const message = "File content contains bare CR (\\r) line endings; readSeek_write refuses to emit anchors that readSeek_digest/readSeek_edit would normalize differently.";
|
|
6299
4886
|
warnings.push(message);
|
|
6300
4887
|
readSeekWarnings.push(buildReadSeekWarning("bare-cr", message));
|
|
6301
4888
|
return buildToolErrorResult("write", "bare-cr", `Cannot write ${filePath}
|
|
@@ -6336,7 +4923,7 @@ async function executeWrite(opts) {
|
|
|
6336
4923
|
}
|
|
6337
4924
|
const fullText = displayLines.join(`
|
|
6338
4925
|
`);
|
|
6339
|
-
const truncated =
|
|
4926
|
+
const truncated = truncateHead2(fullText);
|
|
6340
4927
|
let text = truncated.content;
|
|
6341
4928
|
if (truncated.truncated) {
|
|
6342
4929
|
if (truncated.truncatedBy === "lines") {
|
|
@@ -6386,9 +4973,9 @@ function registerWriteTool(pi, options = {}) {
|
|
|
6386
4973
|
description: promptMetadata.description,
|
|
6387
4974
|
promptSnippet: promptMetadata.promptSnippet,
|
|
6388
4975
|
promptGuidelines: promptMetadata.promptGuidelines,
|
|
6389
|
-
parameters:
|
|
4976
|
+
parameters: Type8.Object({
|
|
6390
4977
|
path: filePathParam(),
|
|
6391
|
-
content:
|
|
4978
|
+
content: Type8.String({ description: "Complete text file content" })
|
|
6392
4979
|
}),
|
|
6393
4980
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
6394
4981
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
@@ -6469,7 +5056,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
6469
5056
|
`);
|
|
6470
5057
|
lines.push(...formatContentPreviewLines(content, theme));
|
|
6471
5058
|
}
|
|
6472
|
-
return new
|
|
5059
|
+
return new Text6(clampLinesToWidth(lines, width).join(`
|
|
6473
5060
|
`), 0, 0);
|
|
6474
5061
|
}
|
|
6475
5062
|
const hasExpandableDiff = !!diffData;
|
|
@@ -6478,7 +5065,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
6478
5065
|
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split(`
|
|
6479
5066
|
`), diffData, theme, expanded: true });
|
|
6480
5067
|
}
|
|
6481
|
-
return new
|
|
5068
|
+
return new Text6(clampLinesToWidth(text.split(`
|
|
6482
5069
|
`), width).join(`
|
|
6483
5070
|
`), 0, 0);
|
|
6484
5071
|
}
|
|
@@ -6487,7 +5074,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
6487
5074
|
}
|
|
6488
5075
|
|
|
6489
5076
|
// src/def.ts
|
|
6490
|
-
import { Type as
|
|
5077
|
+
import { Type as Type9 } from "@sinclair/typebox";
|
|
6491
5078
|
import path7 from "node:path";
|
|
6492
5079
|
var DEF_PROMPT_METADATA = defineToolPromptMetadata({
|
|
6493
5080
|
promptUrl: new URL("../prompts/def.md", import.meta.url),
|
|
@@ -6563,8 +5150,8 @@ function registerDefTool(pi, options = {}) {
|
|
|
6563
5150
|
description: DEF_PROMPT_METADATA.description,
|
|
6564
5151
|
promptSnippet: DEF_PROMPT_METADATA.promptSnippet,
|
|
6565
5152
|
promptGuidelines: DEF_PROMPT_METADATA.promptGuidelines,
|
|
6566
|
-
parameters:
|
|
6567
|
-
name:
|
|
5153
|
+
parameters: Type9.Object({
|
|
5154
|
+
name: Type9.String({ description: PARAM_DESCRIPTIONS.name }),
|
|
6568
5155
|
path: searchPathParam(),
|
|
6569
5156
|
language: languageParam(),
|
|
6570
5157
|
...readSeekGitSearchParams()
|
|
@@ -6591,33 +5178,29 @@ function registerDefTool(pi, options = {}) {
|
|
|
6591
5178
|
}
|
|
6592
5179
|
|
|
6593
5180
|
// src/digest.ts
|
|
6594
|
-
import { Text as
|
|
6595
|
-
import { Type as
|
|
5181
|
+
import { Text as Text7 } from "@earendil-works/pi-tui";
|
|
5182
|
+
import { Type as Type10 } from "@sinclair/typebox";
|
|
6596
5183
|
var DIGEST_FACETS2 = DIGEST_FACETS;
|
|
6597
|
-
var
|
|
6598
|
-
|
|
6599
|
-
promptSnippet: "Digest source facets as a native CLI envelope"
|
|
6600
|
-
});
|
|
6601
|
-
var facetSchema = Type11.Union(DIGEST_FACETS2.map((facet) => Type11.Literal(facet)));
|
|
6602
|
-
var digestSchema = Type11.Object({
|
|
5184
|
+
var facetSchema = Type10.Union(DIGEST_FACETS2.map((facet) => Type10.Literal(facet)));
|
|
5185
|
+
var digestSchema = Type10.Object({
|
|
6603
5186
|
path: filePathParam(),
|
|
6604
|
-
select:
|
|
5187
|
+
select: Type10.Optional(Type10.Union([
|
|
6605
5188
|
facetSchema,
|
|
6606
|
-
|
|
6607
|
-
|
|
5189
|
+
Type10.Array(facetSchema, { minItems: 1 }),
|
|
5190
|
+
Type10.String({
|
|
6608
5191
|
description: PARAM_DESCRIPTIONS.selectComma
|
|
6609
5192
|
})
|
|
6610
5193
|
], {
|
|
6611
5194
|
description: PARAM_DESCRIPTIONS.select
|
|
6612
5195
|
})),
|
|
6613
|
-
at:
|
|
5196
|
+
at: Type10.Optional(Type10.String({
|
|
6614
5197
|
description: PARAM_DESCRIPTIONS.at
|
|
6615
5198
|
})),
|
|
6616
5199
|
end: optionalIntOrString(PARAM_DESCRIPTIONS.end),
|
|
6617
5200
|
limit: optionalIntOrString(PARAM_DESCRIPTIONS.limit),
|
|
6618
|
-
language:
|
|
6619
|
-
visionMode:
|
|
6620
|
-
visionLevel:
|
|
5201
|
+
language: Type10.Optional(Type10.String({ description: PARAM_DESCRIPTIONS.languageOverride })),
|
|
5202
|
+
visionMode: Type10.Optional(Type10.Union(VISION_MODES.map((mode) => Type10.Literal(mode)), { description: PARAM_DESCRIPTIONS.visionModeDigest })),
|
|
5203
|
+
visionLevel: Type10.Optional(Type10.Union(VISION_LEVELS.map((level) => Type10.Literal(level)), { description: PARAM_DESCRIPTIONS.visionLevel }))
|
|
6621
5204
|
});
|
|
6622
5205
|
function hasDigestAnchors(value) {
|
|
6623
5206
|
if (!value || typeof value !== "object")
|
|
@@ -6743,12 +5326,19 @@ async function executeDigest(opts) {
|
|
|
6743
5326
|
}
|
|
6744
5327
|
}
|
|
6745
5328
|
function registerDigestTool(pi, options = {}) {
|
|
5329
|
+
const promptMetadata = defineToolPromptMetadata({
|
|
5330
|
+
promptUrl: new URL("../prompts/digest.md", import.meta.url),
|
|
5331
|
+
promptSnippet: "Digest source facets as a native CLI envelope",
|
|
5332
|
+
registeredName: options.name ?? "readSeek_digest",
|
|
5333
|
+
toolAliases: options.toolAliases
|
|
5334
|
+
});
|
|
6746
5335
|
return registerReadSeekTool(pi, {
|
|
6747
|
-
name: "readSeek_digest",
|
|
5336
|
+
name: options.name ?? "readSeek_digest",
|
|
6748
5337
|
label: "Digest",
|
|
6749
|
-
description:
|
|
6750
|
-
promptSnippet:
|
|
6751
|
-
|
|
5338
|
+
description: promptMetadata.description,
|
|
5339
|
+
promptSnippet: promptMetadata.promptSnippet,
|
|
5340
|
+
toolAliases: options.toolAliases,
|
|
5341
|
+
promptGuidelines: promptMetadata.promptGuidelines,
|
|
6752
5342
|
parameters: digestSchema,
|
|
6753
5343
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
6754
5344
|
return executeDigest({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
@@ -6765,7 +5355,7 @@ function registerDigestTool(pi, options = {}) {
|
|
|
6765
5355
|
}
|
|
6766
5356
|
if (typeof args?.at === "string" && args.at)
|
|
6767
5357
|
text += theme.fg("dim", ` @ ${args.at}`);
|
|
6768
|
-
return new
|
|
5358
|
+
return new Text7(clampLineToWidth(text, context.width), 0, 0);
|
|
6769
5359
|
},
|
|
6770
5360
|
renderResult(result, options2, theme, ...rest) {
|
|
6771
5361
|
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options2, rest);
|
|
@@ -6782,7 +5372,7 @@ function registerDigestTool(pi, options = {}) {
|
|
|
6782
5372
|
if (expanded && textContent)
|
|
6783
5373
|
text += `
|
|
6784
5374
|
${textContent}`;
|
|
6785
|
-
return new
|
|
5375
|
+
return new Text7(clampLinesToWidth(text.split(`
|
|
6786
5376
|
`), width).join(`
|
|
6787
5377
|
`), 0, 0);
|
|
6788
5378
|
}
|
|
@@ -6791,12 +5381,12 @@ ${textContent}`;
|
|
|
6791
5381
|
|
|
6792
5382
|
// src/view.ts
|
|
6793
5383
|
import {
|
|
6794
|
-
DEFAULT_MAX_BYTES as
|
|
6795
|
-
DEFAULT_MAX_LINES as
|
|
6796
|
-
truncateHead as
|
|
5384
|
+
DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES2,
|
|
5385
|
+
DEFAULT_MAX_LINES as DEFAULT_MAX_LINES2,
|
|
5386
|
+
truncateHead as truncateHead3
|
|
6797
5387
|
} from "@earendil-works/pi-coding-agent";
|
|
6798
|
-
import { Text as
|
|
6799
|
-
import { Type as
|
|
5388
|
+
import { Text as Text8 } from "@earendil-works/pi-tui";
|
|
5389
|
+
import { Type as Type11 } from "@sinclair/typebox";
|
|
6800
5390
|
var NODE_KINDS = DOCUMENT_NODE_KINDS;
|
|
6801
5391
|
var VIEW_PROMPT_METADATA = defineToolPromptMetadata({
|
|
6802
5392
|
promptUrl: new URL("../prompts/view.md", import.meta.url),
|
|
@@ -6843,9 +5433,9 @@ async function executeView(opts) {
|
|
|
6843
5433
|
visionLevel: input.visionLevel,
|
|
6844
5434
|
signal
|
|
6845
5435
|
});
|
|
6846
|
-
const truncation =
|
|
6847
|
-
maxLines:
|
|
6848
|
-
maxBytes:
|
|
5436
|
+
const truncation = truncateHead3(output, {
|
|
5437
|
+
maxLines: DEFAULT_MAX_LINES2,
|
|
5438
|
+
maxBytes: DEFAULT_MAX_BYTES2
|
|
6849
5439
|
});
|
|
6850
5440
|
const notice = truncation.truncated ? `
|
|
6851
5441
|
[… document view truncated; narrow it with page, node, kind, or depth]` : "";
|
|
@@ -6871,17 +5461,17 @@ function registerViewTool(pi) {
|
|
|
6871
5461
|
description: VIEW_PROMPT_METADATA.description,
|
|
6872
5462
|
promptSnippet: VIEW_PROMPT_METADATA.promptSnippet,
|
|
6873
5463
|
promptGuidelines: VIEW_PROMPT_METADATA.promptGuidelines,
|
|
6874
|
-
parameters:
|
|
5464
|
+
parameters: Type11.Object({
|
|
6875
5465
|
path: filePathParam(),
|
|
6876
|
-
node:
|
|
5466
|
+
node: Type11.Optional(Type11.String({ description: PARAM_DESCRIPTIONS.node })),
|
|
6877
5467
|
page: optionalIntOrString(PARAM_DESCRIPTIONS.page),
|
|
6878
|
-
kind:
|
|
5468
|
+
kind: Type11.Optional(Type11.Union(NODE_KINDS.map((kind) => Type11.Literal(kind)), {
|
|
6879
5469
|
description: PARAM_DESCRIPTIONS.kind
|
|
6880
5470
|
})),
|
|
6881
5471
|
depth: optionalIntOrString(PARAM_DESCRIPTIONS.depth),
|
|
6882
|
-
visionMode:
|
|
6883
|
-
visionLevel:
|
|
6884
|
-
outline:
|
|
5472
|
+
visionMode: Type11.Optional(Type11.Union(VISION_ANALYSIS_MODES.map((mode) => Type11.Literal(mode)), { description: PARAM_DESCRIPTIONS.visionModeView })),
|
|
5473
|
+
visionLevel: Type11.Optional(Type11.Union(VISION_LEVELS.map((level) => Type11.Literal(level)), { description: PARAM_DESCRIPTIONS.visionLevel })),
|
|
5474
|
+
outline: Type11.Optional(Type11.Boolean({ description: PARAM_DESCRIPTIONS.outline }))
|
|
6885
5475
|
}),
|
|
6886
5476
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
6887
5477
|
return executeView({ params, signal, cwd: ctx.cwd });
|
|
@@ -6891,7 +5481,7 @@ function registerViewTool(pi) {
|
|
|
6891
5481
|
const cwd = context.cwd ?? process.cwd();
|
|
6892
5482
|
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
6893
5483
|
const text = `${theme.fg("toolTitle", theme.bold("view"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
6894
|
-
return new
|
|
5484
|
+
return new Text8(clampLineToWidth(text, context.width), 0, 0);
|
|
6895
5485
|
},
|
|
6896
5486
|
renderResult(result, options, theme, ...rest) {
|
|
6897
5487
|
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
@@ -6911,7 +5501,7 @@ function registerViewTool(pi) {
|
|
|
6911
5501
|
if (expanded && textContent)
|
|
6912
5502
|
text += `
|
|
6913
5503
|
${textContent}`;
|
|
6914
|
-
return new
|
|
5504
|
+
return new Text8(clampLinesToWidth(text.split(`
|
|
6915
5505
|
`), width).join(`
|
|
6916
5506
|
`), 0, 0);
|
|
6917
5507
|
}
|
|
@@ -6937,7 +5527,6 @@ class SessionAnchors {
|
|
|
6937
5527
|
|
|
6938
5528
|
// index.ts
|
|
6939
5529
|
var READSEEK_TOOL_ENTRIES = [
|
|
6940
|
-
{ builtIn: "read", readSeekName: "readSeek_read" },
|
|
6941
5530
|
{ builtIn: "edit", readSeekName: "readSeek_edit" },
|
|
6942
5531
|
{ builtIn: "grep", readSeekName: "readSeek_grep" },
|
|
6943
5532
|
{ builtIn: null, readSeekName: "readSeek_search" },
|
|
@@ -6945,7 +5534,7 @@ var READSEEK_TOOL_ENTRIES = [
|
|
|
6945
5534
|
{ builtIn: null, readSeekName: "readSeek_rename" },
|
|
6946
5535
|
{ builtIn: "write", readSeekName: "readSeek_write" },
|
|
6947
5536
|
{ builtIn: null, readSeekName: "readSeek_def" },
|
|
6948
|
-
{ builtIn:
|
|
5537
|
+
{ builtIn: "read", readSeekName: "readSeek_digest" },
|
|
6949
5538
|
{ builtIn: null, readSeekName: "readSeek_view" }
|
|
6950
5539
|
];
|
|
6951
5540
|
function formatSettingsWarning(warning) {
|
|
@@ -6956,7 +5545,7 @@ function editingPolicy(readName, editName, writeName) {
|
|
|
6956
5545
|
"ReadSeek editing policy:",
|
|
6957
5546
|
`- Read first with ${readName} when preparing to edit; ${editName} needs fresh LINE:HASH anchors.`,
|
|
6958
5547
|
`- Use ${editName} for existing files, ${writeName} for whole-file creation or replacement, and readSeek_rename for symbol renames.`,
|
|
6959
|
-
|
|
5548
|
+
`- Run ${readName} with select diagnostics after source edits for a quick syntax check.`
|
|
6960
5549
|
].join(`
|
|
6961
5550
|
`);
|
|
6962
5551
|
}
|
|
@@ -6969,24 +5558,23 @@ function piReadSeekExtension(pi) {
|
|
|
6969
5558
|
const replacedBuiltIns = new Set(settings.overrideTools ?? []);
|
|
6970
5559
|
const binaryAvailable = readSeekBinaryAvailability().available;
|
|
6971
5560
|
const swap = (builtIn, readSeekName) => binaryAvailable && replacedBuiltIns.has(builtIn) ? builtIn : readSeekName;
|
|
6972
|
-
const readName = swap("read", "
|
|
5561
|
+
const readName = swap("read", "readSeek_digest");
|
|
6973
5562
|
const editName = swap("edit", "readSeek_edit");
|
|
6974
5563
|
const writeName = swap("write", "readSeek_write");
|
|
6975
5564
|
const grepName = swap("grep", "readSeek_grep");
|
|
6976
5565
|
const toolAliases = {
|
|
6977
|
-
|
|
5566
|
+
readSeek_digest: readName,
|
|
6978
5567
|
readSeek_edit: editName,
|
|
6979
5568
|
readSeek_grep: grepName,
|
|
6980
5569
|
readSeek_write: writeName
|
|
6981
5570
|
};
|
|
6982
|
-
registerReadTool(pi, { onSuccessfulRead: markAnchored, name: readName });
|
|
6983
5571
|
registerEditTool(pi, { wasReadInSession: hasFreshAnchors, onFileMutated: forgetAnchors, name: editName, toolAliases });
|
|
6984
5572
|
registerGrepTool(pi, { onFileAnchored: markAnchored, name: grepName });
|
|
6985
5573
|
registerSearchTool(pi, { onFileAnchored: markAnchored });
|
|
6986
5574
|
registerRefsTool(pi, { onFileAnchored: markAnchored });
|
|
6987
5575
|
registerRenameTool(pi, { onFileMutated: forgetAnchors });
|
|
6988
5576
|
registerDefTool(pi, { onFileAnchored: markAnchored });
|
|
6989
|
-
registerDigestTool(pi, { onFileAnchored: markAnchored });
|
|
5577
|
+
registerDigestTool(pi, { onFileAnchored: markAnchored, name: readName, toolAliases });
|
|
6990
5578
|
registerViewTool(pi);
|
|
6991
5579
|
registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
|
|
6992
5580
|
pi.on("before_agent_start", (event) => {
|