@oh-my-pi/pi-coding-agent 14.6.6 → 14.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/03-custom-prompt.ts +7 -4
- package/examples/sdk/README.md +1 -1
- package/package.json +7 -7
- package/src/autoresearch/index.ts +48 -44
- package/src/cli/read-cli.ts +58 -0
- package/src/cli.ts +1 -0
- package/src/commands/read.ts +40 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/analysis/conventional.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/commit/map-reduce/map-phase.ts +1 -1
- package/src/commit/map-reduce/reduce-phase.ts +1 -1
- package/src/config/settings-schema.ts +39 -0
- package/src/edit/line-hash.ts +34 -4
- package/src/edit/modes/hashline.ts +201 -6
- package/src/edit/streaming.ts +4 -1
- package/src/export/html/index.ts +1 -1
- package/src/extensibility/extensions/runner.ts +3 -3
- package/src/extensibility/extensions/types.ts +4 -4
- package/src/main.ts +3 -3
- package/src/memories/index.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/read-tool-group.ts +4 -9
- package/src/modes/components/tool-execution.ts +4 -0
- package/src/modes/controllers/event-controller.ts +2 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/utils/context-usage.ts +12 -5
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/project-prompt.md +36 -0
- package/src/prompts/system/system-prompt.md +0 -29
- package/src/prompts/tools/github.md +1 -0
- package/src/prompts/tools/read.md +15 -14
- package/src/sdk.ts +29 -28
- package/src/session/agent-session.ts +20 -12
- package/src/session/compaction/branch-summarization.ts +1 -1
- package/src/session/compaction/compaction.ts +3 -3
- package/src/session/session-dump-format.ts +10 -5
- package/src/session/streaming-output.ts +1 -1
- package/src/system-prompt.ts +35 -3
- package/src/task/executor.ts +4 -3
- package/src/tools/fetch.ts +4 -4
- package/src/tools/gh.ts +187 -0
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/path-utils.ts +11 -0
- package/src/tools/read.ts +388 -204
- package/src/tools/search.ts +1 -1
- package/src/tools/sqlite-reader.ts +1 -1
- package/src/utils/commit-message-generator.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/workspace-tree.ts +396 -0
package/src/tools/read.ts
CHANGED
|
@@ -3,12 +3,12 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
6
|
-
import { glob } from "@oh-my-pi/pi-natives";
|
|
6
|
+
import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
|
|
7
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
11
|
-
import { formatHashLines } from "../edit/line-hash";
|
|
11
|
+
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../edit/line-hash";
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
14
14
|
import type { InternalUrl } from "../internal-urls/types";
|
|
@@ -28,6 +28,7 @@ import { CachedOutputBlock } from "../tui/output-block";
|
|
|
28
28
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
29
29
|
import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
|
|
30
30
|
import { convertFileWithMarkit } from "../utils/markit";
|
|
31
|
+
import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
|
|
31
32
|
import { type ArchiveReader, openArchive, parseArchivePathCandidates } from "./archive-reader";
|
|
32
33
|
import {
|
|
33
34
|
executeReadUrl,
|
|
@@ -40,8 +41,8 @@ import {
|
|
|
40
41
|
} from "./fetch";
|
|
41
42
|
import { applyListLimit } from "./list-limit";
|
|
42
43
|
import { formatFullOutputReference, formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
|
|
43
|
-
import { expandPath, formatPathRelativeToCwd, resolveReadPath } from "./path-utils";
|
|
44
|
-
import {
|
|
44
|
+
import { expandPath, formatPathRelativeToCwd, resolveReadPath, splitPathAndSel } from "./path-utils";
|
|
45
|
+
import { formatBytes, shortenPath, wrapBrackets } from "./render-utils";
|
|
45
46
|
import {
|
|
46
47
|
executeReadQuery,
|
|
47
48
|
getRowByKey,
|
|
@@ -64,9 +65,25 @@ import { toolResult } from "./tool-result";
|
|
|
64
65
|
// Document types converted to markdown via markit.
|
|
65
66
|
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
66
67
|
|
|
68
|
+
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
69
|
+
const MAX_SUMMARY_LINES = 20_000;
|
|
67
70
|
// Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
|
|
68
71
|
const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
|
|
69
72
|
|
|
73
|
+
const READ_DIRECTORY_EXCLUDED_DIRS = new Set([
|
|
74
|
+
"node_modules",
|
|
75
|
+
".git",
|
|
76
|
+
".next",
|
|
77
|
+
"dist",
|
|
78
|
+
"build",
|
|
79
|
+
"target",
|
|
80
|
+
".venv",
|
|
81
|
+
".cache",
|
|
82
|
+
".turbo",
|
|
83
|
+
".parcel-cache",
|
|
84
|
+
"coverage",
|
|
85
|
+
]);
|
|
86
|
+
|
|
70
87
|
function isRemoteMountPath(absolutePath: string): boolean {
|
|
71
88
|
return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
|
|
72
89
|
}
|
|
@@ -87,6 +104,61 @@ function formatTextWithMode(
|
|
|
87
104
|
return text;
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
const BRACE_PAIRS: Record<string, string> = { "{": "}", "(": ")", "[": "]" };
|
|
108
|
+
const BRACE_TAIL_TRAILING_RE = /^[;,)\]}]*$/;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Decide whether the kept lines surrounding an elided range collapse to a
|
|
112
|
+
* single brace-pair line in the rendered summary. Returns true when the head
|
|
113
|
+
* line ends with `{` / `(` / `[` and the tail line is the matching closer
|
|
114
|
+
* (optionally followed by terminating punctuation like `;`, `,`, or further
|
|
115
|
+
* closers — e.g. `};`, `})`, `]);`).
|
|
116
|
+
*/
|
|
117
|
+
function canMergeBracePair(headLine: string, tailLine: string): boolean {
|
|
118
|
+
const head = headLine.trimEnd();
|
|
119
|
+
const tail = tailLine.trim();
|
|
120
|
+
const opener = head.slice(-1);
|
|
121
|
+
const closer = BRACE_PAIRS[opener];
|
|
122
|
+
if (!closer) return false;
|
|
123
|
+
if (!tail.startsWith(closer)) return false;
|
|
124
|
+
return BRACE_TAIL_TRAILING_RE.test(tail.slice(closer.length));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function formatSingleLine(
|
|
128
|
+
line: number,
|
|
129
|
+
text: string,
|
|
130
|
+
shouldAddHashLines: boolean,
|
|
131
|
+
shouldAddLineNumbers: boolean,
|
|
132
|
+
): string {
|
|
133
|
+
if (shouldAddHashLines) return formatHashLine(line, text);
|
|
134
|
+
if (shouldAddLineNumbers) return `${line}|${text}`;
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatMergedBraceLine(
|
|
139
|
+
startLine: number,
|
|
140
|
+
endLine: number,
|
|
141
|
+
headText: string,
|
|
142
|
+
tailText: string,
|
|
143
|
+
shouldAddHashLines: boolean,
|
|
144
|
+
shouldAddLineNumbers: boolean,
|
|
145
|
+
): { model: string; display: string } {
|
|
146
|
+
const merged = `${headText.trimEnd()} .. ${tailText.trim()}`;
|
|
147
|
+
if (shouldAddHashLines) {
|
|
148
|
+
const start = formatLineHash(startLine, headText);
|
|
149
|
+
const end = formatLineHash(endLine, tailText);
|
|
150
|
+
return { model: `${start}-${end}${HL_BODY_SEP}${merged}`, display: merged };
|
|
151
|
+
}
|
|
152
|
+
if (shouldAddLineNumbers) {
|
|
153
|
+
return { model: `${startLine}-${endLine}|${merged}`, display: merged };
|
|
154
|
+
}
|
|
155
|
+
return { model: merged, display: merged };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function countTextLines(text: string): number {
|
|
159
|
+
if (text.length === 0) return 0;
|
|
160
|
+
return text.split("\n").length;
|
|
161
|
+
}
|
|
90
162
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
91
163
|
|
|
92
164
|
async function streamLinesFromFile(
|
|
@@ -341,8 +413,10 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
|
|
|
341
413
|
}
|
|
342
414
|
|
|
343
415
|
const readSchema = Type.Object({
|
|
344
|
-
path: Type.String({
|
|
345
|
-
|
|
416
|
+
path: Type.String({
|
|
417
|
+
description: 'path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
|
|
418
|
+
examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com:L1-L40"],
|
|
419
|
+
}),
|
|
346
420
|
timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 20 })),
|
|
347
421
|
});
|
|
348
422
|
|
|
@@ -364,11 +438,12 @@ export interface ReadToolDetails {
|
|
|
364
438
|
* Mirrors the same lines the model receives but without hashline/line-number prefixes,
|
|
365
439
|
* so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
|
|
366
440
|
displayContent?: { text: string; startLine: number };
|
|
441
|
+
summary?: { lines: number; elidedSpans: number };
|
|
367
442
|
}
|
|
368
443
|
|
|
369
444
|
type ReadParams = ReadToolInput;
|
|
370
445
|
|
|
371
|
-
/** Parsed representation of
|
|
446
|
+
/** Parsed representation of a path-embedded selector. */
|
|
372
447
|
type ParsedSelector =
|
|
373
448
|
| { kind: "none" }
|
|
374
449
|
| { kind: "raw" }
|
|
@@ -378,12 +453,12 @@ const LINE_RANGE_RE = /^L?(\d+)(?:([-+])L?(\d+))?$/i;
|
|
|
378
453
|
|
|
379
454
|
function parseSel(sel: string | undefined): ParsedSelector {
|
|
380
455
|
if (!sel || sel.length === 0) return { kind: "none" };
|
|
381
|
-
if (sel === "raw") return { kind: "raw" };
|
|
456
|
+
if (sel.toLowerCase() === "raw") return { kind: "raw" };
|
|
382
457
|
const lineMatch = LINE_RANGE_RE.exec(sel);
|
|
383
458
|
if (lineMatch) {
|
|
384
459
|
const rawStart = Number.parseInt(lineMatch[1]!, 10);
|
|
385
460
|
if (rawStart < 1) {
|
|
386
|
-
throw new ToolError("
|
|
461
|
+
throw new ToolError("Line selector 0 is invalid; lines are 1-indexed. Use :1.");
|
|
387
462
|
}
|
|
388
463
|
const sep = lineMatch[2];
|
|
389
464
|
const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : undefined;
|
|
@@ -401,7 +476,7 @@ function parseSel(sel: string | undefined): ParsedSelector {
|
|
|
401
476
|
}
|
|
402
477
|
return { kind: "lines", startLine: rawStart, endLine: rawEnd };
|
|
403
478
|
}
|
|
404
|
-
// Unrecognized selectors fall through; sqlite/archive/url readers consume
|
|
479
|
+
// Unrecognized selectors fall through; sqlite/archive/url readers consume their own colon syntax.
|
|
405
480
|
return { kind: "none" };
|
|
406
481
|
}
|
|
407
482
|
|
|
@@ -427,22 +502,6 @@ interface ResolvedSqliteReadPath {
|
|
|
427
502
|
suffixResolution?: { from: string; to: string };
|
|
428
503
|
}
|
|
429
504
|
|
|
430
|
-
function parseSqliteSelectorInput(selector: string | undefined): { subPath: string; queryString: string } {
|
|
431
|
-
if (!selector) {
|
|
432
|
-
return { subPath: "", queryString: "" };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const queryIndex = selector.indexOf("?");
|
|
436
|
-
if (queryIndex === -1) {
|
|
437
|
-
return { subPath: selector.replace(/^:+/, ""), queryString: "" };
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
subPath: selector.slice(0, queryIndex).replace(/^:+/, ""),
|
|
442
|
-
queryString: selector.slice(queryIndex + 1),
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
505
|
/**
|
|
447
506
|
* Read tool implementation.
|
|
448
507
|
*
|
|
@@ -603,7 +662,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
603
662
|
const suggestion =
|
|
604
663
|
allLines.length === 0
|
|
605
664
|
? `The ${options.entityLabel} is empty.`
|
|
606
|
-
: `Use
|
|
665
|
+
: `Use :1 to read from the start, or :${allLines.length} to read the last line.`;
|
|
607
666
|
return resultBuilder
|
|
608
667
|
.text(
|
|
609
668
|
`Line ${startLineDisplay} is beyond end of ${options.entityLabel} (${allLines.length} lines total). ${suggestion}`,
|
|
@@ -665,7 +724,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
665
724
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
666
725
|
|
|
667
726
|
outputText = formatText(selectedContent, startLineDisplay);
|
|
668
|
-
outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use
|
|
727
|
+
outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`;
|
|
669
728
|
} else {
|
|
670
729
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
671
730
|
}
|
|
@@ -779,15 +838,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
779
838
|
}
|
|
780
839
|
|
|
781
840
|
async #readSqlite(
|
|
782
|
-
sel: string | undefined,
|
|
783
841
|
resolvedSqlitePath: ResolvedSqliteReadPath,
|
|
784
842
|
signal?: AbortSignal,
|
|
785
843
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
786
844
|
throwIfAborted(signal);
|
|
787
845
|
|
|
788
|
-
const selectorInput =
|
|
789
|
-
|
|
790
|
-
|
|
846
|
+
const selectorInput = {
|
|
847
|
+
subPath: resolvedSqlitePath.sqliteSubPath,
|
|
848
|
+
queryString: resolvedSqlitePath.queryString,
|
|
849
|
+
};
|
|
791
850
|
const selector = parseSqliteSelector(selectorInput.subPath, selectorInput.queryString);
|
|
792
851
|
const details: ReadToolDetails = {
|
|
793
852
|
resolvedPath: resolvedSqlitePath.absolutePath,
|
|
@@ -826,7 +885,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
826
885
|
});
|
|
827
886
|
if (sampleRows.rows.length < sampleRows.totalCount) {
|
|
828
887
|
const remaining = sampleRows.totalCount - sampleRows.rows.length;
|
|
829
|
-
output += `\n[${remaining} more rows;
|
|
888
|
+
output += `\n[${remaining} more rows; append :${selector.table}?limit=20&offset=${sampleRows.rows.length} to the database path to continue]`;
|
|
830
889
|
}
|
|
831
890
|
return toolResult<ReadToolDetails>(details)
|
|
832
891
|
.text(prependSuffixResolutionNotice(output, resolvedSqlitePath.suffixResolution))
|
|
@@ -904,6 +963,118 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
904
963
|
}
|
|
905
964
|
}
|
|
906
965
|
|
|
966
|
+
async #trySummarize(absolutePath: string, fileSize: number, signal?: AbortSignal): Promise<SummaryResult | null> {
|
|
967
|
+
if (fileSize > MAX_SUMMARY_BYTES) return null;
|
|
968
|
+
|
|
969
|
+
try {
|
|
970
|
+
throwIfAborted(signal);
|
|
971
|
+
const code = await Bun.file(absolutePath).text();
|
|
972
|
+
throwIfAborted(signal);
|
|
973
|
+
if (countTextLines(code) > MAX_SUMMARY_LINES) return null;
|
|
974
|
+
|
|
975
|
+
return summarizeCode({
|
|
976
|
+
code,
|
|
977
|
+
path: absolutePath,
|
|
978
|
+
minBodyLines: this.session.settings.get("read.summarize.minBodyLines"),
|
|
979
|
+
minCommentLines: this.session.settings.get("read.summarize.minCommentLines"),
|
|
980
|
+
});
|
|
981
|
+
} catch {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
#renderSummary(summary: SummaryResult): {
|
|
987
|
+
text: string;
|
|
988
|
+
displayText: string;
|
|
989
|
+
elidedSpans: number;
|
|
990
|
+
} {
|
|
991
|
+
const displayMode = resolveFileDisplayMode(this.session);
|
|
992
|
+
const shouldAddHashLines = displayMode.hashLines;
|
|
993
|
+
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
994
|
+
|
|
995
|
+
// Flatten segments into per-line units so we can merge a kept-head /
|
|
996
|
+
// elided / kept-tail sandwich into a single brace-pair line when the
|
|
997
|
+
// boundary lines look like `… {` and `}` (or matching variants).
|
|
998
|
+
type Unit =
|
|
999
|
+
| { kind: "line"; line: number; text: string }
|
|
1000
|
+
| { kind: "elided"; startLine: number; endLine: number }
|
|
1001
|
+
| {
|
|
1002
|
+
kind: "merged";
|
|
1003
|
+
startLine: number;
|
|
1004
|
+
endLine: number;
|
|
1005
|
+
headText: string;
|
|
1006
|
+
tailText: string;
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const raw: Unit[] = [];
|
|
1010
|
+
for (const segment of summary.segments) {
|
|
1011
|
+
if (segment.kind === "elided") {
|
|
1012
|
+
raw.push({ kind: "elided", startLine: segment.startLine, endLine: segment.endLine });
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const text = segment.text ?? "";
|
|
1016
|
+
if (text.length === 0) continue;
|
|
1017
|
+
const lines = text.split("\n");
|
|
1018
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1019
|
+
raw.push({ kind: "line", line: segment.startLine + i, text: lines[i] });
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const units: Unit[] = [];
|
|
1024
|
+
let i = 0;
|
|
1025
|
+
while (i < raw.length) {
|
|
1026
|
+
const cur = raw[i];
|
|
1027
|
+
if (cur.kind === "elided") {
|
|
1028
|
+
const prev = units.length > 0 ? units[units.length - 1] : null;
|
|
1029
|
+
const next = i + 1 < raw.length ? raw[i + 1] : null;
|
|
1030
|
+
if (prev?.kind === "line" && next?.kind === "line" && canMergeBracePair(prev.text, next.text)) {
|
|
1031
|
+
units.pop();
|
|
1032
|
+
units.push({
|
|
1033
|
+
kind: "merged",
|
|
1034
|
+
startLine: prev.line,
|
|
1035
|
+
endLine: next.line,
|
|
1036
|
+
headText: prev.text,
|
|
1037
|
+
tailText: next.text,
|
|
1038
|
+
});
|
|
1039
|
+
i += 2;
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
units.push(cur);
|
|
1044
|
+
i++;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const modelParts: string[] = [];
|
|
1048
|
+
const displayParts: string[] = [];
|
|
1049
|
+
let elidedSpans = 0;
|
|
1050
|
+
for (const unit of units) {
|
|
1051
|
+
if (unit.kind === "elided") {
|
|
1052
|
+
modelParts.push("...");
|
|
1053
|
+
displayParts.push("...");
|
|
1054
|
+
elidedSpans++;
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (unit.kind === "merged") {
|
|
1058
|
+
const formatted = formatMergedBraceLine(
|
|
1059
|
+
unit.startLine,
|
|
1060
|
+
unit.endLine,
|
|
1061
|
+
unit.headText,
|
|
1062
|
+
unit.tailText,
|
|
1063
|
+
shouldAddHashLines,
|
|
1064
|
+
shouldAddLineNumbers,
|
|
1065
|
+
);
|
|
1066
|
+
modelParts.push(formatted.model);
|
|
1067
|
+
displayParts.push(formatted.display);
|
|
1068
|
+
elidedSpans++;
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
modelParts.push(formatSingleLine(unit.line, unit.text, shouldAddHashLines, shouldAddLineNumbers));
|
|
1072
|
+
displayParts.push(unit.text);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return { text: modelParts.join("\n"), displayText: displayParts.join("\n"), elidedSpans };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
907
1078
|
async execute(
|
|
908
1079
|
_toolCallId: string,
|
|
909
1080
|
params: ReadParams,
|
|
@@ -911,21 +1082,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
911
1082
|
_onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
|
|
912
1083
|
_toolContext?: AgentToolContext,
|
|
913
1084
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
914
|
-
let { path: readPath,
|
|
1085
|
+
let { path: readPath, timeout } = params;
|
|
915
1086
|
if (readPath.startsWith("file://")) {
|
|
916
1087
|
readPath = expandPath(readPath);
|
|
917
1088
|
}
|
|
918
1089
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
919
1090
|
|
|
920
|
-
|
|
921
|
-
const internalRouter = this.session.internalRouter;
|
|
922
|
-
if (internalRouter?.canHandle(readPath)) {
|
|
923
|
-
const parsed = parseSel(sel);
|
|
924
|
-
const { offset, limit } = selToOffsetLimit(parsed);
|
|
925
|
-
return this.#handleInternalUrl(readPath, offset, limit);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const parsedUrlTarget = parseReadUrlTarget(readPath, sel);
|
|
1091
|
+
const parsedUrlTarget = parseReadUrlTarget(readPath);
|
|
929
1092
|
if (parsedUrlTarget) {
|
|
930
1093
|
if (!this.session.settings.get("fetch.enabled")) {
|
|
931
1094
|
throw new ToolError("URL reads are disabled by settings.");
|
|
@@ -949,20 +1112,39 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
949
1112
|
return executeReadUrl(this.session, { path: parsedUrlTarget.path, timeout, raw: parsedUrlTarget.raw }, signal);
|
|
950
1113
|
}
|
|
951
1114
|
|
|
952
|
-
|
|
953
|
-
const
|
|
1115
|
+
// Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://)
|
|
1116
|
+
const internalTarget = splitPathAndSel(readPath);
|
|
1117
|
+
const internalRouter = this.session.internalRouter;
|
|
1118
|
+
if (internalRouter?.canHandle(internalTarget.path)) {
|
|
1119
|
+
const parsed = parseSel(internalTarget.sel);
|
|
1120
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1121
|
+
return this.#handleInternalUrl(internalTarget.path, offset, limit);
|
|
1122
|
+
}
|
|
954
1123
|
|
|
955
|
-
const archivePath = await this.#resolveArchiveReadPath(
|
|
1124
|
+
const archivePath = await this.#resolveArchiveReadPath(readPath, signal);
|
|
956
1125
|
if (archivePath) {
|
|
957
|
-
const
|
|
958
|
-
|
|
1126
|
+
const archiveSubPath = splitPathAndSel(archivePath.archiveSubPath);
|
|
1127
|
+
const archiveParsed = parseSel(archiveSubPath.sel);
|
|
1128
|
+
const { offset, limit } = selToOffsetLimit(archiveParsed);
|
|
1129
|
+
return this.#readArchive(
|
|
1130
|
+
readPath,
|
|
1131
|
+
offset,
|
|
1132
|
+
limit,
|
|
1133
|
+
{ ...archivePath, archiveSubPath: archiveSubPath.path },
|
|
1134
|
+
signal,
|
|
1135
|
+
{ raw: archiveParsed.kind === "raw" },
|
|
1136
|
+
);
|
|
959
1137
|
}
|
|
960
1138
|
|
|
961
1139
|
const sqlitePath = await this.#resolveSqliteReadPath(readPath, signal);
|
|
962
1140
|
if (sqlitePath) {
|
|
963
|
-
return this.#readSqlite(
|
|
1141
|
+
return this.#readSqlite(sqlitePath, signal);
|
|
964
1142
|
}
|
|
965
1143
|
|
|
1144
|
+
const localTarget = splitPathAndSel(readPath);
|
|
1145
|
+
const localReadPath = localTarget.path;
|
|
1146
|
+
const parsed = parseSel(localTarget.sel);
|
|
1147
|
+
|
|
966
1148
|
let absolutePath = resolveReadPath(localReadPath, this.session.cwd);
|
|
967
1149
|
let suffixResolution: { from: string; to: string } | undefined;
|
|
968
1150
|
|
|
@@ -1014,7 +1196,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1014
1196
|
const _language = getLanguageFromPath(absolutePath);
|
|
1015
1197
|
const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext) || (ext === ".ipynb" && parsed.kind === "raw");
|
|
1016
1198
|
// Read the file based on type
|
|
1017
|
-
let content: Array<TextContent | ImageContent
|
|
1199
|
+
let content: Array<TextContent | ImageContent> | undefined;
|
|
1018
1200
|
let details: ReadToolDetails = {};
|
|
1019
1201
|
let sourcePath: string | undefined;
|
|
1020
1202
|
let truncationInfo:
|
|
@@ -1098,129 +1280,148 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1098
1280
|
content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
|
|
1099
1281
|
}
|
|
1100
1282
|
} else {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
1113
|
-
|
|
1114
|
-
const streamResult = await streamLinesFromFile(
|
|
1115
|
-
absolutePath,
|
|
1116
|
-
startLine,
|
|
1117
|
-
maxLinesToCollect,
|
|
1118
|
-
maxBytesForRead,
|
|
1119
|
-
selectedLineLimit,
|
|
1120
|
-
signal,
|
|
1121
|
-
);
|
|
1283
|
+
if (parsed.kind === "none" && this.session.settings.get("read.summarize.enabled")) {
|
|
1284
|
+
const summary = await this.#trySummarize(absolutePath, fileSize, signal);
|
|
1285
|
+
if (summary?.parsed && summary.elided) {
|
|
1286
|
+
const renderedSummary = this.#renderSummary(summary);
|
|
1287
|
+
details = {
|
|
1288
|
+
displayContent: { text: renderedSummary.displayText, startLine: 1 },
|
|
1289
|
+
summary: {
|
|
1290
|
+
lines: countTextLines(renderedSummary.text),
|
|
1291
|
+
elidedSpans: renderedSummary.elidedSpans,
|
|
1292
|
+
},
|
|
1293
|
+
};
|
|
1122
1294
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
collectedBytes,
|
|
1127
|
-
stoppedByByteLimit,
|
|
1128
|
-
firstLinePreview,
|
|
1129
|
-
firstLineByteLength,
|
|
1130
|
-
} = streamResult;
|
|
1131
|
-
|
|
1132
|
-
// Check if offset is out of bounds - return graceful message instead of throwing
|
|
1133
|
-
if (startLine >= totalFileLines) {
|
|
1134
|
-
const suggestion =
|
|
1135
|
-
totalFileLines === 0
|
|
1136
|
-
? "The file is empty."
|
|
1137
|
-
: `Use sel=1 to read from the start, or sel=${totalFileLines} to read the last line.`;
|
|
1138
|
-
return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
|
|
1139
|
-
.text(`Line ${startLineDisplay} is beyond end of file (${totalFileLines} lines total). ${suggestion}`)
|
|
1140
|
-
.done();
|
|
1295
|
+
sourcePath = absolutePath;
|
|
1296
|
+
content = [{ type: "text", text: renderedSummary.text }];
|
|
1297
|
+
}
|
|
1141
1298
|
}
|
|
1142
1299
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1300
|
+
if (!content) {
|
|
1301
|
+
// Raw text or line-range mode
|
|
1302
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1303
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
1304
|
+
const startLineDisplay = startLine + 1;
|
|
1305
|
+
|
|
1306
|
+
const DEFAULT_LIMIT = this.#defaultLimit;
|
|
1307
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
1308
|
+
const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES);
|
|
1309
|
+
const selectedLineLimit = effectiveLimit;
|
|
1310
|
+
// Scale byte budget with line limit so the configured line count actually fits.
|
|
1311
|
+
// Assume ~512 bytes/line average; never go below the shared default.
|
|
1312
|
+
const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
1313
|
+
|
|
1314
|
+
const streamResult = await streamLinesFromFile(
|
|
1315
|
+
absolutePath,
|
|
1316
|
+
startLine,
|
|
1317
|
+
maxLinesToCollect,
|
|
1318
|
+
maxBytesForRead,
|
|
1319
|
+
selectedLineLimit,
|
|
1320
|
+
signal,
|
|
1321
|
+
);
|
|
1162
1322
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1323
|
+
const {
|
|
1324
|
+
lines: collectedLines,
|
|
1325
|
+
totalFileLines,
|
|
1326
|
+
collectedBytes,
|
|
1327
|
+
stoppedByByteLimit,
|
|
1328
|
+
firstLinePreview,
|
|
1329
|
+
firstLineByteLength,
|
|
1330
|
+
} = streamResult;
|
|
1331
|
+
|
|
1332
|
+
// Check if offset is out of bounds - return graceful message instead of throwing
|
|
1333
|
+
if (startLine >= totalFileLines) {
|
|
1334
|
+
const suggestion =
|
|
1335
|
+
totalFileLines === 0
|
|
1336
|
+
? "The file is empty."
|
|
1337
|
+
: `Use :1 to read from the start, or :${totalFileLines} to read the last line.`;
|
|
1338
|
+
return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
|
|
1339
|
+
.text(`Line ${startLineDisplay} is beyond end of file (${totalFileLines} lines total). ${suggestion}`)
|
|
1340
|
+
.done();
|
|
1341
|
+
}
|
|
1171
1342
|
|
|
1172
|
-
|
|
1343
|
+
const selectedContent = collectedLines.join("\n");
|
|
1344
|
+
const userLimitedLines = collectedLines.length;
|
|
1345
|
+
|
|
1346
|
+
const totalSelectedLines = totalFileLines - startLine;
|
|
1347
|
+
const totalSelectedBytes = collectedBytes;
|
|
1348
|
+
const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
|
|
1349
|
+
const firstLineExceedsLimit = firstLineByteLength !== undefined && firstLineByteLength > maxBytesForRead;
|
|
1350
|
+
|
|
1351
|
+
const truncation: TruncationResult = {
|
|
1352
|
+
content: selectedContent,
|
|
1353
|
+
truncated: wasTruncated,
|
|
1354
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
1355
|
+
totalLines: totalSelectedLines,
|
|
1356
|
+
totalBytes: totalSelectedBytes,
|
|
1357
|
+
outputLines: collectedLines.length,
|
|
1358
|
+
outputBytes: collectedBytes,
|
|
1359
|
+
lastLinePartial: false,
|
|
1360
|
+
firstLineExceedsLimit,
|
|
1361
|
+
};
|
|
1173
1362
|
|
|
1174
|
-
|
|
1175
|
-
const
|
|
1176
|
-
const
|
|
1363
|
+
const isRawMode = parsed.kind === "raw";
|
|
1364
|
+
const shouldAddHashLines = !isRawMode && displayMode.hashLines;
|
|
1365
|
+
const shouldAddLineNumbers = isRawMode ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1366
|
+
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
1367
|
+
const formatText = (text: string, startNum: number): string => {
|
|
1368
|
+
capturedDisplayContent = { text, startLine: startNum };
|
|
1369
|
+
return formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
let outputText: string;
|
|
1373
|
+
|
|
1374
|
+
if (truncation.firstLineExceedsLimit) {
|
|
1375
|
+
const firstLineBytes = firstLineByteLength ?? 0;
|
|
1376
|
+
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
1177
1377
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1378
|
+
if (shouldAddHashLines) {
|
|
1379
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1380
|
+
firstLineBytes,
|
|
1381
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
1382
|
+
} else {
|
|
1383
|
+
outputText = formatText(snippet.text, startLineDisplay);
|
|
1384
|
+
}
|
|
1385
|
+
if (snippet.text.length === 0) {
|
|
1386
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1387
|
+
firstLineBytes,
|
|
1388
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1389
|
+
}
|
|
1390
|
+
details = { truncation };
|
|
1391
|
+
sourcePath = absolutePath;
|
|
1392
|
+
truncationInfo = {
|
|
1393
|
+
result: truncation,
|
|
1394
|
+
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
1395
|
+
};
|
|
1396
|
+
} else if (truncation.truncated) {
|
|
1397
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1398
|
+
details = { truncation };
|
|
1399
|
+
sourcePath = absolutePath;
|
|
1400
|
+
truncationInfo = {
|
|
1401
|
+
result: truncation,
|
|
1402
|
+
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
1403
|
+
};
|
|
1404
|
+
} else if (startLine + userLimitedLines < totalFileLines) {
|
|
1405
|
+
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
1406
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
1407
|
+
|
|
1408
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1409
|
+
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
1410
|
+
details = {};
|
|
1411
|
+
sourcePath = absolutePath;
|
|
1182
1412
|
} else {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
firstLineBytes,
|
|
1188
|
-
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1413
|
+
// No truncation, no user limit exceeded
|
|
1414
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1415
|
+
details = {};
|
|
1416
|
+
sourcePath = absolutePath;
|
|
1189
1417
|
}
|
|
1190
|
-
details = { truncation };
|
|
1191
|
-
sourcePath = absolutePath;
|
|
1192
|
-
truncationInfo = {
|
|
1193
|
-
result: truncation,
|
|
1194
|
-
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
1195
|
-
};
|
|
1196
|
-
} else if (truncation.truncated) {
|
|
1197
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1198
|
-
details = { truncation };
|
|
1199
|
-
sourcePath = absolutePath;
|
|
1200
|
-
truncationInfo = {
|
|
1201
|
-
result: truncation,
|
|
1202
|
-
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
1203
|
-
};
|
|
1204
|
-
} else if (startLine + userLimitedLines < totalFileLines) {
|
|
1205
|
-
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
1206
|
-
const nextOffset = startLine + userLimitedLines + 1;
|
|
1207
1418
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
sourcePath = absolutePath;
|
|
1212
|
-
} else {
|
|
1213
|
-
// No truncation, no user limit exceeded
|
|
1214
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1215
|
-
details = {};
|
|
1216
|
-
sourcePath = absolutePath;
|
|
1217
|
-
}
|
|
1419
|
+
if (capturedDisplayContent) {
|
|
1420
|
+
details.displayContent = capturedDisplayContent;
|
|
1421
|
+
}
|
|
1218
1422
|
|
|
1219
|
-
|
|
1220
|
-
details.displayContent = capturedDisplayContent;
|
|
1423
|
+
content = [{ type: "text", text: outputText }];
|
|
1221
1424
|
}
|
|
1222
|
-
|
|
1223
|
-
content = [{ type: "text", text: outputText }];
|
|
1224
1425
|
}
|
|
1225
1426
|
|
|
1226
1427
|
if (suffixResolution) {
|
|
@@ -1297,61 +1498,41 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1297
1498
|
limit: number | undefined,
|
|
1298
1499
|
signal?: AbortSignal,
|
|
1299
1500
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1300
|
-
const
|
|
1301
|
-
const
|
|
1501
|
+
const READ_DIRECTORY_MAX_DEPTH = 2;
|
|
1502
|
+
const READ_DIRECTORY_CHILD_LIMIT = 12;
|
|
1302
1503
|
|
|
1303
|
-
|
|
1504
|
+
throwIfAborted(signal);
|
|
1505
|
+
let tree: DirectoryTree;
|
|
1304
1506
|
try {
|
|
1305
|
-
|
|
1507
|
+
tree = await buildDirectoryTree(absolutePath, {
|
|
1508
|
+
maxDepth: READ_DIRECTORY_MAX_DEPTH,
|
|
1509
|
+
directoryEntryLimit: READ_DIRECTORY_CHILD_LIMIT,
|
|
1510
|
+
rootEntryLimit: null,
|
|
1511
|
+
lineCap: limit ?? null,
|
|
1512
|
+
lineCapProtectedDepth: 1,
|
|
1513
|
+
hidden: true,
|
|
1514
|
+
gitignore: false,
|
|
1515
|
+
cache: true,
|
|
1516
|
+
excludedDirectoryNames: READ_DIRECTORY_EXCLUDED_DIRS,
|
|
1517
|
+
rootLabel: ".",
|
|
1518
|
+
});
|
|
1306
1519
|
} catch (error) {
|
|
1307
1520
|
const message = error instanceof Error ? error.message : String(error);
|
|
1308
1521
|
throw new ToolError(`Cannot read directory: ${message}`);
|
|
1309
1522
|
}
|
|
1523
|
+
throwIfAborted(signal);
|
|
1310
1524
|
|
|
1311
|
-
|
|
1312
|
-
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
1313
|
-
|
|
1314
|
-
const listLimit = applyListLimit(entries, { limit: effectiveLimit });
|
|
1315
|
-
const limitedEntries = listLimit.items;
|
|
1316
|
-
const limitMeta = listLimit.meta;
|
|
1317
|
-
|
|
1318
|
-
// Format entries with directory indicators and ages
|
|
1319
|
-
const results: string[] = [];
|
|
1320
|
-
|
|
1321
|
-
for (const entry of limitedEntries) {
|
|
1322
|
-
throwIfAborted(signal);
|
|
1323
|
-
const fullPath = path.join(absolutePath, entry);
|
|
1324
|
-
let suffix = "";
|
|
1325
|
-
let age = "";
|
|
1326
|
-
|
|
1327
|
-
try {
|
|
1328
|
-
const entryStat = await fs.stat(fullPath);
|
|
1329
|
-
suffix = entryStat.isDirectory() ? "/" : "";
|
|
1330
|
-
const ageSeconds = Math.floor((Date.now() - entryStat.mtimeMs) / 1000);
|
|
1331
|
-
age = formatAge(ageSeconds);
|
|
1332
|
-
} catch {
|
|
1333
|
-
// Skip entries we can't stat
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
const line = age ? `${entry}${suffix} (${age})` : entry + suffix;
|
|
1338
|
-
results.push(line);
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
if (results.length === 0) {
|
|
1342
|
-
return { content: [{ type: "text", text: "(empty directory)" }], details: {} };
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
const output = results.join("\n");
|
|
1525
|
+
const output = tree.totalLines <= 1 ? "(empty directory)" : tree.rendered;
|
|
1346
1526
|
const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
1347
|
-
|
|
1348
1527
|
const details: ReadToolDetails = {
|
|
1349
1528
|
isDirectory: true,
|
|
1529
|
+
resolvedPath: tree.rootPath,
|
|
1350
1530
|
};
|
|
1351
1531
|
|
|
1352
|
-
const resultBuilder = toolResult(details)
|
|
1353
|
-
|
|
1354
|
-
.limits({ resultLimit:
|
|
1532
|
+
const resultBuilder = toolResult(details).text(truncation.content).sourcePath(tree.rootPath);
|
|
1533
|
+
if (tree.truncated) {
|
|
1534
|
+
resultBuilder.limits({ resultLimit: 1 });
|
|
1535
|
+
}
|
|
1355
1536
|
if (truncation.truncated) {
|
|
1356
1537
|
resultBuilder.truncation(truncation, { direction: "head" });
|
|
1357
1538
|
details.truncation = truncation;
|
|
@@ -1482,6 +1663,9 @@ export const readToolRenderer = {
|
|
|
1482
1663
|
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
|
1483
1664
|
title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
|
|
1484
1665
|
}
|
|
1666
|
+
if (details?.summary) {
|
|
1667
|
+
title += ` (summary: ${details.summary.elidedSpans} elided span${details.summary.elidedSpans === 1 ? "" : "s"})`;
|
|
1668
|
+
}
|
|
1485
1669
|
let cachedWidth: number | undefined;
|
|
1486
1670
|
let cachedLines: string[] | undefined;
|
|
1487
1671
|
return {
|