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