@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
package/src/tools/read.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
|
6
6
|
import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
|
|
7
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
|
-
import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
11
11
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
12
12
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
@@ -25,8 +25,9 @@ import {
|
|
|
25
25
|
type TruncationResult,
|
|
26
26
|
truncateHead,
|
|
27
27
|
truncateHeadBytes,
|
|
28
|
+
truncateLine,
|
|
28
29
|
} from "../session/streaming-output";
|
|
29
|
-
import { renderCodeCell, renderStatusLine } from "../tui";
|
|
30
|
+
import { renderCodeCell, renderMarkdownCell, renderStatusLine } from "../tui";
|
|
30
31
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
31
32
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
32
33
|
import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
|
|
@@ -54,9 +55,14 @@ import {
|
|
|
54
55
|
renderReadUrlResult,
|
|
55
56
|
} from "./fetch";
|
|
56
57
|
import { applyListLimit } from "./list-limit";
|
|
57
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
formatFullOutputReference,
|
|
60
|
+
formatStyledTruncationWarning,
|
|
61
|
+
type OutputMeta,
|
|
62
|
+
resolveOutputMaxColumns,
|
|
63
|
+
} from "./output-meta";
|
|
58
64
|
import { expandPath, formatPathRelativeToCwd, resolveReadPath, splitPathAndSel } from "./path-utils";
|
|
59
|
-
import { formatBytes, shortenPath, wrapBrackets } from "./render-utils";
|
|
65
|
+
import { formatBytes, replaceTabs, shortenPath, wrapBrackets } from "./render-utils";
|
|
60
66
|
import {
|
|
61
67
|
executeReadQuery,
|
|
62
68
|
getRowByKey,
|
|
@@ -81,6 +87,12 @@ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx"
|
|
|
81
87
|
|
|
82
88
|
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
83
89
|
const MAX_SUMMARY_LINES = 20_000;
|
|
90
|
+
/**
|
|
91
|
+
* Per-line column cap for file reads. Lines wider than the value of
|
|
92
|
+
* `tools.outputMaxColumns` are ellipsis-truncated at display time; the file
|
|
93
|
+
* on disk is unchanged. Shared with the streaming sink path so one setting
|
|
94
|
+
* covers `bash`/`ssh`/`python`/`js eval` and `read` uniformly.
|
|
95
|
+
*/
|
|
84
96
|
const PROSE_SUMMARY_EXTENSIONS = new Set([".md", ".txt"]);
|
|
85
97
|
// Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
|
|
86
98
|
const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
|
|
@@ -163,18 +175,25 @@ function countTextLines(text: string): number {
|
|
|
163
175
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
164
176
|
|
|
165
177
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
178
|
+
* Context lines added around an explicit range read. Anchor-stale failures
|
|
179
|
+
* cluster on edits whose anchors land just outside the most recent read
|
|
180
|
+
* window, but the data (`scripts/session-stats/analyze_selector_reads.py`)
|
|
181
|
+
* shows most follow-up reads are disjoint hops, not adjacent extensions —
|
|
182
|
+
* so symmetric padding rarely pays for itself.
|
|
183
|
+
*
|
|
184
|
+
* Leading=1 catches accidental single-line reads where the anchor is the
|
|
185
|
+
* line immediately above the requested start. Trailing=3 buffers the
|
|
186
|
+
* common case where the agent asks for a narrow range and then needs the
|
|
187
|
+
* next few lines to disambiguate an anchor.
|
|
170
188
|
*/
|
|
171
|
-
const
|
|
189
|
+
const RANGE_LEADING_CONTEXT_LINES = 1;
|
|
190
|
+
const RANGE_TRAILING_CONTEXT_LINES = 3;
|
|
172
191
|
|
|
173
192
|
/**
|
|
174
|
-
* Expand a [start, end) range with
|
|
193
|
+
* Expand a [start, end) range with leading/trailing context lines on the
|
|
175
194
|
* sides where the user actually constrained the range. A start of 0 (no
|
|
176
|
-
* explicit offset) does not get leading context — that's already an
|
|
177
|
-
* read from the top.
|
|
195
|
+
* explicit offset) does not get leading context — that's already an
|
|
196
|
+
* open-ended read from the top.
|
|
178
197
|
*/
|
|
179
198
|
function expandRangeWithContext(
|
|
180
199
|
requestedStart: number,
|
|
@@ -184,8 +203,8 @@ function expandRangeWithContext(
|
|
|
184
203
|
expandEnd: boolean,
|
|
185
204
|
): { startLine: number; endLine: number } {
|
|
186
205
|
return {
|
|
187
|
-
startLine: expandStart ? Math.max(0, requestedStart -
|
|
188
|
-
endLine: expandEnd ? Math.min(totalLines, requestedEnd +
|
|
206
|
+
startLine: expandStart ? Math.max(0, requestedStart - RANGE_LEADING_CONTEXT_LINES) : requestedStart,
|
|
207
|
+
endLine: expandEnd ? Math.min(totalLines, requestedEnd + RANGE_TRAILING_CONTEXT_LINES) : requestedEnd,
|
|
189
208
|
};
|
|
190
209
|
}
|
|
191
210
|
|
|
@@ -473,11 +492,13 @@ export interface ReadToolDetails {
|
|
|
473
492
|
type ReadParams = ReadToolInput;
|
|
474
493
|
|
|
475
494
|
/** Parsed representation of a path-embedded selector. */
|
|
495
|
+
type LineRange = { startLine: number; endLine: number | undefined };
|
|
496
|
+
|
|
476
497
|
type ParsedSelector =
|
|
477
498
|
| { kind: "none" }
|
|
478
499
|
| { kind: "raw" }
|
|
479
500
|
| { kind: "conflicts" }
|
|
480
|
-
| { kind: "lines";
|
|
501
|
+
| { kind: "lines"; ranges: [LineRange, ...LineRange[]]; raw?: boolean };
|
|
481
502
|
|
|
482
503
|
const LINE_RANGE_RE = /^L?(\d+)(?:([-+])L?(\d+))?$/i;
|
|
483
504
|
|
|
@@ -486,7 +507,12 @@ function isRawSelector(parsed: ParsedSelector): boolean {
|
|
|
486
507
|
return parsed.kind === "raw" || (parsed.kind === "lines" && parsed.raw === true);
|
|
487
508
|
}
|
|
488
509
|
|
|
489
|
-
|
|
510
|
+
/** Returns true when the selector requested multiple line ranges. */
|
|
511
|
+
function isMultiRange(parsed: ParsedSelector): boolean {
|
|
512
|
+
return parsed.kind === "lines" && parsed.ranges.length > 1;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function parseLineRangeChunk(sel: string): LineRange | null {
|
|
490
516
|
const lineMatch = LINE_RANGE_RE.exec(sel);
|
|
491
517
|
if (!lineMatch) return null;
|
|
492
518
|
const rawStart = Number.parseInt(lineMatch[1]!, 10);
|
|
@@ -510,11 +536,45 @@ function parseLineRangeChunk(sel: string): { startLine: number; endLine: number
|
|
|
510
536
|
return { startLine: rawStart, endLine: rawEnd };
|
|
511
537
|
}
|
|
512
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Parse a comma-separated list of line ranges (e.g. `5-16,960-973`). Returns
|
|
541
|
+
* the ranges in ascending order with overlapping/adjacent ranges merged so
|
|
542
|
+
* downstream consumers can stream the file in a single forward pass per range.
|
|
543
|
+
*/
|
|
544
|
+
function parseLineRanges(sel: string): [LineRange, ...LineRange[]] | null {
|
|
545
|
+
const chunks = sel.split(",");
|
|
546
|
+
const parsed: LineRange[] = [];
|
|
547
|
+
for (const chunk of chunks) {
|
|
548
|
+
const range = parseLineRangeChunk(chunk);
|
|
549
|
+
if (!range) return null;
|
|
550
|
+
parsed.push(range);
|
|
551
|
+
}
|
|
552
|
+
if (parsed.length === 0) return null;
|
|
553
|
+
parsed.sort((a, b) => a.startLine - b.startLine);
|
|
554
|
+
|
|
555
|
+
const merged: LineRange[] = [parsed[0]];
|
|
556
|
+
for (let i = 1; i < parsed.length; i++) {
|
|
557
|
+
const current = parsed[i];
|
|
558
|
+
const last = merged[merged.length - 1];
|
|
559
|
+
// Open-ended (endLine undefined) means "to EOF" — any later range is absorbed.
|
|
560
|
+
if (last.endLine === undefined) continue;
|
|
561
|
+
// Merge when current starts within (or immediately after) the last range.
|
|
562
|
+
if (current.startLine <= last.endLine + 1) {
|
|
563
|
+
if (current.endLine === undefined || current.endLine > last.endLine) {
|
|
564
|
+
merged[merged.length - 1] = { startLine: last.startLine, endLine: current.endLine };
|
|
565
|
+
}
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
merged.push(current);
|
|
569
|
+
}
|
|
570
|
+
return merged as [LineRange, ...LineRange[]];
|
|
571
|
+
}
|
|
572
|
+
|
|
513
573
|
function parseSel(sel: string | undefined): ParsedSelector {
|
|
514
574
|
if (!sel || sel.length === 0) return { kind: "none" };
|
|
515
575
|
|
|
516
576
|
// Compound selector: `1-50:raw` or `raw:1-50`. Split into chunks and accept
|
|
517
|
-
// any combination of one line range and the literal `raw`.
|
|
577
|
+
// any combination of one line range (possibly multi) and the literal `raw`.
|
|
518
578
|
if (sel.includes(":")) {
|
|
519
579
|
const chunks = sel.split(":");
|
|
520
580
|
if (chunks.length === 2) {
|
|
@@ -524,9 +584,9 @@ function parseSel(sel: string | undefined): ParsedSelector {
|
|
|
524
584
|
const rangeChunk = aIsRaw ? b : bIsRaw ? a : null;
|
|
525
585
|
const rawChunk = aIsRaw ? a : bIsRaw ? b : null;
|
|
526
586
|
if (rangeChunk !== null && rawChunk !== null) {
|
|
527
|
-
const
|
|
528
|
-
if (
|
|
529
|
-
return { kind: "lines",
|
|
587
|
+
const ranges = parseLineRanges(rangeChunk);
|
|
588
|
+
if (ranges) {
|
|
589
|
+
return { kind: "lines", ranges, raw: true };
|
|
530
590
|
}
|
|
531
591
|
}
|
|
532
592
|
}
|
|
@@ -536,19 +596,24 @@ function parseSel(sel: string | undefined): ParsedSelector {
|
|
|
536
596
|
|
|
537
597
|
if (sel.toLowerCase() === "raw") return { kind: "raw" };
|
|
538
598
|
if (sel.toLowerCase() === "conflicts") return { kind: "conflicts" };
|
|
539
|
-
const
|
|
540
|
-
if (
|
|
541
|
-
return { kind: "lines",
|
|
599
|
+
const ranges = parseLineRanges(sel);
|
|
600
|
+
if (ranges) {
|
|
601
|
+
return { kind: "lines", ranges };
|
|
542
602
|
}
|
|
543
603
|
// Unrecognized selectors fall through; sqlite/archive/url readers consume their own colon syntax.
|
|
544
604
|
return { kind: "none" };
|
|
545
605
|
}
|
|
546
606
|
|
|
547
|
-
/**
|
|
607
|
+
/**
|
|
608
|
+
* Convert a single-range selector to the offset/limit pair used by internal pagination.
|
|
609
|
+
* Returns the FIRST range only — multi-range callers MUST branch on `isMultiRange` before
|
|
610
|
+
* calling this helper.
|
|
611
|
+
*/
|
|
548
612
|
function selToOffsetLimit(parsed: ParsedSelector): { offset?: number; limit?: number } {
|
|
549
613
|
if (parsed.kind === "lines") {
|
|
550
|
-
const
|
|
551
|
-
|
|
614
|
+
const first = parsed.ranges[0];
|
|
615
|
+
const limit = first.endLine !== undefined ? first.endLine - first.startLine + 1 : undefined;
|
|
616
|
+
return { offset: first.startLine, limit };
|
|
552
617
|
}
|
|
553
618
|
return {};
|
|
554
619
|
}
|
|
@@ -818,6 +883,160 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
818
883
|
return resultBuilder.done();
|
|
819
884
|
}
|
|
820
885
|
|
|
886
|
+
/**
|
|
887
|
+
* Render a multi-range read against in-memory text. Each range emits a
|
|
888
|
+
* formatted block with its own anchors / line numbers, blocks are joined
|
|
889
|
+
* with an elision separator, and ranges past EOF surface as `[…]` notices
|
|
890
|
+
* so the model can correct the next call. No leading/trailing context is
|
|
891
|
+
* added — multi-range callers always specify exact bounds.
|
|
892
|
+
*/
|
|
893
|
+
#buildInMemoryMultiRangeResult(
|
|
894
|
+
text: string,
|
|
895
|
+
ranges: readonly LineRange[],
|
|
896
|
+
options: {
|
|
897
|
+
details?: ReadToolDetails;
|
|
898
|
+
sourcePath?: string;
|
|
899
|
+
sourceUrl?: string;
|
|
900
|
+
sourceInternal?: string;
|
|
901
|
+
entityLabel: string;
|
|
902
|
+
raw?: boolean;
|
|
903
|
+
immutable?: boolean;
|
|
904
|
+
},
|
|
905
|
+
): AgentToolResult<ReadToolDetails> {
|
|
906
|
+
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
907
|
+
const details = options.details ?? {};
|
|
908
|
+
const allLines = text.split("\n");
|
|
909
|
+
const totalLines = allLines.length;
|
|
910
|
+
const shouldAddHashLines = displayMode.hashLines;
|
|
911
|
+
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
912
|
+
|
|
913
|
+
const resultBuilder = toolResult(details);
|
|
914
|
+
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
915
|
+
if (options.sourceUrl) resultBuilder.sourceUrl(options.sourceUrl);
|
|
916
|
+
if (options.sourceInternal) resultBuilder.sourceInternal(options.sourceInternal);
|
|
917
|
+
|
|
918
|
+
const parts: string[] = [];
|
|
919
|
+
const outOfBounds: LineRange[] = [];
|
|
920
|
+
for (const range of ranges) {
|
|
921
|
+
if (range.startLine > totalLines) {
|
|
922
|
+
outOfBounds.push(range);
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const effectiveEnd = Math.min(range.endLine ?? totalLines, totalLines);
|
|
926
|
+
const sliced = allLines.slice(range.startLine - 1, effectiveEnd).join("\n");
|
|
927
|
+
parts.push(formatTextWithMode(sliced, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const outputText = parts.length > 0 ? parts.join("\n\n…\n\n") : "";
|
|
931
|
+
const notices: string[] = [];
|
|
932
|
+
for (const range of outOfBounds) {
|
|
933
|
+
const bound = range.endLine !== undefined ? `${range.startLine}-${range.endLine}` : `${range.startLine}`;
|
|
934
|
+
notices.push(`[Range ${bound} is beyond end of ${options.entityLabel} (${totalLines} lines total); skipped]`);
|
|
935
|
+
}
|
|
936
|
+
const finalText =
|
|
937
|
+
notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
|
|
938
|
+
resultBuilder.text(finalText);
|
|
939
|
+
return resultBuilder.done();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Stream multiple non-contiguous ranges from a local file. ACP bridge takes
|
|
944
|
+
* priority when present (editor buffer is source of truth); otherwise each
|
|
945
|
+
* range is streamed independently with its own line/byte budget. Out-of-bounds
|
|
946
|
+
* ranges surface as inline notices rather than aborting the read.
|
|
947
|
+
*/
|
|
948
|
+
async #readLocalFileMultiRange(
|
|
949
|
+
absolutePath: string,
|
|
950
|
+
ranges: readonly LineRange[],
|
|
951
|
+
parsed: ParsedSelector,
|
|
952
|
+
displayMode: { hashLines: boolean; lineNumbers: boolean },
|
|
953
|
+
suffixResolution: { from: string; to: string } | undefined,
|
|
954
|
+
signal: AbortSignal | undefined,
|
|
955
|
+
): Promise<{
|
|
956
|
+
outputText: string;
|
|
957
|
+
columnTruncated: number;
|
|
958
|
+
bridgeResult?: AgentToolResult<ReadToolDetails>;
|
|
959
|
+
}> {
|
|
960
|
+
const rawSelector = isRawSelector(parsed);
|
|
961
|
+
|
|
962
|
+
// ACP bridge first — the editor's in-memory buffer is source of truth.
|
|
963
|
+
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
964
|
+
if (bridgePromise !== undefined) {
|
|
965
|
+
try {
|
|
966
|
+
const bridgeText = await bridgePromise;
|
|
967
|
+
const bridgeResult = this.#buildInMemoryMultiRangeResult(bridgeText, ranges, {
|
|
968
|
+
details: { resolvedPath: absolutePath, suffixResolution },
|
|
969
|
+
sourcePath: absolutePath,
|
|
970
|
+
entityLabel: "file",
|
|
971
|
+
raw: rawSelector,
|
|
972
|
+
});
|
|
973
|
+
if (suffixResolution) {
|
|
974
|
+
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
975
|
+
const firstText = bridgeResult.content.find((c): c is TextContent => c.type === "text");
|
|
976
|
+
if (firstText) firstText.text = `${notice}\n${firstText.text}`;
|
|
977
|
+
}
|
|
978
|
+
return { outputText: "", columnTruncated: 0, bridgeResult };
|
|
979
|
+
} catch (error) {
|
|
980
|
+
logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
985
|
+
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
986
|
+
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
987
|
+
|
|
988
|
+
const blocks: string[] = [];
|
|
989
|
+
const notices: string[] = [];
|
|
990
|
+
let columnTruncated = 0;
|
|
991
|
+
|
|
992
|
+
for (const range of ranges) {
|
|
993
|
+
const rangeStart = range.startLine - 1; // 0-indexed
|
|
994
|
+
const requestedLength = range.endLine !== undefined ? range.endLine - range.startLine + 1 : this.#defaultLimit;
|
|
995
|
+
const maxLines = Math.min(requestedLength, DEFAULT_MAX_LINES);
|
|
996
|
+
const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLines * 512);
|
|
997
|
+
|
|
998
|
+
const streamResult = await streamLinesFromFile(
|
|
999
|
+
absolutePath,
|
|
1000
|
+
rangeStart,
|
|
1001
|
+
maxLines,
|
|
1002
|
+
maxBytesForRead,
|
|
1003
|
+
maxLines,
|
|
1004
|
+
signal,
|
|
1005
|
+
);
|
|
1006
|
+
const totalFileLines = streamResult.totalFileLines;
|
|
1007
|
+
|
|
1008
|
+
if (rangeStart >= totalFileLines) {
|
|
1009
|
+
const bound = range.endLine !== undefined ? `${range.startLine}-${range.endLine}` : `${range.startLine}`;
|
|
1010
|
+
notices.push(`[Range ${bound} is beyond end of file (${totalFileLines} lines total); skipped]`);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const collectedLines = streamResult.lines;
|
|
1015
|
+
if (!rawSelector && maxColumns > 0) {
|
|
1016
|
+
for (let i = 0; i < collectedLines.length; i++) {
|
|
1017
|
+
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
1018
|
+
if (wasTruncated) {
|
|
1019
|
+
collectedLines[i] = text;
|
|
1020
|
+
columnTruncated = maxColumns;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (collectedLines.length > 0) {
|
|
1026
|
+
getFileReadCache(this.session).recordContiguous(absolutePath, range.startLine, collectedLines);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const blockText = collectedLines.join("\n");
|
|
1030
|
+
blocks.push(formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
let outputText = blocks.join("\n\n…\n\n");
|
|
1034
|
+
if (notices.length > 0) {
|
|
1035
|
+
outputText = outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n");
|
|
1036
|
+
}
|
|
1037
|
+
return { outputText, columnTruncated };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
821
1040
|
async #readArchiveDirectory(
|
|
822
1041
|
archive: ArchiveReader,
|
|
823
1042
|
archivePath: string,
|
|
@@ -861,11 +1080,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
861
1080
|
|
|
862
1081
|
async #readArchive(
|
|
863
1082
|
readPath: string,
|
|
864
|
-
|
|
865
|
-
limit: number | undefined,
|
|
1083
|
+
parsedSel: ParsedSelector,
|
|
866
1084
|
resolvedArchivePath: ResolvedArchiveReadPath,
|
|
867
1085
|
signal?: AbortSignal,
|
|
868
|
-
options?: { raw?: boolean },
|
|
869
1086
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
870
1087
|
throwIfAborted(signal);
|
|
871
1088
|
const archive = await openArchive(resolvedArchivePath.absolutePath);
|
|
@@ -882,6 +1099,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
882
1099
|
}
|
|
883
1100
|
|
|
884
1101
|
if (node.isDirectory) {
|
|
1102
|
+
if (isMultiRange(parsedSel)) {
|
|
1103
|
+
throw new ToolError("Multi-range line selectors are not supported for archive directory listings.");
|
|
1104
|
+
}
|
|
1105
|
+
const { limit } = selToOffsetLimit(parsedSel);
|
|
885
1106
|
return this.#readArchiveDirectory(
|
|
886
1107
|
archive,
|
|
887
1108
|
resolvedArchivePath.absolutePath,
|
|
@@ -906,12 +1127,26 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
906
1127
|
.done();
|
|
907
1128
|
}
|
|
908
1129
|
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1130
|
+
const raw = isRawSelector(parsedSel);
|
|
1131
|
+
const result =
|
|
1132
|
+
isMultiRange(parsedSel) && parsedSel.kind === "lines"
|
|
1133
|
+
? this.#buildInMemoryMultiRangeResult(text, parsedSel.ranges, {
|
|
1134
|
+
details,
|
|
1135
|
+
sourcePath: resolvedArchivePath.absolutePath,
|
|
1136
|
+
entityLabel: "archive entry",
|
|
1137
|
+
raw,
|
|
1138
|
+
})
|
|
1139
|
+
: this.#buildInMemoryTextResult(
|
|
1140
|
+
text,
|
|
1141
|
+
selToOffsetLimit(parsedSel).offset,
|
|
1142
|
+
selToOffsetLimit(parsedSel).limit,
|
|
1143
|
+
{
|
|
1144
|
+
details,
|
|
1145
|
+
sourcePath: resolvedArchivePath.absolutePath,
|
|
1146
|
+
entityLabel: "archive entry",
|
|
1147
|
+
raw,
|
|
1148
|
+
},
|
|
1149
|
+
);
|
|
915
1150
|
const firstText = result.content.find((content): content is TextContent => content.type === "text");
|
|
916
1151
|
if (firstText) {
|
|
917
1152
|
firstText.text = prependSuffixResolutionNotice(firstText.text, resolvedArchivePath.suffixResolution);
|
|
@@ -1045,12 +1280,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1045
1280
|
}
|
|
1046
1281
|
}
|
|
1047
1282
|
|
|
1283
|
+
#routeReadThroughBridge(
|
|
1284
|
+
absolutePath: string,
|
|
1285
|
+
options?: { line?: number; limit?: number },
|
|
1286
|
+
): Promise<string> | undefined {
|
|
1287
|
+
const bridge = this.session.getClientBridge?.();
|
|
1288
|
+
if (!bridge?.capabilities.readTextFile || !bridge.readTextFile) return undefined;
|
|
1289
|
+
return bridge.readTextFile({ path: absolutePath, ...options });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1048
1292
|
async #trySummarize(absolutePath: string, fileSize: number, signal?: AbortSignal): Promise<SummaryResult | null> {
|
|
1049
1293
|
if (fileSize > MAX_SUMMARY_BYTES) return null;
|
|
1050
1294
|
|
|
1051
1295
|
try {
|
|
1052
1296
|
throwIfAborted(signal);
|
|
1053
|
-
const
|
|
1297
|
+
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
1298
|
+
const code =
|
|
1299
|
+
bridgePromise !== undefined
|
|
1300
|
+
? await bridgePromise.catch(() => Bun.file(absolutePath).text())
|
|
1301
|
+
: await Bun.file(absolutePath).text();
|
|
1054
1302
|
throwIfAborted(signal);
|
|
1055
1303
|
if (countTextLines(code) > MAX_SUMMARY_LINES) return null;
|
|
1056
1304
|
|
|
@@ -1173,7 +1421,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1173
1421
|
if (conflictUri) {
|
|
1174
1422
|
if (conflictUri.id === "*") {
|
|
1175
1423
|
throw new ToolError(
|
|
1176
|
-
"`
|
|
1424
|
+
"Reading `conflict://*` is not supported — wildcards are write-only. Use the `<path>:conflicts` read selector for the full list of conflicts in a file, or read `conflict://<N>` to inspect a single block.",
|
|
1177
1425
|
);
|
|
1178
1426
|
}
|
|
1179
1427
|
return this.#readConflictRegion(conflictUri.id, conflictUri.scope);
|
|
@@ -1210,22 +1458,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1210
1458
|
const internalRouter = InternalUrlRouter.instance();
|
|
1211
1459
|
if (internalRouter.canHandle(internalTarget.path)) {
|
|
1212
1460
|
const parsed = parseSel(internalTarget.sel);
|
|
1213
|
-
|
|
1214
|
-
return this.#handleInternalUrl(internalTarget.path, offset, limit, { raw: isRawSelector(parsed) });
|
|
1461
|
+
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
1215
1462
|
}
|
|
1216
1463
|
|
|
1217
1464
|
const archivePath = await this.#resolveArchiveReadPath(readPath, signal);
|
|
1218
1465
|
if (archivePath) {
|
|
1219
1466
|
const archiveSubPath = splitPathAndSel(archivePath.archiveSubPath);
|
|
1220
1467
|
const archiveParsed = parseSel(archiveSubPath.sel);
|
|
1221
|
-
const { offset, limit } = selToOffsetLimit(archiveParsed);
|
|
1222
1468
|
return this.#readArchive(
|
|
1223
1469
|
readPath,
|
|
1224
|
-
|
|
1225
|
-
limit,
|
|
1470
|
+
archiveParsed,
|
|
1226
1471
|
{ ...archivePath, archiveSubPath: archiveSubPath.path },
|
|
1227
1472
|
signal,
|
|
1228
|
-
{ raw: isRawSelector(archiveParsed) },
|
|
1229
1473
|
);
|
|
1230
1474
|
}
|
|
1231
1475
|
|
|
@@ -1274,6 +1518,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1274
1518
|
}
|
|
1275
1519
|
|
|
1276
1520
|
if (isDirectory) {
|
|
1521
|
+
if (isMultiRange(parsed)) {
|
|
1522
|
+
throw new ToolError("Multi-range line selectors are not supported for directory listings.");
|
|
1523
|
+
}
|
|
1277
1524
|
const dirResult = await this.#readDirectory(absolutePath, selToOffsetLimit(parsed).limit, signal);
|
|
1278
1525
|
if (suffixResolution) {
|
|
1279
1526
|
dirResult.details ??= {};
|
|
@@ -1289,13 +1536,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1289
1536
|
const imageMetadata = await readImageMetadata(absolutePath);
|
|
1290
1537
|
const mimeType = imageMetadata?.mimeType;
|
|
1291
1538
|
const ext = path.extname(absolutePath).toLowerCase();
|
|
1292
|
-
const _hasEditTool = this.session.hasEditTool ?? true;
|
|
1293
|
-
const _language = getLanguageFromPath(absolutePath);
|
|
1294
1539
|
const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
|
|
1295
1540
|
// Read the file based on type
|
|
1296
1541
|
let content: Array<TextContent | ImageContent> | undefined;
|
|
1297
1542
|
let details: ReadToolDetails = {};
|
|
1298
1543
|
let sourcePath: string | undefined;
|
|
1544
|
+
let columnTruncated = 0;
|
|
1299
1545
|
let truncationInfo:
|
|
1300
1546
|
| { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
|
|
1301
1547
|
| undefined;
|
|
@@ -1359,17 +1605,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1359
1605
|
}
|
|
1360
1606
|
}
|
|
1361
1607
|
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
offset,
|
|
1366
|
-
limit,
|
|
1367
|
-
{
|
|
1608
|
+
const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
|
|
1609
|
+
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
1610
|
+
return this.#buildInMemoryMultiRangeResult(notebookText, parsed.ranges, {
|
|
1368
1611
|
details: { resolvedPath: absolutePath },
|
|
1369
1612
|
sourcePath: absolutePath,
|
|
1370
1613
|
entityLabel: "notebook",
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1617
|
+
return this.#buildInMemoryTextResult(notebookText, offset, limit, {
|
|
1618
|
+
details: { resolvedPath: absolutePath },
|
|
1619
|
+
sourcePath: absolutePath,
|
|
1620
|
+
entityLabel: "notebook",
|
|
1621
|
+
});
|
|
1373
1622
|
} else if (shouldConvertWithMarkit) {
|
|
1374
1623
|
// Convert document via markit.
|
|
1375
1624
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
@@ -1411,174 +1660,230 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1411
1660
|
}
|
|
1412
1661
|
|
|
1413
1662
|
if (!content) {
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
absolutePath
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1663
|
+
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
1664
|
+
const multiResult = await this.#readLocalFileMultiRange(
|
|
1665
|
+
absolutePath,
|
|
1666
|
+
parsed.ranges,
|
|
1667
|
+
parsed,
|
|
1668
|
+
displayMode,
|
|
1669
|
+
suffixResolution,
|
|
1670
|
+
signal,
|
|
1671
|
+
);
|
|
1672
|
+
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
1673
|
+
content = [{ type: "text", text: multiResult.outputText }];
|
|
1674
|
+
sourcePath = absolutePath;
|
|
1675
|
+
details = {};
|
|
1676
|
+
if (multiResult.columnTruncated > 0) {
|
|
1677
|
+
columnTruncated = multiResult.columnTruncated;
|
|
1678
|
+
}
|
|
1679
|
+
} else {
|
|
1680
|
+
// Raw text or line-range mode
|
|
1681
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1682
|
+
// Try ACP bridge first — editor's in-memory buffer is source of truth.
|
|
1683
|
+
// Request full text so local range rendering keeps normal context and line numbers.
|
|
1684
|
+
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
1685
|
+
if (bridgePromise !== undefined) {
|
|
1686
|
+
try {
|
|
1687
|
+
const bridgeText = await bridgePromise;
|
|
1688
|
+
const bridgeResult = this.#buildInMemoryTextResult(bridgeText, offset, limit, {
|
|
1689
|
+
details: { resolvedPath: absolutePath, suffixResolution },
|
|
1690
|
+
sourcePath: absolutePath,
|
|
1691
|
+
entityLabel: "file",
|
|
1692
|
+
raw: isRawSelector(parsed),
|
|
1693
|
+
});
|
|
1694
|
+
if (suffixResolution) {
|
|
1695
|
+
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
1696
|
+
const firstText = bridgeResult.content.find((c): c is TextContent => c.type === "text");
|
|
1697
|
+
if (firstText) firstText.text = `${notice}\n${firstText.text}`;
|
|
1698
|
+
}
|
|
1699
|
+
return bridgeResult;
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1442
1704
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1705
|
+
// User-requested 0-indexed range start. Lines BEFORE this become
|
|
1706
|
+
// leading context (added below if offset is explicit).
|
|
1707
|
+
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
1708
|
+
const expandStart = offset !== undefined && offset > 1;
|
|
1709
|
+
const expandEnd = limit !== undefined;
|
|
1710
|
+
const leadingContext = expandStart ? Math.min(requestedStart, RANGE_LEADING_CONTEXT_LINES) : 0;
|
|
1711
|
+
const trailingContext = expandEnd ? RANGE_TRAILING_CONTEXT_LINES : 0;
|
|
1712
|
+
const startLine = requestedStart - leadingContext;
|
|
1713
|
+
const startLineDisplay = startLine + 1;
|
|
1714
|
+
|
|
1715
|
+
const DEFAULT_LIMIT = this.#defaultLimit;
|
|
1716
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
1717
|
+
const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES);
|
|
1718
|
+
const selectedLineLimit = effectiveLimit + leadingContext + trailingContext;
|
|
1719
|
+
// Scale byte budget with line limit so the configured line count actually fits.
|
|
1720
|
+
// Assume ~512 bytes/line average; never go below the shared default.
|
|
1721
|
+
const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
1722
|
+
|
|
1723
|
+
const streamResult = await streamLinesFromFile(
|
|
1724
|
+
absolutePath,
|
|
1725
|
+
startLine,
|
|
1726
|
+
maxLinesToCollect,
|
|
1727
|
+
maxBytesForRead,
|
|
1728
|
+
selectedLineLimit,
|
|
1729
|
+
signal,
|
|
1730
|
+
);
|
|
1464
1731
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1732
|
+
const {
|
|
1733
|
+
lines: collectedLines,
|
|
1734
|
+
totalFileLines,
|
|
1735
|
+
collectedBytes,
|
|
1736
|
+
stoppedByByteLimit,
|
|
1737
|
+
firstLinePreview,
|
|
1738
|
+
firstLineByteLength,
|
|
1739
|
+
} = streamResult;
|
|
1740
|
+
|
|
1741
|
+
// Check if offset is out of bounds - return graceful message instead of throwing
|
|
1742
|
+
if (requestedStart >= totalFileLines) {
|
|
1743
|
+
const suggestion =
|
|
1744
|
+
totalFileLines === 0
|
|
1745
|
+
? "The file is empty."
|
|
1746
|
+
: `Use :1 to read from the start, or :${totalFileLines} to read the last line.`;
|
|
1747
|
+
return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
|
|
1748
|
+
.text(
|
|
1749
|
+
`Line ${requestedStart + 1} is beyond end of file (${totalFileLines} lines total). ${suggestion}`,
|
|
1750
|
+
)
|
|
1751
|
+
.done();
|
|
1752
|
+
}
|
|
1484
1753
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1754
|
+
// Per-line column cap. Skipped in raw mode so `:raw` always returns
|
|
1755
|
+
// verbatim bytes for paste-back-into-tool workflows. Total byte/line
|
|
1756
|
+
// counts in `truncation` keep reflecting the source, not the trimmed
|
|
1757
|
+
// view — column truncation surfaces separately via `.limits()`.
|
|
1758
|
+
const rawSelector = isRawSelector(parsed);
|
|
1759
|
+
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1760
|
+
if (!rawSelector && maxColumns > 0) {
|
|
1761
|
+
for (let i = 0; i < collectedLines.length; i++) {
|
|
1762
|
+
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
1763
|
+
if (wasTruncated) {
|
|
1764
|
+
collectedLines[i] = text;
|
|
1765
|
+
columnTruncated = maxColumns;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1488
1769
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1770
|
+
const selectedContent = collectedLines.join("\n");
|
|
1771
|
+
const userLimitedLines = collectedLines.length;
|
|
1772
|
+
|
|
1773
|
+
const totalSelectedLines = totalFileLines - startLine;
|
|
1774
|
+
const totalSelectedBytes = collectedBytes;
|
|
1775
|
+
const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
|
|
1776
|
+
const firstLineExceedsLimit = firstLineByteLength !== undefined && firstLineByteLength > maxBytesForRead;
|
|
1777
|
+
|
|
1778
|
+
const truncation: TruncationResult = {
|
|
1779
|
+
content: selectedContent,
|
|
1780
|
+
truncated: wasTruncated,
|
|
1781
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
1782
|
+
totalLines: totalSelectedLines,
|
|
1783
|
+
totalBytes: totalSelectedBytes,
|
|
1784
|
+
outputLines: collectedLines.length,
|
|
1785
|
+
outputBytes: collectedBytes,
|
|
1786
|
+
lastLinePartial: false,
|
|
1787
|
+
firstLineExceedsLimit,
|
|
1788
|
+
};
|
|
1497
1789
|
|
|
1498
|
-
|
|
1790
|
+
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1791
|
+
getFileReadCache(this.session).recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1795
|
+
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1796
|
+
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
1797
|
+
const formatText = (text: string, startNum: number): string => {
|
|
1798
|
+
capturedDisplayContent = { text, startLine: startNum };
|
|
1799
|
+
return formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1800
|
+
};
|
|
1499
1801
|
|
|
1500
|
-
|
|
1501
|
-
const firstLineBytes = firstLineByteLength ?? 0;
|
|
1502
|
-
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
1802
|
+
let outputText: string;
|
|
1503
1803
|
|
|
1504
|
-
if (
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1804
|
+
if (truncation.firstLineExceedsLimit) {
|
|
1805
|
+
const firstLineBytes = firstLineByteLength ?? 0;
|
|
1806
|
+
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
1807
|
+
|
|
1808
|
+
if (shouldAddHashLines) {
|
|
1809
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1810
|
+
firstLineBytes,
|
|
1811
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
1812
|
+
} else {
|
|
1813
|
+
outputText = formatText(snippet.text, startLineDisplay);
|
|
1814
|
+
}
|
|
1815
|
+
if (snippet.text.length === 0) {
|
|
1816
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1817
|
+
firstLineBytes,
|
|
1818
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1819
|
+
}
|
|
1820
|
+
details = { truncation };
|
|
1821
|
+
sourcePath = absolutePath;
|
|
1822
|
+
truncationInfo = {
|
|
1823
|
+
result: truncation,
|
|
1824
|
+
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
1825
|
+
};
|
|
1826
|
+
} else if (truncation.truncated) {
|
|
1827
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1828
|
+
details = { truncation };
|
|
1829
|
+
sourcePath = absolutePath;
|
|
1830
|
+
truncationInfo = {
|
|
1831
|
+
result: truncation,
|
|
1832
|
+
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
1833
|
+
};
|
|
1834
|
+
} else if (startLine + userLimitedLines < totalFileLines) {
|
|
1835
|
+
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
1836
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
1837
|
+
|
|
1838
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1839
|
+
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
1840
|
+
details = {};
|
|
1841
|
+
sourcePath = absolutePath;
|
|
1508
1842
|
} else {
|
|
1509
|
-
|
|
1843
|
+
// No truncation, no user limit exceeded
|
|
1844
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1845
|
+
details = {};
|
|
1846
|
+
sourcePath = absolutePath;
|
|
1510
1847
|
}
|
|
1511
|
-
if (snippet.text.length === 0) {
|
|
1512
|
-
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1513
|
-
firstLineBytes,
|
|
1514
|
-
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1515
|
-
}
|
|
1516
|
-
details = { truncation };
|
|
1517
|
-
sourcePath = absolutePath;
|
|
1518
|
-
truncationInfo = {
|
|
1519
|
-
result: truncation,
|
|
1520
|
-
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
1521
|
-
};
|
|
1522
|
-
} else if (truncation.truncated) {
|
|
1523
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1524
|
-
details = { truncation };
|
|
1525
|
-
sourcePath = absolutePath;
|
|
1526
|
-
truncationInfo = {
|
|
1527
|
-
result: truncation,
|
|
1528
|
-
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
1529
|
-
};
|
|
1530
|
-
} else if (startLine + userLimitedLines < totalFileLines) {
|
|
1531
|
-
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
1532
|
-
const nextOffset = startLine + userLimitedLines + 1;
|
|
1533
|
-
|
|
1534
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1535
|
-
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
1536
|
-
details = {};
|
|
1537
|
-
sourcePath = absolutePath;
|
|
1538
|
-
} else {
|
|
1539
|
-
// No truncation, no user limit exceeded
|
|
1540
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1541
|
-
details = {};
|
|
1542
|
-
sourcePath = absolutePath;
|
|
1543
|
-
}
|
|
1544
1848
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1849
|
+
if (capturedDisplayContent) {
|
|
1850
|
+
details.displayContent = capturedDisplayContent;
|
|
1851
|
+
}
|
|
1548
1852
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1853
|
+
if (!firstLineExceedsLimit && collectedLines.length > 0) {
|
|
1854
|
+
const blocks = scanConflictLines(collectedLines, startLineDisplay);
|
|
1855
|
+
if (blocks.length > 0) {
|
|
1856
|
+
const history = getConflictHistory(this.session);
|
|
1857
|
+
const displayPathForWarning = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
1858
|
+
const entries = blocks.map(block =>
|
|
1859
|
+
history.register({
|
|
1860
|
+
absolutePath,
|
|
1861
|
+
displayPath: displayPathForWarning,
|
|
1862
|
+
...block,
|
|
1863
|
+
}),
|
|
1864
|
+
);
|
|
1865
|
+
// Cheap full-file scan only when the window already showed
|
|
1866
|
+
// at least one conflict — otherwise pay nothing on clean files.
|
|
1867
|
+
let totalInFile = entries.length;
|
|
1868
|
+
let scanTruncated = false;
|
|
1869
|
+
try {
|
|
1870
|
+
const fileScan = await scanFileForConflicts(absolutePath);
|
|
1871
|
+
totalInFile = Math.max(entries.length, fileScan.blocks.length);
|
|
1872
|
+
scanTruncated = fileScan.scanTruncated;
|
|
1873
|
+
} catch {
|
|
1874
|
+
// Best-effort enrichment; fall back to window-only count.
|
|
1875
|
+
}
|
|
1876
|
+
outputText += formatConflictWarning(entries, {
|
|
1877
|
+
totalInFile,
|
|
1557
1878
|
displayPath: displayPathForWarning,
|
|
1558
|
-
|
|
1559
|
-
})
|
|
1560
|
-
|
|
1561
|
-
// Cheap full-file scan only when the window already showed
|
|
1562
|
-
// at least one conflict — otherwise pay nothing on clean files.
|
|
1563
|
-
let totalInFile = entries.length;
|
|
1564
|
-
let scanTruncated = false;
|
|
1565
|
-
try {
|
|
1566
|
-
const fileScan = await scanFileForConflicts(absolutePath);
|
|
1567
|
-
totalInFile = Math.max(entries.length, fileScan.blocks.length);
|
|
1568
|
-
scanTruncated = fileScan.scanTruncated;
|
|
1569
|
-
} catch {
|
|
1570
|
-
// Best-effort enrichment; fall back to window-only count.
|
|
1879
|
+
scanTruncated,
|
|
1880
|
+
});
|
|
1881
|
+
details.conflictCount = entries.length;
|
|
1571
1882
|
}
|
|
1572
|
-
outputText += formatConflictWarning(entries, {
|
|
1573
|
-
totalInFile,
|
|
1574
|
-
displayPath: displayPathForWarning,
|
|
1575
|
-
scanTruncated,
|
|
1576
|
-
});
|
|
1577
|
-
details.conflictCount = entries.length;
|
|
1578
1883
|
}
|
|
1579
|
-
}
|
|
1580
1884
|
|
|
1581
|
-
|
|
1885
|
+
content = [{ type: "text", text: outputText }];
|
|
1886
|
+
}
|
|
1582
1887
|
}
|
|
1583
1888
|
}
|
|
1584
1889
|
|
|
@@ -1600,6 +1905,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1600
1905
|
if (truncationInfo) {
|
|
1601
1906
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
1602
1907
|
}
|
|
1908
|
+
if (columnTruncated > 0) {
|
|
1909
|
+
resultBuilder.limits({ columnMax: columnTruncated });
|
|
1910
|
+
}
|
|
1603
1911
|
return resultBuilder.done();
|
|
1604
1912
|
}
|
|
1605
1913
|
|
|
@@ -1633,7 +1941,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1633
1941
|
}
|
|
1634
1942
|
|
|
1635
1943
|
/**
|
|
1636
|
-
* Implement
|
|
1944
|
+
* Implement the `<path>:conflicts` read selector: scan the whole file once, register
|
|
1637
1945
|
* every block in the session's conflict history, and return a compact
|
|
1638
1946
|
* `#N L_a-L_b` index instead of file content. Designed for heavily
|
|
1639
1947
|
* conflicted files where dumping every body would be wasteful.
|
|
@@ -1674,43 +1982,59 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1674
1982
|
*/
|
|
1675
1983
|
async #handleInternalUrl(
|
|
1676
1984
|
url: string,
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
options?: { raw?: boolean },
|
|
1985
|
+
parsedSel: ParsedSelector,
|
|
1986
|
+
signal?: AbortSignal,
|
|
1680
1987
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1681
1988
|
const internalRouter = InternalUrlRouter.instance();
|
|
1682
1989
|
|
|
1683
1990
|
// Check if URL has query extraction (agent:// only).
|
|
1684
1991
|
// Use parseInternalUrl which handles colons in host (namespaced skills).
|
|
1685
|
-
let
|
|
1992
|
+
let urlMeta: InternalUrl;
|
|
1686
1993
|
try {
|
|
1687
|
-
|
|
1994
|
+
urlMeta = parseInternalUrl(url);
|
|
1688
1995
|
} catch (e) {
|
|
1689
1996
|
throw new ToolError(e instanceof Error ? e.message : String(e));
|
|
1690
1997
|
}
|
|
1691
|
-
const scheme =
|
|
1998
|
+
const scheme = urlMeta.protocol.replace(/:$/, "").toLowerCase();
|
|
1692
1999
|
let hasExtraction = false;
|
|
1693
2000
|
if (scheme === "agent") {
|
|
1694
|
-
const hasPathExtraction =
|
|
1695
|
-
const queryParam =
|
|
2001
|
+
const hasPathExtraction = urlMeta.pathname && urlMeta.pathname !== "/" && urlMeta.pathname !== "";
|
|
2002
|
+
const queryParam = urlMeta.searchParams.get("q");
|
|
1696
2003
|
const hasQueryExtraction = queryParam !== null && queryParam !== "";
|
|
1697
2004
|
hasExtraction = hasPathExtraction || hasQueryExtraction;
|
|
1698
2005
|
}
|
|
1699
2006
|
|
|
1700
|
-
// Reject
|
|
1701
|
-
if (hasExtraction &&
|
|
1702
|
-
throw new ToolError("Cannot combine query extraction with
|
|
2007
|
+
// Reject line selectors when query extraction is used
|
|
2008
|
+
if (hasExtraction && parsedSel.kind !== "none" && parsedSel.kind !== "raw") {
|
|
2009
|
+
throw new ToolError("Cannot combine query extraction with line selectors");
|
|
1703
2010
|
}
|
|
1704
2011
|
|
|
1705
2012
|
// Resolve the internal URL
|
|
1706
|
-
const resource = await internalRouter.resolve(url
|
|
1707
|
-
|
|
2013
|
+
const resource = await internalRouter.resolve(url, {
|
|
2014
|
+
cwd: this.session.cwd,
|
|
2015
|
+
settings: this.session.settings,
|
|
2016
|
+
signal,
|
|
2017
|
+
});
|
|
2018
|
+
const details: ReadToolDetails = { resolvedPath: resource.sourcePath, contentType: resource.contentType };
|
|
1708
2019
|
|
|
1709
2020
|
// If extraction was used, return directly (no pagination)
|
|
1710
2021
|
if (hasExtraction) {
|
|
1711
2022
|
return toolResult(details).text(resource.content).sourceInternal(url).done();
|
|
1712
2023
|
}
|
|
1713
2024
|
|
|
2025
|
+
const raw = isRawSelector(parsedSel);
|
|
2026
|
+
if (isMultiRange(parsedSel) && parsedSel.kind === "lines") {
|
|
2027
|
+
return this.#buildInMemoryMultiRangeResult(resource.content, parsedSel.ranges, {
|
|
2028
|
+
details,
|
|
2029
|
+
sourcePath: resource.sourcePath,
|
|
2030
|
+
sourceInternal: url,
|
|
2031
|
+
entityLabel: "resource",
|
|
2032
|
+
immutable: resource.immutable,
|
|
2033
|
+
raw,
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
const { offset, limit } = selToOffsetLimit(parsedSel);
|
|
1714
2038
|
return this.#buildInMemoryTextResult(resource.content, offset, limit, {
|
|
1715
2039
|
details,
|
|
1716
2040
|
sourcePath: resource.sourcePath,
|
|
@@ -1718,7 +2042,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1718
2042
|
entityLabel: "resource",
|
|
1719
2043
|
ignoreResultLimits: scheme === "skill",
|
|
1720
2044
|
immutable: resource.immutable,
|
|
1721
|
-
raw
|
|
2045
|
+
raw,
|
|
1722
2046
|
});
|
|
1723
2047
|
}
|
|
1724
2048
|
|
|
@@ -1803,20 +2127,44 @@ export const readToolRenderer = {
|
|
|
1803
2127
|
},
|
|
1804
2128
|
|
|
1805
2129
|
renderResult(
|
|
1806
|
-
result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails },
|
|
1807
|
-
|
|
2130
|
+
result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails; isError?: boolean },
|
|
2131
|
+
options: RenderResultOptions,
|
|
1808
2132
|
uiTheme: Theme,
|
|
1809
2133
|
args?: ReadRenderArgs,
|
|
1810
2134
|
): Component {
|
|
1811
2135
|
const urlDetails = result.details as ReadUrlToolDetails | undefined;
|
|
1812
2136
|
if (urlDetails?.kind === "url" || isReadableUrlPath(args?.file_path || args?.path || "")) {
|
|
1813
2137
|
return renderReadUrlResult(
|
|
1814
|
-
result as {
|
|
1815
|
-
|
|
2138
|
+
result as {
|
|
2139
|
+
content: Array<{ type: string; text?: string }>;
|
|
2140
|
+
details?: ReadUrlToolDetails;
|
|
2141
|
+
isError?: boolean;
|
|
2142
|
+
},
|
|
2143
|
+
options,
|
|
1816
2144
|
uiTheme,
|
|
1817
2145
|
);
|
|
1818
2146
|
}
|
|
1819
2147
|
|
|
2148
|
+
if (result.isError) {
|
|
2149
|
+
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
2150
|
+
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
2151
|
+
const rawPath = args?.file_path || args?.path || "";
|
|
2152
|
+
const filePath = shortenPath(rawPath);
|
|
2153
|
+
let title = filePath ? `Read ${filePath}` : "Read";
|
|
2154
|
+
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
2155
|
+
const startLine = args.offset ?? 1;
|
|
2156
|
+
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
|
2157
|
+
title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
|
|
2158
|
+
}
|
|
2159
|
+
const header = renderStatusLine({ icon: "error", title }, uiTheme);
|
|
2160
|
+
const errorLines = errorText.split("\n").map(line => uiTheme.fg("error", replaceTabs(line)));
|
|
2161
|
+
const outputBlock = new CachedOutputBlock();
|
|
2162
|
+
return {
|
|
2163
|
+
render: (width: number) =>
|
|
2164
|
+
outputBlock.render({ header, state: "error", sections: [{ lines: errorLines }], width }, uiTheme),
|
|
2165
|
+
invalidate: () => outputBlock.invalidate(),
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
1820
2168
|
const details = result.details;
|
|
1821
2169
|
const rawText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1822
2170
|
// Prefer structured `displayContent` from details when available so the TUI
|
|
@@ -1825,7 +2173,7 @@ export const readToolRenderer = {
|
|
|
1825
2173
|
const imageContent = result.content?.find(c => c.type === "image");
|
|
1826
2174
|
const rawPath = args?.file_path || args?.path || "";
|
|
1827
2175
|
const filePath = shortenPath(rawPath);
|
|
1828
|
-
const lang = getLanguageFromPath(rawPath);
|
|
2176
|
+
const lang = getLanguageFromPath(splitPathAndSel(rawPath).path);
|
|
1829
2177
|
|
|
1830
2178
|
const warningLines: string[] = [];
|
|
1831
2179
|
const truncation = details?.meta?.truncation;
|
|
@@ -1893,28 +2241,46 @@ export const readToolRenderer = {
|
|
|
1893
2241
|
const n = details.conflictCount;
|
|
1894
2242
|
title += ` ${uiTheme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
|
|
1895
2243
|
}
|
|
2244
|
+
const rawRequested = args?.raw === true || isRawSelector(parseSel(splitPathAndSel(rawPath).sel));
|
|
2245
|
+
const isMarkdown = details?.contentType === "text/markdown" && !rawRequested;
|
|
1896
2246
|
let cachedWidth: number | undefined;
|
|
2247
|
+
let cachedExpanded: boolean | undefined;
|
|
1897
2248
|
let cachedLines: string[] | undefined;
|
|
1898
2249
|
return {
|
|
1899
2250
|
render: (width: number) => {
|
|
1900
|
-
|
|
1901
|
-
cachedLines
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2251
|
+
const expanded = options.expanded;
|
|
2252
|
+
if (cachedLines && cachedWidth === width && cachedExpanded === expanded) return cachedLines;
|
|
2253
|
+
cachedLines = isMarkdown
|
|
2254
|
+
? renderMarkdownCell(
|
|
2255
|
+
{
|
|
2256
|
+
content: contentText,
|
|
2257
|
+
title,
|
|
2258
|
+
status: "complete",
|
|
2259
|
+
output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
|
|
2260
|
+
expanded,
|
|
2261
|
+
width,
|
|
2262
|
+
},
|
|
2263
|
+
uiTheme,
|
|
2264
|
+
)
|
|
2265
|
+
: renderCodeCell(
|
|
2266
|
+
{
|
|
2267
|
+
code: contentText,
|
|
2268
|
+
language: lang,
|
|
2269
|
+
title,
|
|
2270
|
+
status: "complete",
|
|
2271
|
+
output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
|
|
2272
|
+
expanded,
|
|
2273
|
+
width,
|
|
2274
|
+
},
|
|
2275
|
+
uiTheme,
|
|
2276
|
+
);
|
|
1913
2277
|
cachedWidth = width;
|
|
2278
|
+
cachedExpanded = expanded;
|
|
1914
2279
|
return cachedLines;
|
|
1915
2280
|
},
|
|
1916
2281
|
invalidate: () => {
|
|
1917
2282
|
cachedWidth = undefined;
|
|
2283
|
+
cachedExpanded = undefined;
|
|
1918
2284
|
cachedLines = undefined;
|
|
1919
2285
|
},
|
|
1920
2286
|
};
|