@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.12
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 +53 -0
- package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
- package/dist/cli.js +10775 -10762
- package/dist/types/advisor/runtime.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +11 -0
- package/dist/types/capability/skill.d.ts +6 -0
- package/dist/types/cli/command-help.d.ts +3 -0
- package/dist/types/commands/share.d.ts +25 -0
- package/dist/types/commit/agentic/index.d.ts +3 -1
- package/dist/types/commit/execute.d.ts +32 -0
- package/dist/types/commit/index.d.ts +2 -1
- package/dist/types/commit/pipeline.d.ts +7 -1
- package/dist/types/config/custom-models.d.ts +31 -0
- package/dist/types/config/model-config-values.d.ts +19 -0
- package/dist/types/config/model-patch.d.ts +93 -0
- package/dist/types/config/model-provider-discovery.d.ts +51 -0
- package/dist/types/config/model-registry.d.ts +9 -52
- package/dist/types/config/settings-schema.d.ts +1 -34
- package/dist/types/config/settings.d.ts +5 -3
- package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
- package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
- package/dist/types/discovery/agent-plugins.d.ts +1 -0
- package/dist/types/discovery/contained-path.d.ts +33 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/eval/executor-base.d.ts +8 -2
- package/dist/types/eval/kernel-session-registry.d.ts +60 -0
- package/dist/types/export/share.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -2
- package/dist/types/extensibility/skills.d.ts +5 -0
- package/dist/types/lsp/diagnostics.d.ts +96 -0
- package/dist/types/lsp/index.d.ts +7 -128
- package/dist/types/lsp/servers.d.ts +72 -0
- package/dist/types/lsp/tool.d.ts +42 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
- package/dist/types/lsp/writethrough.d.ts +33 -0
- package/dist/types/mcp/transports/header-policy.d.ts +46 -0
- package/dist/types/mcp/types.d.ts +15 -0
- package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
- package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
- package/dist/types/modes/components/agent-hub.d.ts +2 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -2
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/theme/color.d.ts +19 -0
- package/dist/types/modes/theme/loader.d.ts +19 -0
- package/dist/types/modes/theme/schema.d.ts +175 -0
- package/dist/types/modes/theme/symbols.d.ts +28 -0
- package/dist/types/modes/theme/theme-class.d.ts +244 -0
- package/dist/types/modes/theme/theme.d.ts +9 -280
- package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
- package/dist/types/registry/agent-registry.d.ts +1 -3
- package/dist/types/secrets/index.d.ts +26 -2
- package/dist/types/secrets/message-transform.d.ts +40 -0
- package/dist/types/secrets/obfuscator.d.ts +0 -108
- package/dist/types/secrets/placeholder-scan.d.ts +95 -0
- package/dist/types/secrets/placeholder.d.ts +94 -0
- package/dist/types/secrets/replacement.d.ts +82 -0
- package/dist/types/session/agent-session-events.d.ts +2 -2
- package/dist/types/session/agent-session-types.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/prewalk.d.ts +3 -1
- package/dist/types/session/session-handoff.d.ts +3 -3
- package/dist/types/session/session-manager.d.ts +32 -0
- package/dist/types/session/session-provider-boundary.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +8 -0
- package/dist/types/session/turn-recovery.d.ts +9 -4
- package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
- package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
- package/dist/types/slash-commands/builtin-control.d.ts +2 -0
- package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
- package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
- package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/builtin-session.d.ts +2 -0
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/tools/gh-common.d.ts +69 -0
- package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
- package/dist/types/tools/gh-pr-diff.d.ts +102 -0
- package/dist/types/tools/gh-run-watch.d.ts +78 -0
- package/dist/types/tools/gh-search.d.ts +61 -0
- package/dist/types/tools/gh-types.d.ts +340 -0
- package/dist/types/tools/gh-view.d.ts +88 -0
- package/dist/types/tools/gh.d.ts +4 -201
- package/dist/types/tools/read-archive.d.ts +16 -0
- package/dist/types/tools/read-format.d.ts +104 -0
- package/dist/types/tools/read-path-resolution.d.ts +19 -0
- package/dist/types/tools/read-pdf-images.d.ts +12 -0
- package/dist/types/tools/read-renderer.d.ts +24 -0
- package/dist/types/tools/read-selector.d.ts +27 -0
- package/dist/types/tools/read-sqlite.d.ts +16 -0
- package/dist/types/tools/read-summary.d.ts +19 -0
- package/dist/types/tools/read.d.ts +1 -23
- package/dist/types/tools/shell-tokenize.d.ts +18 -1
- package/dist/types/utils/changelog.d.ts +0 -5
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +4 -1
- package/src/advisor/runtime.ts +12 -7
- package/src/capability/mcp.ts +11 -0
- package/src/capability/skill.ts +6 -0
- package/src/cli/command-help.ts +4 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli-commands.ts +5 -0
- package/src/commands/commit.ts +16 -3
- package/src/commands/share.ts +71 -0
- package/src/commit/agentic/index.ts +39 -21
- package/src/commit/execute.ts +56 -0
- package/src/commit/index.ts +2 -1
- package/src/commit/pipeline.ts +20 -7
- package/src/config/custom-models.ts +188 -0
- package/src/config/model-config-values.ts +128 -0
- package/src/config/model-discovery.ts +1 -1
- package/src/config/model-patch.ts +252 -0
- package/src/config/model-provider-discovery.ts +132 -0
- package/src/config/model-registry.ts +84 -704
- package/src/config/settings-schema.ts +2 -33
- package/src/config/settings.ts +53 -3
- package/src/debug/raw-sse-buffer.ts +20 -0
- package/src/discovery/agent-plugin-format.ts +551 -0
- package/src/discovery/agent-plugins.ts +341 -0
- package/src/discovery/claude-plugins.ts +21 -5
- package/src/discovery/contained-path.ts +75 -0
- package/src/discovery/helpers.ts +23 -9
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-plugins.ts +20 -7
- package/src/edit/hashline/diff.ts +5 -1
- package/src/eval/executor-base.ts +39 -6
- package/src/eval/jl/executor.ts +82 -371
- package/src/eval/kernel-session-registry.ts +398 -0
- package/src/eval/py/executor.ts +53 -261
- package/src/eval/rb/executor.ts +36 -279
- package/src/exec/non-interactive-env.ts +1 -0
- package/src/export/share.ts +2 -1
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +78 -14
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +12 -0
- package/src/extensibility/plugins/marketplace/manager.ts +2 -1
- package/src/extensibility/shared-events.ts +3 -2
- package/src/extensibility/skills.ts +9 -0
- package/src/internal-urls/skill-protocol.ts +14 -0
- package/src/internal-urls/vault-protocol.ts +2 -2
- package/src/launch/client.ts +11 -1
- package/src/launch/protocol.ts +7 -2
- package/src/lsp/diagnostics.ts +516 -0
- package/src/lsp/index.ts +20 -2819
- package/src/lsp/servers.ts +296 -0
- package/src/lsp/tool.ts +1352 -0
- package/src/lsp/workspace-diagnostics.ts +170 -0
- package/src/lsp/writethrough.ts +561 -0
- package/src/main.ts +3 -2
- package/src/mcp/config.ts +3 -0
- package/src/mcp/manager.ts +12 -9
- package/src/mcp/transports/header-policy.ts +95 -0
- package/src/mcp/transports/http.ts +35 -52
- package/src/mcp/transports/sse.ts +20 -27
- package/src/mcp/types.ts +15 -0
- package/src/modes/acp/acp-agent.ts +15 -4
- package/src/modes/components/agent-dashboard.ts +2 -0
- package/src/modes/components/agent-hub-projection.ts +2 -2
- package/src/modes/components/agent-hub-renderer.ts +3 -7
- package/src/modes/components/agent-hub.ts +5 -0
- package/src/modes/components/agent-transcript-viewer.ts +3 -0
- package/src/modes/components/chat-transcript-builder.ts +3 -0
- package/src/modes/components/custom-editor.ts +0 -9
- package/src/modes/components/status-line/component.ts +1 -0
- package/src/modes/components/status-line/segments.ts +6 -4
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tool-execution.ts +13 -16
- package/src/modes/components/tree-selector.ts +62 -3
- package/src/modes/controllers/command-controller.ts +8 -2
- package/src/modes/controllers/event-controller.ts +15 -15
- package/src/modes/controllers/input-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/theme/color.ts +133 -0
- package/src/modes/theme/loader.ts +178 -0
- package/src/modes/theme/schema.ts +263 -0
- package/src/modes/theme/symbols.ts +1000 -0
- package/src/modes/theme/theme-class.ts +611 -0
- package/src/modes/theme/theme.ts +27 -2453
- package/src/modes/theme/tui-adapters.ts +279 -0
- package/src/modes/utils/transcript-render-helpers.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/registry/agent-registry.ts +2 -2
- package/src/sdk.ts +6 -49
- package/src/secrets/index.ts +51 -6
- package/src/secrets/message-transform.ts +287 -0
- package/src/secrets/obfuscator.ts +39 -1303
- package/src/secrets/placeholder-scan.ts +506 -0
- package/src/secrets/placeholder.ts +309 -0
- package/src/secrets/replacement.ts +216 -0
- package/src/session/agent-session-events.ts +2 -2
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +146 -21
- package/src/session/indexed-session-storage.ts +7 -2
- package/src/session/messages.ts +1 -0
- package/src/session/prewalk.ts +57 -17
- package/src/session/session-advisors.ts +32 -34
- package/src/session/session-context.ts +3 -5
- package/src/session/session-handoff.ts +39 -16
- package/src/session/session-manager.ts +67 -3
- package/src/session/session-provider-boundary.ts +3 -6
- package/src/session/session-tools.ts +37 -3
- package/src/session/todo-tracker.ts +11 -1
- package/src/session/turn-recovery.ts +115 -84
- package/src/slash-commands/builtin-collaboration.ts +504 -0
- package/src/slash-commands/builtin-completions.ts +291 -0
- package/src/slash-commands/builtin-control.ts +78 -0
- package/src/slash-commands/builtin-lifecycle.ts +506 -0
- package/src/slash-commands/builtin-marketplace.ts +567 -0
- package/src/slash-commands/builtin-modes.ts +518 -0
- package/src/slash-commands/builtin-registry.ts +21 -2996
- package/src/slash-commands/builtin-session.ts +592 -0
- package/src/task/executor.ts +17 -14
- package/src/task/index.ts +21 -6
- package/src/tools/bash-skill-urls.ts +15 -0
- package/src/tools/bash.ts +10 -11
- package/src/tools/debug.ts +1 -1
- package/src/tools/gh-common.ts +288 -0
- package/src/tools/gh-pr-checkout.ts +679 -0
- package/src/tools/gh-pr-diff.ts +473 -0
- package/src/tools/gh-run-watch.ts +1015 -0
- package/src/tools/gh-search.ts +500 -0
- package/src/tools/gh-types.ts +379 -0
- package/src/tools/gh-view.ts +612 -0
- package/src/tools/gh.ts +109 -3821
- package/src/tools/jtd-to-json-schema.ts +123 -21
- package/src/tools/read-archive.ts +209 -0
- package/src/tools/read-format.ts +593 -0
- package/src/tools/read-path-resolution.ts +36 -0
- package/src/tools/read-pdf-images.ts +250 -0
- package/src/tools/read-renderer.ts +289 -0
- package/src/tools/read-selector.ts +84 -0
- package/src/tools/read-sqlite.ts +215 -0
- package/src/tools/read-summary.ts +199 -0
- package/src/tools/read.ts +82 -1824
- package/src/tools/shell-tokenize.ts +99 -1
- package/src/utils/changelog.ts +1 -1
- package/src/utils/title-generator.ts +3 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/zai.ts +12 -3
package/src/tools/read.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
1
|
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
2
|
import * as path from "node:path";
|
|
5
|
-
import { formatHashlineHeader, formatNumberedLine, formatNumberedLines } from "@oh-my-pi/hashline";
|
|
6
3
|
import { type } from "@oh-my-pi/omptype";
|
|
7
4
|
import type {
|
|
8
5
|
AgentTool,
|
|
@@ -12,51 +9,31 @@ import type {
|
|
|
12
9
|
ToolTier,
|
|
13
10
|
} from "@oh-my-pi/pi-agent-core";
|
|
14
11
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
15
|
-
import { type
|
|
16
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
17
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
18
|
-
import {
|
|
19
|
-
getRemoteDir,
|
|
20
|
-
type ImageMetadata,
|
|
21
|
-
isEexist,
|
|
22
|
-
isEnotempty,
|
|
23
|
-
isProbablyBinary,
|
|
24
|
-
logger,
|
|
25
|
-
prompt,
|
|
26
|
-
readImageMetadata,
|
|
27
|
-
untilAborted,
|
|
28
|
-
} from "@oh-my-pi/pi-utils";
|
|
29
|
-
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
12
|
+
import { type ImageMetadata, isProbablyBinary, logger, prompt, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
30
13
|
import {
|
|
31
14
|
canonicalSnapshotKey,
|
|
32
15
|
getFileSnapshotStore,
|
|
33
16
|
recordFileSnapshot,
|
|
34
|
-
recordSeenLines,
|
|
35
17
|
recordSeenLinesFromBody,
|
|
36
18
|
SNAPSHOT_MAX_BYTES,
|
|
37
19
|
} from "../edit/file-snapshot-store";
|
|
38
20
|
import { normalizeToLF } from "../edit/normalize";
|
|
39
21
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
40
|
-
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
41
22
|
import { InternalUrlRouter, resolveLocalUrlToFile, resolveLocalUrlToPath } from "../internal-urls";
|
|
42
23
|
import { type ResolvedArtifactFile, resolveArtifactFile } from "../internal-urls/artifact-protocol";
|
|
43
24
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
44
25
|
import type { InternalUrl } from "../internal-urls/types";
|
|
45
|
-
import { getLanguageFromPath, isMarkdownPath, type Theme } from "../modes/theme/theme";
|
|
46
26
|
import readDescription from "../prompts/tools/read.md" with { type: "text" };
|
|
47
27
|
import type { ToolSession } from "../sdk";
|
|
48
28
|
import {
|
|
49
29
|
DEFAULT_MAX_BYTES,
|
|
50
30
|
DEFAULT_MAX_LINES,
|
|
51
|
-
noTruncResult,
|
|
52
31
|
type TruncationResult,
|
|
53
32
|
truncateHead,
|
|
54
33
|
truncateHeadBytes,
|
|
55
34
|
truncateLine,
|
|
56
35
|
} from "../session/streaming-output";
|
|
57
|
-
import {
|
|
58
|
-
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
59
|
-
import { buildLineEntriesWithBlockContext, type LineEntry, lineEntriesToPlainText } from "../utils/block-context";
|
|
36
|
+
import { buildLineEntriesWithBlockContext, lineEntriesToPlainText } from "../utils/block-context";
|
|
60
37
|
import { isCpuProfilePath, renderCpuProfile } from "../utils/cpuprofile";
|
|
61
38
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
62
39
|
import {
|
|
@@ -68,7 +45,6 @@ import {
|
|
|
68
45
|
import { isInspectImageToolActive } from "../utils/inspect-image-mode";
|
|
69
46
|
import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit";
|
|
70
47
|
import { isSampleProfilePath, renderSampleProfile } from "../utils/sample-profile";
|
|
71
|
-
import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "../utils/zip";
|
|
72
48
|
import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
|
|
73
49
|
import {
|
|
74
50
|
type ConflictEntry,
|
|
@@ -81,29 +57,12 @@ import {
|
|
|
81
57
|
scanConflictLines,
|
|
82
58
|
scanFileForConflicts,
|
|
83
59
|
} from "./conflict-detect";
|
|
84
|
-
import {
|
|
85
|
-
|
|
86
|
-
fetchReadUrl,
|
|
87
|
-
parseReadUrlTarget,
|
|
88
|
-
type ReadUrlToolDetails,
|
|
89
|
-
renderReadUrlCall,
|
|
90
|
-
renderReadUrlResult,
|
|
91
|
-
} from "./fetch";
|
|
92
|
-
import { applyListLimit } from "./list-limit";
|
|
93
|
-
import {
|
|
94
|
-
formatFullOutputReference,
|
|
95
|
-
formatStyledTruncationWarning,
|
|
96
|
-
type OutputMeta,
|
|
97
|
-
resolveOutputMaxColumns,
|
|
98
|
-
stripOutputNotice,
|
|
99
|
-
} from "./output-meta";
|
|
60
|
+
import { executeReadUrl, fetchReadUrl, parseReadUrlTarget } from "./fetch";
|
|
61
|
+
import { type OutputMeta, resolveOutputMaxColumns } from "./output-meta";
|
|
100
62
|
import {
|
|
101
63
|
expandPath,
|
|
102
|
-
findUniqueWorkspaceSuffix,
|
|
103
64
|
formatPathRelativeToCwd,
|
|
104
|
-
isReadableUrlPath,
|
|
105
65
|
type LineRange,
|
|
106
|
-
parseLineRanges,
|
|
107
66
|
pathTargetsSsh,
|
|
108
67
|
probeLiteralPathExists,
|
|
109
68
|
resolveReadPath,
|
|
@@ -112,67 +71,50 @@ import {
|
|
|
112
71
|
splitPathAndSel,
|
|
113
72
|
splitPathAndSelPreferringLiteral,
|
|
114
73
|
} from "./path-utils";
|
|
115
|
-
import {
|
|
74
|
+
import { readArchive, resolveArchiveReadPath } from "./read-archive";
|
|
75
|
+
import {
|
|
76
|
+
BRACKET_CONTEXT_ELLIPSIS,
|
|
77
|
+
buildInMemoryMultiRangeResult,
|
|
78
|
+
buildInMemoryTextResult,
|
|
79
|
+
contiguousLineNumbers,
|
|
80
|
+
countTextLines,
|
|
81
|
+
formatLineEntriesWithMode,
|
|
82
|
+
formatReadHashlineHeader,
|
|
83
|
+
formatSummaryElisionFooter,
|
|
84
|
+
formatTextWithMode,
|
|
85
|
+
type HashlineHeaderContext,
|
|
86
|
+
hashlineHeaderContext,
|
|
87
|
+
lineNumbersFromSpans,
|
|
88
|
+
markMarkdownContentType,
|
|
89
|
+
prependHashlineHeader,
|
|
90
|
+
prependSuffixResolutionNotice,
|
|
91
|
+
RANGE_LEADING_CONTEXT_LINES,
|
|
92
|
+
RANGE_TRAILING_CONTEXT_LINES,
|
|
93
|
+
READ_CHUNK_SIZE,
|
|
94
|
+
readHashlineHeaderContext,
|
|
95
|
+
} from "./read-format";
|
|
96
|
+
import {
|
|
97
|
+
findSuffixMatchCached,
|
|
98
|
+
isNotFoundError,
|
|
99
|
+
isRemoteMountPath,
|
|
100
|
+
type SuffixMatchCache,
|
|
101
|
+
} from "./read-path-resolution";
|
|
102
|
+
import { readPdfImageMember, rewritePdfImagePlaceholders, splitPdfImageMemberReadPath } from "./read-pdf-images";
|
|
103
|
+
import { isMultiRange, isRawSelector, type ParsedSelector, parseSel, selToOffsetLimit } from "./read-selector";
|
|
104
|
+
import { readSqlite, resolveSqliteReadPath } from "./read-sqlite";
|
|
105
|
+
import { isProseSummaryPath, renderSummary, routeReadThroughBridge, trySummarize } from "./read-summary";
|
|
106
|
+
import { formatBytes, shortenPath } from "./render-utils";
|
|
116
107
|
import { REPORT_ISSUE_DEVICE_NAME, reportIssueDeviceUsage } from "./report-tool-issue";
|
|
117
108
|
import { isResolutionDeviceName, resolutionDeviceUsage } from "./resolve";
|
|
118
|
-
import {
|
|
119
|
-
executeReadQuery,
|
|
120
|
-
getRowByKey,
|
|
121
|
-
getRowByRowId,
|
|
122
|
-
getTableSchema,
|
|
123
|
-
isSqliteFile,
|
|
124
|
-
listTables,
|
|
125
|
-
MAX_RAW_QUERY_ROWS,
|
|
126
|
-
parseSqlitePathCandidates,
|
|
127
|
-
parseSqliteSelector,
|
|
128
|
-
queryRows,
|
|
129
|
-
renderRow,
|
|
130
|
-
renderSchema,
|
|
131
|
-
renderTable,
|
|
132
|
-
renderTableList,
|
|
133
|
-
resolveTableRowLookup,
|
|
134
|
-
} from "./sqlite-reader";
|
|
135
109
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
136
110
|
import { toolResult } from "./tool-result";
|
|
137
111
|
import { xdevDocs, xdevListing } from "./xdev";
|
|
138
112
|
|
|
139
|
-
|
|
140
|
-
// of (code, path, fold settings) but costs ~12-18ms for a ~1500-line file, and a
|
|
141
|
-
// repeat summary read of the same unchanged file re-parses from scratch. Key on
|
|
142
|
-
// the content hash of the freshly-read bytes (+ path + fold settings): the file
|
|
143
|
-
// is still read fresh on every call, so a hit only reuses the deterministic
|
|
144
|
-
// parse — there is no staleness window and no stat guard is needed. Bounded LRU,
|
|
145
|
-
// aged out with the session via WeakMap.
|
|
146
|
-
// Unusable results (not parsed, or nothing elided) are memoized as `false`: the
|
|
147
|
-
// full SummaryResult embeds the whole source in kept segments, and the caller
|
|
148
|
-
// only ever renders `parsed && elided` summaries — caching the segments would
|
|
149
|
-
// retain up to 48 near-2MiB sources just to remember "no summary".
|
|
150
|
-
const SUMMARY_CACHE_MAX = 48;
|
|
151
|
-
const summaryParseCaches = new WeakMap<object, LRUCache<string, SummaryResult | false>>();
|
|
152
|
-
function getSummaryParseCache(session: object): LRUCache<string, SummaryResult | false> {
|
|
153
|
-
let cache = summaryParseCaches.get(session);
|
|
154
|
-
if (!cache) {
|
|
155
|
-
cache = new LRUCache<string, SummaryResult | false>({ max: SUMMARY_CACHE_MAX });
|
|
156
|
-
summaryParseCaches.set(session, cache);
|
|
157
|
-
}
|
|
158
|
-
return cache;
|
|
159
|
-
}
|
|
113
|
+
export { readToolRenderer } from "./read-renderer";
|
|
160
114
|
|
|
161
|
-
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
162
115
|
/** Largest profile (`*.sample.txt`, `*.cpuprofile`) converted to a bottleneck summary; bigger files read as plain text. */
|
|
163
116
|
const MAX_PROFILE_SUMMARY_BYTES = 32 * 1024 * 1024;
|
|
164
|
-
const MAX_SUMMARY_LINES = 20_000;
|
|
165
117
|
const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
|
|
166
|
-
/**
|
|
167
|
-
* Prose files (Markdown flavors and plain text) skip code-block summarization
|
|
168
|
-
* unless `read.summarize.prose` opts them in.
|
|
169
|
-
*/
|
|
170
|
-
function isProseSummaryPath(filePath: string): boolean {
|
|
171
|
-
return isMarkdownPath(filePath) || path.extname(filePath).toLowerCase() === ".txt";
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
|
|
175
|
-
const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
|
|
176
118
|
|
|
177
119
|
async function readBracketContextFullLines(absolutePath: string, fileSize: number): Promise<string[] | undefined> {
|
|
178
120
|
if (fileSize > SNAPSHOT_MAX_BYTES) return undefined;
|
|
@@ -183,262 +125,6 @@ async function readBracketContextFullLines(absolutePath: string, fileSize: numbe
|
|
|
183
125
|
}
|
|
184
126
|
}
|
|
185
127
|
|
|
186
|
-
function isRemoteMountPath(absolutePath: string): boolean {
|
|
187
|
-
return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function prependLineNumbers(text: string, startNum: number): string {
|
|
191
|
-
const textLines = text.split("\n");
|
|
192
|
-
return textLines.map((line, i) => `${startNum + i}|${line}`).join("\n");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
interface HashlineHeaderContext {
|
|
196
|
-
header: string;
|
|
197
|
-
tag: string;
|
|
198
|
-
fullText?: string;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function formatReadHashlineHeader(displayPath: string, tag: string): string {
|
|
202
|
-
// In-workspace reads collapse to the bare filename for brevity: the edit
|
|
203
|
-
// tool's snapshot-tag recovery rebinds a bare `[name#tag]` onto the in-tree
|
|
204
|
-
// file it uniquely names. Out-of-workspace reads can't lean on that —
|
|
205
|
-
// recovery refuses to redirect a write outside the cwd/sandbox
|
|
206
|
-
// (HashlineFilesystem.allowTagPathRecovery) — so an absolute displayPath
|
|
207
|
-
// must stay directly resolvable, otherwise the basename resolves against
|
|
208
|
-
// cwd, misses, and the edit fails with "File not found" (e.g. ~/.claude/*).
|
|
209
|
-
// `shortenPath` keeps `~/.claude/...` (round-trips through resolveToCwd's ~
|
|
210
|
-
// expansion) instead of leaking the full home path into the read output.
|
|
211
|
-
const anchor = path.isAbsolute(displayPath) ? shortenPath(displayPath) : path.basename(displayPath);
|
|
212
|
-
return formatHashlineHeader(anchor, tag);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function recordFullHashlineContext(
|
|
216
|
-
session: ToolSession,
|
|
217
|
-
absolutePath: string | undefined,
|
|
218
|
-
displayPath: string,
|
|
219
|
-
fullText: string,
|
|
220
|
-
): HashlineHeaderContext | undefined {
|
|
221
|
-
if (!absolutePath || !path.isAbsolute(absolutePath)) return undefined;
|
|
222
|
-
const normalized = normalizeToLF(fullText);
|
|
223
|
-
const tag = getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalized);
|
|
224
|
-
return {
|
|
225
|
-
header: formatReadHashlineHeader(displayPath, tag),
|
|
226
|
-
tag,
|
|
227
|
-
fullText: normalized,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function readHashlineHeaderContext(
|
|
232
|
-
session: ToolSession,
|
|
233
|
-
absolutePath: string,
|
|
234
|
-
cwd: string,
|
|
235
|
-
): Promise<HashlineHeaderContext> {
|
|
236
|
-
const fullText = await Bun.file(absolutePath).text();
|
|
237
|
-
const context = recordFullHashlineContext(
|
|
238
|
-
session,
|
|
239
|
-
absolutePath,
|
|
240
|
-
formatPathRelativeToCwd(absolutePath, cwd),
|
|
241
|
-
fullText,
|
|
242
|
-
);
|
|
243
|
-
if (!context) throw new ToolError(`Cannot record hashline snapshot for non-absolute path: ${absolutePath}`);
|
|
244
|
-
return context;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function hashlineHeaderContext(displayPath: string, tag: string): HashlineHeaderContext {
|
|
248
|
-
return { header: formatReadHashlineHeader(displayPath, tag), tag };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function prependHashlineHeader(text: string, context: HashlineHeaderContext | undefined): string {
|
|
252
|
-
return context ? `${context.header}\n${text}` : text;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function formatTextWithMode(
|
|
256
|
-
text: string,
|
|
257
|
-
startNum: number,
|
|
258
|
-
shouldAddHashLines: boolean,
|
|
259
|
-
shouldAddLineNumbers: boolean,
|
|
260
|
-
): string {
|
|
261
|
-
if (shouldAddHashLines) return formatNumberedLines(text, startNum);
|
|
262
|
-
if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
|
|
263
|
-
return text;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const BRACKET_CONTEXT_ELLIPSIS = "…";
|
|
267
|
-
|
|
268
|
-
function formatLineEntryWithMode(entry: LineEntry, shouldAddHashLines: boolean, shouldAddLineNumbers: boolean): string {
|
|
269
|
-
if (entry.kind === "ellipsis") return BRACKET_CONTEXT_ELLIPSIS;
|
|
270
|
-
return formatSingleLine(entry.lineNumber, entry.text, shouldAddHashLines, shouldAddLineNumbers);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function formatLineEntriesWithMode(
|
|
274
|
-
entries: readonly LineEntry[],
|
|
275
|
-
shouldAddHashLines: boolean,
|
|
276
|
-
shouldAddLineNumbers: boolean,
|
|
277
|
-
): string {
|
|
278
|
-
return entries.map(entry => formatLineEntryWithMode(entry, shouldAddHashLines, shouldAddLineNumbers)).join("\n");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const BRACE_PAIRS: Record<string, string> = { "{": "}", "(": ")", "[": "]" };
|
|
282
|
-
const BRACE_TAIL_TRAILING_RE = /^[;,)\]}]*$/;
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Decide whether the kept lines surrounding an elided range collapse to a
|
|
286
|
-
* single brace-pair line in the rendered summary. Returns true when the head
|
|
287
|
-
* line ends with `{` / `(` / `[` and the tail line is the matching closer
|
|
288
|
-
* (optionally followed by terminating punctuation like `;`, `,`, or further
|
|
289
|
-
* closers — e.g. `};`, `})`, `]);`).
|
|
290
|
-
*/
|
|
291
|
-
function canMergeBracePair(headLine: string, tailLine: string): boolean {
|
|
292
|
-
const head = headLine.trimEnd();
|
|
293
|
-
const tail = tailLine.trim();
|
|
294
|
-
const opener = head.slice(-1);
|
|
295
|
-
const closer = BRACE_PAIRS[opener];
|
|
296
|
-
if (!closer) return false;
|
|
297
|
-
if (!tail.startsWith(closer)) return false;
|
|
298
|
-
return BRACE_TAIL_TRAILING_RE.test(tail.slice(closer.length));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function formatSingleLine(
|
|
302
|
-
line: number,
|
|
303
|
-
text: string,
|
|
304
|
-
shouldAddHashLines: boolean,
|
|
305
|
-
shouldAddLineNumbers: boolean,
|
|
306
|
-
): string {
|
|
307
|
-
if (shouldAddHashLines) return formatNumberedLine(line, text);
|
|
308
|
-
if (shouldAddLineNumbers) return `${line}|${text}`;
|
|
309
|
-
return text;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function formatMergedBraceLine(
|
|
313
|
-
startLine: number,
|
|
314
|
-
endLine: number,
|
|
315
|
-
headText: string,
|
|
316
|
-
tailText: string,
|
|
317
|
-
shouldAddHashLines: boolean,
|
|
318
|
-
shouldAddLineNumbers: boolean,
|
|
319
|
-
): { model: string; display: string } {
|
|
320
|
-
const merged = `${headText.trimEnd()} … ${tailText.trim()}`;
|
|
321
|
-
if (shouldAddHashLines) {
|
|
322
|
-
return { model: `${startLine}-${endLine}:${merged}`, display: merged };
|
|
323
|
-
}
|
|
324
|
-
if (shouldAddLineNumbers) {
|
|
325
|
-
return { model: `${startLine}-${endLine}|${merged}`, display: merged };
|
|
326
|
-
}
|
|
327
|
-
return { model: merged, display: merged };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function countTextLines(text: string): number {
|
|
331
|
-
if (text.length === 0) return 0;
|
|
332
|
-
// Count newlines directly instead of allocating an array via split("\n").
|
|
333
|
-
// Called on every read of file content; the result is identical (N newlines
|
|
334
|
-
// ⇒ N+1 lines for non-empty text).
|
|
335
|
-
let lines = 1;
|
|
336
|
-
for (let i = 0; i < text.length; i++) {
|
|
337
|
-
if (text.charCodeAt(i) === 10) lines++;
|
|
338
|
-
}
|
|
339
|
-
return lines;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function contiguousLineNumbers(startLine: number, count: number): number[] {
|
|
343
|
-
const lines: number[] = [];
|
|
344
|
-
for (let offset = 0; offset < count; offset++) lines.push(startLine + offset);
|
|
345
|
-
return lines;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function lineNumbersFromSpans(spans: readonly { startLine: number; endLine: number }[]): number[] {
|
|
349
|
-
const lines: number[] = [];
|
|
350
|
-
for (const span of spans) {
|
|
351
|
-
for (let line = span.startLine; line <= span.endLine; line++) lines.push(line);
|
|
352
|
-
}
|
|
353
|
-
return lines;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function recordInMemorySeenLines(
|
|
357
|
-
session: ToolSession,
|
|
358
|
-
absolutePath: string | undefined,
|
|
359
|
-
fullText: string,
|
|
360
|
-
seenLines: readonly number[] | undefined,
|
|
361
|
-
): void {
|
|
362
|
-
if (!absolutePath || !path.isAbsolute(absolutePath) || !seenLines || seenLines.length === 0) return;
|
|
363
|
-
getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalizeToLF(fullText), seenLines);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function lineNumbersFromEntries(entries: readonly LineEntry[]): number[] {
|
|
367
|
-
const lines: number[] = [];
|
|
368
|
-
for (const entry of entries) {
|
|
369
|
-
if (entry.kind === "line") lines.push(entry.lineNumber);
|
|
370
|
-
}
|
|
371
|
-
return lines;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** Inclusive line range describing one elided span in a structural summary. */
|
|
375
|
-
interface ElidedRange {
|
|
376
|
-
start: number;
|
|
377
|
-
end: number;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Sample ranges shown in the footer to demonstrate the multi-range syntax. */
|
|
381
|
-
const FOOTER_RANGE_SAMPLES = 2;
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Footer appended to summarized reads telling the model how to recover the
|
|
385
|
-
* elided body. Without this hint, agents either ignore the `…`/`{ … }`
|
|
386
|
-
* markers or burn a turn guessing the right selector (see issue #1046). The
|
|
387
|
-
* footer demonstrates the multi-range selector syntax with concrete sample
|
|
388
|
-
* ranges drawn from the actual elision so the model re-reads only what it
|
|
389
|
-
* needs instead of falling back to `:raw` or whole-file reads.
|
|
390
|
-
*/
|
|
391
|
-
function formatSummaryElisionFooter(
|
|
392
|
-
readPath: string,
|
|
393
|
-
elidedRanges: ReadonlyArray<ElidedRange>,
|
|
394
|
-
elidedLines: number,
|
|
395
|
-
): string {
|
|
396
|
-
if (elidedRanges.length === 0) return "";
|
|
397
|
-
const sampleCount = Math.min(elidedRanges.length, FOOTER_RANGE_SAMPLES);
|
|
398
|
-
const selector = elidedRanges
|
|
399
|
-
.slice(0, sampleCount)
|
|
400
|
-
.map(r => `${r.start}-${r.end}`)
|
|
401
|
-
.join(",");
|
|
402
|
-
const example = `${readPath}:${selector}`;
|
|
403
|
-
const tail = elidedRanges.length > sampleCount ? `, e.g. ${example}` : ` with ${example}`;
|
|
404
|
-
return `[…${elidedLines}ln elided; re-read needed ranges${tail}]`;
|
|
405
|
-
}
|
|
406
|
-
const READ_CHUNK_SIZE = 8 * 1024;
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Context lines added around an explicit range read. Anchor-stale failures
|
|
410
|
-
* cluster on edits whose anchors land just outside the most recent read
|
|
411
|
-
* window, but the data (`scripts/session-stats/analyze_selector_reads.py`)
|
|
412
|
-
* shows most follow-up reads are disjoint hops, not adjacent extensions —
|
|
413
|
-
* so symmetric padding rarely pays for itself.
|
|
414
|
-
*
|
|
415
|
-
* Leading=1 catches accidental single-line reads where the anchor is the
|
|
416
|
-
* line immediately above the requested start. Trailing=3 buffers the
|
|
417
|
-
* common case where the agent asks for a narrow range and then needs the
|
|
418
|
-
* next few lines to disambiguate an anchor.
|
|
419
|
-
*/
|
|
420
|
-
const RANGE_LEADING_CONTEXT_LINES = 1;
|
|
421
|
-
const RANGE_TRAILING_CONTEXT_LINES = 3;
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Expand a [start, end) range with leading/trailing context lines on the
|
|
425
|
-
* sides where the user actually constrained the range. A start of 0 (no
|
|
426
|
-
* explicit offset) does not get leading context — that's already an
|
|
427
|
-
* open-ended read from the top.
|
|
428
|
-
*/
|
|
429
|
-
function expandRangeWithContext(
|
|
430
|
-
requestedStart: number,
|
|
431
|
-
requestedEnd: number,
|
|
432
|
-
totalLines: number,
|
|
433
|
-
expandStart: boolean,
|
|
434
|
-
expandEnd: boolean,
|
|
435
|
-
): { startLine: number; endLine: number } {
|
|
436
|
-
return {
|
|
437
|
-
startLine: expandStart ? Math.max(0, requestedStart - RANGE_LEADING_CONTEXT_LINES) : requestedStart,
|
|
438
|
-
endLine: expandEnd ? Math.min(totalLines, requestedEnd + RANGE_TRAILING_CONTEXT_LINES) : requestedEnd,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
128
|
async function streamLinesFromFile(
|
|
443
129
|
filePath: string,
|
|
444
130
|
startLine: number,
|
|
@@ -650,71 +336,6 @@ async function streamLinesFromFile(
|
|
|
650
336
|
// Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
|
|
651
337
|
const MAX_IMAGE_SIZE = MAX_IMAGE_INPUT_BYTES;
|
|
652
338
|
|
|
653
|
-
function isNotFoundError(error: unknown): boolean {
|
|
654
|
-
if (!error || typeof error !== "object") return false;
|
|
655
|
-
const code = (error as { code?: string }).code;
|
|
656
|
-
return code === "ENOENT" || code === "ENOTDIR";
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function decodeUtf8Text(bytes: Uint8Array): string | null {
|
|
660
|
-
if (bytes.indexOf(0) !== -1) return null;
|
|
661
|
-
|
|
662
|
-
try {
|
|
663
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
664
|
-
} catch {
|
|
665
|
-
return null;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function prependSuffixResolutionNotice(text: string, suffixResolution?: { from: string; to: string }): string {
|
|
670
|
-
if (!suffixResolution) return text;
|
|
671
|
-
|
|
672
|
-
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
673
|
-
return text ? `${notice}\n${text}` : notice;
|
|
674
|
-
}
|
|
675
|
-
const PDF_IMAGE_PLACEHOLDER_RE = /<!--\s*image:\s*([^\s<>]+)(.*?)-->/g;
|
|
676
|
-
const PDF_IMAGE_MEMBER_RE = /^(.*\.pdf):(.*)$/i;
|
|
677
|
-
const PDF_IMAGE_MEMBER_EXTENSION_RE = /\.png$/i;
|
|
678
|
-
const PDF_IMAGE_CACHE_BASENAME_MAX_LENGTH = 96;
|
|
679
|
-
|
|
680
|
-
interface PdfImageSnapshot {
|
|
681
|
-
directory: string;
|
|
682
|
-
filePath: string;
|
|
683
|
-
digest: string;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
interface PdfImageExtraction {
|
|
687
|
-
controller: AbortController;
|
|
688
|
-
promise: Promise<string>;
|
|
689
|
-
settled: boolean;
|
|
690
|
-
waiters: number;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const pdfImageExtractions = new Map<string, PdfImageExtraction>();
|
|
694
|
-
|
|
695
|
-
function pdfImageMemberPath(pdfPath: string, imageId: string): string {
|
|
696
|
-
const member = PDF_IMAGE_MEMBER_EXTENSION_RE.test(imageId) ? imageId : `${imageId}.png`;
|
|
697
|
-
return `${pdfPath}:${member}`;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function rewritePdfImagePlaceholders(markdown: string, pdfPath: string): string {
|
|
701
|
-
return markdown.replace(PDF_IMAGE_PLACEHOLDER_RE, (_match: string, imageId: string, metadataText: string) => {
|
|
702
|
-
const metadata = metadataText.trim();
|
|
703
|
-
const suffix = metadata.length > 0 ? ` (${metadata})` : "";
|
|
704
|
-
return `Image ${imageId}${suffix}: read \`${pdfImageMemberPath(pdfPath, imageId)}\``;
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function splitPdfImageMemberReadPath(readPath: string): { pdfPath: string; member: string } | null {
|
|
709
|
-
const match = PDF_IMAGE_MEMBER_RE.exec(readPath);
|
|
710
|
-
if (!match) return null;
|
|
711
|
-
const pdfPath = match[1];
|
|
712
|
-
const member = match[2];
|
|
713
|
-
if (pdfPath === undefined || member === undefined) return null;
|
|
714
|
-
if (member.length !== 0 && !PDF_IMAGE_MEMBER_EXTENSION_RE.test(member)) return null;
|
|
715
|
-
return { pdfPath, member };
|
|
716
|
-
}
|
|
717
|
-
|
|
718
339
|
const readSchema = type({
|
|
719
340
|
path: type("string").describe(
|
|
720
341
|
"Local path, internal URI (e.g. memory://, skill://), or URL. Inline selectors are supported.",
|
|
@@ -759,104 +380,6 @@ export interface ReadToolDetails {
|
|
|
759
380
|
}
|
|
760
381
|
type ReadParams = ReadToolInput;
|
|
761
382
|
|
|
762
|
-
/** Parsed representation of a path-embedded selector. */
|
|
763
|
-
type ParsedSelector =
|
|
764
|
-
| { kind: "none" }
|
|
765
|
-
| { kind: "raw" }
|
|
766
|
-
| { kind: "conflicts" }
|
|
767
|
-
| { kind: "lines"; ranges: [LineRange, ...LineRange[]]; raw?: boolean };
|
|
768
|
-
|
|
769
|
-
/** Returns true when the selector requested verbatim/raw output (alone or combined with a range). */
|
|
770
|
-
function isRawSelector(parsed: ParsedSelector): boolean {
|
|
771
|
-
return parsed.kind === "raw" || (parsed.kind === "lines" && parsed.raw === true);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/** Returns true when the selector requested multiple line ranges. */
|
|
775
|
-
function isMultiRange(parsed: ParsedSelector): boolean {
|
|
776
|
-
return parsed.kind === "lines" && parsed.ranges.length > 1;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function selectorChunkLooksReadLike(chunk: string): boolean {
|
|
780
|
-
const lower = chunk.toLowerCase();
|
|
781
|
-
return (
|
|
782
|
-
lower === "raw" || lower === "conflicts" || /^-\d+(?:[-+]\d+)?$/.test(chunk) || parseLineRanges(chunk) !== null
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function invalidSelector(sel: string): ToolError {
|
|
787
|
-
return new ToolError(
|
|
788
|
-
`Invalid selector ':${sel}'. Use :N, :N-M, :N+K, :N- (open-ended), a comma-separated list of ranges, :raw, or a range combined with raw (e.g. :raw:50-100).`,
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function parseSel(sel: string | undefined): ParsedSelector {
|
|
793
|
-
if (!sel || sel.length === 0) return { kind: "none" };
|
|
794
|
-
|
|
795
|
-
// Compound selector: `1-50:raw` or `raw:1-50`. Split into chunks and accept
|
|
796
|
-
// exactly one line range (possibly multi) plus the literal `raw`. Selector-like
|
|
797
|
-
// compounds that are not in that accepted set are invalid rather than "none";
|
|
798
|
-
// otherwise `read` can silently widen a malformed selector like
|
|
799
|
-
// `artifact://5:conflicts:1-1` while `grep` rejects it.
|
|
800
|
-
if (sel.includes(":")) {
|
|
801
|
-
const chunks = sel.split(":");
|
|
802
|
-
if (chunks.length === 2) {
|
|
803
|
-
const [a, b] = chunks as [string, string];
|
|
804
|
-
const aIsRaw = a.toLowerCase() === "raw";
|
|
805
|
-
const bIsRaw = b.toLowerCase() === "raw";
|
|
806
|
-
const rangeChunk = aIsRaw ? b : bIsRaw ? a : null;
|
|
807
|
-
const rawChunk = aIsRaw ? a : bIsRaw ? b : null;
|
|
808
|
-
if (rangeChunk !== null && rawChunk !== null) {
|
|
809
|
-
const ranges = parseLineRanges(rangeChunk);
|
|
810
|
-
if (ranges) {
|
|
811
|
-
return { kind: "lines", ranges, raw: true };
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (chunks.every(selectorChunkLooksReadLike)) throw invalidSelector(sel);
|
|
816
|
-
// Unrecognized compound — fall through (sqlite/archive/url consume their own colon syntax).
|
|
817
|
-
return { kind: "none" };
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
if (sel.toLowerCase() === "raw") return { kind: "raw" };
|
|
821
|
-
if (sel.toLowerCase() === "conflicts") return { kind: "conflicts" };
|
|
822
|
-
const ranges = parseLineRanges(sel);
|
|
823
|
-
if (ranges) {
|
|
824
|
-
return { kind: "lines", ranges };
|
|
825
|
-
}
|
|
826
|
-
// Unrecognized selectors fall through; sqlite/archive/url readers consume their own colon syntax.
|
|
827
|
-
return { kind: "none" };
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Convert a single-range selector to the offset/limit pair used by internal pagination.
|
|
832
|
-
* Returns the FIRST range only — multi-range callers MUST branch on `isMultiRange` before
|
|
833
|
-
* calling this helper.
|
|
834
|
-
*/
|
|
835
|
-
function selToOffsetLimit(parsed: ParsedSelector): { offset?: number; limit?: number } {
|
|
836
|
-
if (parsed.kind === "lines") {
|
|
837
|
-
const first = parsed.ranges[0];
|
|
838
|
-
const limit = first.endLine !== undefined ? first.endLine - first.startLine + 1 : undefined;
|
|
839
|
-
return { offset: first.startLine, limit };
|
|
840
|
-
}
|
|
841
|
-
return {};
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
interface ResolvedArchiveReadPath {
|
|
845
|
-
absolutePath: string;
|
|
846
|
-
archiveSubPath: string;
|
|
847
|
-
suffixResolution?: { from: string; to: string };
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
interface ResolvedSqliteReadPath {
|
|
851
|
-
absolutePath: string;
|
|
852
|
-
sqliteSubPath: string;
|
|
853
|
-
queryString: string;
|
|
854
|
-
suffixResolution?: { from: string; to: string };
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
/** Per-execute memo of suffix-glob lookups; `null` records a confirmed miss. */
|
|
858
|
-
type SuffixMatchCache = Map<string, { absolutePath: string; displayPath: string } | null>;
|
|
859
|
-
|
|
860
383
|
/**
|
|
861
384
|
* Read tool implementation.
|
|
862
385
|
*
|
|
@@ -1013,307 +536,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1013
536
|
return toolResult<ReadToolDetails>({ notes, displayReadTargets }).content(content).done();
|
|
1014
537
|
}
|
|
1015
538
|
|
|
1016
|
-
/**
|
|
1017
|
-
* Memoized {@link findUniqueWorkspaceSuffix} for a single read call. A missing
|
|
1018
|
-
* path with archive/sqlite extensions probes the workspace once per stage
|
|
1019
|
-
* (archive candidates, sqlite candidates, plain path) — each glob carries a
|
|
1020
|
-
* 5s timeout, so repeated lookups of the same string stack into a long
|
|
1021
|
-
* stall before erroring. The cache collapses repeats within one execute().
|
|
1022
|
-
*/
|
|
1023
|
-
async #findSuffixMatchCached(
|
|
1024
|
-
cache: SuffixMatchCache,
|
|
1025
|
-
rawPath: string,
|
|
1026
|
-
signal?: AbortSignal,
|
|
1027
|
-
): Promise<{ absolutePath: string; displayPath: string } | null> {
|
|
1028
|
-
const hit = cache.get(rawPath);
|
|
1029
|
-
if (hit !== undefined) return hit;
|
|
1030
|
-
const result = await findUniqueWorkspaceSuffix(rawPath, this.session.cwd, signal);
|
|
1031
|
-
cache.set(rawPath, result);
|
|
1032
|
-
return result;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
async #resolveArchiveReadPath(
|
|
1036
|
-
readPath: string,
|
|
1037
|
-
suffixCache: SuffixMatchCache,
|
|
1038
|
-
signal?: AbortSignal,
|
|
1039
|
-
): Promise<ResolvedArchiveReadPath | null> {
|
|
1040
|
-
const candidates = parseArchivePathCandidates(readPath);
|
|
1041
|
-
for (const candidate of candidates) {
|
|
1042
|
-
let absolutePath = resolveReadPath(candidate.archivePath, this.session.cwd);
|
|
1043
|
-
let suffixResolution: { from: string; to: string } | undefined;
|
|
1044
|
-
|
|
1045
|
-
try {
|
|
1046
|
-
const stat = await Bun.file(absolutePath).stat();
|
|
1047
|
-
if (stat.isDirectory()) continue;
|
|
1048
|
-
return {
|
|
1049
|
-
absolutePath,
|
|
1050
|
-
archiveSubPath: candidate.archivePath === readPath ? "" : candidate.subPath,
|
|
1051
|
-
suffixResolution,
|
|
1052
|
-
};
|
|
1053
|
-
} catch (error) {
|
|
1054
|
-
if (!isNotFoundError(error) || isRemoteMountPath(absolutePath)) continue;
|
|
1055
|
-
|
|
1056
|
-
const suffixMatch = await this.#findSuffixMatchCached(suffixCache, candidate.archivePath, signal);
|
|
1057
|
-
if (!suffixMatch) continue;
|
|
1058
|
-
|
|
1059
|
-
try {
|
|
1060
|
-
const retryStat = await Bun.file(suffixMatch.absolutePath).stat();
|
|
1061
|
-
if (retryStat.isDirectory()) continue;
|
|
1062
|
-
|
|
1063
|
-
absolutePath = suffixMatch.absolutePath;
|
|
1064
|
-
suffixResolution = { from: candidate.archivePath, to: suffixMatch.displayPath };
|
|
1065
|
-
return {
|
|
1066
|
-
absolutePath,
|
|
1067
|
-
archiveSubPath: candidate.archivePath === readPath ? "" : candidate.subPath,
|
|
1068
|
-
suffixResolution,
|
|
1069
|
-
};
|
|
1070
|
-
} catch (retryError) {
|
|
1071
|
-
if (!isNotFoundError(retryError)) {
|
|
1072
|
-
throw retryError;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
return null;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
async #resolveSqliteReadPath(
|
|
1082
|
-
readPath: string,
|
|
1083
|
-
suffixCache: SuffixMatchCache,
|
|
1084
|
-
signal?: AbortSignal,
|
|
1085
|
-
): Promise<ResolvedSqliteReadPath | null> {
|
|
1086
|
-
const candidates = parseSqlitePathCandidates(readPath);
|
|
1087
|
-
for (const candidate of candidates) {
|
|
1088
|
-
let absolutePath = resolveReadPath(candidate.sqlitePath, this.session.cwd);
|
|
1089
|
-
let suffixResolution: { from: string; to: string } | undefined;
|
|
1090
|
-
|
|
1091
|
-
try {
|
|
1092
|
-
const stat = await Bun.file(absolutePath).stat();
|
|
1093
|
-
if (stat.isDirectory()) continue;
|
|
1094
|
-
if (!(await isSqliteFile(absolutePath))) continue;
|
|
1095
|
-
|
|
1096
|
-
return {
|
|
1097
|
-
absolutePath,
|
|
1098
|
-
sqliteSubPath: candidate.subPath,
|
|
1099
|
-
queryString: candidate.queryString,
|
|
1100
|
-
suffixResolution,
|
|
1101
|
-
};
|
|
1102
|
-
} catch (error) {
|
|
1103
|
-
if (!isNotFoundError(error) || isRemoteMountPath(absolutePath)) continue;
|
|
1104
|
-
|
|
1105
|
-
const suffixMatch = await this.#findSuffixMatchCached(suffixCache, candidate.sqlitePath, signal);
|
|
1106
|
-
if (!suffixMatch) continue;
|
|
1107
|
-
|
|
1108
|
-
try {
|
|
1109
|
-
const retryStat = await Bun.file(suffixMatch.absolutePath).stat();
|
|
1110
|
-
if (retryStat.isDirectory()) continue;
|
|
1111
|
-
if (!(await isSqliteFile(suffixMatch.absolutePath))) continue;
|
|
1112
|
-
|
|
1113
|
-
absolutePath = suffixMatch.absolutePath;
|
|
1114
|
-
suffixResolution = { from: candidate.sqlitePath, to: suffixMatch.displayPath };
|
|
1115
|
-
return {
|
|
1116
|
-
absolutePath,
|
|
1117
|
-
sqliteSubPath: candidate.subPath,
|
|
1118
|
-
queryString: candidate.queryString,
|
|
1119
|
-
suffixResolution,
|
|
1120
|
-
};
|
|
1121
|
-
} catch (retryError) {
|
|
1122
|
-
if (!isNotFoundError(retryError)) {
|
|
1123
|
-
throw retryError;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
return null;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
#pdfImageCacheDir(absolutePdfPath: string, contentDigest: string): string {
|
|
1133
|
-
const artifactsDir = this.session.getArtifactsDir?.();
|
|
1134
|
-
let root = artifactsDir ?? undefined;
|
|
1135
|
-
if (root === undefined) {
|
|
1136
|
-
const sessionFile = this.session.getSessionFile();
|
|
1137
|
-
root = sessionFile?.endsWith(".jsonl")
|
|
1138
|
-
? sessionFile.slice(0, -6)
|
|
1139
|
-
: path.join(os.tmpdir(), "omp-read-pdf-images");
|
|
1140
|
-
}
|
|
1141
|
-
const basename = path
|
|
1142
|
-
.basename(absolutePdfPath)
|
|
1143
|
-
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
1144
|
-
.slice(0, PDF_IMAGE_CACHE_BASENAME_MAX_LENGTH);
|
|
1145
|
-
const pathDigest = Bun.hash(absolutePdfPath).toString(36);
|
|
1146
|
-
return path.join(root, "read-pdf-images", `${basename}-${pathDigest}-${contentDigest}`);
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
async #snapshotPdfSource(absolutePdfPath: string, signal?: AbortSignal): Promise<PdfImageSnapshot> {
|
|
1150
|
-
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "omp-read-pdf-"));
|
|
1151
|
-
try {
|
|
1152
|
-
const bytes = await untilAborted(signal, () => Bun.file(absolutePdfPath).bytes());
|
|
1153
|
-
signal?.throwIfAborted();
|
|
1154
|
-
const digest = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
1155
|
-
const filePath = path.join(directory, "source.pdf");
|
|
1156
|
-
await Bun.write(filePath, bytes);
|
|
1157
|
-
signal?.throwIfAborted();
|
|
1158
|
-
return { directory, filePath, digest };
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
1161
|
-
throw error;
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
async #listPdfImageMembers(imageDir: string): Promise<string[]> {
|
|
1166
|
-
try {
|
|
1167
|
-
const entries = await fs.readdir(imageDir, { withFileTypes: true });
|
|
1168
|
-
const members: string[] = [];
|
|
1169
|
-
for (const entry of entries) {
|
|
1170
|
-
if (entry.isFile() && PDF_IMAGE_MEMBER_EXTENSION_RE.test(entry.name)) members.push(entry.name);
|
|
1171
|
-
}
|
|
1172
|
-
return members.sort();
|
|
1173
|
-
} catch (error) {
|
|
1174
|
-
if (isNotFoundError(error)) return [];
|
|
1175
|
-
throw error;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
async #extractPdfImages(snapshot: PdfImageSnapshot, imageDir: string, signal: AbortSignal): Promise<string> {
|
|
1180
|
-
const markerPath = path.join(imageDir, ".extracted");
|
|
1181
|
-
try {
|
|
1182
|
-
await fs.stat(markerPath);
|
|
1183
|
-
return imageDir;
|
|
1184
|
-
} catch (error) {
|
|
1185
|
-
if (!isNotFoundError(error)) throw error;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
await fs.mkdir(path.dirname(imageDir), { recursive: true });
|
|
1189
|
-
const stagingDir = await fs.mkdtemp(`${imageDir}.tmp-`);
|
|
1190
|
-
let published = false;
|
|
1191
|
-
try {
|
|
1192
|
-
const result = await convertFileWithMarkit(snapshot.filePath, signal, { imageDir: stagingDir });
|
|
1193
|
-
if (!result.ok) {
|
|
1194
|
-
throw new ToolError(`Cannot extract images from PDF: ${result.error ?? "conversion failed"}`);
|
|
1195
|
-
}
|
|
1196
|
-
await Bun.write(path.join(stagingDir, ".extracted"), "ok");
|
|
1197
|
-
try {
|
|
1198
|
-
await fs.rename(stagingDir, imageDir);
|
|
1199
|
-
published = true;
|
|
1200
|
-
} catch (error) {
|
|
1201
|
-
if (!isEexist(error) && !isEnotempty(error)) throw error;
|
|
1202
|
-
try {
|
|
1203
|
-
await fs.stat(markerPath);
|
|
1204
|
-
} catch (markerError) {
|
|
1205
|
-
if (isNotFoundError(markerError)) throw error;
|
|
1206
|
-
throw markerError;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
return imageDir;
|
|
1210
|
-
} finally {
|
|
1211
|
-
if (!published) await fs.rm(stagingDir, { recursive: true, force: true });
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
#createPdfImageExtraction(snapshot: PdfImageSnapshot, imageDir: string): PdfImageExtraction {
|
|
1216
|
-
const controller = new AbortController();
|
|
1217
|
-
const promise = this.#extractPdfImages(snapshot, imageDir, controller.signal).finally(() =>
|
|
1218
|
-
fs.rm(snapshot.directory, { recursive: true, force: true }),
|
|
1219
|
-
);
|
|
1220
|
-
const extraction: PdfImageExtraction = { controller, promise, settled: false, waiters: 0 };
|
|
1221
|
-
const settle = () => {
|
|
1222
|
-
extraction.settled = true;
|
|
1223
|
-
if (pdfImageExtractions.get(imageDir) === extraction) pdfImageExtractions.delete(imageDir);
|
|
1224
|
-
};
|
|
1225
|
-
void promise.then(settle, settle);
|
|
1226
|
-
return extraction;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
async #waitForPdfImageExtraction(extraction: PdfImageExtraction, signal: AbortSignal | undefined): Promise<string> {
|
|
1230
|
-
extraction.waiters++;
|
|
1231
|
-
try {
|
|
1232
|
-
return await untilAborted(signal, extraction.promise);
|
|
1233
|
-
} finally {
|
|
1234
|
-
extraction.waiters--;
|
|
1235
|
-
if (extraction.waiters === 0 && !extraction.settled) {
|
|
1236
|
-
extraction.controller.abort();
|
|
1237
|
-
try {
|
|
1238
|
-
await extraction.promise;
|
|
1239
|
-
} catch {}
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
async #ensurePdfImageCache(absolutePdfPath: string, signal?: AbortSignal): Promise<string> {
|
|
1245
|
-
const snapshot = await this.#snapshotPdfSource(absolutePdfPath, signal);
|
|
1246
|
-
const imageDir = this.#pdfImageCacheDir(absolutePdfPath, snapshot.digest);
|
|
1247
|
-
const existing = pdfImageExtractions.get(imageDir);
|
|
1248
|
-
if (existing && !existing.settled && !existing.controller.signal.aborted) {
|
|
1249
|
-
await fs.rm(snapshot.directory, { recursive: true, force: true });
|
|
1250
|
-
return this.#waitForPdfImageExtraction(existing, signal);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
const extraction = this.#createPdfImageExtraction(snapshot, imageDir);
|
|
1254
|
-
pdfImageExtractions.set(imageDir, extraction);
|
|
1255
|
-
return this.#waitForPdfImageExtraction(extraction, signal);
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
async #readPdfImageMember(
|
|
1259
|
-
absolutePdfPath: string,
|
|
1260
|
-
pdfDisplayPath: string,
|
|
1261
|
-
member: string,
|
|
1262
|
-
suffixResolution: { from: string; to: string } | undefined,
|
|
1263
|
-
signal?: AbortSignal,
|
|
1264
|
-
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1265
|
-
const imageDir = await this.#ensurePdfImageCache(absolutePdfPath, signal);
|
|
1266
|
-
const members = await this.#listPdfImageMembers(imageDir);
|
|
1267
|
-
if (member.length === 0) {
|
|
1268
|
-
const text =
|
|
1269
|
-
members.length === 0
|
|
1270
|
-
? "No extractable PDF image members found."
|
|
1271
|
-
: `Extractable PDF image members:\n${members
|
|
1272
|
-
.map(imageMember => `- read \`${pdfDisplayPath}:${imageMember}\``)
|
|
1273
|
-
.join("\n")}`;
|
|
1274
|
-
return toolResult<ReadToolDetails>({ resolvedPath: absolutePdfPath, suffixResolution })
|
|
1275
|
-
.text(prependSuffixResolutionNotice(text, suffixResolution))
|
|
1276
|
-
.sourcePath(absolutePdfPath)
|
|
1277
|
-
.done();
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (!members.includes(member)) {
|
|
1281
|
-
const available = members.length === 0 ? "(none)" : members.join(", ");
|
|
1282
|
-
throw new ToolError(`PDF image member '${member}' not found. Available members: ${available}`);
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
const imagePath = path.join(imageDir, member);
|
|
1286
|
-
const imageStat = await Bun.file(imagePath).stat();
|
|
1287
|
-
if (imageStat.size > MAX_IMAGE_SIZE) {
|
|
1288
|
-
const sizeStr = formatBytes(imageStat.size);
|
|
1289
|
-
const maxStr = formatBytes(MAX_IMAGE_SIZE);
|
|
1290
|
-
throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
|
|
1291
|
-
}
|
|
1292
|
-
const metadata = await readImageMetadata(imagePath);
|
|
1293
|
-
const mimeType = metadata?.mimeType;
|
|
1294
|
-
if (!mimeType) throw new ToolError(`PDF image member '${member}' is not a supported image.`);
|
|
1295
|
-
const imageInput = await loadImageInput({
|
|
1296
|
-
path: `${pdfDisplayPath}:${member}`,
|
|
1297
|
-
cwd: this.session.cwd,
|
|
1298
|
-
autoResize: this.#autoResizeImages,
|
|
1299
|
-
maxBytes: MAX_IMAGE_SIZE,
|
|
1300
|
-
resolvedPath: imagePath,
|
|
1301
|
-
detectedMimeType: mimeType,
|
|
1302
|
-
excludeWebP: webpExclusionForModel(this.session.getActiveModel?.()),
|
|
1303
|
-
});
|
|
1304
|
-
if (!imageInput) {
|
|
1305
|
-
throw new ToolError(`Read image file [${mimeType}] failed: unsupported image format.`);
|
|
1306
|
-
}
|
|
1307
|
-
const textNote = prependSuffixResolutionNotice(imageInput.textNote, suffixResolution);
|
|
1308
|
-
return toolResult<ReadToolDetails>({ resolvedPath: absolutePdfPath, suffixResolution })
|
|
1309
|
-
.content([
|
|
1310
|
-
{ type: "text", text: textNote },
|
|
1311
|
-
{ type: "image", data: imageInput.data, mimeType: imageInput.mimeType },
|
|
1312
|
-
])
|
|
1313
|
-
.sourcePath(imageInput.resolvedPath)
|
|
1314
|
-
.done();
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
539
|
/**
|
|
1318
540
|
* Build content blocks for an on-disk image file: an `inspect_image`
|
|
1319
541
|
* metadata note when inspection is active, otherwise the decoded image
|
|
@@ -1387,291 +609,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1387
609
|
}
|
|
1388
610
|
}
|
|
1389
611
|
|
|
1390
|
-
#buildInMemoryTextResult(
|
|
1391
|
-
text: string,
|
|
1392
|
-
offset: number | undefined,
|
|
1393
|
-
limit: number | undefined,
|
|
1394
|
-
options: {
|
|
1395
|
-
details?: ReadToolDetails;
|
|
1396
|
-
sourcePath?: string;
|
|
1397
|
-
sourceUrl?: string;
|
|
1398
|
-
sourceInternal?: string;
|
|
1399
|
-
entityLabel: string;
|
|
1400
|
-
ignoreResultLimits?: boolean;
|
|
1401
|
-
raw?: boolean;
|
|
1402
|
-
immutable?: boolean;
|
|
1403
|
-
},
|
|
1404
|
-
): AgentToolResult<ReadToolDetails> {
|
|
1405
|
-
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
1406
|
-
const details = options.details ?? {};
|
|
1407
|
-
const allLines = text.split("\n");
|
|
1408
|
-
const totalLines = allLines.length;
|
|
1409
|
-
details.totalLines = totalLines;
|
|
1410
|
-
// User-requested 0-indexed range start. Lines BEFORE this are leading
|
|
1411
|
-
// context (added below if offset is explicit).
|
|
1412
|
-
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
1413
|
-
const ignoreResultLimits = options.ignoreResultLimits ?? false;
|
|
1414
|
-
const requestedEnd = limit !== undefined ? Math.min(requestedStart + limit, allLines.length) : allLines.length;
|
|
1415
|
-
// Expand only on sides the user actually constrained: leading context
|
|
1416
|
-
// when offset>1, trailing context when a finite limit was set. Raw mode
|
|
1417
|
-
// never expands — without line numbers the padding is indistinguishable
|
|
1418
|
-
// from requested content, so `raw:31-31` must return line 31 and nothing
|
|
1419
|
-
// else (verbatim-extraction contract).
|
|
1420
|
-
const rawDisplay = options.raw === true;
|
|
1421
|
-
const expanded = expandRangeWithContext(
|
|
1422
|
-
requestedStart,
|
|
1423
|
-
requestedEnd,
|
|
1424
|
-
allLines.length,
|
|
1425
|
-
!rawDisplay && offset !== undefined && offset > 1,
|
|
1426
|
-
!rawDisplay && limit !== undefined,
|
|
1427
|
-
);
|
|
1428
|
-
const startLine = expanded.startLine;
|
|
1429
|
-
const endLineExpanded = expanded.endLine;
|
|
1430
|
-
const startLineDisplay = startLine + 1;
|
|
1431
|
-
|
|
1432
|
-
const resultBuilder = toolResult(details);
|
|
1433
|
-
if (options.sourcePath) {
|
|
1434
|
-
resultBuilder.sourcePath(options.sourcePath);
|
|
1435
|
-
}
|
|
1436
|
-
if (options.sourceUrl) {
|
|
1437
|
-
resultBuilder.sourceUrl(options.sourceUrl);
|
|
1438
|
-
}
|
|
1439
|
-
if (options.sourceInternal) {
|
|
1440
|
-
resultBuilder.sourceInternal(options.sourceInternal);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
if (requestedStart >= allLines.length) {
|
|
1444
|
-
const suggestion =
|
|
1445
|
-
allLines.length === 0
|
|
1446
|
-
? `The ${options.entityLabel} is empty.`
|
|
1447
|
-
: `Use :1 to read from the start, or :${allLines.length} to read the last line.`;
|
|
1448
|
-
return resultBuilder
|
|
1449
|
-
.text(
|
|
1450
|
-
`Line ${requestedStart + 1} is beyond end of ${options.entityLabel} (${allLines.length} lines total). ${suggestion}`,
|
|
1451
|
-
)
|
|
1452
|
-
.done();
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
const endLine = endLineExpanded;
|
|
1456
|
-
const selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
1457
|
-
const userLimitedLines = limit !== undefined ? endLine - startLine : undefined;
|
|
1458
|
-
const truncation = ignoreResultLimits ? noTruncResult(selectedContent) : truncateHead(selectedContent);
|
|
1459
|
-
|
|
1460
|
-
const shouldAddHashLines = displayMode.hashLines;
|
|
1461
|
-
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1462
|
-
const hashContext =
|
|
1463
|
-
shouldAddHashLines && options.sourcePath
|
|
1464
|
-
? recordFullHashlineContext(
|
|
1465
|
-
this.session,
|
|
1466
|
-
options.sourcePath,
|
|
1467
|
-
formatPathRelativeToCwd(options.sourcePath, this.session.cwd),
|
|
1468
|
-
text,
|
|
1469
|
-
)
|
|
1470
|
-
: undefined;
|
|
1471
|
-
let emittedHashlineHeader = false;
|
|
1472
|
-
let seenLines: number[] | undefined;
|
|
1473
|
-
let rawSeenLines: number[] | undefined;
|
|
1474
|
-
const formatText = (content: string, startNum: number): string => {
|
|
1475
|
-
const lineCount = countTextLines(content);
|
|
1476
|
-
details.displayContent = {
|
|
1477
|
-
text: content,
|
|
1478
|
-
startLine: startNum,
|
|
1479
|
-
lineNumbers: Array.from({ length: lineCount }, (_, i) => startNum + i),
|
|
1480
|
-
};
|
|
1481
|
-
if (shouldAddHashLines) seenLines = contiguousLineNumbers(startNum, lineCount);
|
|
1482
|
-
const formatted = formatTextWithMode(content, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1483
|
-
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
1484
|
-
emittedHashlineHeader = true;
|
|
1485
|
-
return prependHashlineHeader(formatted, hashContext);
|
|
1486
|
-
};
|
|
1487
|
-
const formatLineEntries = (entries: readonly LineEntry[], startNum: number): string => {
|
|
1488
|
-
const firstLine = entries.find(entry => entry.kind === "line");
|
|
1489
|
-
details.displayContent = {
|
|
1490
|
-
text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
|
|
1491
|
-
startLine: firstLine?.kind === "line" ? firstLine.lineNumber : startNum,
|
|
1492
|
-
lineNumbers: entries.map(entry => (entry.kind === "line" ? entry.lineNumber : null)),
|
|
1493
|
-
};
|
|
1494
|
-
if (shouldAddHashLines) seenLines = lineNumbersFromEntries(entries);
|
|
1495
|
-
const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
|
|
1496
|
-
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
1497
|
-
emittedHashlineHeader = true;
|
|
1498
|
-
return prependHashlineHeader(formatted, hashContext);
|
|
1499
|
-
};
|
|
1500
|
-
const buildLineEntries = (endLineDisplay: number): LineEntry[] =>
|
|
1501
|
-
buildLineEntriesWithBlockContext(allLines, [{ startLine: startLineDisplay, endLine: endLineDisplay }], {
|
|
1502
|
-
path: options.sourcePath,
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
let outputText: string;
|
|
1506
|
-
let truncationInfo:
|
|
1507
|
-
| { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
|
|
1508
|
-
| undefined;
|
|
1509
|
-
|
|
1510
|
-
if (truncation.firstLineExceedsLimit) {
|
|
1511
|
-
const firstLine = allLines[startLine] ?? "";
|
|
1512
|
-
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
1513
|
-
const snippet = truncateHeadBytes(firstLine, DEFAULT_MAX_BYTES);
|
|
1514
|
-
|
|
1515
|
-
if (shouldAddHashLines) {
|
|
1516
|
-
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1517
|
-
firstLineBytes,
|
|
1518
|
-
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
1519
|
-
} else {
|
|
1520
|
-
outputText = formatText(snippet.text, startLineDisplay);
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
if (snippet.text.length === 0) {
|
|
1524
|
-
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1525
|
-
firstLineBytes,
|
|
1526
|
-
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
details.truncation = truncation;
|
|
1530
|
-
truncationInfo = {
|
|
1531
|
-
result: truncation,
|
|
1532
|
-
options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
|
|
1533
|
-
};
|
|
1534
|
-
} else if (truncation.truncated) {
|
|
1535
|
-
const outputLines = truncation.outputLines ?? countTextLines(truncation.content);
|
|
1536
|
-
const endLineDisplay = startLineDisplay + Math.max(0, outputLines - 1);
|
|
1537
|
-
if (options.raw === true) {
|
|
1538
|
-
rawSeenLines = contiguousLineNumbers(startLineDisplay, outputLines);
|
|
1539
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1540
|
-
} else {
|
|
1541
|
-
outputText = formatLineEntries(buildLineEntries(endLineDisplay), startLineDisplay);
|
|
1542
|
-
}
|
|
1543
|
-
details.truncation = truncation;
|
|
1544
|
-
truncationInfo = {
|
|
1545
|
-
result: truncation,
|
|
1546
|
-
options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
|
|
1547
|
-
};
|
|
1548
|
-
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
|
1549
|
-
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
1550
|
-
const nextOffset = startLine + userLimitedLines + 1;
|
|
1551
|
-
|
|
1552
|
-
if (options.raw === true) {
|
|
1553
|
-
rawSeenLines = contiguousLineNumbers(startLineDisplay, userLimitedLines);
|
|
1554
|
-
outputText = formatText(selectedContent, startLineDisplay);
|
|
1555
|
-
} else {
|
|
1556
|
-
outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay);
|
|
1557
|
-
}
|
|
1558
|
-
outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`;
|
|
1559
|
-
} else {
|
|
1560
|
-
if (options.raw === true) {
|
|
1561
|
-
rawSeenLines = contiguousLineNumbers(startLineDisplay, endLine - startLine);
|
|
1562
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1563
|
-
} else {
|
|
1564
|
-
outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay);
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
if (hashContext?.tag && options.sourcePath && seenLines) {
|
|
1569
|
-
recordSeenLines(this.session, options.sourcePath, hashContext.tag, seenLines);
|
|
1570
|
-
}
|
|
1571
|
-
if (options.raw === true && options.sourcePath && options.immutable !== true && rawSeenLines) {
|
|
1572
|
-
recordInMemorySeenLines(this.session, options.sourcePath, text, rawSeenLines);
|
|
1573
|
-
}
|
|
1574
|
-
resultBuilder.text(outputText);
|
|
1575
|
-
if (truncationInfo) {
|
|
1576
|
-
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
1577
|
-
}
|
|
1578
|
-
return resultBuilder.done();
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
/**
|
|
1582
|
-
* Render a multi-range read against in-memory text. Each range emits a
|
|
1583
|
-
* formatted block with its own anchors / line numbers, blocks are joined
|
|
1584
|
-
* with an elision separator, and ranges past EOF surface as `[…]` notices
|
|
1585
|
-
* so the model can correct the next call. No leading/trailing context is
|
|
1586
|
-
* added — multi-range callers always specify exact bounds.
|
|
1587
|
-
*/
|
|
1588
|
-
#buildInMemoryMultiRangeResult(
|
|
1589
|
-
text: string,
|
|
1590
|
-
ranges: readonly LineRange[],
|
|
1591
|
-
options: {
|
|
1592
|
-
details?: ReadToolDetails;
|
|
1593
|
-
sourcePath?: string;
|
|
1594
|
-
sourceUrl?: string;
|
|
1595
|
-
sourceInternal?: string;
|
|
1596
|
-
entityLabel: string;
|
|
1597
|
-
raw?: boolean;
|
|
1598
|
-
immutable?: boolean;
|
|
1599
|
-
},
|
|
1600
|
-
): AgentToolResult<ReadToolDetails> {
|
|
1601
|
-
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
1602
|
-
const details = options.details ?? {};
|
|
1603
|
-
const allLines = text.split("\n");
|
|
1604
|
-
const totalLines = allLines.length;
|
|
1605
|
-
details.totalLines = totalLines;
|
|
1606
|
-
const shouldAddHashLines = displayMode.hashLines;
|
|
1607
|
-
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1608
|
-
const hashContext =
|
|
1609
|
-
shouldAddHashLines && options.sourcePath
|
|
1610
|
-
? recordFullHashlineContext(
|
|
1611
|
-
this.session,
|
|
1612
|
-
options.sourcePath,
|
|
1613
|
-
formatPathRelativeToCwd(options.sourcePath, this.session.cwd),
|
|
1614
|
-
text,
|
|
1615
|
-
)
|
|
1616
|
-
: undefined;
|
|
1617
|
-
let emittedHashlineHeader = false;
|
|
1618
|
-
|
|
1619
|
-
let seenLines: number[] | undefined;
|
|
1620
|
-
const resultBuilder = toolResult(details);
|
|
1621
|
-
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
1622
|
-
if (options.sourceUrl) resultBuilder.sourceUrl(options.sourceUrl);
|
|
1623
|
-
if (options.sourceInternal) resultBuilder.sourceInternal(options.sourceInternal);
|
|
1624
|
-
|
|
1625
|
-
const outOfBounds: LineRange[] = [];
|
|
1626
|
-
const visibleSpans: Array<{ startLine: number; endLine: number }> = [];
|
|
1627
|
-
const rawParts: string[] = [];
|
|
1628
|
-
for (const range of ranges) {
|
|
1629
|
-
if (range.startLine > totalLines) {
|
|
1630
|
-
outOfBounds.push(range);
|
|
1631
|
-
continue;
|
|
1632
|
-
}
|
|
1633
|
-
const effectiveEnd = Math.min(range.endLine ?? totalLines, totalLines);
|
|
1634
|
-
visibleSpans.push({ startLine: range.startLine, endLine: effectiveEnd });
|
|
1635
|
-
if (options.raw === true) {
|
|
1636
|
-
rawParts.push(allLines.slice(range.startLine - 1, effectiveEnd).join("\n"));
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
let outputText = "";
|
|
1641
|
-
if (options.raw === true) {
|
|
1642
|
-
outputText = rawParts.length > 0 ? rawParts.join("\n\n…\n\n") : "";
|
|
1643
|
-
} else if (visibleSpans.length > 0) {
|
|
1644
|
-
const entries = buildLineEntriesWithBlockContext(allLines, visibleSpans, { path: options.sourcePath });
|
|
1645
|
-
if (shouldAddHashLines) seenLines = lineNumbersFromEntries(entries);
|
|
1646
|
-
const firstLine = entries.find(entry => entry.kind === "line");
|
|
1647
|
-
if (firstLine?.kind === "line") {
|
|
1648
|
-
details.displayContent = {
|
|
1649
|
-
text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
|
|
1650
|
-
startLine: firstLine.lineNumber,
|
|
1651
|
-
lineNumbers: entries.map(entry => (entry.kind === "line" ? entry.lineNumber : null)),
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
|
|
1655
|
-
outputText = hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted;
|
|
1656
|
-
if (hashContext) emittedHashlineHeader = true;
|
|
1657
|
-
}
|
|
1658
|
-
const notices: string[] = [];
|
|
1659
|
-
for (const range of outOfBounds) {
|
|
1660
|
-
const bound = range.endLine !== undefined ? `${range.startLine}-${range.endLine}` : `${range.startLine}`;
|
|
1661
|
-
notices.push(`[Range ${bound} is beyond end of ${options.entityLabel} (${totalLines} lines total); skipped]`);
|
|
1662
|
-
}
|
|
1663
|
-
const finalText =
|
|
1664
|
-
notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
|
|
1665
|
-
if (hashContext?.tag && options.sourcePath && seenLines) {
|
|
1666
|
-
recordSeenLines(this.session, options.sourcePath, hashContext.tag, seenLines);
|
|
1667
|
-
}
|
|
1668
|
-
if (options.raw === true && options.sourcePath && options.immutable !== true && visibleSpans.length > 0) {
|
|
1669
|
-
recordInMemorySeenLines(this.session, options.sourcePath, text, lineNumbersFromSpans(visibleSpans));
|
|
1670
|
-
}
|
|
1671
|
-
resultBuilder.text(finalText);
|
|
1672
|
-
return resultBuilder.done();
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
612
|
/**
|
|
1676
613
|
* Stream multiple non-contiguous ranges from a local file. ACP bridge takes
|
|
1677
614
|
* priority when present (editor buffer is source of truth); otherwise each
|
|
@@ -1696,12 +633,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1696
633
|
const rawSelector = isRawSelector(parsed);
|
|
1697
634
|
|
|
1698
635
|
// ACP bridge first — the editor's in-memory buffer is source of truth.
|
|
1699
|
-
const bridgePromise = allowBridge ?
|
|
636
|
+
const bridgePromise = allowBridge ? routeReadThroughBridge(this.session, absolutePath) : undefined;
|
|
1700
637
|
if (bridgePromise !== undefined) {
|
|
1701
638
|
try {
|
|
1702
639
|
const bridgeText = await bridgePromise;
|
|
1703
|
-
const bridgeResult =
|
|
1704
|
-
details:
|
|
640
|
+
const bridgeResult = buildInMemoryMultiRangeResult(this.session, bridgeText, ranges, {
|
|
641
|
+
details: markMarkdownContentType(
|
|
642
|
+
this.session,
|
|
643
|
+
{ resolvedPath: absolutePath, suffixResolution },
|
|
644
|
+
absolutePath,
|
|
645
|
+
),
|
|
1705
646
|
sourcePath: absolutePath,
|
|
1706
647
|
entityLabel: "file",
|
|
1707
648
|
raw: rawSelector,
|
|
@@ -1836,423 +777,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1836
777
|
return { outputText, columnTruncated, displayContent };
|
|
1837
778
|
}
|
|
1838
779
|
|
|
1839
|
-
async #readArchiveDirectory(
|
|
1840
|
-
archive: ArchiveReader,
|
|
1841
|
-
archivePath: string,
|
|
1842
|
-
subPath: string,
|
|
1843
|
-
offset: number | undefined,
|
|
1844
|
-
limit: number | undefined,
|
|
1845
|
-
details: ReadToolDetails,
|
|
1846
|
-
signal?: AbortSignal,
|
|
1847
|
-
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1848
|
-
const DEFAULT_LIMIT = 500;
|
|
1849
|
-
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
1850
|
-
const allEntries = archive.listDirectory(subPath);
|
|
1851
|
-
// `offset` is 1-indexed (line-selector semantics): `a.zip:dir:50` starts
|
|
1852
|
-
// the listing at the 50th entry instead of being silently ignored.
|
|
1853
|
-
const entries = offset !== undefined && offset > 1 ? allEntries.slice(offset - 1) : allEntries;
|
|
1854
|
-
|
|
1855
|
-
const listLimit = applyListLimit(entries, { limit: effectiveLimit });
|
|
1856
|
-
const limitedEntries = listLimit.items;
|
|
1857
|
-
const limitMeta = listLimit.meta;
|
|
1858
|
-
|
|
1859
|
-
for (let index = 0; index < limitedEntries.length; index++) {
|
|
1860
|
-
throwIfAborted(signal);
|
|
1861
|
-
}
|
|
1862
|
-
const results = formatArchiveEntryLines(limitedEntries);
|
|
1863
|
-
|
|
1864
|
-
const output = results.length > 0 ? results.join("\n") : "(empty archive directory)";
|
|
1865
|
-
const text = prependSuffixResolutionNotice(output, details.suffixResolution);
|
|
1866
|
-
const truncation = truncateHead(text, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
1867
|
-
const directoryDetails: ReadToolDetails = { ...details, isDirectory: true };
|
|
1868
|
-
const resultBuilder = toolResult<ReadToolDetails>(directoryDetails).text(truncation.content);
|
|
1869
|
-
resultBuilder.sourcePath(archivePath).limits({ resultLimit: limitMeta.resultLimit?.reached });
|
|
1870
|
-
if (truncation.truncated) {
|
|
1871
|
-
directoryDetails.truncation = truncation;
|
|
1872
|
-
resultBuilder.truncation(truncation, { direction: "head" });
|
|
1873
|
-
}
|
|
1874
|
-
return resultBuilder.done();
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
async #readArchive(
|
|
1878
|
-
readPath: string,
|
|
1879
|
-
parsedSel: ParsedSelector,
|
|
1880
|
-
resolvedArchivePath: ResolvedArchiveReadPath,
|
|
1881
|
-
signal?: AbortSignal,
|
|
1882
|
-
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1883
|
-
throwIfAborted(signal);
|
|
1884
|
-
const archive = await openArchive(resolvedArchivePath.absolutePath);
|
|
1885
|
-
throwIfAborted(signal);
|
|
1886
|
-
|
|
1887
|
-
const details: ReadToolDetails = this.#markMarkdownContentType(
|
|
1888
|
-
{
|
|
1889
|
-
resolvedPath: resolvedArchivePath.absolutePath,
|
|
1890
|
-
suffixResolution: resolvedArchivePath.suffixResolution,
|
|
1891
|
-
},
|
|
1892
|
-
resolvedArchivePath.archiveSubPath,
|
|
1893
|
-
);
|
|
1894
|
-
|
|
1895
|
-
let archiveSubPath = resolvedArchivePath.archiveSubPath;
|
|
1896
|
-
let sel = parsedSel;
|
|
1897
|
-
let node = archive.getNode(archiveSubPath);
|
|
1898
|
-
if (!node && archiveSubPath) {
|
|
1899
|
-
// `archive.zip:500` / `archive.zip:raw`: the whole subPath is a
|
|
1900
|
-
// selector on the archive root, not a member name. Member names take
|
|
1901
|
-
// precedence (getNode above); fall back to root + selector.
|
|
1902
|
-
const wholeSel = parseSel(archiveSubPath);
|
|
1903
|
-
if (wholeSel.kind !== "none") {
|
|
1904
|
-
node = archive.getNode("");
|
|
1905
|
-
archiveSubPath = "";
|
|
1906
|
-
sel = wholeSel;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
if (!node) {
|
|
1910
|
-
throw new ToolError(`Path '${readPath}' not found inside archive`);
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
if (node.isDirectory) {
|
|
1914
|
-
if (isMultiRange(sel)) {
|
|
1915
|
-
throw new ToolError("Multi-range line selectors are not supported for archive directory listings.");
|
|
1916
|
-
}
|
|
1917
|
-
const { offset, limit } = selToOffsetLimit(sel);
|
|
1918
|
-
return this.#readArchiveDirectory(
|
|
1919
|
-
archive,
|
|
1920
|
-
resolvedArchivePath.absolutePath,
|
|
1921
|
-
archiveSubPath,
|
|
1922
|
-
offset,
|
|
1923
|
-
limit,
|
|
1924
|
-
details,
|
|
1925
|
-
signal,
|
|
1926
|
-
);
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
const entry = await archive.readFile(archiveSubPath);
|
|
1930
|
-
const text = decodeUtf8Text(entry.bytes);
|
|
1931
|
-
if (text === null) {
|
|
1932
|
-
return toolResult<ReadToolDetails>(details)
|
|
1933
|
-
.text(
|
|
1934
|
-
prependSuffixResolutionNotice(
|
|
1935
|
-
`[Cannot read binary archive entry '${entry.path}' (${formatBytes(entry.size)})]`,
|
|
1936
|
-
resolvedArchivePath.suffixResolution,
|
|
1937
|
-
),
|
|
1938
|
-
)
|
|
1939
|
-
.sourcePath(resolvedArchivePath.absolutePath)
|
|
1940
|
-
.done();
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
// Archive members are immutable: there is no edit path for bytes inside
|
|
1944
|
-
// an archive, and a hashline tag keyed to the archive file would invite
|
|
1945
|
-
// (and fail) edits while clobbering sibling members' snapshots.
|
|
1946
|
-
const raw = isRawSelector(sel);
|
|
1947
|
-
const result =
|
|
1948
|
-
isMultiRange(sel) && sel.kind === "lines"
|
|
1949
|
-
? this.#buildInMemoryMultiRangeResult(text, sel.ranges, {
|
|
1950
|
-
details,
|
|
1951
|
-
sourcePath: resolvedArchivePath.absolutePath,
|
|
1952
|
-
entityLabel: "archive entry",
|
|
1953
|
-
raw,
|
|
1954
|
-
immutable: true,
|
|
1955
|
-
})
|
|
1956
|
-
: this.#buildInMemoryTextResult(text, selToOffsetLimit(sel).offset, selToOffsetLimit(sel).limit, {
|
|
1957
|
-
details,
|
|
1958
|
-
sourcePath: resolvedArchivePath.absolutePath,
|
|
1959
|
-
entityLabel: "archive entry",
|
|
1960
|
-
raw,
|
|
1961
|
-
immutable: true,
|
|
1962
|
-
});
|
|
1963
|
-
const firstText = result.content.find((content): content is TextContent => content.type === "text");
|
|
1964
|
-
if (firstText) {
|
|
1965
|
-
firstText.text = prependSuffixResolutionNotice(firstText.text, resolvedArchivePath.suffixResolution);
|
|
1966
|
-
}
|
|
1967
|
-
return result;
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
async #readSqlite(
|
|
1971
|
-
resolvedSqlitePath: ResolvedSqliteReadPath,
|
|
1972
|
-
signal?: AbortSignal,
|
|
1973
|
-
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1974
|
-
throwIfAborted(signal);
|
|
1975
|
-
|
|
1976
|
-
const selectorInput = {
|
|
1977
|
-
subPath: resolvedSqlitePath.sqliteSubPath,
|
|
1978
|
-
queryString: resolvedSqlitePath.queryString,
|
|
1979
|
-
};
|
|
1980
|
-
const selector = parseSqliteSelector(selectorInput.subPath, selectorInput.queryString);
|
|
1981
|
-
const details: ReadToolDetails = {
|
|
1982
|
-
resolvedPath: resolvedSqlitePath.absolutePath,
|
|
1983
|
-
suffixResolution: resolvedSqlitePath.suffixResolution,
|
|
1984
|
-
};
|
|
1985
|
-
|
|
1986
|
-
let db: Database | null = null;
|
|
1987
|
-
try {
|
|
1988
|
-
db = new Database(resolvedSqlitePath.absolutePath, { readonly: true, strict: true });
|
|
1989
|
-
db.run("PRAGMA busy_timeout = 3000");
|
|
1990
|
-
throwIfAborted(signal);
|
|
1991
|
-
|
|
1992
|
-
switch (selector.kind) {
|
|
1993
|
-
case "list": {
|
|
1994
|
-
const listLimit = applyListLimit(listTables(db), { limit: 500 });
|
|
1995
|
-
const output = prependSuffixResolutionNotice(
|
|
1996
|
-
renderTableList(listLimit.items),
|
|
1997
|
-
resolvedSqlitePath.suffixResolution,
|
|
1998
|
-
);
|
|
1999
|
-
const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
2000
|
-
details.truncation = truncation.truncated ? truncation : undefined;
|
|
2001
|
-
const resultBuilder = toolResult<ReadToolDetails>(details)
|
|
2002
|
-
.text(truncation.content)
|
|
2003
|
-
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
2004
|
-
.limits({ resultLimit: listLimit.meta.resultLimit?.reached });
|
|
2005
|
-
if (truncation.truncated) {
|
|
2006
|
-
resultBuilder.truncation(truncation, { direction: "head" });
|
|
2007
|
-
}
|
|
2008
|
-
return resultBuilder.done();
|
|
2009
|
-
}
|
|
2010
|
-
case "schema": {
|
|
2011
|
-
const sampleRows = queryRows(db, selector.table, { limit: selector.sampleLimit, offset: 0 });
|
|
2012
|
-
let output = renderSchema(getTableSchema(db, selector.table), {
|
|
2013
|
-
columns: sampleRows.columns,
|
|
2014
|
-
rows: sampleRows.rows,
|
|
2015
|
-
});
|
|
2016
|
-
if (sampleRows.rows.length < sampleRows.totalCount) {
|
|
2017
|
-
const remaining = sampleRows.totalCount - sampleRows.rows.length;
|
|
2018
|
-
output += `\n[${remaining} more rows; append :${selector.table}?limit=20&offset=${sampleRows.rows.length} to the database path to continue]`;
|
|
2019
|
-
}
|
|
2020
|
-
return toolResult<ReadToolDetails>(details)
|
|
2021
|
-
.text(prependSuffixResolutionNotice(output, resolvedSqlitePath.suffixResolution))
|
|
2022
|
-
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
2023
|
-
.done();
|
|
2024
|
-
}
|
|
2025
|
-
case "row": {
|
|
2026
|
-
const lookup = resolveTableRowLookup(db, selector.table);
|
|
2027
|
-
const row =
|
|
2028
|
-
lookup.kind === "pk"
|
|
2029
|
-
? getRowByKey(db, selector.table, lookup, selector.key)
|
|
2030
|
-
: getRowByRowId(db, selector.table, selector.key);
|
|
2031
|
-
if (!row) {
|
|
2032
|
-
return toolResult<ReadToolDetails>(details)
|
|
2033
|
-
.text(
|
|
2034
|
-
prependSuffixResolutionNotice(
|
|
2035
|
-
`No row found in table '${selector.table}' for key '${selector.key}'.`,
|
|
2036
|
-
resolvedSqlitePath.suffixResolution,
|
|
2037
|
-
),
|
|
2038
|
-
)
|
|
2039
|
-
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
2040
|
-
.done();
|
|
2041
|
-
}
|
|
2042
|
-
return toolResult<ReadToolDetails>(details)
|
|
2043
|
-
.text(prependSuffixResolutionNotice(renderRow(row), resolvedSqlitePath.suffixResolution))
|
|
2044
|
-
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
2045
|
-
.done();
|
|
2046
|
-
}
|
|
2047
|
-
case "query": {
|
|
2048
|
-
const page = queryRows(db, selector.table, selector);
|
|
2049
|
-
return toolResult<ReadToolDetails>(details)
|
|
2050
|
-
.text(
|
|
2051
|
-
prependSuffixResolutionNotice(
|
|
2052
|
-
renderTable(page.columns, page.rows, {
|
|
2053
|
-
totalCount: page.totalCount,
|
|
2054
|
-
offset: selector.offset,
|
|
2055
|
-
limit: selector.limit,
|
|
2056
|
-
table: selector.table,
|
|
2057
|
-
dbPath: resolvedSqlitePath.absolutePath,
|
|
2058
|
-
}),
|
|
2059
|
-
resolvedSqlitePath.suffixResolution,
|
|
2060
|
-
),
|
|
2061
|
-
)
|
|
2062
|
-
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
2063
|
-
.done();
|
|
2064
|
-
}
|
|
2065
|
-
case "raw": {
|
|
2066
|
-
const result = executeReadQuery(db, selector.sql);
|
|
2067
|
-
let output = renderTable(result.columns, result.rows, {
|
|
2068
|
-
totalCount: result.rows.length,
|
|
2069
|
-
offset: 0,
|
|
2070
|
-
limit: result.rows.length || DEFAULT_MAX_LINES,
|
|
2071
|
-
table: "query",
|
|
2072
|
-
dbPath: resolvedSqlitePath.absolutePath,
|
|
2073
|
-
});
|
|
2074
|
-
if (result.truncated) {
|
|
2075
|
-
output += `\n[Output capped at ${MAX_RAW_QUERY_ROWS} rows; add a LIMIT/OFFSET clause to the query to page through more]`;
|
|
2076
|
-
}
|
|
2077
|
-
return toolResult<ReadToolDetails>(details)
|
|
2078
|
-
.text(prependSuffixResolutionNotice(output, resolvedSqlitePath.suffixResolution))
|
|
2079
|
-
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
2080
|
-
.done();
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
throw new ToolError("Unsupported SQLite selector");
|
|
2085
|
-
} catch (error) {
|
|
2086
|
-
if (error instanceof ToolError) {
|
|
2087
|
-
throw error;
|
|
2088
|
-
}
|
|
2089
|
-
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
2090
|
-
} finally {
|
|
2091
|
-
db?.close();
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
#routeReadThroughBridge(
|
|
2096
|
-
absolutePath: string,
|
|
2097
|
-
options?: { line?: number; limit?: number },
|
|
2098
|
-
): Promise<string> | undefined {
|
|
2099
|
-
const bridge = this.session.getClientBridge?.();
|
|
2100
|
-
if (!bridge?.capabilities.readTextFile || !bridge.readTextFile) return undefined;
|
|
2101
|
-
return bridge.readTextFile({ path: absolutePath, ...options });
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
/**
|
|
2105
|
-
* Tag Markdown reads for the TUI's formatted preview, gated on the opt-in
|
|
2106
|
-
* `read.renderMarkdown` setting. Off by default; when disabled, no local
|
|
2107
|
-
* read is tagged `text/markdown`, so the renderer output is identical to
|
|
2108
|
-
* the pre-setting behavior. Internal-URL reads keep their protocol-supplied
|
|
2109
|
-
* `contentType` and render as Markdown regardless of the setting.
|
|
2110
|
-
*/
|
|
2111
|
-
#markMarkdownContentType(details: ReadToolDetails, filePath: string): ReadToolDetails {
|
|
2112
|
-
if (!details.contentType && this.session.settings.get("read.renderMarkdown") && isMarkdownPath(filePath)) {
|
|
2113
|
-
details.contentType = "text/markdown";
|
|
2114
|
-
}
|
|
2115
|
-
return details;
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
async #trySummarize(absolutePath: string, fileSize: number, signal?: AbortSignal): Promise<SummaryResult | null> {
|
|
2119
|
-
if (fileSize > MAX_SUMMARY_BYTES) return null;
|
|
2120
|
-
|
|
2121
|
-
try {
|
|
2122
|
-
throwIfAborted(signal);
|
|
2123
|
-
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
2124
|
-
const code =
|
|
2125
|
-
bridgePromise !== undefined
|
|
2126
|
-
? await bridgePromise.catch(() => Bun.file(absolutePath).text())
|
|
2127
|
-
: await Bun.file(absolutePath).text();
|
|
2128
|
-
throwIfAborted(signal);
|
|
2129
|
-
const lineCount = countTextLines(code);
|
|
2130
|
-
if (lineCount > MAX_SUMMARY_LINES) return null;
|
|
2131
|
-
if (lineCount < this.session.settings.get("read.summarize.minTotalLines")) return null;
|
|
2132
|
-
|
|
2133
|
-
const minBodyLines = this.session.settings.get("read.summarize.minBodyLines");
|
|
2134
|
-
const minCommentLines = this.session.settings.get("read.summarize.minCommentLines");
|
|
2135
|
-
const unfoldUntilLines = this.session.settings.get("read.summarize.unfoldUntil");
|
|
2136
|
-
const unfoldLimitLines = this.session.settings.get("read.summarize.unfoldLimit");
|
|
2137
|
-
const cache = getSummaryParseCache(this.session);
|
|
2138
|
-
const cacheKey = `${absolutePath}\0${Bun.hash(code)}\0${minBodyLines},${minCommentLines},${unfoldUntilLines},${unfoldLimitLines}`;
|
|
2139
|
-
const memoized = cache.get(cacheKey);
|
|
2140
|
-
if (memoized !== undefined) return memoized || null;
|
|
2141
|
-
const result = summarizeCode({
|
|
2142
|
-
code,
|
|
2143
|
-
path: absolutePath,
|
|
2144
|
-
minBodyLines,
|
|
2145
|
-
minCommentLines,
|
|
2146
|
-
unfoldUntilLines,
|
|
2147
|
-
unfoldLimitLines,
|
|
2148
|
-
});
|
|
2149
|
-
const usable = result.parsed && result.elided ? result : false;
|
|
2150
|
-
cache.set(cacheKey, usable);
|
|
2151
|
-
return usable || null;
|
|
2152
|
-
} catch {
|
|
2153
|
-
return null;
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
#renderSummary(summary: SummaryResult): {
|
|
2158
|
-
text: string;
|
|
2159
|
-
displayText: string;
|
|
2160
|
-
elidedRanges: ElidedRange[];
|
|
2161
|
-
elidedLines: number;
|
|
2162
|
-
} {
|
|
2163
|
-
const displayMode = resolveFileDisplayMode(this.session);
|
|
2164
|
-
const shouldAddHashLines = displayMode.hashLines;
|
|
2165
|
-
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
2166
|
-
|
|
2167
|
-
// Flatten segments into per-line units so we can merge a kept-head /
|
|
2168
|
-
// elided / kept-tail sandwich into a single brace-pair line when the
|
|
2169
|
-
// boundary lines look like `… {` and `}` (or matching variants).
|
|
2170
|
-
type Unit =
|
|
2171
|
-
| { kind: "line"; line: number; text: string }
|
|
2172
|
-
| { kind: "elided"; startLine: number; endLine: number }
|
|
2173
|
-
| {
|
|
2174
|
-
kind: "merged";
|
|
2175
|
-
startLine: number;
|
|
2176
|
-
endLine: number;
|
|
2177
|
-
headText: string;
|
|
2178
|
-
tailText: string;
|
|
2179
|
-
};
|
|
2180
|
-
|
|
2181
|
-
const raw: Unit[] = [];
|
|
2182
|
-
for (const segment of summary.segments) {
|
|
2183
|
-
if (segment.kind === "elided") {
|
|
2184
|
-
raw.push({ kind: "elided", startLine: segment.startLine, endLine: segment.endLine });
|
|
2185
|
-
continue;
|
|
2186
|
-
}
|
|
2187
|
-
const text = segment.text ?? "";
|
|
2188
|
-
if (text.length === 0) continue;
|
|
2189
|
-
const lines = text.split("\n");
|
|
2190
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2191
|
-
raw.push({ kind: "line", line: segment.startLine + i, text: lines[i] });
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
const units: Unit[] = [];
|
|
2196
|
-
let i = 0;
|
|
2197
|
-
while (i < raw.length) {
|
|
2198
|
-
const cur = raw[i];
|
|
2199
|
-
if (cur.kind === "elided") {
|
|
2200
|
-
const prev = units.length > 0 ? units[units.length - 1] : null;
|
|
2201
|
-
const next = i + 1 < raw.length ? raw[i + 1] : null;
|
|
2202
|
-
if (prev?.kind === "line" && next?.kind === "line" && canMergeBracePair(prev.text, next.text)) {
|
|
2203
|
-
units.pop();
|
|
2204
|
-
units.push({
|
|
2205
|
-
kind: "merged",
|
|
2206
|
-
startLine: prev.line,
|
|
2207
|
-
endLine: next.line,
|
|
2208
|
-
headText: prev.text,
|
|
2209
|
-
tailText: next.text,
|
|
2210
|
-
});
|
|
2211
|
-
i += 2;
|
|
2212
|
-
continue;
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
units.push(cur);
|
|
2216
|
-
i++;
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
const modelParts: string[] = [];
|
|
2220
|
-
const displayParts: string[] = [];
|
|
2221
|
-
const elidedRanges: ElidedRange[] = [];
|
|
2222
|
-
let elidedLines = 0;
|
|
2223
|
-
for (const unit of units) {
|
|
2224
|
-
if (unit.kind === "elided") {
|
|
2225
|
-
modelParts.push("…");
|
|
2226
|
-
displayParts.push("…");
|
|
2227
|
-
elidedRanges.push({ start: unit.startLine, end: unit.endLine });
|
|
2228
|
-
elidedLines += unit.endLine - unit.startLine + 1;
|
|
2229
|
-
continue;
|
|
2230
|
-
}
|
|
2231
|
-
if (unit.kind === "merged") {
|
|
2232
|
-
const formatted = formatMergedBraceLine(
|
|
2233
|
-
unit.startLine,
|
|
2234
|
-
unit.endLine,
|
|
2235
|
-
unit.headText,
|
|
2236
|
-
unit.tailText,
|
|
2237
|
-
shouldAddHashLines,
|
|
2238
|
-
shouldAddLineNumbers,
|
|
2239
|
-
);
|
|
2240
|
-
modelParts.push(formatted.model);
|
|
2241
|
-
displayParts.push(formatted.display);
|
|
2242
|
-
// Suggest the full brace range so re-reading shows both braces
|
|
2243
|
-
// plus the elided body in one shot.
|
|
2244
|
-
elidedRanges.push({ start: unit.startLine, end: unit.endLine });
|
|
2245
|
-
// Merged brace pair encloses (start+1)..(end-1) as elided.
|
|
2246
|
-
elidedLines += Math.max(0, unit.endLine - unit.startLine - 1);
|
|
2247
|
-
continue;
|
|
2248
|
-
}
|
|
2249
|
-
modelParts.push(formatSingleLine(unit.line, unit.text, shouldAddHashLines, shouldAddLineNumbers));
|
|
2250
|
-
displayParts.push(unit.text);
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
return { text: modelParts.join("\n"), displayText: displayParts.join("\n"), elidedRanges, elidedLines };
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
780
|
async execute(
|
|
2257
781
|
_toolCallId: string,
|
|
2258
782
|
params: ReadParams,
|
|
@@ -2287,7 +811,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2287
811
|
const entry = await fetchReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal, {
|
|
2288
812
|
ensureArtifact: true,
|
|
2289
813
|
});
|
|
2290
|
-
return
|
|
814
|
+
return buildInMemoryMultiRangeResult(this.session, entry.output, urlRanges, {
|
|
2291
815
|
details: { ...entry.details },
|
|
2292
816
|
sourceUrl: entry.details.finalUrl,
|
|
2293
817
|
entityLabel: "URL output",
|
|
@@ -2301,7 +825,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2301
825
|
const entry = await fetchReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal, {
|
|
2302
826
|
ensureArtifact: true,
|
|
2303
827
|
});
|
|
2304
|
-
return
|
|
828
|
+
return buildInMemoryTextResult(this.session, entry.output, urlOffset, urlLimit, {
|
|
2305
829
|
details: { ...entry.details },
|
|
2306
830
|
sourceUrl: entry.details.finalUrl,
|
|
2307
831
|
entityLabel: "URL output",
|
|
@@ -2370,14 +894,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2370
894
|
: literalSplit.sel === undefined && splitPathAndSel(readPath).sel !== undefined;
|
|
2371
895
|
|
|
2372
896
|
if (!rawPathIsLiteral) {
|
|
2373
|
-
const archivePath = await
|
|
897
|
+
const archivePath = await resolveArchiveReadPath(this.session, readPath, suffixCache, signal);
|
|
2374
898
|
if (archivePath) {
|
|
2375
899
|
const archiveSubPath =
|
|
2376
900
|
promotedSelector === undefined
|
|
2377
901
|
? splitPathAndSel(archivePath.archiveSubPath)
|
|
2378
902
|
: { path: archivePath.archiveSubPath, sel: promotedSelector };
|
|
2379
903
|
const archiveParsed = parseSel(archiveSubPath.sel);
|
|
2380
|
-
return
|
|
904
|
+
return readArchive(
|
|
905
|
+
this.session,
|
|
2381
906
|
readPath,
|
|
2382
907
|
archiveParsed,
|
|
2383
908
|
{ ...archivePath, archiveSubPath: archiveSubPath.path },
|
|
@@ -2385,9 +910,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2385
910
|
);
|
|
2386
911
|
}
|
|
2387
912
|
|
|
2388
|
-
const sqlitePath = await
|
|
913
|
+
const sqlitePath = await resolveSqliteReadPath(this.session, readPath, suffixCache, signal);
|
|
2389
914
|
if (sqlitePath) {
|
|
2390
|
-
return
|
|
915
|
+
return readSqlite(sqlitePath, signal);
|
|
2391
916
|
}
|
|
2392
917
|
|
|
2393
918
|
const pdfImageMemberPath = splitPdfImageMemberReadPath(readPath);
|
|
@@ -2400,12 +925,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2400
925
|
throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' is a directory, not a PDF file`);
|
|
2401
926
|
} catch (error) {
|
|
2402
927
|
if (!isNotFoundError(error) || isRemoteMountPath(absolutePdfPath)) throw error;
|
|
2403
|
-
const suffixMatch = await
|
|
928
|
+
const suffixMatch = await findSuffixMatchCached(
|
|
929
|
+
this.session,
|
|
930
|
+
suffixCache,
|
|
931
|
+
pdfImageMemberPath.pdfPath,
|
|
932
|
+
signal,
|
|
933
|
+
);
|
|
2404
934
|
if (!suffixMatch) throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' not found`);
|
|
2405
935
|
absolutePdfPath = suffixMatch.absolutePath;
|
|
2406
936
|
suffixResolution = { from: pdfImageMemberPath.pdfPath, to: suffixMatch.displayPath };
|
|
2407
937
|
}
|
|
2408
|
-
return
|
|
938
|
+
return readPdfImageMember(
|
|
939
|
+
this.session,
|
|
940
|
+
this.#autoResizeImages,
|
|
2409
941
|
absolutePdfPath,
|
|
2410
942
|
pdfImageMemberPath.pdfPath,
|
|
2411
943
|
pdfImageMemberPath.member,
|
|
@@ -2433,7 +965,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2433
965
|
// Attempt unique suffix resolution before falling back to the approved-plan
|
|
2434
966
|
// alias or fuzzy suggestions. Existing workspace files retain precedence.
|
|
2435
967
|
if (!isRemoteMountPath(absolutePath)) {
|
|
2436
|
-
const suffixMatch = await
|
|
968
|
+
const suffixMatch = await findSuffixMatchCached(this.session, suffixCache, localReadPath, signal);
|
|
2437
969
|
if (suffixMatch) {
|
|
2438
970
|
try {
|
|
2439
971
|
const retryStat = await Bun.file(suffixMatch.absolutePath).stat();
|
|
@@ -2497,6 +1029,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2497
1029
|
const imageMetadata = await readImageMetadata(absolutePath);
|
|
2498
1030
|
const mimeType = imageMetadata?.mimeType;
|
|
2499
1031
|
const ext = path.extname(absolutePath).toLowerCase();
|
|
1032
|
+
const resolvedDisplayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
2500
1033
|
const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
|
|
2501
1034
|
|
|
2502
1035
|
// Profiler reports (macOS `sample` call trees, V8 `.cpuprofile` JSON):
|
|
@@ -2509,14 +1042,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2509
1042
|
else if (isCpuProfilePath(absolutePath)) rendered = renderCpuProfile(await Bun.file(absolutePath).text());
|
|
2510
1043
|
if (rendered) {
|
|
2511
1044
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
2512
|
-
return
|
|
1045
|
+
return buildInMemoryMultiRangeResult(this.session, rendered, parsed.ranges, {
|
|
2513
1046
|
details: { resolvedPath: absolutePath },
|
|
2514
1047
|
sourcePath: absolutePath,
|
|
2515
1048
|
entityLabel: "profile summary",
|
|
2516
1049
|
});
|
|
2517
1050
|
}
|
|
2518
1051
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
2519
|
-
return
|
|
1052
|
+
return buildInMemoryTextResult(this.session, rendered, offset, limit, {
|
|
2520
1053
|
details: { resolvedPath: absolutePath },
|
|
2521
1054
|
sourcePath: absolutePath,
|
|
2522
1055
|
entityLabel: "profile summary",
|
|
@@ -2541,16 +1074,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2541
1074
|
fileSize,
|
|
2542
1075
|
}));
|
|
2543
1076
|
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
2544
|
-
const notebookText = await readEditableNotebookText(absolutePath,
|
|
1077
|
+
const notebookText = await readEditableNotebookText(absolutePath, resolvedDisplayPath);
|
|
2545
1078
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
2546
|
-
return
|
|
1079
|
+
return buildInMemoryMultiRangeResult(this.session, notebookText, parsed.ranges, {
|
|
2547
1080
|
details: { resolvedPath: absolutePath },
|
|
2548
1081
|
sourcePath: absolutePath,
|
|
2549
1082
|
entityLabel: "notebook",
|
|
2550
1083
|
});
|
|
2551
1084
|
}
|
|
2552
1085
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
2553
|
-
return
|
|
1086
|
+
return buildInMemoryTextResult(this.session, notebookText, offset, limit, {
|
|
2554
1087
|
details: { resolvedPath: absolutePath },
|
|
2555
1088
|
sourcePath: absolutePath,
|
|
2556
1089
|
entityLabel: "notebook",
|
|
@@ -2560,14 +1093,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2560
1093
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
2561
1094
|
if (result.ok) {
|
|
2562
1095
|
const renderedContent =
|
|
2563
|
-
ext === ".pdf" ? rewritePdfImagePlaceholders(result.content,
|
|
1096
|
+
ext === ".pdf" ? rewritePdfImagePlaceholders(result.content, resolvedDisplayPath) : result.content;
|
|
2564
1097
|
// Route the converted markdown through the in-memory text builder
|
|
2565
1098
|
// so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
|
|
2566
1099
|
// raw mode apply against the converted output. Without this,
|
|
2567
1100
|
// `file.pdf:50-100` silently returned the head of the document
|
|
2568
1101
|
// because only `truncateHead` was being applied.
|
|
2569
1102
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
2570
|
-
return
|
|
1103
|
+
return buildInMemoryMultiRangeResult(this.session, renderedContent, parsed.ranges, {
|
|
2571
1104
|
details: {
|
|
2572
1105
|
resolvedPath: absolutePath,
|
|
2573
1106
|
contentType: this.session.settings.get("read.renderMarkdown") ? "text/markdown" : undefined,
|
|
@@ -2577,7 +1110,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2577
1110
|
});
|
|
2578
1111
|
}
|
|
2579
1112
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
2580
|
-
return
|
|
1113
|
+
return buildInMemoryTextResult(this.session, renderedContent, offset, limit, {
|
|
2581
1114
|
details: {
|
|
2582
1115
|
resolvedPath: absolutePath,
|
|
2583
1116
|
contentType: this.session.settings.get("read.renderMarkdown") ? "text/markdown" : undefined,
|
|
@@ -2603,7 +1136,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2603
1136
|
return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
|
|
2604
1137
|
.text(
|
|
2605
1138
|
prependSuffixResolutionNotice(
|
|
2606
|
-
`[Cannot read binary file '${
|
|
1139
|
+
`[Cannot read binary file '${resolvedDisplayPath}' (${formatBytes(fileSize)}); not valid UTF-8 text. Use ':raw' to read bytes verbatim.]`,
|
|
2607
1140
|
suffixResolution,
|
|
2608
1141
|
),
|
|
2609
1142
|
)
|
|
@@ -2616,11 +1149,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2616
1149
|
this.session.settings.get("read.summarize.enabled") &&
|
|
2617
1150
|
(this.session.settings.get("read.summarize.prose") || !isProseSummaryPath(absolutePath))
|
|
2618
1151
|
) {
|
|
2619
|
-
const summary = await
|
|
1152
|
+
const summary = await trySummarize(this.session, absolutePath, fileSize, signal);
|
|
2620
1153
|
if (summary?.parsed && summary.elided) {
|
|
2621
|
-
const renderedSummary =
|
|
1154
|
+
const renderedSummary = renderSummary(this.session, summary);
|
|
2622
1155
|
const footer = formatSummaryElisionFooter(
|
|
2623
|
-
|
|
1156
|
+
resolvedDisplayPath,
|
|
2624
1157
|
renderedSummary.elidedRanges,
|
|
2625
1158
|
renderedSummary.elidedLines,
|
|
2626
1159
|
);
|
|
@@ -2669,12 +1202,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2669
1202
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
2670
1203
|
// Try ACP bridge first — editor's in-memory buffer is source of truth.
|
|
2671
1204
|
// Request full text so local range rendering keeps normal context and line numbers.
|
|
2672
|
-
const bridgePromise =
|
|
1205
|
+
const bridgePromise = routeReadThroughBridge(this.session, absolutePath);
|
|
2673
1206
|
if (bridgePromise !== undefined) {
|
|
2674
1207
|
try {
|
|
2675
1208
|
const bridgeText = await bridgePromise;
|
|
2676
|
-
const bridgeResult =
|
|
2677
|
-
details:
|
|
1209
|
+
const bridgeResult = buildInMemoryTextResult(this.session, bridgeText, offset, limit, {
|
|
1210
|
+
details: markMarkdownContentType(
|
|
1211
|
+
this.session,
|
|
2678
1212
|
{ resolvedPath: absolutePath, suffixResolution },
|
|
2679
1213
|
absolutePath,
|
|
2680
1214
|
),
|
|
@@ -2978,7 +1512,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2978
1512
|
}
|
|
2979
1513
|
|
|
2980
1514
|
details.fileSize = fileSize;
|
|
2981
|
-
|
|
1515
|
+
markMarkdownContentType(this.session, details, absolutePath);
|
|
2982
1516
|
if (suffixResolution) {
|
|
2983
1517
|
details.suffixResolution = suffixResolution;
|
|
2984
1518
|
// Inline resolution notice into first text block so the model sees the actual path
|
|
@@ -3339,7 +1873,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
3339
1873
|
|
|
3340
1874
|
const raw = isRawSelector(parsedSel);
|
|
3341
1875
|
if (isMultiRange(parsedSel) && parsedSel.kind === "lines") {
|
|
3342
|
-
return
|
|
1876
|
+
return buildInMemoryMultiRangeResult(this.session, resource.content, parsedSel.ranges, {
|
|
3343
1877
|
details,
|
|
3344
1878
|
sourcePath: resource.sourcePath,
|
|
3345
1879
|
sourceInternal: url,
|
|
@@ -3350,7 +1884,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
3350
1884
|
}
|
|
3351
1885
|
|
|
3352
1886
|
const { offset, limit } = selToOffsetLimit(parsedSel);
|
|
3353
|
-
return
|
|
1887
|
+
return buildInMemoryTextResult(this.session, resource.content, offset, limit, {
|
|
3354
1888
|
details,
|
|
3355
1889
|
sourcePath: resource.sourcePath,
|
|
3356
1890
|
sourceInternal: url,
|
|
@@ -3484,279 +2018,3 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
3484
2018
|
return resultBuilder.done();
|
|
3485
2019
|
}
|
|
3486
2020
|
}
|
|
3487
|
-
|
|
3488
|
-
// =============================================================================
|
|
3489
|
-
// TUI Renderer
|
|
3490
|
-
// =============================================================================
|
|
3491
|
-
|
|
3492
|
-
interface ReadRenderArgs {
|
|
3493
|
-
path?: unknown;
|
|
3494
|
-
file_path?: unknown;
|
|
3495
|
-
// Legacy fields from old schema — tolerated for in-flight tool calls during transition
|
|
3496
|
-
offset?: number;
|
|
3497
|
-
limit?: number;
|
|
3498
|
-
raw?: boolean;
|
|
3499
|
-
}
|
|
3500
|
-
|
|
3501
|
-
const INTERNAL_URL_LIKE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
3502
|
-
|
|
3503
|
-
function splitReadRenderPath(rawPath: string): { path: string; sel?: string } {
|
|
3504
|
-
if (INTERNAL_URL_LIKE_RE.test(rawPath)) {
|
|
3505
|
-
const internal = splitInternalUrlSel(rawPath);
|
|
3506
|
-
if (internal.sel) return internal;
|
|
3507
|
-
}
|
|
3508
|
-
return splitPathAndSel(rawPath);
|
|
3509
|
-
}
|
|
3510
|
-
|
|
3511
|
-
function firstReadSelectorLine(sel: string | undefined): number | undefined {
|
|
3512
|
-
if (!sel) return undefined;
|
|
3513
|
-
try {
|
|
3514
|
-
const parsed = parseSel(sel);
|
|
3515
|
-
if (parsed.kind !== "lines") return undefined;
|
|
3516
|
-
return parsed.ranges[0].startLine;
|
|
3517
|
-
} catch {
|
|
3518
|
-
return undefined;
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
|
-
|
|
3522
|
-
/** Absolute fs path the read result actually resolved to, used as the OSC 8 link
|
|
3523
|
-
* target when the structured `resolvedPath` isn't set (the common plain-file and
|
|
3524
|
-
* image reads only record the path in `meta.source`). URL/internal sources are
|
|
3525
|
-
* not fs paths, so only `type: "path"` qualifies. */
|
|
3526
|
-
function readSourceFsPath(details: ReadToolDetails | undefined): string | undefined {
|
|
3527
|
-
const source = details?.meta?.source;
|
|
3528
|
-
return source?.type === "path" ? source.value : undefined;
|
|
3529
|
-
}
|
|
3530
|
-
|
|
3531
|
-
function formatReadPathLink(
|
|
3532
|
-
rawPath: string,
|
|
3533
|
-
options: {
|
|
3534
|
-
resolvedPath?: string;
|
|
3535
|
-
sourcePath?: string;
|
|
3536
|
-
suffixResolution?: { from: string; to: string };
|
|
3537
|
-
offset?: number;
|
|
3538
|
-
fallbackLabel?: string;
|
|
3539
|
-
},
|
|
3540
|
-
): string {
|
|
3541
|
-
const split = splitReadRenderPath(rawPath);
|
|
3542
|
-
const basePath = split.path || rawPath;
|
|
3543
|
-
const selectorSuffix = split.sel ? `:${split.sel}` : "";
|
|
3544
|
-
const plainDisplayPath = options.suffixResolution
|
|
3545
|
-
? shortenPath(options.suffixResolution.to)
|
|
3546
|
-
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
3547
|
-
const absoluteInputPath = path.isAbsolute(basePath) ? basePath : undefined;
|
|
3548
|
-
const target =
|
|
3549
|
-
options.resolvedPath ?? options.sourcePath ?? tryResolveInternalUrlSync(basePath) ?? absoluteInputPath;
|
|
3550
|
-
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
3551
|
-
const linkOptions = line !== undefined ? { line } : undefined;
|
|
3552
|
-
const linkedPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
3553
|
-
return `${linkedPath}${selectorSuffix}`;
|
|
3554
|
-
}
|
|
3555
|
-
|
|
3556
|
-
export const readToolRenderer = {
|
|
3557
|
-
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
3558
|
-
const rawPath =
|
|
3559
|
-
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
3560
|
-
if (isReadableUrlPath(rawPath)) {
|
|
3561
|
-
return renderReadUrlCall({ path: rawPath, raw: args.raw }, _options, uiTheme);
|
|
3562
|
-
}
|
|
3563
|
-
|
|
3564
|
-
const offset = args.offset;
|
|
3565
|
-
const limit = args.limit;
|
|
3566
|
-
|
|
3567
|
-
let pathDisplay = formatReadPathLink(rawPath, { offset, fallbackLabel: "…" }) || "…";
|
|
3568
|
-
if (offset !== undefined || limit !== undefined) {
|
|
3569
|
-
const startLine = offset ?? 1;
|
|
3570
|
-
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
3571
|
-
pathDisplay += `:${startLine}${endLine ? `-${endLine}` : ""}`;
|
|
3572
|
-
}
|
|
3573
|
-
|
|
3574
|
-
const text = renderStatusLine({ icon: "pending", title: "Read", description: pathDisplay }, uiTheme);
|
|
3575
|
-
return new Text(text, 0, 0);
|
|
3576
|
-
},
|
|
3577
|
-
|
|
3578
|
-
renderResult(
|
|
3579
|
-
result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails; isError?: boolean },
|
|
3580
|
-
options: RenderResultOptions,
|
|
3581
|
-
uiTheme: Theme,
|
|
3582
|
-
args?: ReadRenderArgs,
|
|
3583
|
-
): Component {
|
|
3584
|
-
const urlDetails = result.details as ReadUrlToolDetails | undefined;
|
|
3585
|
-
const baseRawPathForKind =
|
|
3586
|
-
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3587
|
-
if (urlDetails?.kind === "url" || isReadableUrlPath(baseRawPathForKind)) {
|
|
3588
|
-
return renderReadUrlResult(
|
|
3589
|
-
result as {
|
|
3590
|
-
content: Array<{ type: string; text?: string }>;
|
|
3591
|
-
details?: ReadUrlToolDetails;
|
|
3592
|
-
isError?: boolean;
|
|
3593
|
-
},
|
|
3594
|
-
options,
|
|
3595
|
-
uiTheme,
|
|
3596
|
-
);
|
|
3597
|
-
}
|
|
3598
|
-
|
|
3599
|
-
if (result.isError) {
|
|
3600
|
-
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
3601
|
-
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
3602
|
-
const rawPath =
|
|
3603
|
-
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3604
|
-
const filePath =
|
|
3605
|
-
formatReadPathLink(rawPath, { offset: args?.offset, sourcePath: readSourceFsPath(result.details) }) ||
|
|
3606
|
-
shortenPath(rawPath);
|
|
3607
|
-
let title = filePath ? `Read ${filePath}` : "Read";
|
|
3608
|
-
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
3609
|
-
const startLine = args.offset ?? 1;
|
|
3610
|
-
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
|
3611
|
-
title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
|
|
3612
|
-
}
|
|
3613
|
-
const header = renderStatusLine({ icon: "error", title }, uiTheme);
|
|
3614
|
-
const errorLines = errorText.split("\n").map(line => uiTheme.fg("error", replaceTabs(line)));
|
|
3615
|
-
const outputBlock = new CachedOutputBlock();
|
|
3616
|
-
return markFramedBlockComponent({
|
|
3617
|
-
render: (width: number) =>
|
|
3618
|
-
outputBlock.render({ header, state: "error", sections: [{ lines: errorLines }], width }, uiTheme),
|
|
3619
|
-
invalidate: () => outputBlock.invalidate(),
|
|
3620
|
-
});
|
|
3621
|
-
}
|
|
3622
|
-
const details = result.details;
|
|
3623
|
-
const rawText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
3624
|
-
// Prefer structured `displayContent` from details when available so the TUI
|
|
3625
|
-
// shows clean file content (no model-only hashline anchors) without parsing the formatted text.
|
|
3626
|
-
// Fall back to the raw text, but strip the LLM-facing notice so it doesn't
|
|
3627
|
-
// echo next to the styled warning line below.
|
|
3628
|
-
const contentText = details?.displayContent?.text ?? stripOutputNotice(rawText, details?.meta);
|
|
3629
|
-
const imageContent = result.content?.find(c => c.type === "image");
|
|
3630
|
-
const rawPath =
|
|
3631
|
-
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3632
|
-
const renderPath = splitReadRenderPath(rawPath);
|
|
3633
|
-
const lang = getLanguageFromPath(renderPath.path);
|
|
3634
|
-
|
|
3635
|
-
const warningLines: string[] = [];
|
|
3636
|
-
const truncation = details?.meta?.truncation;
|
|
3637
|
-
const fallback = details?.truncation;
|
|
3638
|
-
if (details?.resolvedPath) {
|
|
3639
|
-
warningLines.push(uiTheme.fg("dim", wrapBrackets(`Resolved path: ${details.resolvedPath}`, uiTheme)));
|
|
3640
|
-
}
|
|
3641
|
-
if (truncation) {
|
|
3642
|
-
if (fallback?.firstLineExceedsLimit) {
|
|
3643
|
-
let warning = `First line exceeds ${formatBytes(fallback.outputBytes ?? fallback.totalBytes)} limit`;
|
|
3644
|
-
if (truncation.artifactId) {
|
|
3645
|
-
warning += `. ${formatFullOutputReference(truncation.artifactId)}`;
|
|
3646
|
-
}
|
|
3647
|
-
warningLines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
|
|
3648
|
-
} else {
|
|
3649
|
-
const warning = formatStyledTruncationWarning(details?.meta, uiTheme);
|
|
3650
|
-
if (warning) warningLines.push(warning);
|
|
3651
|
-
}
|
|
3652
|
-
}
|
|
3653
|
-
|
|
3654
|
-
if (imageContent) {
|
|
3655
|
-
const suffix = details?.suffixResolution;
|
|
3656
|
-
const displayPath = formatReadPathLink(rawPath, {
|
|
3657
|
-
resolvedPath: details?.resolvedPath,
|
|
3658
|
-
sourcePath: readSourceFsPath(details),
|
|
3659
|
-
suffixResolution: suffix,
|
|
3660
|
-
fallbackLabel: "image",
|
|
3661
|
-
});
|
|
3662
|
-
const correction = suffix ? ` ${uiTheme.fg("dim", `(corrected from ${shortenPath(suffix.from)})`)}` : "";
|
|
3663
|
-
const header = renderStatusLine(
|
|
3664
|
-
{ icon: suffix ? "warning" : "success", title: "Read", description: `${displayPath}${correction}` },
|
|
3665
|
-
uiTheme,
|
|
3666
|
-
);
|
|
3667
|
-
const detailLines = contentText ? contentText.split("\n").map(line => uiTheme.fg("toolOutput", line)) : [];
|
|
3668
|
-
const lines = [...detailLines, ...warningLines];
|
|
3669
|
-
const outputBlock = new CachedOutputBlock();
|
|
3670
|
-
return markFramedBlockComponent({
|
|
3671
|
-
render: (width: number) =>
|
|
3672
|
-
outputBlock.render(
|
|
3673
|
-
{
|
|
3674
|
-
header,
|
|
3675
|
-
state: "success",
|
|
3676
|
-
sections: [
|
|
3677
|
-
{
|
|
3678
|
-
label: uiTheme.fg("toolTitle", "Details"),
|
|
3679
|
-
lines: lines.length > 0 ? lines : [uiTheme.fg("dim", "(image)")],
|
|
3680
|
-
},
|
|
3681
|
-
],
|
|
3682
|
-
width,
|
|
3683
|
-
},
|
|
3684
|
-
uiTheme,
|
|
3685
|
-
),
|
|
3686
|
-
invalidate: () => outputBlock.invalidate(),
|
|
3687
|
-
});
|
|
3688
|
-
}
|
|
3689
|
-
|
|
3690
|
-
const suffix = details?.suffixResolution;
|
|
3691
|
-
// resolvedPath is the absolute fs path when a read resolved/corrected the
|
|
3692
|
-
// input (suffix match, internal URL, archive/sqlite/notebook); plain file
|
|
3693
|
-
// reads only record the absolute path in meta.source, so fall back to that
|
|
3694
|
-
// (and then to a sync internal-URL resolver) to keep the title clickable.
|
|
3695
|
-
const displayPath = formatReadPathLink(rawPath, {
|
|
3696
|
-
resolvedPath: details?.resolvedPath,
|
|
3697
|
-
sourcePath: readSourceFsPath(details),
|
|
3698
|
-
suffixResolution: suffix,
|
|
3699
|
-
offset: args?.offset,
|
|
3700
|
-
});
|
|
3701
|
-
const correction = suffix ? ` ${uiTheme.fg("dim", `(corrected from ${shortenPath(suffix.from)})`)}` : "";
|
|
3702
|
-
let title = displayPath ? `Read ${displayPath}${correction}` : "Read";
|
|
3703
|
-
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
3704
|
-
const startLine = args.offset ?? 1;
|
|
3705
|
-
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
|
3706
|
-
title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
|
|
3707
|
-
}
|
|
3708
|
-
if (details?.summary) {
|
|
3709
|
-
title += ` (summary: ${details.summary.elidedSpans} elided span${details.summary.elidedSpans === 1 ? "" : "s"})`;
|
|
3710
|
-
}
|
|
3711
|
-
if (details?.conflictCount && details.conflictCount > 0) {
|
|
3712
|
-
const n = details.conflictCount;
|
|
3713
|
-
title += ` ${uiTheme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
|
|
3714
|
-
}
|
|
3715
|
-
const rawRequested = args?.raw === true || isRawSelector(parseSel(renderPath.sel));
|
|
3716
|
-
const isMarkdown = details?.contentType === "text/markdown" && !rawRequested;
|
|
3717
|
-
let cachedWidth: number | undefined;
|
|
3718
|
-
let cachedExpanded: boolean | undefined;
|
|
3719
|
-
let cachedLines: string[] | undefined;
|
|
3720
|
-
return markFramedBlockComponent({
|
|
3721
|
-
render: (width: number) => {
|
|
3722
|
-
const expanded = options.expanded;
|
|
3723
|
-
if (cachedLines && cachedWidth === width && cachedExpanded === expanded) return cachedLines;
|
|
3724
|
-
cachedLines = isMarkdown
|
|
3725
|
-
? renderMarkdownCell(
|
|
3726
|
-
{
|
|
3727
|
-
content: contentText,
|
|
3728
|
-
title,
|
|
3729
|
-
status: "complete",
|
|
3730
|
-
output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
|
|
3731
|
-
expanded,
|
|
3732
|
-
width,
|
|
3733
|
-
},
|
|
3734
|
-
uiTheme,
|
|
3735
|
-
)
|
|
3736
|
-
: renderCodeCell(
|
|
3737
|
-
{
|
|
3738
|
-
code: contentText,
|
|
3739
|
-
language: lang,
|
|
3740
|
-
title,
|
|
3741
|
-
status: "complete",
|
|
3742
|
-
output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
|
|
3743
|
-
expanded,
|
|
3744
|
-
codeStartLine: details?.displayContent?.startLine,
|
|
3745
|
-
codeLineNumbers: details?.displayContent?.lineNumbers,
|
|
3746
|
-
width,
|
|
3747
|
-
},
|
|
3748
|
-
uiTheme,
|
|
3749
|
-
);
|
|
3750
|
-
cachedWidth = width;
|
|
3751
|
-
cachedExpanded = expanded;
|
|
3752
|
-
return cachedLines;
|
|
3753
|
-
},
|
|
3754
|
-
invalidate: () => {
|
|
3755
|
-
cachedWidth = undefined;
|
|
3756
|
-
cachedExpanded = undefined;
|
|
3757
|
-
cachedLines = undefined;
|
|
3758
|
-
},
|
|
3759
|
-
});
|
|
3760
|
-
},
|
|
3761
|
-
mergeCallAndResult: true,
|
|
3762
|
-
};
|