@gajae-code/coding-agent 0.11.11 → 0.12.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 +131 -4
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +83 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +38 -4
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +10 -19
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
- package/dist/types/modes/components/settings-selector.d.ts +11 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +44 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +82 -12
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +48 -19
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +96 -12
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -4
- package/dist/types/skill-state/workflow-hud.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +44 -3
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -7
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +11 -13
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/async/job-manager.ts +78 -20
- package/src/capability/index.ts +8 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +20 -2
- package/src/config/file-lock.ts +11 -2
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +74 -1
- package/src/config/settings.ts +246 -108
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
- package/src/defaults/gjc/skills/team/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/eval/py/executor.ts +7 -2
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +46 -5
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +42 -5
- package/src/gjc-runtime/team-runtime.ts +1085 -40
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
- package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
- package/src/gjc-runtime/workflow-manifest.ts +38 -247
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +21 -22
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +98 -0
- package/src/modes/acp/acp-agent.ts +339 -191
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +96 -28
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +457 -83
- package/src/modes/components/settings-selector.ts +116 -23
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +26 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +127 -36
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +22 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -5
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +984 -146
- package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +303 -0
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +101 -10
- package/src/session/agent-session.ts +1286 -220
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2364 -438
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +500 -111
- package/src/setup/credential-import.ts +75 -13
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +32 -10
- package/src/skill-state/workflow-hud.ts +42 -2
- package/src/skill-state/workflow-mutation-guard.ts +560 -8
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +169 -118
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +281 -20
- package/src/tools/path-utils.ts +8 -3
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1560 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
package/src/tools/read.ts
CHANGED
|
@@ -19,14 +19,16 @@ import type { InternalUrl } from "../internal-urls/types";
|
|
|
19
19
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
20
20
|
import readDescription from "../prompts/tools/read.md" with { type: "text" };
|
|
21
21
|
import type { ToolSession } from "../sdk";
|
|
22
|
+
import type { ReadSegment, ReadWindow, TruncationDirection, TruncationResult } from "../session/streaming-output";
|
|
22
23
|
import {
|
|
23
24
|
DEFAULT_MAX_BYTES,
|
|
24
25
|
DEFAULT_MAX_LINES,
|
|
25
|
-
|
|
26
|
-
type TruncationResult,
|
|
26
|
+
formatMiddleElisionMarker,
|
|
27
27
|
truncateHead,
|
|
28
28
|
truncateHeadBytes,
|
|
29
29
|
truncateLine,
|
|
30
|
+
truncateMiddleWindows,
|
|
31
|
+
truncateTailBytes,
|
|
30
32
|
} from "../session/streaming-output";
|
|
31
33
|
import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui";
|
|
32
34
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
isReadableUrlPath,
|
|
52
54
|
loadReadUrlCacheEntry,
|
|
53
55
|
parseReadUrlTarget,
|
|
56
|
+
prepareReadUrlSelectorInput,
|
|
54
57
|
type ReadUrlToolDetails,
|
|
55
58
|
renderReadUrlCall,
|
|
56
59
|
renderReadUrlResult,
|
|
@@ -71,6 +74,7 @@ import {
|
|
|
71
74
|
splitInternalUrlSel,
|
|
72
75
|
splitPathAndSel,
|
|
73
76
|
} from "./path-utils";
|
|
77
|
+
import { type ReadRoute, resolveEffectiveDirection } from "./read-internals";
|
|
74
78
|
import { formatBytes, replaceTabs, shortenPath, wrapBrackets } from "./render-utils";
|
|
75
79
|
import {
|
|
76
80
|
enforceSqliteQueryOnly,
|
|
@@ -99,6 +103,12 @@ const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
|
99
103
|
const MAX_SUMMARY_LINES = 20_000;
|
|
100
104
|
const RAW_COLLECTOR_MAX_BYTES = MAX_SUMMARY_BYTES;
|
|
101
105
|
|
|
106
|
+
const BODY_TRUNCATION_FOOTER_KEY = "__bodyTruncationFooter";
|
|
107
|
+
|
|
108
|
+
function rememberBodyTruncationFooter(details: ReadToolDetails, footer: string): void {
|
|
109
|
+
Object.defineProperty(details, BODY_TRUNCATION_FOOTER_KEY, { value: footer, configurable: true, enumerable: false });
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
/**
|
|
103
113
|
* Per-line column cap for file reads. Lines wider than the value of
|
|
104
114
|
* `tools.outputMaxColumns` are ellipsis-truncated at display time; the file
|
|
@@ -185,6 +195,259 @@ function countTextLines(text: string): number {
|
|
|
185
195
|
return text.split("\n").length;
|
|
186
196
|
}
|
|
187
197
|
|
|
198
|
+
function windowSegments(window: ReadWindow): ReadSegment[] {
|
|
199
|
+
return [window.head, window.tail].filter((segment): segment is ReadSegment => segment !== undefined);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function windowHasPartialSegment(window: ReadWindow): boolean {
|
|
203
|
+
return windowSegments(window).some(segment => segment.kind === "partial-line");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function partialTailOf(window: ReadWindow): Extract<ReadSegment, { kind: "partial-line" }> | undefined {
|
|
207
|
+
const tail = window.tail;
|
|
208
|
+
return tail?.kind === "partial-line" ? tail : undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function linesOfSegment(segment: ReadSegment): string[] {
|
|
212
|
+
return segment.content.split("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function rebaseWindow(window: ReadWindow, offset: number): ReadWindow {
|
|
216
|
+
const rebaseSegment = (segment: ReadSegment | undefined): ReadSegment | undefined => {
|
|
217
|
+
if (!segment) return undefined;
|
|
218
|
+
return {
|
|
219
|
+
...segment,
|
|
220
|
+
origin: {
|
|
221
|
+
startLine: segment.origin.startLine + offset,
|
|
222
|
+
endLine: segment.origin.endLine + offset,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
const rebased: ReadWindow = {
|
|
227
|
+
...window,
|
|
228
|
+
head: rebaseSegment(window.head),
|
|
229
|
+
tail: rebaseSegment(window.tail),
|
|
230
|
+
};
|
|
231
|
+
for (const key of ["maxBytes", "truncatedBy", "outputLinesOverride", "outputBytesOverride"] as const) {
|
|
232
|
+
const descriptor = Object.getOwnPropertyDescriptor(window, key);
|
|
233
|
+
if (descriptor) Object.defineProperty(rebased, key, descriptor);
|
|
234
|
+
}
|
|
235
|
+
return rebased;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function renderReadWindow(
|
|
239
|
+
window: ReadWindow,
|
|
240
|
+
shouldAddHashLines: boolean,
|
|
241
|
+
shouldAddLineNumbers: boolean,
|
|
242
|
+
maxColumns = 0,
|
|
243
|
+
): string {
|
|
244
|
+
const renderSegment = (segment: ReadSegment): string => {
|
|
245
|
+
const hash = shouldAddHashLines && segment.kind === "lines";
|
|
246
|
+
const numbers = !hash && shouldAddLineNumbers;
|
|
247
|
+
const renderContent =
|
|
248
|
+
!hash && maxColumns > 0
|
|
249
|
+
? segment.content
|
|
250
|
+
.split("\n")
|
|
251
|
+
.map(line => truncateLine(line, maxColumns).text)
|
|
252
|
+
.join("\n")
|
|
253
|
+
: segment.content;
|
|
254
|
+
return formatTextWithMode(renderContent, segment.origin.startLine, hash, numbers);
|
|
255
|
+
};
|
|
256
|
+
const marker = formatMiddleElisionMarker(window.elidedLines, window.elidedBytes);
|
|
257
|
+
if (window.kind === "full") return window.head ? renderSegment(window.head) : "";
|
|
258
|
+
if (window.kind === "head-only") return window.head ? renderSegment(window.head) : "";
|
|
259
|
+
if (window.kind === "tail-only") return window.tail ? renderSegment(window.tail) : "";
|
|
260
|
+
if (!window.head || !window.tail) return "";
|
|
261
|
+
if (window.elidedLines <= 0) return `${renderSegment(window.head)}\n${renderSegment(window.tail)}`;
|
|
262
|
+
return `${renderSegment(window.head)}\n${marker}\n${renderSegment(window.tail)}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function windowOutputLines(window: ReadWindow): number {
|
|
266
|
+
if (window.kind === "full") return window.head?.lines ?? 0;
|
|
267
|
+
if (window.kind === "head-only") return window.head?.lines ?? 0;
|
|
268
|
+
if (window.kind === "tail-only") return window.tail?.lines ?? 0;
|
|
269
|
+
return (window.head?.lines ?? 0) + (window.tail?.lines ?? 0) + (window.elidedLines > 0 ? 1 : 0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function windowOutputBytes(window: ReadWindow): number {
|
|
273
|
+
if (window.kind === "full") return window.head?.bytes ?? 0;
|
|
274
|
+
if (window.kind === "head-only") return window.head?.bytes ?? 0;
|
|
275
|
+
if (window.kind === "tail-only") return window.tail?.bytes ?? 0;
|
|
276
|
+
if (window.elidedLines <= 0) return (window.head?.bytes ?? 0) + (window.tail?.bytes ?? 0) + 1;
|
|
277
|
+
const marker = formatMiddleElisionMarker(window.elidedLines, window.elidedBytes);
|
|
278
|
+
return (window.head?.bytes ?? 0) + (window.tail?.bytes ?? 0) + Buffer.byteLength(marker, "utf-8") + 2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function windowTruncatedBy(window: ReadWindow): "lines" | "bytes" | "middle" | undefined {
|
|
282
|
+
return (window as ReadWindow & { truncatedBy?: "lines" | "bytes" | "middle" }).truncatedBy;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function makeWindowResult(window: ReadWindow, content: string): TruncationResult {
|
|
286
|
+
const truncatedBy = windowTruncatedBy(window);
|
|
287
|
+
const lastLineExceedsLimit = window.tail?.kind === "partial-line";
|
|
288
|
+
return {
|
|
289
|
+
content,
|
|
290
|
+
truncated: window.kind !== "full",
|
|
291
|
+
truncatedBy: truncatedBy ?? (window.kind === "middle" ? "middle" : "lines"),
|
|
292
|
+
totalLines: window.totalLines,
|
|
293
|
+
totalBytes: window.totalBytes,
|
|
294
|
+
outputLines: windowOutputLines(window),
|
|
295
|
+
outputBytes: windowOutputBytes(window),
|
|
296
|
+
// `elidedLines`/`elidedBytes` are documented as middle-only on the legacy
|
|
297
|
+
// `TruncationResult`; head/tail windows leave them absent so one-sided
|
|
298
|
+
// results stay byte-identical to the pre-change shape.
|
|
299
|
+
...(window.kind === "middle" ? { elidedLines: window.elidedLines, elidedBytes: window.elidedBytes } : {}),
|
|
300
|
+
lastLinePartial: windowHasPartialSegment(window),
|
|
301
|
+
firstLineExceedsLimit: false,
|
|
302
|
+
...(lastLineExceedsLimit ? { lastLineExceedsLimit: true } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function fullReadWindow(content: string, totalLines: number, totalBytes: number): ReadWindow {
|
|
307
|
+
return {
|
|
308
|
+
kind: "full",
|
|
309
|
+
head: {
|
|
310
|
+
kind: "lines",
|
|
311
|
+
content,
|
|
312
|
+
lines: totalLines,
|
|
313
|
+
bytes: totalBytes,
|
|
314
|
+
origin: { startLine: 1, endLine: totalLines },
|
|
315
|
+
lastLinePartial: false,
|
|
316
|
+
},
|
|
317
|
+
overlap: "disjoint",
|
|
318
|
+
elidedLines: 0,
|
|
319
|
+
elidedBytes: 0,
|
|
320
|
+
totalLines,
|
|
321
|
+
totalBytes,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function selectReceiptWindow(
|
|
326
|
+
allLines: readonly string[],
|
|
327
|
+
direction: "head" | "tail",
|
|
328
|
+
budgetLines: number,
|
|
329
|
+
budgetBytes: number,
|
|
330
|
+
): ReadWindow {
|
|
331
|
+
const totalLines = allLines.length;
|
|
332
|
+
const fullContent = allLines.join("\n");
|
|
333
|
+
const totalBytes = Buffer.byteLength(fullContent, "utf-8");
|
|
334
|
+
const take = (from: number, step: 1 | -1): { picked: string[]; bytes: number; truncatedBy?: "lines" | "bytes" } => {
|
|
335
|
+
const picked: string[] = [];
|
|
336
|
+
let bytes = 0;
|
|
337
|
+
let truncatedBy: "lines" | "bytes" | undefined;
|
|
338
|
+
for (let i = from; i >= 0 && i < allLines.length; i += step) {
|
|
339
|
+
if (picked.length >= budgetLines) {
|
|
340
|
+
truncatedBy = "lines";
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
const lineBytes = Buffer.byteLength(allLines[i] ?? "", "utf-8") + (picked.length > 0 ? 1 : 0);
|
|
344
|
+
if (bytes + lineBytes > budgetBytes) {
|
|
345
|
+
// Report the constraint that actually bound the window. When the
|
|
346
|
+
// line budget was already full, the byte overflow is incidental and
|
|
347
|
+
// the materialized oracle calls it "lines"; matching keeps A26
|
|
348
|
+
// parity honest.
|
|
349
|
+
truncatedBy = picked.length >= budgetLines ? "lines" : "bytes";
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
picked.push(allLines[i] ?? "");
|
|
353
|
+
bytes += lineBytes;
|
|
354
|
+
}
|
|
355
|
+
return { picked, bytes, truncatedBy };
|
|
356
|
+
};
|
|
357
|
+
const taken = direction === "head" ? take(0, 1) : take(allLines.length - 1, -1);
|
|
358
|
+
if (direction === "tail") taken.picked.reverse();
|
|
359
|
+
const startLine = direction === "head" ? 1 : totalLines - taken.picked.length + 1;
|
|
360
|
+
const selected = taken.picked.join("\n");
|
|
361
|
+
if (taken.picked.length === totalLines) return fullReadWindow(fullContent, totalLines, totalBytes);
|
|
362
|
+
const segment: ReadSegment | undefined =
|
|
363
|
+
taken.picked.length > 0
|
|
364
|
+
? {
|
|
365
|
+
kind: "lines",
|
|
366
|
+
content: selected,
|
|
367
|
+
lines: taken.picked.length,
|
|
368
|
+
bytes: taken.bytes,
|
|
369
|
+
origin: { startLine, endLine: startLine + taken.picked.length - 1 },
|
|
370
|
+
lastLinePartial: false,
|
|
371
|
+
}
|
|
372
|
+
: undefined;
|
|
373
|
+
const window: ReadWindow = {
|
|
374
|
+
kind: direction === "head" ? "head-only" : "tail-only",
|
|
375
|
+
...(direction === "head" ? (segment ? { head: segment } : {}) : segment ? { tail: segment } : {}),
|
|
376
|
+
overlap: "disjoint",
|
|
377
|
+
elidedLines: Math.max(0, totalLines - taken.picked.length),
|
|
378
|
+
elidedBytes: Math.max(0, totalBytes - taken.bytes),
|
|
379
|
+
totalLines,
|
|
380
|
+
totalBytes,
|
|
381
|
+
};
|
|
382
|
+
if (taken.truncatedBy) Object.defineProperty(window, "truncatedBy", { value: taken.truncatedBy, enumerable: false });
|
|
383
|
+
return window;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function selectReadWindow(
|
|
387
|
+
text: string,
|
|
388
|
+
allLines: readonly string[],
|
|
389
|
+
direction: TruncationDirection,
|
|
390
|
+
budgetLines: number,
|
|
391
|
+
budgetBytes: number,
|
|
392
|
+
): ReadWindow {
|
|
393
|
+
if (direction === "both") {
|
|
394
|
+
return truncateMiddleWindows(text, { maxLines: budgetLines, maxBytes: budgetBytes });
|
|
395
|
+
}
|
|
396
|
+
return selectReceiptWindow(allLines, direction === "head" ? "head" : "tail", budgetLines, budgetBytes);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function formatDirectionalFooter(readPath: string, window: ReadWindow): string {
|
|
400
|
+
if (window.kind === "full") return "";
|
|
401
|
+
const total = window.totalLines;
|
|
402
|
+
const shownSegments = windowSegments(window);
|
|
403
|
+
const shownBytes =
|
|
404
|
+
shownSegments.reduce((sum, segment) => sum + segment.bytes, 0) + Math.max(0, shownSegments.length - 1);
|
|
405
|
+
const head = window.head;
|
|
406
|
+
const tail = window.tail;
|
|
407
|
+
const partialTail = partialTailOf(window);
|
|
408
|
+
if (partialTail) {
|
|
409
|
+
return `[Showing last ${formatBytes(partialTail.bytes)} of line ${partialTail.origin.startLine} (line is ${formatBytes(partialTail.sourceLineBytes)}); re-read ${readPath}:1-${total} or ${readPath}:raw for the full file]`;
|
|
410
|
+
}
|
|
411
|
+
if (window.kind === "tail-only" && tail) {
|
|
412
|
+
const start = tail.origin.startLine;
|
|
413
|
+
return `[Showing last ${tail.lines} of ${total} lines (lines ${start}-${tail.origin.endLine}, ~${Math.ceil(shownBytes / 1024)} KiB); omitted lines 1-${start - 1}; re-read ${readPath}:1-${total} or ${readPath}:raw for the full file]`;
|
|
414
|
+
}
|
|
415
|
+
if (window.kind === "middle" && head && tail) {
|
|
416
|
+
const omittedStart = head.origin.endLine + 1;
|
|
417
|
+
const omittedEnd = tail.origin.startLine - 1;
|
|
418
|
+
return `[Showing lines 1-${head.origin.endLine} and ${tail.origin.startLine}-${tail.origin.endLine} of ${total} (~${Math.ceil(shownBytes / 1024)} KiB); omitted lines ${omittedStart}-${omittedEnd}; re-read ${readPath}:1-${total} or ${readPath}:raw for the full file]`;
|
|
419
|
+
}
|
|
420
|
+
const shown = head?.lines ?? 0;
|
|
421
|
+
return `[Showing first ${shown} of ${total} lines (~${Math.ceil(shownBytes / 1024)} KiB); re-read ${readPath}:1-${total} or ${readPath}:raw for the full file]`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function formatOversizedLineWarning(
|
|
425
|
+
readPath: string,
|
|
426
|
+
lineNumber: number,
|
|
427
|
+
lineBytes: number,
|
|
428
|
+
maxBytes: number,
|
|
429
|
+
fullFileLines: number,
|
|
430
|
+
): string {
|
|
431
|
+
return `[Line ${lineNumber} is ${formatBytes(lineBytes)}, exceeds ${formatBytes(maxBytes)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview. Re-read ${readPath}:1-${fullFileLines} or ${readPath}:raw for the full file]`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function formatListingTruncationNotice(
|
|
435
|
+
shownLines: number,
|
|
436
|
+
shownBytes: number,
|
|
437
|
+
absolutePath: string,
|
|
438
|
+
direction: TruncationDirection,
|
|
439
|
+
): string {
|
|
440
|
+
const retained = direction === "last" ? "last" : direction === "both" ? "first and last" : "first";
|
|
441
|
+
const directionPart = direction === "head" ? "" : `; retained ${retained} lines`;
|
|
442
|
+
return `[Listing truncated at ${shownLines} lines / ${Math.ceil(shownBytes / 1024)} KiB${directionPart}; read a deeper subpath (e.g. ${absolutePath}/<subdir>) to narrow the view]`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function formatRawReadCapNotice(maxBytes: number, fileSize: number, direction: TruncationDirection): string {
|
|
446
|
+
const retained = direction === "last" ? "last" : direction === "both" ? "first and last" : "first";
|
|
447
|
+
const directionPart = direction === "head" ? "" : `; retained ${retained} bytes`;
|
|
448
|
+
return `[Raw read capped at ${formatBytes(maxBytes)} of ${formatBytes(fileSize)}${directionPart}; re-read a line range to narrow the view]`;
|
|
449
|
+
}
|
|
450
|
+
|
|
188
451
|
/**
|
|
189
452
|
* Footer appended to summarized reads telling the model how to recover the
|
|
190
453
|
* elided body. Without this hint, agents either ignore the `...`/`{ .. }`
|
|
@@ -202,8 +465,15 @@ function formatReceiptFooter(readPath: string, shownLines: number, totalLines: n
|
|
|
202
465
|
return `[Showing first ${shownLines} of ${totalLines} lines (~${Math.ceil(shownBytes / 1024)} KiB); re-read ${readPath}:1-${totalLines} or ${readPath}:raw for the full file]`;
|
|
203
466
|
}
|
|
204
467
|
|
|
205
|
-
function formatSummaryCapFooter(
|
|
206
|
-
|
|
468
|
+
function formatSummaryCapFooter(
|
|
469
|
+
readPath: string,
|
|
470
|
+
maxBytes: number,
|
|
471
|
+
totalLines: number,
|
|
472
|
+
direction: TruncationDirection = "head",
|
|
473
|
+
): string {
|
|
474
|
+
const retained = direction === "last" ? "last" : direction === "both" ? "first and last" : "first";
|
|
475
|
+
const directionPart = direction === "head" ? "" : `; retained ${retained} summary units`;
|
|
476
|
+
return `[Summary truncated at ${Math.ceil(maxBytes / 1024)} KiB${directionPart}; re-read ${readPath}:1-${totalLines} or ${readPath}:raw for the full source]`;
|
|
207
477
|
}
|
|
208
478
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
209
479
|
|
|
@@ -241,13 +511,15 @@ function expandRangeWithContext(
|
|
|
241
511
|
};
|
|
242
512
|
}
|
|
243
513
|
|
|
244
|
-
async function streamLinesFromFile(
|
|
514
|
+
export async function streamLinesFromFile(
|
|
245
515
|
filePath: string,
|
|
246
516
|
startLine: number,
|
|
247
517
|
maxLinesToCollect: number,
|
|
248
518
|
maxBytes: number,
|
|
249
519
|
selectedLineLimit: number | null,
|
|
250
520
|
signal?: AbortSignal,
|
|
521
|
+
collect: "head" | "tail" | "both" = "head",
|
|
522
|
+
collectEndExclusive: number | null = null,
|
|
251
523
|
): Promise<{
|
|
252
524
|
lines: string[];
|
|
253
525
|
totalFileLines: number;
|
|
@@ -256,6 +528,22 @@ async function streamLinesFromFile(
|
|
|
256
528
|
firstLinePreview?: { text: string; bytes: number };
|
|
257
529
|
firstLineByteLength?: number;
|
|
258
530
|
selectedBytesTotal: number;
|
|
531
|
+
windowBytesTotal: number;
|
|
532
|
+
windowLinesTotal: number;
|
|
533
|
+
windowStartIndex: number;
|
|
534
|
+
tailPartial?: { text: string; bytes: number; sourceIndex: number; sourceLineBytes: number };
|
|
535
|
+
maxBytes: number;
|
|
536
|
+
headLines?: string[];
|
|
537
|
+
headBytes?: number;
|
|
538
|
+
headStartIndex?: number;
|
|
539
|
+
degenerateTailBudget: boolean;
|
|
540
|
+
degenerateHeadBudget: boolean;
|
|
541
|
+
headStoppedByBytes: boolean;
|
|
542
|
+
headStoppedByLines: boolean;
|
|
543
|
+
ringBudgetLines: number;
|
|
544
|
+
ringEvictedForBytes: boolean;
|
|
545
|
+
ringEvictedForLines: boolean;
|
|
546
|
+
tailDroppedForBytes: boolean;
|
|
259
547
|
}> {
|
|
260
548
|
const bufferChunk = Buffer.allocUnsafe(READ_CHUNK_SIZE);
|
|
261
549
|
const collectedLines: string[] = [];
|
|
@@ -273,30 +561,72 @@ async function streamLinesFromFile(
|
|
|
273
561
|
let firstLineByteLength: number | undefined;
|
|
274
562
|
let selectedBytesTotal = 0;
|
|
275
563
|
let selectedLinesSeen = 0;
|
|
564
|
+
let windowBytesTotal = 0;
|
|
565
|
+
let windowLinesTotal = 0;
|
|
276
566
|
let captureLine = false;
|
|
277
567
|
let discardLineChunks = false;
|
|
278
568
|
let lineCaptureLimit = 0;
|
|
279
569
|
|
|
570
|
+
// A middle read uses the same split as truncateMiddleWindows. The candidate
|
|
571
|
+
// state below is deliberately full-fit first: a file that fits the unsplit
|
|
572
|
+
// budget must not lose bytes merely because the eventual mode is "both".
|
|
573
|
+
const headBudgetBytes = collect === "both" ? Math.floor(maxBytes / 2) : 0;
|
|
574
|
+
const ringBudgetBytes = collect === "both" ? Math.max(0, maxBytes - headBudgetBytes) : maxBytes;
|
|
575
|
+
const headBudgetLines = collect === "both" ? Math.max(1, Math.floor(maxLinesToCollect / 2)) : 0;
|
|
576
|
+
const ringBudgetLines = collect === "both" ? Math.max(0, maxLinesToCollect - headBudgetLines) : maxLinesToCollect;
|
|
577
|
+
// Keep the full-fit candidate ahead of this classification. Once a split is
|
|
578
|
+
// required, these predicates dispatch the effective one-sided window kind.
|
|
579
|
+
const degenerateHeadBudget = collect === "both" && (headBudgetBytes <= 0 || headBudgetLines <= 0);
|
|
580
|
+
const degenerateTailBudget = collect === "both" && (ringBudgetBytes <= 0 || ringBudgetLines <= 0);
|
|
581
|
+
type BothState = "full-fit" | "split";
|
|
582
|
+
let oneSidedDispatch: "head" | "tail" | undefined;
|
|
583
|
+
let state: BothState = collect === "both" ? "full-fit" : "split";
|
|
584
|
+
let fitLines: string[] = [];
|
|
585
|
+
let fitByteLens: number[] = [];
|
|
586
|
+
let fitBytes = 0;
|
|
587
|
+
let fitStartIndex = -1;
|
|
588
|
+
|
|
589
|
+
const ring: string[] = [];
|
|
590
|
+
const ringLineBytes: number[] = [];
|
|
591
|
+
let ringHead = 0;
|
|
592
|
+
let ringBytes = 0;
|
|
593
|
+
let ringStartIndex = 0;
|
|
594
|
+
let ringOriginPending = true;
|
|
595
|
+
let ringEvictedForBytes = false;
|
|
596
|
+
let ringEvictedForLines = false;
|
|
597
|
+
let tailDroppedForBytes = false;
|
|
598
|
+
|
|
599
|
+
const headLines: string[] = [];
|
|
600
|
+
let headBytes = 0;
|
|
601
|
+
let headSealed = false;
|
|
602
|
+
let headStoppedByBytes = false;
|
|
603
|
+
let headStoppedByLines = false;
|
|
604
|
+
|
|
605
|
+
let currentLineKept = 0;
|
|
606
|
+
let currentLineDropped = false;
|
|
607
|
+
let oversizedPending: { index: number; byteLength: number; preview: { text: string; bytes: number } } | undefined;
|
|
608
|
+
|
|
609
|
+
const windowEndExclusive = collectEndExclusive === null ? Number.POSITIVE_INFINITY : collectEndExclusive;
|
|
610
|
+
const inWindow = () => lineIndex >= startLine && lineIndex < windowEndExclusive;
|
|
611
|
+
|
|
280
612
|
const setupLineState = () => {
|
|
281
|
-
captureLine = !doneCollecting && lineIndex >= startLine;
|
|
613
|
+
captureLine = collect === "head" && !doneCollecting && lineIndex >= startLine;
|
|
282
614
|
discardLineChunks = !captureLine;
|
|
283
615
|
if (captureLine) {
|
|
284
616
|
const separatorBytes = collectedLines.length > 0 ? 1 : 0;
|
|
285
617
|
lineCaptureLimit = maxBytes - collectedBytes - separatorBytes;
|
|
286
|
-
if (lineCaptureLimit <= 0)
|
|
287
|
-
discardLineChunks = true;
|
|
288
|
-
}
|
|
618
|
+
if (lineCaptureLimit <= 0) discardLineChunks = true;
|
|
289
619
|
} else {
|
|
290
620
|
lineCaptureLimit = 0;
|
|
291
621
|
}
|
|
292
622
|
};
|
|
293
623
|
|
|
294
624
|
const decodeLine = (): string => {
|
|
295
|
-
if (
|
|
296
|
-
if (currentLineChunks.length === 1 && currentLineChunks[0]?.length ===
|
|
625
|
+
if (currentLineKept === 0) return "";
|
|
626
|
+
if (currentLineChunks.length === 1 && currentLineChunks[0]?.length === currentLineKept) {
|
|
297
627
|
return currentLineChunks[0].toString("utf-8");
|
|
298
628
|
}
|
|
299
|
-
return Buffer.concat(currentLineChunks,
|
|
629
|
+
return Buffer.concat(currentLineChunks, currentLineKept).toString("utf-8");
|
|
300
630
|
};
|
|
301
631
|
|
|
302
632
|
const maybeCapturePreview = (segment: Uint8Array) => {
|
|
@@ -309,57 +639,189 @@ async function streamLinesFromFile(
|
|
|
309
639
|
firstLinePreviewBytes += slice.length;
|
|
310
640
|
};
|
|
311
641
|
|
|
642
|
+
const trimCurrentLineBuffer = (cap: number) => {
|
|
643
|
+
while (currentLineKept > cap && currentLineChunks.length > 0) {
|
|
644
|
+
const front = currentLineChunks[0];
|
|
645
|
+
const excess = currentLineKept - cap;
|
|
646
|
+
if (front.length <= excess) {
|
|
647
|
+
currentLineChunks.shift();
|
|
648
|
+
currentLineKept -= front.length;
|
|
649
|
+
} else {
|
|
650
|
+
currentLineChunks[0] = Buffer.from(front.subarray(excess));
|
|
651
|
+
currentLineKept -= excess;
|
|
652
|
+
}
|
|
653
|
+
currentLineDropped = true;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
312
657
|
const appendSegment = (segment: Uint8Array) => {
|
|
313
658
|
currentLineLength += segment.length;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
659
|
+
if (collect === "head") {
|
|
660
|
+
maybeCapturePreview(segment);
|
|
661
|
+
if (!captureLine || discardLineChunks || segment.length === 0) return;
|
|
662
|
+
if (currentLineLength <= lineCaptureLimit) {
|
|
663
|
+
currentLineChunks.push(Buffer.from(segment));
|
|
664
|
+
currentLineKept += segment.length;
|
|
665
|
+
} else {
|
|
666
|
+
discardLineChunks = true;
|
|
667
|
+
}
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (!inWindow() || segment.length === 0) return;
|
|
671
|
+
currentLineChunks.push(Buffer.from(segment));
|
|
672
|
+
currentLineKept += segment.length;
|
|
673
|
+
trimCurrentLineBuffer(collect === "both" && state === "full-fit" ? maxBytes : ringBudgetBytes);
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const capturePartialPreview = (): { text: string; bytes: number } => {
|
|
677
|
+
const raw = Buffer.concat(currentLineChunks, currentLineKept);
|
|
678
|
+
let start = 0;
|
|
679
|
+
while (start < raw.length && (raw[start]! & 0xc0) === 0x80) start++;
|
|
680
|
+
const aligned = raw.subarray(start);
|
|
681
|
+
return { text: aligned.toString("utf-8"), bytes: aligned.length };
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const pushSplit = (
|
|
685
|
+
index: number,
|
|
686
|
+
text: string,
|
|
687
|
+
bytes: number,
|
|
688
|
+
dropped: boolean,
|
|
689
|
+
previewOverride?: { text: string; bytes: number },
|
|
690
|
+
) => {
|
|
691
|
+
if (collect === "both" && oneSidedDispatch !== "tail" && !headSealed) {
|
|
692
|
+
if (dropped || bytes > headBudgetBytes) {
|
|
693
|
+
headSealed = true;
|
|
694
|
+
headStoppedByBytes = true;
|
|
695
|
+
} else {
|
|
696
|
+
const separatorBytes = headLines.length > 0 ? 1 : 0;
|
|
697
|
+
if (headLines.length < headBudgetLines && headBytes + separatorBytes + bytes <= headBudgetBytes) {
|
|
698
|
+
headLines.push(text);
|
|
699
|
+
headBytes += separatorBytes + bytes;
|
|
700
|
+
} else {
|
|
701
|
+
headSealed = true;
|
|
702
|
+
if (headLines.length >= headBudgetLines) headStoppedByLines = true;
|
|
703
|
+
else headStoppedByBytes = true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (collect === "both" && oneSidedDispatch === "head") return;
|
|
708
|
+
|
|
709
|
+
if (dropped || bytes > ringBudgetBytes) {
|
|
710
|
+
const preview = previewOverride ?? truncateTailBytes(text, ringBudgetBytes);
|
|
711
|
+
oversizedPending = { index, byteLength: bytes, preview };
|
|
712
|
+
ring.length = 0;
|
|
713
|
+
ringHead = 0;
|
|
714
|
+
ringLineBytes.length = 0;
|
|
715
|
+
ringBytes = 0;
|
|
716
|
+
ringOriginPending = true;
|
|
717
|
+
tailDroppedForBytes = true;
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (ringOriginPending) {
|
|
722
|
+
ringStartIndex = index;
|
|
723
|
+
ringOriginPending = false;
|
|
724
|
+
}
|
|
725
|
+
ring.push(text);
|
|
726
|
+
ringLineBytes.push(bytes);
|
|
727
|
+
ringBytes += (ring.length - ringHead > 1 ? 1 : 0) + bytes;
|
|
728
|
+
oversizedPending = undefined;
|
|
729
|
+
|
|
730
|
+
while (ring.length - ringHead > ringBudgetLines || (ringBytes > ringBudgetBytes && ring.length - ringHead > 1)) {
|
|
731
|
+
if (ring.length - ringHead > ringBudgetLines) ringEvictedForLines = true;
|
|
732
|
+
if (ringBytes > ringBudgetBytes) ringEvictedForBytes = true;
|
|
733
|
+
const evictedBytes = ringLineBytes[ringHead] ?? 0;
|
|
734
|
+
const evictedSep = ring.length - ringHead > 1 ? 1 : 0;
|
|
735
|
+
ringBytes -= evictedBytes + evictedSep;
|
|
736
|
+
ring[ringHead] = undefined as unknown as string;
|
|
737
|
+
ringHead++;
|
|
738
|
+
ringStartIndex++;
|
|
739
|
+
}
|
|
740
|
+
if (ring.length - ringHead === 0) ringOriginPending = true;
|
|
741
|
+
if (ringHead > 4096) {
|
|
742
|
+
ring.splice(0, ringHead);
|
|
743
|
+
ringLineBytes.splice(0, ringHead);
|
|
744
|
+
ringHead = 0;
|
|
320
745
|
}
|
|
321
746
|
};
|
|
322
747
|
|
|
748
|
+
const transitionToSplit = () => {
|
|
749
|
+
state = "split";
|
|
750
|
+
oneSidedDispatch = degenerateHeadBudget ? "tail" : degenerateTailBudget ? "head" : undefined;
|
|
751
|
+
for (let i = 0; i < fitLines.length; i++) {
|
|
752
|
+
pushSplit(fitStartIndex + i, fitLines[i] ?? "", fitByteLens[i] ?? 0, false);
|
|
753
|
+
}
|
|
754
|
+
fitLines = [];
|
|
755
|
+
fitByteLens = [];
|
|
756
|
+
fitBytes = 0;
|
|
757
|
+
fitStartIndex = -1;
|
|
758
|
+
trimCurrentLineBuffer(ringBudgetBytes);
|
|
759
|
+
};
|
|
760
|
+
|
|
323
761
|
const finalizeLine = () => {
|
|
324
762
|
if (lineIndex >= startLine && (selectedLineLimit === null || selectedLinesSeen < selectedLineLimit)) {
|
|
325
763
|
selectedBytesTotal += currentLineLength + (selectedLinesSeen > 0 ? 1 : 0);
|
|
326
764
|
selectedLinesSeen++;
|
|
327
765
|
}
|
|
766
|
+
if (inWindow()) {
|
|
767
|
+
windowBytesTotal += currentLineLength + (windowLinesTotal > 0 ? 1 : 0);
|
|
768
|
+
windowLinesTotal++;
|
|
769
|
+
}
|
|
328
770
|
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
doneCollecting = true;
|
|
336
|
-
if (firstLineByteLength === undefined) {
|
|
337
|
-
firstLineByteLength = currentLineLength;
|
|
338
|
-
}
|
|
339
|
-
} else if (collectedLines.length > 0 && collectedBytes + separatorBytes + currentLineLength > maxBytes) {
|
|
340
|
-
stoppedByByteLimit = true;
|
|
341
|
-
doneCollecting = true;
|
|
342
|
-
} else {
|
|
343
|
-
const lineText = decodeLine();
|
|
344
|
-
collectedLines.push(lineText);
|
|
345
|
-
collectedBytes += separatorBytes + currentLineLength;
|
|
346
|
-
if (firstLineByteLength === undefined) {
|
|
347
|
-
firstLineByteLength = currentLineLength;
|
|
348
|
-
}
|
|
349
|
-
if (collectedBytes > maxBytes) {
|
|
771
|
+
if (collect === "head") {
|
|
772
|
+
if (!doneCollecting && lineIndex >= startLine) {
|
|
773
|
+
const separatorBytes = collectedLines.length > 0 ? 1 : 0;
|
|
774
|
+
if (collectedLines.length >= maxLinesToCollect) {
|
|
775
|
+
doneCollecting = true;
|
|
776
|
+
} else if (collectedLines.length === 0 && currentLineLength > maxBytes) {
|
|
350
777
|
stoppedByByteLimit = true;
|
|
351
778
|
doneCollecting = true;
|
|
352
|
-
|
|
779
|
+
if (firstLineByteLength === undefined) firstLineByteLength = currentLineLength;
|
|
780
|
+
} else if (collectedLines.length > 0 && collectedBytes + separatorBytes + currentLineLength > maxBytes) {
|
|
781
|
+
stoppedByByteLimit = true;
|
|
353
782
|
doneCollecting = true;
|
|
783
|
+
} else {
|
|
784
|
+
const lineText = decodeLine();
|
|
785
|
+
collectedLines.push(lineText);
|
|
786
|
+
collectedBytes += separatorBytes + currentLineLength;
|
|
787
|
+
if (firstLineByteLength === undefined) firstLineByteLength = currentLineLength;
|
|
788
|
+
if (collectedBytes > maxBytes) {
|
|
789
|
+
stoppedByByteLimit = true;
|
|
790
|
+
doneCollecting = true;
|
|
791
|
+
} else if (collectedLines.length >= maxLinesToCollect) {
|
|
792
|
+
doneCollecting = true;
|
|
793
|
+
}
|
|
354
794
|
}
|
|
795
|
+
} else if (lineIndex >= startLine && firstLineByteLength === undefined) {
|
|
796
|
+
firstLineByteLength = currentLineLength;
|
|
797
|
+
}
|
|
798
|
+
} else if (inWindow()) {
|
|
799
|
+
const lineText = decodeLine();
|
|
800
|
+
if (collect === "both" && state === "full-fit") {
|
|
801
|
+
const separatorBytes = fitLines.length > 0 ? 1 : 0;
|
|
802
|
+
const stillFits =
|
|
803
|
+
!currentLineDropped &&
|
|
804
|
+
fitLines.length + 1 <= maxLinesToCollect &&
|
|
805
|
+
fitBytes + separatorBytes + currentLineLength <= maxBytes;
|
|
806
|
+
if (stillFits) {
|
|
807
|
+
if (fitStartIndex < 0) fitStartIndex = lineIndex;
|
|
808
|
+
fitLines.push(lineText);
|
|
809
|
+
fitByteLens.push(currentLineLength);
|
|
810
|
+
fitBytes += separatorBytes + currentLineLength;
|
|
811
|
+
} else {
|
|
812
|
+
transitionToSplit();
|
|
813
|
+
pushSplit(lineIndex, lineText, currentLineLength, currentLineDropped, capturePartialPreview());
|
|
814
|
+
}
|
|
815
|
+
} else {
|
|
816
|
+
pushSplit(lineIndex, lineText, currentLineLength, currentLineDropped, capturePartialPreview());
|
|
355
817
|
}
|
|
356
|
-
} else if (lineIndex >= startLine && firstLineByteLength === undefined) {
|
|
357
|
-
firstLineByteLength = currentLineLength;
|
|
358
818
|
}
|
|
359
819
|
|
|
360
820
|
lineIndex++;
|
|
361
821
|
currentLineLength = 0;
|
|
822
|
+
currentLineKept = 0;
|
|
362
823
|
currentLineChunks = [];
|
|
824
|
+
currentLineDropped = false;
|
|
363
825
|
setupLineState();
|
|
364
826
|
};
|
|
365
827
|
|
|
@@ -367,7 +829,6 @@ async function streamLinesFromFile(
|
|
|
367
829
|
|
|
368
830
|
try {
|
|
369
831
|
fileHandle = await fs.open(filePath, "r");
|
|
370
|
-
|
|
371
832
|
while (true) {
|
|
372
833
|
throwIfAborted(signal);
|
|
373
834
|
const { bytesRead } = await fileHandle.read(bufferChunk, 0, bufferChunk.length, null);
|
|
@@ -381,27 +842,18 @@ async function streamLinesFromFile(
|
|
|
381
842
|
for (let i = 0; i < chunk.length; i++) {
|
|
382
843
|
if (chunk[i] === 0x0a) {
|
|
383
844
|
const segment = chunk.subarray(start, i);
|
|
384
|
-
if (segment.length > 0)
|
|
385
|
-
appendSegment(segment);
|
|
386
|
-
}
|
|
845
|
+
if (segment.length > 0) appendSegment(segment);
|
|
387
846
|
finalizeLine();
|
|
388
847
|
start = i + 1;
|
|
389
848
|
}
|
|
390
849
|
}
|
|
391
|
-
|
|
392
|
-
if (start < chunk.length) {
|
|
393
|
-
appendSegment(chunk.subarray(start));
|
|
394
|
-
}
|
|
850
|
+
if (start < chunk.length) appendSegment(chunk.subarray(start));
|
|
395
851
|
}
|
|
396
852
|
} finally {
|
|
397
|
-
if (fileHandle)
|
|
398
|
-
await fileHandle.close();
|
|
399
|
-
}
|
|
853
|
+
if (fileHandle) await fileHandle.close();
|
|
400
854
|
}
|
|
401
855
|
|
|
402
|
-
if (endedWithNewline || currentLineLength > 0 || !sawAnyByte)
|
|
403
|
-
finalizeLine();
|
|
404
|
-
}
|
|
856
|
+
if (endedWithNewline || currentLineLength > 0 || !sawAnyByte) finalizeLine();
|
|
405
857
|
|
|
406
858
|
let firstLinePreview: { text: string; bytes: number } | undefined;
|
|
407
859
|
if (firstLinePreviewBytes > 0) {
|
|
@@ -409,17 +861,271 @@ async function streamLinesFromFile(
|
|
|
409
861
|
firstLinePreview = { text, bytes };
|
|
410
862
|
}
|
|
411
863
|
|
|
864
|
+
const tailPartial =
|
|
865
|
+
collect !== "head" && oversizedPending
|
|
866
|
+
? {
|
|
867
|
+
text: oversizedPending.preview.text,
|
|
868
|
+
bytes: oversizedPending.preview.bytes,
|
|
869
|
+
sourceIndex: oversizedPending.index,
|
|
870
|
+
sourceLineBytes: oversizedPending.byteLength,
|
|
871
|
+
}
|
|
872
|
+
: undefined;
|
|
873
|
+
// I8: selected-window line totals are defined on the valid domain only;
|
|
874
|
+
// out-of-bounds starts clamp the mathematical count at zero.
|
|
875
|
+
// `inWindow()` above enforces the same predicate while collecting.
|
|
876
|
+
// I8 is therefore `windowLinesTotal === Math.max(0, windowEnd - startLine)`.
|
|
877
|
+
const ringSize = ring.length - ringHead;
|
|
878
|
+
const ringLines = ring.slice(ringHead);
|
|
879
|
+
const windowEnd = Math.min(collectEndExclusive ?? lineIndex, lineIndex);
|
|
880
|
+
const windowStartIndex =
|
|
881
|
+
collect === "head"
|
|
882
|
+
? startLine
|
|
883
|
+
: ringSize > 0
|
|
884
|
+
? ringStartIndex
|
|
885
|
+
: tailPartial
|
|
886
|
+
? tailPartial.sourceIndex
|
|
887
|
+
: windowEnd;
|
|
888
|
+
|
|
889
|
+
if (collect !== "head") collectedBytes = ringBytes;
|
|
890
|
+
|
|
891
|
+
const resolvedHeadLines = collect === "both" && state === "full-fit" ? fitLines : headLines;
|
|
892
|
+
const resolvedHeadBytes = collect === "both" && state === "full-fit" ? fitBytes : headBytes;
|
|
412
893
|
return {
|
|
413
|
-
lines: collectedLines,
|
|
894
|
+
lines: collect === "head" ? collectedLines : ringLines,
|
|
414
895
|
totalFileLines: lineIndex,
|
|
415
896
|
collectedBytes,
|
|
416
|
-
stoppedByByteLimit,
|
|
897
|
+
stoppedByByteLimit: collect === "head" ? stoppedByByteLimit : ringEvictedForBytes,
|
|
417
898
|
firstLinePreview,
|
|
418
899
|
firstLineByteLength,
|
|
419
900
|
selectedBytesTotal,
|
|
901
|
+
windowBytesTotal,
|
|
902
|
+
windowLinesTotal,
|
|
903
|
+
windowStartIndex,
|
|
904
|
+
tailPartial,
|
|
905
|
+
maxBytes,
|
|
906
|
+
headLines: collect === "both" ? resolvedHeadLines : undefined,
|
|
907
|
+
headBytes: collect === "both" ? resolvedHeadBytes : undefined,
|
|
908
|
+
headStartIndex: collect === "both" ? startLine : undefined,
|
|
909
|
+
degenerateTailBudget,
|
|
910
|
+
degenerateHeadBudget,
|
|
911
|
+
headStoppedByBytes,
|
|
912
|
+
headStoppedByLines,
|
|
913
|
+
ringBudgetLines,
|
|
914
|
+
ringEvictedForBytes,
|
|
915
|
+
ringEvictedForLines,
|
|
916
|
+
tailDroppedForBytes,
|
|
420
917
|
};
|
|
421
918
|
}
|
|
422
919
|
|
|
920
|
+
export function streamResultWindow(
|
|
921
|
+
result: Awaited<ReturnType<typeof streamLinesFromFile>>,
|
|
922
|
+
collect: "tail" | "both",
|
|
923
|
+
startLine: number,
|
|
924
|
+
collectEndExclusive: number | null,
|
|
925
|
+
): ReadWindow {
|
|
926
|
+
const windowEnd = Math.min(collectEndExclusive ?? result.totalFileLines, result.totalFileLines);
|
|
927
|
+
const totalLines = result.windowLinesTotal;
|
|
928
|
+
const totalBytes = result.windowBytesTotal;
|
|
929
|
+
const ringLines = result.lines;
|
|
930
|
+
const partial = result.tailPartial;
|
|
931
|
+
const attachReason = (window: ReadWindow, reason: "lines" | "bytes" | "middle" | undefined): ReadWindow => {
|
|
932
|
+
Object.defineProperty(window, "maxBytes", { value: result.maxBytes, enumerable: false });
|
|
933
|
+
if (reason !== undefined) Object.defineProperty(window, "truncatedBy", { value: reason, enumerable: false });
|
|
934
|
+
return window;
|
|
935
|
+
};
|
|
936
|
+
const linesSegment = (lines: readonly string[], bytes: number, sourceStart: number): ReadSegment | undefined => {
|
|
937
|
+
if (lines.length === 0) return undefined;
|
|
938
|
+
return {
|
|
939
|
+
kind: "lines",
|
|
940
|
+
content: lines.join("\n"),
|
|
941
|
+
lines: lines.length,
|
|
942
|
+
bytes,
|
|
943
|
+
origin: { startLine: sourceStart + 1, endLine: sourceStart + lines.length },
|
|
944
|
+
lastLinePartial: false,
|
|
945
|
+
};
|
|
946
|
+
};
|
|
947
|
+
const partialSegment = (): ReadSegment | undefined => {
|
|
948
|
+
if (!partial) return undefined;
|
|
949
|
+
return {
|
|
950
|
+
kind: "partial-line",
|
|
951
|
+
content: partial.text,
|
|
952
|
+
lines: 1,
|
|
953
|
+
bytes: partial.bytes,
|
|
954
|
+
origin: { startLine: partial.sourceIndex + 1, endLine: partial.sourceIndex + 1 },
|
|
955
|
+
sourceLineBytes: partial.sourceLineBytes,
|
|
956
|
+
lastLinePartial: true,
|
|
957
|
+
};
|
|
958
|
+
};
|
|
959
|
+
const sourceBytes = (lines: readonly string[]): number => Buffer.byteLength(lines.join("\n"), "utf-8");
|
|
960
|
+
const tail = partialSegment() ?? linesSegment(ringLines, result.collectedBytes, result.windowStartIndex);
|
|
961
|
+
const tailStartIndex = partial?.sourceIndex ?? (ringLines.length > 0 ? result.windowStartIndex : windowEnd);
|
|
962
|
+
const tailCount = partial ? 1 : ringLines.length;
|
|
963
|
+
const lineBudgetWon = !partial && result.ringEvictedForLines && tailCount >= result.ringBudgetLines;
|
|
964
|
+
const tailReason: "lines" | "bytes" | undefined =
|
|
965
|
+
partial || result.tailDroppedForBytes || (result.ringEvictedForBytes && !lineBudgetWon)
|
|
966
|
+
? "bytes"
|
|
967
|
+
: lineBudgetWon || tailCount < totalLines
|
|
968
|
+
? "lines"
|
|
969
|
+
: undefined;
|
|
970
|
+
|
|
971
|
+
if (collect === "tail") {
|
|
972
|
+
if (
|
|
973
|
+
tailCount === totalLines &&
|
|
974
|
+
!partial &&
|
|
975
|
+
!result.ringEvictedForBytes &&
|
|
976
|
+
!result.ringEvictedForLines &&
|
|
977
|
+
!result.tailDroppedForBytes
|
|
978
|
+
) {
|
|
979
|
+
return {
|
|
980
|
+
kind: "full",
|
|
981
|
+
head: linesSegment(ringLines, totalBytes, startLine),
|
|
982
|
+
overlap: "disjoint",
|
|
983
|
+
elidedLines: 0,
|
|
984
|
+
elidedBytes: 0,
|
|
985
|
+
totalLines,
|
|
986
|
+
totalBytes,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
return attachReason(
|
|
990
|
+
{
|
|
991
|
+
kind: "tail-only",
|
|
992
|
+
...(tail ? { tail } : {}),
|
|
993
|
+
overlap: "disjoint",
|
|
994
|
+
elidedLines: Math.max(0, totalLines - tailCount),
|
|
995
|
+
elidedBytes: partial
|
|
996
|
+
? Math.max(0, totalBytes - partial.bytes)
|
|
997
|
+
: Math.max(0, totalBytes - result.collectedBytes),
|
|
998
|
+
totalLines,
|
|
999
|
+
totalBytes,
|
|
1000
|
+
},
|
|
1001
|
+
tailReason,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const headLines = result.headLines ?? [];
|
|
1006
|
+
const headCount = headLines.length;
|
|
1007
|
+
const headBytes = result.headBytes ?? sourceBytes(headLines);
|
|
1008
|
+
const head = linesSegment(headLines, headBytes, result.headStartIndex ?? startLine);
|
|
1009
|
+
const headEndExclusive = startLine + headCount;
|
|
1010
|
+
const windowLineCount = totalLines;
|
|
1011
|
+
const headReason: "lines" | "bytes" | undefined = result.degenerateTailBudget
|
|
1012
|
+
? result.headStoppedByBytes
|
|
1013
|
+
? "bytes"
|
|
1014
|
+
: result.headStoppedByLines || headCount < windowLineCount
|
|
1015
|
+
? "lines"
|
|
1016
|
+
: undefined
|
|
1017
|
+
: result.ringEvictedForBytes || result.tailDroppedForBytes
|
|
1018
|
+
? "bytes"
|
|
1019
|
+
: result.ringEvictedForLines || headCount < windowLineCount
|
|
1020
|
+
? "lines"
|
|
1021
|
+
: undefined;
|
|
1022
|
+
const makeHeadOnly = (): ReadWindow =>
|
|
1023
|
+
attachReason(
|
|
1024
|
+
{
|
|
1025
|
+
kind: "head-only",
|
|
1026
|
+
...(head ? { head } : {}),
|
|
1027
|
+
overlap: "disjoint",
|
|
1028
|
+
elidedLines: Math.max(0, windowLineCount - headCount),
|
|
1029
|
+
elidedBytes: Math.max(0, totalBytes - headBytes),
|
|
1030
|
+
totalLines: windowLineCount,
|
|
1031
|
+
totalBytes,
|
|
1032
|
+
},
|
|
1033
|
+
headReason,
|
|
1034
|
+
);
|
|
1035
|
+
const makeTailOnly = (): ReadWindow =>
|
|
1036
|
+
attachReason(
|
|
1037
|
+
{
|
|
1038
|
+
kind: "tail-only",
|
|
1039
|
+
...(tail ? { tail } : {}),
|
|
1040
|
+
overlap: "disjoint",
|
|
1041
|
+
elidedLines: Math.max(0, windowLineCount - tailCount),
|
|
1042
|
+
elidedBytes: partial
|
|
1043
|
+
? Math.max(0, totalBytes - partial.bytes)
|
|
1044
|
+
: Math.max(0, totalBytes - result.collectedBytes),
|
|
1045
|
+
totalLines: windowLineCount,
|
|
1046
|
+
totalBytes,
|
|
1047
|
+
},
|
|
1048
|
+
tailReason,
|
|
1049
|
+
);
|
|
1050
|
+
if (
|
|
1051
|
+
collect === "both" &&
|
|
1052
|
+
headCount === windowLineCount &&
|
|
1053
|
+
!partial &&
|
|
1054
|
+
!result.ringEvictedForBytes &&
|
|
1055
|
+
!result.ringEvictedForLines &&
|
|
1056
|
+
!result.tailDroppedForBytes
|
|
1057
|
+
) {
|
|
1058
|
+
return {
|
|
1059
|
+
kind: "full",
|
|
1060
|
+
head: linesSegment(headLines, totalBytes, startLine),
|
|
1061
|
+
overlap: "disjoint",
|
|
1062
|
+
elidedLines: 0,
|
|
1063
|
+
elidedBytes: 0,
|
|
1064
|
+
totalLines: windowLineCount,
|
|
1065
|
+
totalBytes,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (collect === "both" && result.degenerateHeadBudget) return makeTailOnly();
|
|
1070
|
+
if (collect === "both" && result.degenerateTailBudget) return makeHeadOnly();
|
|
1071
|
+
if (headCount === 0) return makeTailOnly();
|
|
1072
|
+
if (ringLines.length === 0 && !partial) return makeHeadOnly();
|
|
1073
|
+
if (!tail) return makeHeadOnly();
|
|
1074
|
+
|
|
1075
|
+
const headTailGap = tailStartIndex - headEndExclusive;
|
|
1076
|
+
const overlap: ReadWindow["overlap"] = headTailGap > 0 ? "disjoint" : headTailGap === 0 ? "adjacent" : "overlapping";
|
|
1077
|
+
if (headTailGap <= 0) {
|
|
1078
|
+
const headKeep = Math.max(0, tailStartIndex - startLine);
|
|
1079
|
+
if (headKeep === 0) return makeTailOnly();
|
|
1080
|
+
if (!partial && headKeep + ringLines.length >= windowLineCount) {
|
|
1081
|
+
const fullLines = headLines.slice(0, headKeep).concat(ringLines);
|
|
1082
|
+
return {
|
|
1083
|
+
kind: "full",
|
|
1084
|
+
head: linesSegment(fullLines, totalBytes, startLine),
|
|
1085
|
+
overlap,
|
|
1086
|
+
elidedLines: 0,
|
|
1087
|
+
elidedBytes: 0,
|
|
1088
|
+
totalLines: windowLineCount,
|
|
1089
|
+
totalBytes,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
const keptHeadLines = headLines.slice(0, headKeep);
|
|
1093
|
+
const keptHead = linesSegment(keptHeadLines, sourceBytes(keptHeadLines), startLine);
|
|
1094
|
+
if (!keptHead) return makeTailOnly();
|
|
1095
|
+
return attachReason(
|
|
1096
|
+
{
|
|
1097
|
+
kind: "middle",
|
|
1098
|
+
head: keptHead,
|
|
1099
|
+
tail,
|
|
1100
|
+
overlap,
|
|
1101
|
+
elidedLines: partial ? Math.max(0, tailStartIndex - (startLine + headKeep)) : 0,
|
|
1102
|
+
elidedBytes: partial
|
|
1103
|
+
? Math.max(0, totalBytes - keptHead.bytes - partial.sourceLineBytes)
|
|
1104
|
+
: Math.max(0, totalBytes - keptHead.bytes - tail.bytes),
|
|
1105
|
+
totalLines: windowLineCount,
|
|
1106
|
+
totalBytes,
|
|
1107
|
+
},
|
|
1108
|
+
"middle",
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const elidedLines = partial ? Math.max(0, tailStartIndex - headEndExclusive) : headTailGap;
|
|
1113
|
+
const partialSourceBytes = partial ? partial.sourceLineBytes : tail.bytes;
|
|
1114
|
+
return attachReason(
|
|
1115
|
+
{
|
|
1116
|
+
kind: "middle",
|
|
1117
|
+
...(head ? { head } : {}),
|
|
1118
|
+
tail,
|
|
1119
|
+
overlap,
|
|
1120
|
+
elidedLines,
|
|
1121
|
+
elidedBytes: Math.max(0, totalBytes - headBytes - partialSourceBytes),
|
|
1122
|
+
totalLines: windowLineCount,
|
|
1123
|
+
totalBytes,
|
|
1124
|
+
},
|
|
1125
|
+
"middle",
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
423
1129
|
// Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
|
|
424
1130
|
const MAX_IMAGE_SIZE = MAX_IMAGE_INPUT_BYTES;
|
|
425
1131
|
const GLOB_TIMEOUT_MS = 5000;
|
|
@@ -495,6 +1201,12 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
|
|
|
495
1201
|
const readSchema = z
|
|
496
1202
|
.object({
|
|
497
1203
|
path: z.string().describe('path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")'),
|
|
1204
|
+
truncation: z
|
|
1205
|
+
.enum(["head", "last", "both"])
|
|
1206
|
+
.optional()
|
|
1207
|
+
.describe(
|
|
1208
|
+
"which end of an over-budget result to keep: head | last | both. Route defaults are route-aware: read.truncation is consulted for bare local and archive-member routes (factory default: last), while URL, converted, directory, range, internal, and other routes default to head. SQLite row, schema, and query reads ignore this parameter; raw reads honor explicit directions.",
|
|
1209
|
+
),
|
|
498
1210
|
})
|
|
499
1211
|
.strict();
|
|
500
1212
|
|
|
@@ -707,6 +1419,21 @@ interface ResolvedSqliteReadPath {
|
|
|
707
1419
|
* Reads files with support for images, converted documents (via markit), and text.
|
|
708
1420
|
* Directories return a formatted listing with modification times.
|
|
709
1421
|
*/
|
|
1422
|
+
/**
|
|
1423
|
+
* A client-authority denial is a decision, not a transport failure: falling back to
|
|
1424
|
+
* disk would bypass the permission the client just refused. Availability failures
|
|
1425
|
+
* still fall back so an unreachable bridge cannot break local reads.
|
|
1426
|
+
*/
|
|
1427
|
+
function isClientAuthorityDenial(error: unknown): boolean {
|
|
1428
|
+
const code =
|
|
1429
|
+
typeof error === "object" && error !== null && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
1430
|
+
// ACP clients surface refusals as an application error; -32001 is the reserved
|
|
1431
|
+
// client-authority denial code and -32603 covers hosts without a dedicated code.
|
|
1432
|
+
if (code === "permission_denied" || code === "forbidden" || code === -32001 || code === -32603) return true;
|
|
1433
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
1434
|
+
return /permission denied|not permitted|access denied|forbidden/i.test(message);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
710
1437
|
export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
711
1438
|
readonly name = "read";
|
|
712
1439
|
readonly label = "Read";
|
|
@@ -730,6 +1457,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
730
1457
|
this.description = prompt.render(readDescription, {
|
|
731
1458
|
DEFAULT_LIMIT: String(this.#defaultLimit),
|
|
732
1459
|
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
|
|
1460
|
+
TRUNCATION_DEFAULT: String(this.session.settings.get("read.truncation") ?? "last"),
|
|
1461
|
+
RECEIPT_LINES: String(this.session.settings.get("read.receiptBudgetLines")),
|
|
1462
|
+
RECEIPT_KIB: String(this.session.settings.get("read.receiptBudgetBytes")),
|
|
733
1463
|
IS_HL_MODE: displayMode.hashLines,
|
|
734
1464
|
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
735
1465
|
});
|
|
@@ -833,6 +1563,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
833
1563
|
sourcePath?: string;
|
|
834
1564
|
spillEligible?: boolean;
|
|
835
1565
|
receiptPath?: string;
|
|
1566
|
+
bodyFooterPath?: string;
|
|
836
1567
|
sourceUrl?: string;
|
|
837
1568
|
sourceInternal?: string;
|
|
838
1569
|
entityLabel: string;
|
|
@@ -841,19 +1572,21 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
841
1572
|
raw?: boolean;
|
|
842
1573
|
immutable?: boolean;
|
|
843
1574
|
wrapUntrusted?: boolean;
|
|
1575
|
+
truncationDirection?: TruncationDirection;
|
|
1576
|
+
/** Truncation metadata stays relative to the selected body, not a protected prefix. */
|
|
1577
|
+
truncationMetadataBodyRelative?: boolean;
|
|
1578
|
+
/** Preserve this leading URL preamble while truncating only the selected body. */
|
|
1579
|
+
protectedPrefixChars?: number;
|
|
1580
|
+
cacheLinesFor?: string;
|
|
844
1581
|
},
|
|
845
1582
|
): AgentToolResult<ReadToolDetails> {
|
|
846
1583
|
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
847
1584
|
const details = options.details ?? {};
|
|
848
1585
|
const allLines = text.split("\n");
|
|
849
1586
|
const totalLines = allLines.length;
|
|
850
|
-
// User-requested 0-indexed range start. Lines BEFORE this are leading
|
|
851
|
-
// context (added below if offset is explicit).
|
|
852
1587
|
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
853
1588
|
const ignoreResultLimits = options.ignoreResultLimits ?? false;
|
|
854
1589
|
const requestedEnd = limit !== undefined ? Math.min(requestedStart + limit, allLines.length) : allLines.length;
|
|
855
|
-
// Expand only on sides the user actually constrained: leading context
|
|
856
|
-
// when offset>1, trailing context when a finite limit was set.
|
|
857
1590
|
const expanded = expandRangeWithContext(
|
|
858
1591
|
requestedStart,
|
|
859
1592
|
requestedEnd,
|
|
@@ -862,19 +1595,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
862
1595
|
limit !== undefined,
|
|
863
1596
|
);
|
|
864
1597
|
const startLine = expanded.startLine;
|
|
865
|
-
const
|
|
1598
|
+
const endLine = expanded.endLine;
|
|
866
1599
|
const startLineDisplay = startLine + 1;
|
|
867
1600
|
|
|
868
1601
|
const resultBuilder = toolResult(details);
|
|
869
|
-
if (options.sourcePath)
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (options.sourceUrl) {
|
|
873
|
-
resultBuilder.sourceUrl(options.sourceUrl);
|
|
874
|
-
}
|
|
875
|
-
if (options.sourceInternal) {
|
|
876
|
-
resultBuilder.sourceInternal(options.sourceInternal);
|
|
877
|
-
}
|
|
1602
|
+
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
1603
|
+
if (options.sourceUrl) resultBuilder.sourceUrl(options.sourceUrl);
|
|
1604
|
+
if (options.sourceInternal) resultBuilder.sourceInternal(options.sourceInternal);
|
|
878
1605
|
|
|
879
1606
|
if (requestedStart >= allLines.length) {
|
|
880
1607
|
const suggestion =
|
|
@@ -888,92 +1615,217 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
888
1615
|
.done();
|
|
889
1616
|
}
|
|
890
1617
|
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
?
|
|
910
|
-
:
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1618
|
+
const direction = options.truncationDirection ?? "head";
|
|
1619
|
+
const receipt = options.receiptPath !== undefined;
|
|
1620
|
+
const exposeReceiptHeadMeta = receipt && options.cacheLinesFor !== undefined;
|
|
1621
|
+
const bodyFooterPath = options.receiptPath ?? options.bodyFooterPath;
|
|
1622
|
+
const preservePrefix =
|
|
1623
|
+
direction !== "head" && options.protectedPrefixChars !== undefined && options.protectedPrefixChars > 0;
|
|
1624
|
+
const protectedPrefix = preservePrefix ? text.slice(0, options.protectedPrefixChars) : "";
|
|
1625
|
+
const protectedPrefixLines = preservePrefix ? protectedPrefix.split("\n").length - 1 : 0;
|
|
1626
|
+
const rangeStart = preservePrefix ? Math.max(startLine, protectedPrefixLines) : startLine;
|
|
1627
|
+
|
|
1628
|
+
const rangeLines = allLines.slice(rangeStart, endLine);
|
|
1629
|
+
const rangeText = rangeLines.join("\n");
|
|
1630
|
+
const budgetLines = receipt
|
|
1631
|
+
? this.session.settings.get("read.receiptBudgetLines")
|
|
1632
|
+
: options.resultByteCeiling === undefined
|
|
1633
|
+
? DEFAULT_MAX_LINES
|
|
1634
|
+
: Number.MAX_SAFE_INTEGER;
|
|
1635
|
+
const budgetBytes = receipt
|
|
1636
|
+
? this.session.settings.get("read.receiptBudgetBytes") * 1024
|
|
1637
|
+
: (options.resultByteCeiling ?? DEFAULT_MAX_BYTES);
|
|
1638
|
+
const relativeWindow = ignoreResultLimits
|
|
1639
|
+
? fullReadWindow(rangeText, rangeLines.length, Buffer.byteLength(rangeText, "utf-8"))
|
|
1640
|
+
: selectReadWindow(
|
|
1641
|
+
receipt ? text : rangeText,
|
|
1642
|
+
receipt ? allLines : rangeLines,
|
|
1643
|
+
direction,
|
|
1644
|
+
budgetLines,
|
|
1645
|
+
budgetBytes,
|
|
915
1646
|
);
|
|
916
|
-
|
|
1647
|
+
// A directional head window cannot render anything when its first source line
|
|
1648
|
+
// exceeds the byte budget. Fall back to the historical head truncator so ACP
|
|
1649
|
+
// still emits the same UTF-8 preview and metadata as the pre-direction code.
|
|
1650
|
+
const oversizedHeadWindow =
|
|
1651
|
+
!receipt &&
|
|
1652
|
+
direction === "head" &&
|
|
1653
|
+
options.resultByteCeiling === undefined &&
|
|
1654
|
+
relativeWindow.kind === "head-only" &&
|
|
1655
|
+
relativeWindow.head === undefined;
|
|
1656
|
+
const legacyHeadByteCeiling = options.resultByteCeiling ?? (oversizedHeadWindow ? budgetBytes : undefined);
|
|
1657
|
+
const window = rebaseWindow(relativeWindow, receipt ? 0 : rangeStart);
|
|
1658
|
+
// Keep explicit ranges in the selected-window coordinate system for truncation metadata.
|
|
1659
|
+
// Rendered output still uses `window`, whose anchors retain absolute file line numbers.
|
|
1660
|
+
const metadataWindow =
|
|
1661
|
+
options.truncationMetadataBodyRelative || (offset !== undefined && direction !== "head")
|
|
1662
|
+
? relativeWindow
|
|
1663
|
+
: window;
|
|
1664
|
+
const metadataRangeBase =
|
|
1665
|
+
options.truncationMetadataBodyRelative || (offset !== undefined && direction !== "head")
|
|
1666
|
+
? "window"
|
|
1667
|
+
: undefined;
|
|
917
1668
|
const shouldAddHashLines = displayMode.hashLines;
|
|
918
|
-
const shouldAddLineNumbers = shouldAddHashLines
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
1669
|
+
const shouldAddLineNumbers = !shouldAddHashLines && displayMode.lineNumbers;
|
|
1670
|
+
const maxColumns = options.raw ? 0 : resolveOutputMaxColumns(this.session.settings);
|
|
1671
|
+
const bodyRawWindow = renderReadWindow(window, false, false, maxColumns);
|
|
1672
|
+
const bodyRenderedWindow = renderReadWindow(window, shouldAddHashLines, shouldAddLineNumbers, maxColumns);
|
|
1673
|
+
const rawWindow = preservePrefix ? `${protectedPrefix}${bodyRawWindow}` : bodyRawWindow;
|
|
1674
|
+
const renderedWindow = preservePrefix ? `${protectedPrefix}${bodyRenderedWindow}` : bodyRenderedWindow;
|
|
1675
|
+
const partialTail = partialTailOf(window);
|
|
1676
|
+
const partialTailWarning = partialTail !== undefined && shouldAddHashLines && bodyFooterPath !== undefined;
|
|
1677
|
+
if (partialTailWarning) {
|
|
1678
|
+
Object.defineProperty(metadataWindow, "outputLinesOverride", { value: 0, enumerable: false });
|
|
1679
|
+
Object.defineProperty(metadataWindow, "outputBytesOverride", { value: 0, enumerable: false });
|
|
1680
|
+
}
|
|
1681
|
+
const selectedContent = windowSegments(window)
|
|
1682
|
+
.map(segment => segment.content)
|
|
1683
|
+
.join("\n");
|
|
1684
|
+
details.displayContent = {
|
|
1685
|
+
text: rawWindow,
|
|
1686
|
+
startLine: preservePrefix
|
|
1687
|
+
? 1
|
|
1688
|
+
: (window.head?.origin.startLine ?? window.tail?.origin.startLine ?? startLineDisplay),
|
|
922
1689
|
};
|
|
923
1690
|
|
|
924
|
-
let
|
|
925
|
-
let
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1691
|
+
let usedLegacyHeadTruncation = false;
|
|
1692
|
+
let outputText = partialTailWarning
|
|
1693
|
+
? formatOversizedLineWarning(
|
|
1694
|
+
bodyFooterPath!,
|
|
1695
|
+
partialTail!.origin.startLine,
|
|
1696
|
+
partialTail!.sourceLineBytes,
|
|
1697
|
+
budgetBytes,
|
|
1698
|
+
totalLines,
|
|
1699
|
+
)
|
|
1700
|
+
: renderedWindow;
|
|
1701
|
+
let truncationResult: TruncationResult | undefined;
|
|
1702
|
+
if (window.kind !== "full" && (!receipt || direction !== "head" || exposeReceiptHeadMeta)) {
|
|
1703
|
+
const windowResult = makeWindowResult(metadataWindow, selectedContent);
|
|
1704
|
+
const baseTruncation =
|
|
1705
|
+
!receipt && direction === "head"
|
|
1706
|
+
? {
|
|
1707
|
+
...windowResult,
|
|
1708
|
+
truncated: true,
|
|
1709
|
+
}
|
|
1710
|
+
: windowResult;
|
|
1711
|
+
truncationResult = partialTailWarning
|
|
1712
|
+
? { ...baseTruncation, outputLines: 0, outputBytes: 0, lastLineExceedsLimit: true }
|
|
1713
|
+
: baseTruncation;
|
|
1714
|
+
details.truncation = truncationResult;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Preserve the historical head byte-cap oversized-line behavior. The
|
|
1718
|
+
// directional window model is used for tail/both; head remains the old
|
|
1719
|
+
// oracle so existing hashline and snippet contracts stay byte-identical.
|
|
1720
|
+
if (!receipt && direction === "head" && legacyHeadByteCeiling !== undefined) {
|
|
1721
|
+
const legacy = truncateHead(rangeText, {
|
|
1722
|
+
maxBytes: legacyHeadByteCeiling,
|
|
1723
|
+
maxLines: Number.MAX_SAFE_INTEGER,
|
|
1724
|
+
});
|
|
1725
|
+
truncationResult = legacy;
|
|
1726
|
+
usedLegacyHeadTruncation = legacy.truncated === true;
|
|
1727
|
+
details.truncation = legacy.truncated ? legacy : undefined;
|
|
1728
|
+
if (legacy.firstLineExceedsLimit) {
|
|
1729
|
+
const firstLine = allLines[startLine] ?? "";
|
|
1730
|
+
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
1731
|
+
const snippet = truncateHeadBytes(firstLine, legacyHeadByteCeiling);
|
|
1732
|
+
if (shouldAddHashLines) {
|
|
1733
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(legacyHeadByteCeiling)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
1734
|
+
} else {
|
|
1735
|
+
outputText = formatTextWithMode(snippet.text, startLineDisplay, false, shouldAddLineNumbers);
|
|
1736
|
+
}
|
|
1737
|
+
if (snippet.text.length === 0) {
|
|
1738
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(legacyHeadByteCeiling)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1739
|
+
}
|
|
1740
|
+
details.displayContent = { text: snippet.text, startLine: startLineDisplay };
|
|
1741
|
+
} else if (legacy.truncated) {
|
|
1742
|
+
outputText = formatTextWithMode(legacy.content, startLineDisplay, shouldAddHashLines, shouldAddLineNumbers);
|
|
938
1743
|
} else {
|
|
939
|
-
outputText =
|
|
1744
|
+
outputText = formatTextWithMode(rangeText, startLineDisplay, shouldAddHashLines, shouldAddLineNumbers);
|
|
940
1745
|
}
|
|
1746
|
+
}
|
|
941
1747
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1748
|
+
const userLimitedLines = limit !== undefined ? endLine - startLine : undefined;
|
|
1749
|
+
if (bodyFooterPath !== undefined && window.kind !== "full" && !partialTailWarning) {
|
|
1750
|
+
// A head window only owns a body footer on the historical receipt paths.
|
|
1751
|
+
// Archive members and internal URLs reach here through `bodyFooterPath`
|
|
1752
|
+
// alone; adding a head footer there would emit a second notice beside the
|
|
1753
|
+
// wrapper's, changing output that must stay byte-identical when the
|
|
1754
|
+
// direction is rolled back to head.
|
|
1755
|
+
const footer =
|
|
1756
|
+
direction === "head"
|
|
1757
|
+
? receipt
|
|
1758
|
+
? formatReceiptFooter(
|
|
1759
|
+
bodyFooterPath,
|
|
1760
|
+
metadataWindow.head?.lines ?? 0,
|
|
1761
|
+
metadataWindow.totalLines,
|
|
1762
|
+
windowSegments(metadataWindow).reduce((sum, segment) => sum + segment.bytes, 0),
|
|
1763
|
+
)
|
|
1764
|
+
: undefined
|
|
1765
|
+
: formatDirectionalFooter(bodyFooterPath, metadataWindow);
|
|
1766
|
+
if (footer) {
|
|
1767
|
+
outputText += `\n\n${footer}`;
|
|
1768
|
+
if (direction !== "head") rememberBodyTruncationFooter(details, footer);
|
|
946
1769
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
} else if (truncation.truncated) {
|
|
954
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
955
|
-
details.truncation = truncation;
|
|
956
|
-
truncationInfo = {
|
|
957
|
-
result: truncation,
|
|
958
|
-
options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
|
|
959
|
-
};
|
|
960
|
-
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
|
1770
|
+
} else if (
|
|
1771
|
+
!receipt &&
|
|
1772
|
+
window.kind === "full" &&
|
|
1773
|
+
userLimitedLines !== undefined &&
|
|
1774
|
+
startLine + userLimitedLines < allLines.length
|
|
1775
|
+
) {
|
|
961
1776
|
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
962
1777
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
963
|
-
|
|
964
|
-
outputText = formatText(selectedContent, startLineDisplay);
|
|
965
1778
|
outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`;
|
|
966
|
-
} else {
|
|
967
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
968
|
-
}
|
|
969
|
-
if (options.receiptPath && (truncation.truncated || countTextLines(selectedContent) < totalLines)) {
|
|
970
|
-
outputText += `\n\n${formatReceiptFooter(options.receiptPath, countTextLines(selectedContent), totalLines, Buffer.byteLength(selectedContent, "utf-8"))}`;
|
|
971
1779
|
}
|
|
972
1780
|
|
|
973
|
-
details.spillEligible =
|
|
1781
|
+
details.spillEligible =
|
|
1782
|
+
options.spillEligible === true && selectedContent.length > 0 && !windowHasPartialSegment(window);
|
|
1783
|
+
if (options.cacheLinesFor && !windowHasPartialSegment(window)) {
|
|
1784
|
+
for (const segment of windowSegments(window)) {
|
|
1785
|
+
if (segment.kind === "lines" && segment.lines > 0) {
|
|
1786
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1787
|
+
options.cacheLinesFor,
|
|
1788
|
+
segment.origin.startLine,
|
|
1789
|
+
linesOfSegment(segment),
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
974
1794
|
resultBuilder.text(options.wrapUntrusted ? wrapUntrustedContent(outputText) : outputText);
|
|
975
|
-
if (
|
|
976
|
-
|
|
1795
|
+
if (window.kind !== "full" && (!receipt || direction !== "head" || exposeReceiptHeadMeta)) {
|
|
1796
|
+
const receiptHead = receipt && direction === "head";
|
|
1797
|
+
if (receiptHead && truncationResult?.truncated) {
|
|
1798
|
+
const diskLike = {
|
|
1799
|
+
...truncationResult,
|
|
1800
|
+
totalBytes: Buffer.byteLength(selectedContent, "utf-8"),
|
|
1801
|
+
};
|
|
1802
|
+
resultBuilder.truncation(diskLike, {
|
|
1803
|
+
direction: "head",
|
|
1804
|
+
startLine: startLineDisplay,
|
|
1805
|
+
totalFileLines: totalLines,
|
|
1806
|
+
});
|
|
1807
|
+
} else if (usedLegacyHeadTruncation && truncationResult?.truncated) {
|
|
1808
|
+
resultBuilder.truncation(truncationResult, {
|
|
1809
|
+
direction: "head",
|
|
1810
|
+
startLine: startLineDisplay,
|
|
1811
|
+
totalFileLines: totalLines,
|
|
1812
|
+
});
|
|
1813
|
+
} else {
|
|
1814
|
+
// Only a directional window moves notice ownership into the body: its
|
|
1815
|
+
// footer names the kept range and the re-read path. A head window keeps
|
|
1816
|
+
// the wrapper notice, which carries the `:N to continue` hint instead.
|
|
1817
|
+
resultBuilder.truncationWindows(metadataWindow, {
|
|
1818
|
+
noticeOwner: bodyFooterPath !== undefined && direction !== "head" ? "body" : undefined,
|
|
1819
|
+
maxBytes: direction === "head" ? undefined : budgetBytes,
|
|
1820
|
+
rangeBase: metadataRangeBase,
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
} else if (truncationResult?.truncated) {
|
|
1824
|
+
resultBuilder.truncation(truncationResult, {
|
|
1825
|
+
direction: "head",
|
|
1826
|
+
startLine: startLineDisplay,
|
|
1827
|
+
totalFileLines: totalLines,
|
|
1828
|
+
});
|
|
977
1829
|
}
|
|
978
1830
|
return resultBuilder.done();
|
|
979
1831
|
}
|
|
@@ -996,6 +1848,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
996
1848
|
entityLabel: string;
|
|
997
1849
|
raw?: boolean;
|
|
998
1850
|
immutable?: boolean;
|
|
1851
|
+
truncationDirection?: TruncationDirection;
|
|
1852
|
+
cacheLinesFor?: string;
|
|
999
1853
|
},
|
|
1000
1854
|
): AgentToolResult<ReadToolDetails> {
|
|
1001
1855
|
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
@@ -1004,13 +1858,54 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1004
1858
|
const totalLines = allLines.length;
|
|
1005
1859
|
const shouldAddHashLines = displayMode.hashLines;
|
|
1006
1860
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1861
|
+
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1862
|
+
const direction = options.truncationDirection ?? "head";
|
|
1863
|
+
let columnTruncated = 0;
|
|
1007
1864
|
|
|
1008
1865
|
const resultBuilder = toolResult(details);
|
|
1009
1866
|
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
1010
1867
|
if (options.sourceUrl) resultBuilder.sourceUrl(options.sourceUrl);
|
|
1011
1868
|
if (options.sourceInternal) resultBuilder.sourceInternal(options.sourceInternal);
|
|
1012
1869
|
|
|
1870
|
+
const renderBlockWindow = (
|
|
1871
|
+
window: ReadWindow,
|
|
1872
|
+
): { text: string; partialTail?: Extract<ReadSegment, { kind: "partial-line" }> } => {
|
|
1873
|
+
const renderSegment = (segment: ReadSegment): string => {
|
|
1874
|
+
const hash = shouldAddHashLines && segment.kind === "lines";
|
|
1875
|
+
// A partial line is a byte suffix, not a complete source line. Never
|
|
1876
|
+
// give it an anchor that could make the preview look fully readable.
|
|
1877
|
+
const numbers = !hash && shouldAddLineNumbers && segment.kind === "lines";
|
|
1878
|
+
const renderContent =
|
|
1879
|
+
!options.raw && !hash && maxColumns > 0
|
|
1880
|
+
? segment.content
|
|
1881
|
+
.split("\n")
|
|
1882
|
+
.map(line => {
|
|
1883
|
+
const result = truncateLine(line, maxColumns);
|
|
1884
|
+
if (result.wasTruncated) columnTruncated = maxColumns;
|
|
1885
|
+
return result.text;
|
|
1886
|
+
})
|
|
1887
|
+
.join("\n")
|
|
1888
|
+
: segment.content;
|
|
1889
|
+
return formatTextWithMode(renderContent, segment.origin.startLine, hash, numbers);
|
|
1890
|
+
};
|
|
1891
|
+
let text = "";
|
|
1892
|
+
if (window.kind === "full") text = window.head ? renderSegment(window.head) : "";
|
|
1893
|
+
else if (window.kind === "head-only") text = window.head ? renderSegment(window.head) : "";
|
|
1894
|
+
else if (window.kind === "tail-only") text = window.tail ? renderSegment(window.tail) : "";
|
|
1895
|
+
else {
|
|
1896
|
+
const marker = formatMiddleElisionMarker(window.elidedLines, window.elidedBytes);
|
|
1897
|
+
if (window.head && window.tail) {
|
|
1898
|
+
text =
|
|
1899
|
+
window.elidedLines <= 0
|
|
1900
|
+
? `${renderSegment(window.head)}\n${renderSegment(window.tail)}`
|
|
1901
|
+
: `${renderSegment(window.head)}\n${marker}\n${renderSegment(window.tail)}`;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return { text, partialTail: partialTailOf(window) };
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1013
1907
|
const parts: string[] = [];
|
|
1908
|
+
let hasPartialSegment = false;
|
|
1014
1909
|
const outOfBounds: LineRange[] = [];
|
|
1015
1910
|
for (const range of ranges) {
|
|
1016
1911
|
if (range.startLine > totalLines) {
|
|
@@ -1018,8 +1913,41 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1018
1913
|
continue;
|
|
1019
1914
|
}
|
|
1020
1915
|
const effectiveEnd = Math.min(range.endLine ?? totalLines, totalLines);
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1916
|
+
const blockLines = allLines.slice(range.startLine - 1, effectiveEnd);
|
|
1917
|
+
const blockText = blockLines.join("\n");
|
|
1918
|
+
const requestedLength = range.endLine !== undefined ? range.endLine - range.startLine + 1 : this.#defaultLimit;
|
|
1919
|
+
const blockMaxLines = Math.min(requestedLength, DEFAULT_MAX_LINES);
|
|
1920
|
+
const blockMaxBytes = Math.max(DEFAULT_MAX_BYTES, blockMaxLines * 512);
|
|
1921
|
+
const relative = selectReadWindow(blockText, blockLines, direction, blockMaxLines, blockMaxBytes);
|
|
1922
|
+
const window = rebaseWindow(relative, range.startLine - 1);
|
|
1923
|
+
hasPartialSegment ||= windowHasPartialSegment(window);
|
|
1924
|
+
const rendered = renderBlockWindow(window);
|
|
1925
|
+
let blockOutput = rendered.text;
|
|
1926
|
+
if (rendered.partialTail) {
|
|
1927
|
+
const disclosurePath =
|
|
1928
|
+
options.sourceInternal ?? options.sourceUrl ?? options.sourcePath ?? options.entityLabel;
|
|
1929
|
+
blockOutput = shouldAddHashLines
|
|
1930
|
+
? formatOversizedLineWarning(
|
|
1931
|
+
disclosurePath,
|
|
1932
|
+
rendered.partialTail.origin.startLine,
|
|
1933
|
+
rendered.partialTail.sourceLineBytes,
|
|
1934
|
+
blockMaxBytes,
|
|
1935
|
+
totalLines,
|
|
1936
|
+
)
|
|
1937
|
+
: `${blockOutput}\n\n${formatDirectionalFooter(disclosurePath, window)}`;
|
|
1938
|
+
}
|
|
1939
|
+
parts.push(blockOutput);
|
|
1940
|
+
if (options.cacheLinesFor && !windowHasPartialSegment(window)) {
|
|
1941
|
+
for (const segment of windowSegments(window)) {
|
|
1942
|
+
if (segment.kind === "lines" && segment.lines > 0) {
|
|
1943
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1944
|
+
options.cacheLinesFor,
|
|
1945
|
+
segment.origin.startLine,
|
|
1946
|
+
linesOfSegment(segment),
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1023
1951
|
}
|
|
1024
1952
|
|
|
1025
1953
|
const outputText = parts.length > 0 ? parts.join("\n\n…\n\n") : "";
|
|
@@ -1030,9 +1958,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1030
1958
|
}
|
|
1031
1959
|
const finalText =
|
|
1032
1960
|
notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
|
|
1033
|
-
details.spillEligible = parts.length > 0;
|
|
1034
|
-
|
|
1961
|
+
details.spillEligible = parts.length > 0 && !hasPartialSegment;
|
|
1035
1962
|
resultBuilder.text(finalText);
|
|
1963
|
+
if (columnTruncated > 0) resultBuilder.limits({ columnMax: columnTruncated });
|
|
1036
1964
|
return resultBuilder.done();
|
|
1037
1965
|
}
|
|
1038
1966
|
|
|
@@ -1049,6 +1977,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1049
1977
|
displayMode: { hashLines: boolean; lineNumbers: boolean },
|
|
1050
1978
|
suffixResolution: { from: string; to: string } | undefined,
|
|
1051
1979
|
signal: AbortSignal | undefined,
|
|
1980
|
+
direction: TruncationDirection,
|
|
1052
1981
|
): Promise<{
|
|
1053
1982
|
outputText: string;
|
|
1054
1983
|
contentLineCount: number;
|
|
@@ -1067,6 +1996,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1067
1996
|
sourcePath: absolutePath,
|
|
1068
1997
|
entityLabel: "file",
|
|
1069
1998
|
raw: rawSelector,
|
|
1999
|
+
truncationDirection: direction,
|
|
2000
|
+
cacheLinesFor: absolutePath,
|
|
1070
2001
|
});
|
|
1071
2002
|
if (suffixResolution) {
|
|
1072
2003
|
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
@@ -1075,10 +2006,32 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1075
2006
|
}
|
|
1076
2007
|
return { outputText: "", contentLineCount: 0, columnTruncated: 0, bridgeResult };
|
|
1077
2008
|
} catch (error) {
|
|
2009
|
+
if (isClientAuthorityDenial(error)) throw error;
|
|
1078
2010
|
logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
|
|
1079
2011
|
}
|
|
1080
2012
|
}
|
|
1081
2013
|
|
|
2014
|
+
// The legacy stream collector remains the head/default implementation.
|
|
2015
|
+
// Directional multi-range reads use the same materialized window oracle as
|
|
2016
|
+
// ACP so each block has identical bytes, anchors, and cache entries.
|
|
2017
|
+
if (direction !== "head") {
|
|
2018
|
+
const diskText = await Bun.file(absolutePath).text();
|
|
2019
|
+
const result = this.#buildInMemoryMultiRangeResult(diskText, ranges, {
|
|
2020
|
+
details: { resolvedPath: absolutePath, suffixResolution },
|
|
2021
|
+
sourcePath: absolutePath,
|
|
2022
|
+
entityLabel: "file",
|
|
2023
|
+
raw: rawSelector,
|
|
2024
|
+
truncationDirection: direction,
|
|
2025
|
+
cacheLinesFor: absolutePath,
|
|
2026
|
+
});
|
|
2027
|
+
const firstText = result.content.find((content): content is TextContent => content.type === "text");
|
|
2028
|
+
if (suffixResolution && firstText) {
|
|
2029
|
+
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
2030
|
+
firstText.text = `${notice}\n${firstText.text}`;
|
|
2031
|
+
}
|
|
2032
|
+
return { outputText: "", contentLineCount: 0, columnTruncated: 0, bridgeResult: result };
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1082
2035
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1083
2036
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1084
2037
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
@@ -1111,6 +2064,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1111
2064
|
}
|
|
1112
2065
|
|
|
1113
2066
|
const collectedLines = streamResult.lines;
|
|
2067
|
+
const cacheLines = collectedLines.slice();
|
|
1114
2068
|
if (!rawSelector && !shouldAddHashLines && maxColumns > 0) {
|
|
1115
2069
|
for (let i = 0; i < collectedLines.length; i++) {
|
|
1116
2070
|
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
@@ -1122,8 +2076,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1122
2076
|
}
|
|
1123
2077
|
|
|
1124
2078
|
contentLineCount += collectedLines.length;
|
|
1125
|
-
if (
|
|
1126
|
-
getFileReadCache(this.session).recordContiguous(absolutePath, range.startLine,
|
|
2079
|
+
if (cacheLines.length > 0) {
|
|
2080
|
+
getFileReadCache(this.session).recordContiguous(absolutePath, range.startLine, cacheLines);
|
|
1127
2081
|
}
|
|
1128
2082
|
|
|
1129
2083
|
const blockText = collectedLines.join("\n");
|
|
@@ -1143,6 +2097,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1143
2097
|
subPath: string,
|
|
1144
2098
|
limit: number | undefined,
|
|
1145
2099
|
details: ReadToolDetails,
|
|
2100
|
+
direction: TruncationDirection,
|
|
1146
2101
|
signal?: AbortSignal,
|
|
1147
2102
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1148
2103
|
const DEFAULT_LIMIT = 500;
|
|
@@ -1166,14 +2121,21 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1166
2121
|
}
|
|
1167
2122
|
|
|
1168
2123
|
const output = results.length > 0 ? results.join("\n") : "(empty archive directory)";
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1171
|
-
const directoryDetails: ReadToolDetails = {
|
|
1172
|
-
|
|
2124
|
+
const window = selectReadWindow(output, results, direction, Number.MAX_SAFE_INTEGER, DEFAULT_MAX_BYTES);
|
|
2125
|
+
const rendered = prependSuffixResolutionNotice(renderReadWindow(window, false, false), details.suffixResolution);
|
|
2126
|
+
const directoryDetails: ReadToolDetails = {
|
|
2127
|
+
...details,
|
|
2128
|
+
isDirectory: true,
|
|
2129
|
+
...(window.kind !== "full" ? { truncation: makeWindowResult(window, output) } : {}),
|
|
2130
|
+
};
|
|
2131
|
+
const resultBuilder = toolResult<ReadToolDetails>(directoryDetails).text(rendered);
|
|
1173
2132
|
resultBuilder.sourcePath(archivePath).limits({ resultLimit: limitMeta.resultLimit?.reached });
|
|
1174
|
-
if (
|
|
1175
|
-
|
|
1176
|
-
|
|
2133
|
+
if (window.kind !== "full") {
|
|
2134
|
+
if (direction === "head") {
|
|
2135
|
+
resultBuilder.truncation(makeWindowResult(window, output), { direction: "head" });
|
|
2136
|
+
} else {
|
|
2137
|
+
resultBuilder.truncationWindows(window, { maxBytes: DEFAULT_MAX_BYTES });
|
|
2138
|
+
}
|
|
1177
2139
|
}
|
|
1178
2140
|
return resultBuilder.done();
|
|
1179
2141
|
}
|
|
@@ -1183,6 +2145,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1183
2145
|
parsedSel: ParsedSelector,
|
|
1184
2146
|
resolvedArchivePath: ResolvedArchiveReadPath,
|
|
1185
2147
|
signal?: AbortSignal,
|
|
2148
|
+
truncationParam?: TruncationDirection,
|
|
1186
2149
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1187
2150
|
throwIfAborted(signal);
|
|
1188
2151
|
const archive = await openArchive(resolvedArchivePath.absolutePath);
|
|
@@ -1197,6 +2160,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1197
2160
|
if (!node) {
|
|
1198
2161
|
throw new ToolError(`Path '${readPath}' not found inside archive`);
|
|
1199
2162
|
}
|
|
2163
|
+
const route: ReadRoute = node.isDirectory
|
|
2164
|
+
? "dir-archive"
|
|
2165
|
+
: isMultiRange(parsedSel)
|
|
2166
|
+
? "local-multi-range"
|
|
2167
|
+
: parsedSel.kind === "none"
|
|
2168
|
+
? "archive-member-bare"
|
|
2169
|
+
: "local-range";
|
|
2170
|
+
const direction = resolveEffectiveDirection(truncationParam, route, this.session.settings);
|
|
1200
2171
|
|
|
1201
2172
|
if (node.isDirectory) {
|
|
1202
2173
|
if (isMultiRange(parsedSel)) {
|
|
@@ -1209,12 +2180,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1209
2180
|
resolvedArchivePath.archiveSubPath,
|
|
1210
2181
|
limit,
|
|
1211
2182
|
details,
|
|
2183
|
+
direction,
|
|
1212
2184
|
signal,
|
|
1213
2185
|
);
|
|
1214
2186
|
}
|
|
1215
2187
|
|
|
1216
2188
|
const entry = await archive.readFile(resolvedArchivePath.archiveSubPath);
|
|
1217
2189
|
const text = decodeUtf8Text(entry.bytes);
|
|
2190
|
+
if (text === null && truncationParam !== undefined && truncationParam !== "head") {
|
|
2191
|
+
throw new ToolError("Explicit truncation for binary archive entries is not yet supported.");
|
|
2192
|
+
}
|
|
1218
2193
|
if (text === null) {
|
|
1219
2194
|
return toolResult<ReadToolDetails>(details)
|
|
1220
2195
|
.text(
|
|
@@ -1235,6 +2210,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1235
2210
|
sourcePath: resolvedArchivePath.absolutePath,
|
|
1236
2211
|
entityLabel: "archive entry",
|
|
1237
2212
|
raw,
|
|
2213
|
+
truncationDirection: direction,
|
|
1238
2214
|
})
|
|
1239
2215
|
: this.#buildInMemoryTextResult(
|
|
1240
2216
|
text,
|
|
@@ -1245,6 +2221,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1245
2221
|
sourcePath: resolvedArchivePath.absolutePath,
|
|
1246
2222
|
entityLabel: "archive entry",
|
|
1247
2223
|
raw,
|
|
2224
|
+
truncationDirection: direction,
|
|
2225
|
+
bodyFooterPath: parsedSel.kind === "none" ? readPath : undefined,
|
|
1248
2226
|
},
|
|
1249
2227
|
);
|
|
1250
2228
|
const firstText = result.content.find((content): content is TextContent => content.type === "text");
|
|
@@ -1257,6 +2235,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1257
2235
|
async #readSqlite(
|
|
1258
2236
|
resolvedSqlitePath: ResolvedSqliteReadPath,
|
|
1259
2237
|
signal?: AbortSignal,
|
|
2238
|
+
truncationParam?: TruncationDirection,
|
|
1260
2239
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1261
2240
|
throwIfAborted(signal);
|
|
1262
2241
|
|
|
@@ -1269,6 +2248,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1269
2248
|
resolvedPath: resolvedSqlitePath.absolutePath,
|
|
1270
2249
|
suffixResolution: resolvedSqlitePath.suffixResolution,
|
|
1271
2250
|
};
|
|
2251
|
+
const sqliteRoute: ReadRoute = selector.kind === "list" ? "sqlite-list" : "sqlite-rows";
|
|
2252
|
+
const direction = resolveEffectiveDirection(truncationParam, sqliteRoute, this.session.settings);
|
|
2253
|
+
// SQLite row/schema/query/raw selectors are row-selection routes: queryRows and
|
|
2254
|
+
// executeReadQuery apply their SQL row limits, while renderRow/renderTable only
|
|
2255
|
+
// cap individual cell width. No rendered-output truncator runs on these paths.
|
|
2256
|
+
// Keep explicit direction as a deliberate no-op there; sqlite-list below
|
|
2257
|
+
// truncates only its rendered table listing.
|
|
1272
2258
|
|
|
1273
2259
|
let db: Database | null = null;
|
|
1274
2260
|
try {
|
|
@@ -1280,18 +2266,29 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1280
2266
|
switch (selector.kind) {
|
|
1281
2267
|
case "list": {
|
|
1282
2268
|
const listLimit = applyListLimit(listTables(db), { limit: 500 });
|
|
1283
|
-
const
|
|
1284
|
-
|
|
2269
|
+
const body = renderTableList(listLimit.items);
|
|
2270
|
+
const window = selectReadWindow(
|
|
2271
|
+
body,
|
|
2272
|
+
body.split("\n"),
|
|
2273
|
+
direction,
|
|
2274
|
+
Number.MAX_SAFE_INTEGER,
|
|
2275
|
+
DEFAULT_MAX_BYTES,
|
|
2276
|
+
);
|
|
2277
|
+
const rendered = prependSuffixResolutionNotice(
|
|
2278
|
+
renderReadWindow(window, false, false),
|
|
1285
2279
|
resolvedSqlitePath.suffixResolution,
|
|
1286
2280
|
);
|
|
1287
|
-
|
|
1288
|
-
details.truncation = truncation.truncated ? truncation : undefined;
|
|
2281
|
+
details.truncation = window.kind !== "full" ? makeWindowResult(window, body) : undefined;
|
|
1289
2282
|
const resultBuilder = toolResult<ReadToolDetails>(details)
|
|
1290
|
-
.text(
|
|
2283
|
+
.text(rendered)
|
|
1291
2284
|
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
1292
2285
|
.limits({ resultLimit: listLimit.meta.resultLimit?.reached });
|
|
1293
|
-
if (
|
|
1294
|
-
|
|
2286
|
+
if (window.kind !== "full") {
|
|
2287
|
+
if (direction === "head") {
|
|
2288
|
+
resultBuilder.truncation(makeWindowResult(window, body), { direction: "head" });
|
|
2289
|
+
} else {
|
|
2290
|
+
resultBuilder.truncationWindows(window, { maxBytes: DEFAULT_MAX_BYTES });
|
|
2291
|
+
}
|
|
1295
2292
|
}
|
|
1296
2293
|
return resultBuilder.done();
|
|
1297
2294
|
}
|
|
@@ -1553,6 +2550,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1553
2550
|
"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.",
|
|
1554
2551
|
);
|
|
1555
2552
|
}
|
|
2553
|
+
if (params.truncation !== undefined && params.truncation !== "head") {
|
|
2554
|
+
throw new ToolError("Explicit truncation for conflict:// regions is not yet supported.");
|
|
2555
|
+
}
|
|
1556
2556
|
return this.#readConflictRegion(conflictUri.id, conflictUri.scope);
|
|
1557
2557
|
}
|
|
1558
2558
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
@@ -1572,15 +2572,36 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1572
2572
|
preferCached: true,
|
|
1573
2573
|
},
|
|
1574
2574
|
);
|
|
1575
|
-
|
|
2575
|
+
const requestedDirection = resolveEffectiveDirection(
|
|
2576
|
+
params.truncation,
|
|
2577
|
+
"url-cache-page",
|
|
2578
|
+
this.session.settings,
|
|
2579
|
+
);
|
|
2580
|
+
const direction =
|
|
2581
|
+
requestedDirection !== "head" && cached.wrappedPreambleChars === undefined ? "head" : requestedDirection;
|
|
2582
|
+
const selectorInput = prepareReadUrlSelectorInput(
|
|
2583
|
+
cached.output,
|
|
2584
|
+
cached.preambleChars,
|
|
2585
|
+
cached.wrappedPreambleChars,
|
|
2586
|
+
);
|
|
2587
|
+
|
|
2588
|
+
return this.#buildInMemoryTextResult(selectorInput.text, parsedUrlTarget.offset, parsedUrlTarget.limit, {
|
|
1576
2589
|
details: { ...cached.details },
|
|
1577
2590
|
sourceUrl: cached.details.finalUrl,
|
|
1578
2591
|
entityLabel: "URL output",
|
|
1579
2592
|
immutable: true,
|
|
1580
2593
|
wrapUntrusted: true,
|
|
2594
|
+
truncationDirection: direction,
|
|
2595
|
+
protectedPrefixChars: selectorInput.preambleChars,
|
|
2596
|
+
truncationMetadataBodyRelative: true,
|
|
1581
2597
|
});
|
|
1582
2598
|
}
|
|
1583
|
-
return executeReadUrl(
|
|
2599
|
+
return executeReadUrl(
|
|
2600
|
+
this.session,
|
|
2601
|
+
{ path: parsedUrlTarget.path, raw: parsedUrlTarget.raw },
|
|
2602
|
+
signal,
|
|
2603
|
+
resolveEffectiveDirection(params.truncation, "url-reader", this.session.settings),
|
|
2604
|
+
);
|
|
1584
2605
|
}
|
|
1585
2606
|
|
|
1586
2607
|
// Handle internal URLs (agent://, artifact://, memory://, rule://, local://, gjc://, issue://, pr://).
|
|
@@ -1595,7 +2616,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1595
2616
|
if (internalTarget.sel !== undefined && parsed.kind === "none") {
|
|
1596
2617
|
throw new ToolError(`Invalid internal URL selector "${internalTarget.sel}".`);
|
|
1597
2618
|
}
|
|
1598
|
-
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
2619
|
+
return this.#handleInternalUrl(internalTarget.path, parsed, signal, params.truncation);
|
|
1599
2620
|
}
|
|
1600
2621
|
|
|
1601
2622
|
const archivePath = await this.#resolveArchiveReadPath(readPath, signal);
|
|
@@ -1607,12 +2628,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1607
2628
|
archiveParsed,
|
|
1608
2629
|
{ ...archivePath, archiveSubPath: archiveSubPath.path },
|
|
1609
2630
|
signal,
|
|
2631
|
+
params.truncation,
|
|
1610
2632
|
);
|
|
1611
2633
|
}
|
|
1612
2634
|
|
|
1613
2635
|
const sqlitePath = await this.#resolveSqliteReadPath(readPath, signal);
|
|
1614
2636
|
if (sqlitePath) {
|
|
1615
|
-
return this.#readSqlite(sqlitePath, signal);
|
|
2637
|
+
return this.#readSqlite(sqlitePath, signal, params.truncation);
|
|
1616
2638
|
}
|
|
1617
2639
|
|
|
1618
2640
|
const localTarget = splitPathAndSel(readPath);
|
|
@@ -1658,7 +2680,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1658
2680
|
if (isMultiRange(parsed)) {
|
|
1659
2681
|
throw new ToolError("Multi-range line selectors are not supported for directory listings.");
|
|
1660
2682
|
}
|
|
1661
|
-
const
|
|
2683
|
+
const dirDirection = resolveEffectiveDirection(params.truncation, "dir-local", this.session.settings);
|
|
2684
|
+
const dirResult = await this.#readDirectory(
|
|
2685
|
+
absolutePath,
|
|
2686
|
+
selToOffsetLimit(parsed).limit,
|
|
2687
|
+
dirDirection,
|
|
2688
|
+
signal,
|
|
2689
|
+
);
|
|
2690
|
+
|
|
1662
2691
|
if (suffixResolution) {
|
|
1663
2692
|
dirResult.details ??= {};
|
|
1664
2693
|
dirResult.details.suffixResolution = suffixResolution;
|
|
@@ -1667,7 +2696,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1667
2696
|
}
|
|
1668
2697
|
|
|
1669
2698
|
if (parsed.kind === "conflicts") {
|
|
1670
|
-
return this.#readFileConflicts(absolutePath, suffixResolution, signal);
|
|
2699
|
+
return this.#readFileConflicts(absolutePath, suffixResolution, signal, params.truncation);
|
|
1671
2700
|
}
|
|
1672
2701
|
|
|
1673
2702
|
const imageMetadata = await readImageMetadata(absolutePath);
|
|
@@ -1680,10 +2709,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1680
2709
|
let sourcePath: string | undefined;
|
|
1681
2710
|
let columnTruncated = 0;
|
|
1682
2711
|
let truncationInfo:
|
|
1683
|
-
| {
|
|
2712
|
+
| {
|
|
2713
|
+
result: TruncationResult;
|
|
2714
|
+
options: {
|
|
2715
|
+
direction: "head";
|
|
2716
|
+
startLine?: number;
|
|
2717
|
+
totalFileLines?: number;
|
|
2718
|
+
noticeOwner?: "body";
|
|
2719
|
+
maxBytes?: number;
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
1684
2722
|
| undefined;
|
|
2723
|
+
let directionalWindow: ReadWindow | undefined;
|
|
2724
|
+
let directionalNoticeOwner: "body" | undefined;
|
|
2725
|
+
let directionalRangeBase: "file" | "window" | undefined;
|
|
1685
2726
|
|
|
1686
2727
|
if (mimeType) {
|
|
2728
|
+
if (params.truncation !== undefined && params.truncation !== "head") {
|
|
2729
|
+
throw new ToolError("Explicit truncation for image reads is not yet supported.");
|
|
2730
|
+
}
|
|
1687
2731
|
if (fileSize > MAX_IMAGE_SIZE) {
|
|
1688
2732
|
const sizeStr = formatBytes(fileSize);
|
|
1689
2733
|
const maxStr = formatBytes(MAX_IMAGE_SIZE);
|
|
@@ -1715,11 +2759,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1715
2759
|
}
|
|
1716
2760
|
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
1717
2761
|
const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
|
|
2762
|
+
const direction = resolveEffectiveDirection(params.truncation, "converted", this.session.settings);
|
|
1718
2763
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
1719
2764
|
return this.#buildInMemoryMultiRangeResult(notebookText, parsed.ranges, {
|
|
1720
2765
|
details: { resolvedPath: absolutePath },
|
|
1721
2766
|
sourcePath: absolutePath,
|
|
1722
2767
|
entityLabel: "notebook",
|
|
2768
|
+
truncationDirection: direction,
|
|
1723
2769
|
});
|
|
1724
2770
|
}
|
|
1725
2771
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
@@ -1736,10 +2782,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1736
2782
|
)
|
|
1737
2783
|
: undefined,
|
|
1738
2784
|
receiptPath: parsed.kind === "none" ? localReadPath : undefined,
|
|
2785
|
+
truncationDirection: direction,
|
|
1739
2786
|
});
|
|
1740
2787
|
} else if (shouldConvertWithMarkit) {
|
|
1741
2788
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1742
2789
|
if (result.ok) {
|
|
2790
|
+
const direction = resolveEffectiveDirection(params.truncation, "converted", this.session.settings);
|
|
1743
2791
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1744
2792
|
return this.#buildInMemoryTextResult(result.content, offset, limit, {
|
|
1745
2793
|
details: { resolvedPath: absolutePath },
|
|
@@ -1755,6 +2803,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1755
2803
|
)
|
|
1756
2804
|
: undefined,
|
|
1757
2805
|
receiptPath: parsed.kind === "none" ? localReadPath : undefined,
|
|
2806
|
+
truncationDirection: direction,
|
|
1758
2807
|
});
|
|
1759
2808
|
}
|
|
1760
2809
|
content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
|
|
@@ -1766,6 +2815,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1766
2815
|
(this.session.settings.get("read.summarize.prose") || !PROSE_SUMMARY_EXTENSIONS.has(ext))
|
|
1767
2816
|
) {
|
|
1768
2817
|
const summary = await this.#trySummarize(absolutePath, fileSize, signal);
|
|
2818
|
+
if (params.truncation !== undefined && params.truncation !== "head" && summary?.parsed) {
|
|
2819
|
+
throw new ToolError(
|
|
2820
|
+
"Explicit truncation for structural summaries is not yet supported; summary direction will be applied after summary units become directional.",
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
1769
2823
|
if (summary?.parsed && summary.elided) {
|
|
1770
2824
|
const renderedSummary = this.#renderSummary(summary);
|
|
1771
2825
|
const footer = formatSummaryElisionFooter(
|
|
@@ -1804,6 +2858,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1804
2858
|
|
|
1805
2859
|
if (!content) {
|
|
1806
2860
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
2861
|
+
const multiDirection = resolveEffectiveDirection(
|
|
2862
|
+
params.truncation,
|
|
2863
|
+
"local-multi-range",
|
|
2864
|
+
this.session.settings,
|
|
2865
|
+
);
|
|
1807
2866
|
const multiResult = await this.#readLocalFileMultiRange(
|
|
1808
2867
|
absolutePath,
|
|
1809
2868
|
parsed.ranges,
|
|
@@ -1811,6 +2870,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1811
2870
|
displayMode,
|
|
1812
2871
|
suffixResolution,
|
|
1813
2872
|
signal,
|
|
2873
|
+
multiDirection,
|
|
1814
2874
|
);
|
|
1815
2875
|
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
1816
2876
|
content = [{ type: "text", text: multiResult.outputText }];
|
|
@@ -1822,6 +2882,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1822
2882
|
} else {
|
|
1823
2883
|
// Raw text or line-range mode
|
|
1824
2884
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
2885
|
+
const bareEligible = parsed.kind === "none";
|
|
2886
|
+
const localDirection = resolveEffectiveDirection(
|
|
2887
|
+
params.truncation,
|
|
2888
|
+
bareEligible ? "local-bare-stream" : isRawSelector(parsed) ? "local-raw" : "local-range",
|
|
2889
|
+
this.session.settings,
|
|
2890
|
+
);
|
|
1825
2891
|
// Try ACP bridge first — editor's in-memory buffer is source of truth.
|
|
1826
2892
|
// Request full text so local range rendering keeps normal context and line numbers.
|
|
1827
2893
|
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
@@ -1833,6 +2899,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1833
2899
|
sourcePath: absolutePath,
|
|
1834
2900
|
entityLabel: "file",
|
|
1835
2901
|
raw: isRawSelector(parsed),
|
|
2902
|
+
cacheLinesFor: absolutePath,
|
|
2903
|
+
truncationDirection: localDirection,
|
|
1836
2904
|
});
|
|
1837
2905
|
if (suffixResolution) {
|
|
1838
2906
|
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
@@ -1841,6 +2909,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1841
2909
|
}
|
|
1842
2910
|
return bridgeResult;
|
|
1843
2911
|
} catch (error) {
|
|
2912
|
+
if (isClientAuthorityDenial(error)) throw error;
|
|
1844
2913
|
logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
|
|
1845
2914
|
}
|
|
1846
2915
|
}
|
|
@@ -1874,6 +2943,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1874
2943
|
: isBareReceipt
|
|
1875
2944
|
? receiptBudgetBytes
|
|
1876
2945
|
: Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
2946
|
+
const collectMode = localDirection === "head" ? "head" : localDirection === "last" ? "tail" : "both";
|
|
2947
|
+
const collectEndExclusive =
|
|
2948
|
+
rawSelector || isBareReceipt ? null : startLine + effectiveLimit + leadingContext + trailingContext;
|
|
1877
2949
|
|
|
1878
2950
|
const streamResult = await streamLinesFromFile(
|
|
1879
2951
|
absolutePath,
|
|
@@ -1882,6 +2954,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1882
2954
|
maxBytesForRead,
|
|
1883
2955
|
selectedLineLimit,
|
|
1884
2956
|
signal,
|
|
2957
|
+
collectMode,
|
|
2958
|
+
collectEndExclusive,
|
|
1885
2959
|
);
|
|
1886
2960
|
|
|
1887
2961
|
const {
|
|
@@ -1906,148 +2980,295 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1906
2980
|
.done();
|
|
1907
2981
|
}
|
|
1908
2982
|
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2983
|
+
if (collectMode !== "head") {
|
|
2984
|
+
const window = streamResultWindow(streamResult, collectMode, startLine, collectEndExclusive);
|
|
2985
|
+
const metadataWindow = rebaseWindow(window, -startLine);
|
|
2986
|
+
directionalWindow = metadataWindow;
|
|
2987
|
+
directionalNoticeOwner = isBareReceipt ? "body" : undefined;
|
|
2988
|
+
directionalRangeBase = offset !== undefined && localDirection !== "head" ? "window" : undefined;
|
|
2989
|
+
const directionalPartial = windowHasPartialSegment(window);
|
|
2990
|
+
const shouldAddDirectionalHashLines = !rawSelector && displayMode.hashLines;
|
|
2991
|
+
const shouldAddDirectionalLineNumbers = rawSelector
|
|
2992
|
+
? false
|
|
2993
|
+
: !shouldAddDirectionalHashLines && displayMode.lineNumbers;
|
|
2994
|
+
const directionalMaxColumns = rawSelector ? 0 : resolveOutputMaxColumns(this.session.settings);
|
|
2995
|
+
if (!rawSelector && !shouldAddDirectionalHashLines && directionalMaxColumns > 0) {
|
|
2996
|
+
for (const segment of windowSegments(window)) {
|
|
2997
|
+
if (
|
|
2998
|
+
segment.content
|
|
2999
|
+
.split("\n")
|
|
3000
|
+
.some(line => truncateLine(line, directionalMaxColumns).wasTruncated)
|
|
3001
|
+
) {
|
|
3002
|
+
columnTruncated = directionalMaxColumns;
|
|
3003
|
+
break;
|
|
3004
|
+
}
|
|
1922
3005
|
}
|
|
1923
3006
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
const truncation: TruncationResult = {
|
|
1935
|
-
content: selectedContent,
|
|
1936
|
-
truncated: wasTruncated,
|
|
1937
|
-
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
1938
|
-
totalLines: totalSelectedLines,
|
|
1939
|
-
totalBytes: totalSelectedBytes,
|
|
1940
|
-
outputLines: collectedLines.length,
|
|
1941
|
-
outputBytes: collectedBytes,
|
|
1942
|
-
lastLinePartial: false,
|
|
1943
|
-
firstLineExceedsLimit,
|
|
1944
|
-
};
|
|
1945
|
-
|
|
1946
|
-
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1947
|
-
getFileReadCache(this.session).recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1951
|
-
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
1952
|
-
const formatText = (text: string, startNum: number): string => {
|
|
1953
|
-
capturedDisplayContent = { text, startLine: startNum };
|
|
1954
|
-
return formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1955
|
-
};
|
|
1956
|
-
|
|
1957
|
-
let outputText: string;
|
|
1958
|
-
|
|
1959
|
-
if (truncation.firstLineExceedsLimit) {
|
|
1960
|
-
const firstLineBytes = firstLineByteLength ?? 0;
|
|
1961
|
-
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
1962
|
-
|
|
1963
|
-
if (shouldAddHashLines) {
|
|
1964
|
-
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1965
|
-
firstLineBytes,
|
|
1966
|
-
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
1967
|
-
} else {
|
|
1968
|
-
outputText = formatText(snippet.text, startLineDisplay);
|
|
3007
|
+
const selectedContent = windowSegments(window)
|
|
3008
|
+
.map(segment => segment.content)
|
|
3009
|
+
.join("\n");
|
|
3010
|
+
const partialTail = partialTailOf(window);
|
|
3011
|
+
const partialTailWarning = partialTail !== undefined && shouldAddDirectionalHashLines;
|
|
3012
|
+
if (partialTailWarning) {
|
|
3013
|
+
Object.defineProperty(window, "outputLinesOverride", { value: 0, enumerable: false });
|
|
3014
|
+
Object.defineProperty(window, "outputBytesOverride", { value: 0, enumerable: false });
|
|
3015
|
+
Object.defineProperty(metadataWindow, "outputLinesOverride", { value: 0, enumerable: false });
|
|
3016
|
+
Object.defineProperty(metadataWindow, "outputBytesOverride", { value: 0, enumerable: false });
|
|
1969
3017
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
3018
|
+
const baseWindowResult =
|
|
3019
|
+
window.kind === "full" ? undefined : makeWindowResult(metadataWindow, selectedContent);
|
|
3020
|
+
const windowResult =
|
|
3021
|
+
baseWindowResult && partialTailWarning
|
|
3022
|
+
? { ...baseWindowResult, outputLines: 0, outputBytes: 0, lastLineExceedsLimit: true }
|
|
3023
|
+
: baseWindowResult;
|
|
3024
|
+
const rawWindow = renderReadWindow(window, false, false, directionalMaxColumns);
|
|
3025
|
+
let outputText = partialTailWarning
|
|
3026
|
+
? formatOversizedLineWarning(
|
|
3027
|
+
localReadPath,
|
|
3028
|
+
partialTail.origin.startLine,
|
|
3029
|
+
partialTail.sourceLineBytes,
|
|
3030
|
+
maxBytesForRead,
|
|
3031
|
+
totalFileLines,
|
|
3032
|
+
)
|
|
3033
|
+
: renderReadWindow(
|
|
3034
|
+
window,
|
|
3035
|
+
shouldAddDirectionalHashLines,
|
|
3036
|
+
shouldAddDirectionalLineNumbers,
|
|
3037
|
+
directionalMaxColumns,
|
|
3038
|
+
);
|
|
3039
|
+
const windowEnd = Math.min(collectEndExclusive ?? totalFileLines, totalFileLines);
|
|
3040
|
+
let bodyFooter: string | undefined;
|
|
3041
|
+
if (isBareReceipt && window.kind !== "full" && !partialTailWarning) {
|
|
3042
|
+
const footer = formatDirectionalFooter(localReadPath, metadataWindow);
|
|
3043
|
+
if (footer) {
|
|
3044
|
+
outputText += `\n\n${footer}`;
|
|
3045
|
+
bodyFooter = footer;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (rawSelector && window.kind !== "full") {
|
|
3049
|
+
outputText += `\n\n${formatRawReadCapNotice(maxBytesForRead, fileSize, localDirection)}`;
|
|
3050
|
+
}
|
|
3051
|
+
if (
|
|
3052
|
+
!isBareReceipt &&
|
|
3053
|
+
window.kind === "full" &&
|
|
3054
|
+
limit !== undefined &&
|
|
3055
|
+
startLine < windowEnd &&
|
|
3056
|
+
windowEnd < totalFileLines
|
|
3057
|
+
) {
|
|
3058
|
+
const remaining = totalFileLines - windowEnd;
|
|
3059
|
+
outputText += `\n\n[${remaining} more lines in file. Use :${windowEnd + 1} to continue]`;
|
|
3060
|
+
}
|
|
3061
|
+
details = {
|
|
3062
|
+
resolvedPath: absolutePath,
|
|
3063
|
+
suffixResolution,
|
|
3064
|
+
displayContent: {
|
|
3065
|
+
text: rawWindow,
|
|
3066
|
+
startLine: window.head?.origin.startLine ?? window.tail?.origin.startLine ?? startLineDisplay,
|
|
3067
|
+
},
|
|
3068
|
+
...(windowResult ? { truncation: windowResult } : {}),
|
|
3069
|
+
spillEligible: !isBareReceipt && selectedContent.length > 0 && !directionalPartial,
|
|
3070
|
+
};
|
|
3071
|
+
if (bodyFooter) rememberBodyTruncationFooter(details, bodyFooter);
|
|
3072
|
+
if (!directionalPartial) {
|
|
3073
|
+
for (const segment of windowSegments(window)) {
|
|
3074
|
+
if (segment.kind === "lines" && segment.lines > 0) {
|
|
3075
|
+
getFileReadCache(this.session).recordContiguous(
|
|
3076
|
+
absolutePath,
|
|
3077
|
+
segment.origin.startLine,
|
|
3078
|
+
linesOfSegment(segment),
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
if (!partialTailWarning) {
|
|
3084
|
+
const conflictBlocks = windowSegments(window).flatMap(segment =>
|
|
3085
|
+
segment.kind === "lines"
|
|
3086
|
+
? scanConflictLines(linesOfSegment(segment), segment.origin.startLine)
|
|
3087
|
+
: [],
|
|
3088
|
+
);
|
|
3089
|
+
if (conflictBlocks.length > 0) {
|
|
3090
|
+
const history = getConflictHistory(this.session);
|
|
3091
|
+
const displayPathForWarning = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
3092
|
+
const entries = conflictBlocks.map(block =>
|
|
3093
|
+
history.register({
|
|
3094
|
+
absolutePath,
|
|
3095
|
+
displayPath: displayPathForWarning,
|
|
3096
|
+
...block,
|
|
3097
|
+
}),
|
|
3098
|
+
);
|
|
3099
|
+
// Cheap full-file scan only when the window already showed
|
|
3100
|
+
// at least one conflict — otherwise pay nothing on clean files.
|
|
3101
|
+
let totalInFile = entries.length;
|
|
3102
|
+
let scanTruncated = false;
|
|
3103
|
+
try {
|
|
3104
|
+
const fileScan = await scanFileForConflicts(absolutePath);
|
|
3105
|
+
totalInFile = Math.max(entries.length, fileScan.blocks.length);
|
|
3106
|
+
scanTruncated = fileScan.scanTruncated;
|
|
3107
|
+
} catch {
|
|
3108
|
+
// Best-effort enrichment; fall back to window-only count.
|
|
3109
|
+
}
|
|
3110
|
+
outputText += formatConflictWarning(entries, {
|
|
3111
|
+
totalInFile,
|
|
3112
|
+
displayPath: displayPathForWarning,
|
|
3113
|
+
scanTruncated,
|
|
3114
|
+
});
|
|
3115
|
+
details.conflictCount = entries.length;
|
|
3116
|
+
}
|
|
1974
3117
|
}
|
|
1975
|
-
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1976
|
-
|
|
1977
3118
|
sourcePath = absolutePath;
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
3119
|
+
content = [{ type: "text", text: outputText }];
|
|
3120
|
+
} else {
|
|
3121
|
+
// Per-line column cap. Skipped in raw mode so `:raw` always returns
|
|
3122
|
+
// verbatim bytes for paste-back-into-tool workflows. Total byte/line
|
|
3123
|
+
// counts in `truncation` keep reflecting the source, not the trimmed
|
|
3124
|
+
// view — column truncation surfaces separately via `.limits()`.
|
|
3125
|
+
|
|
3126
|
+
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
3127
|
+
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
3128
|
+
if (!rawSelector && !shouldAddHashLines && maxColumns > 0) {
|
|
3129
|
+
for (let i = 0; i < collectedLines.length; i++) {
|
|
3130
|
+
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
3131
|
+
if (wasTruncated) {
|
|
3132
|
+
collectedLines[i] = text;
|
|
3133
|
+
columnTruncated = maxColumns;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
const selectedContent = collectedLines.join("\n");
|
|
3139
|
+
const userLimitedLines = collectedLines.length;
|
|
3140
|
+
|
|
3141
|
+
const totalSelectedLines = totalFileLines - startLine;
|
|
3142
|
+
const totalSelectedBytes = collectedBytes;
|
|
3143
|
+
const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
|
|
3144
|
+
const firstLineExceedsLimit =
|
|
3145
|
+
firstLineByteLength !== undefined && firstLineByteLength > maxBytesForRead;
|
|
3146
|
+
|
|
3147
|
+
const truncation: TruncationResult = {
|
|
3148
|
+
content: selectedContent,
|
|
3149
|
+
truncated: wasTruncated,
|
|
3150
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
3151
|
+
totalLines: totalSelectedLines,
|
|
3152
|
+
totalBytes: totalSelectedBytes,
|
|
3153
|
+
outputLines: collectedLines.length,
|
|
3154
|
+
outputBytes: collectedBytes,
|
|
3155
|
+
lastLinePartial: false,
|
|
3156
|
+
firstLineExceedsLimit,
|
|
1981
3157
|
};
|
|
1982
|
-
} else if (truncation.truncated) {
|
|
1983
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1984
|
-
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1985
3158
|
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
3159
|
+
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
3160
|
+
getFileReadCache(this.session).recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
const shouldAddLineNumbers = rawSelector
|
|
3164
|
+
? false
|
|
3165
|
+
: shouldAddHashLines
|
|
3166
|
+
? false
|
|
3167
|
+
: displayMode.lineNumbers;
|
|
3168
|
+
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
3169
|
+
const formatText = (text: string, startNum: number): string => {
|
|
3170
|
+
capturedDisplayContent = { text, startLine: startNum };
|
|
3171
|
+
return formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1990
3172
|
};
|
|
1991
|
-
} else if (startLine + userLimitedLines < totalFileLines) {
|
|
1992
|
-
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
1993
|
-
const nextOffset = startLine + userLimitedLines + 1;
|
|
1994
3173
|
|
|
1995
|
-
outputText
|
|
1996
|
-
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
1997
|
-
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3174
|
+
let outputText: string;
|
|
1998
3175
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
2003
|
-
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3176
|
+
if (truncation.firstLineExceedsLimit) {
|
|
3177
|
+
const firstLineBytes = firstLineByteLength ?? 0;
|
|
3178
|
+
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
2004
3179
|
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
3180
|
+
if (shouldAddHashLines) {
|
|
3181
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
3182
|
+
firstLineBytes,
|
|
3183
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
3184
|
+
} else {
|
|
3185
|
+
outputText = formatText(snippet.text, startLineDisplay);
|
|
3186
|
+
}
|
|
3187
|
+
if (snippet.text.length === 0) {
|
|
3188
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
3189
|
+
firstLineBytes,
|
|
3190
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
3191
|
+
}
|
|
3192
|
+
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3193
|
+
|
|
3194
|
+
sourcePath = absolutePath;
|
|
3195
|
+
truncationInfo = {
|
|
3196
|
+
result: truncation,
|
|
3197
|
+
// Head receipts keep the wrapper notice: it carries the `:N to
|
|
3198
|
+
// continue` hint, which the body footer does not duplicate.
|
|
3199
|
+
// Only directional windows transfer notice ownership to the body.
|
|
3200
|
+
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
3201
|
+
};
|
|
3202
|
+
} else if (truncation.truncated) {
|
|
3203
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
3204
|
+
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3205
|
+
|
|
3206
|
+
sourcePath = absolutePath;
|
|
3207
|
+
truncationInfo = {
|
|
3208
|
+
result: truncation,
|
|
3209
|
+
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
3210
|
+
};
|
|
3211
|
+
} else if (startLine + userLimitedLines < totalFileLines) {
|
|
3212
|
+
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
3213
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
3214
|
+
|
|
3215
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
3216
|
+
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
3217
|
+
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3218
|
+
|
|
3219
|
+
sourcePath = absolutePath;
|
|
3220
|
+
} else {
|
|
3221
|
+
// No truncation, no user limit exceeded
|
|
3222
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
3223
|
+
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
2013
3224
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
3225
|
+
sourcePath = absolutePath;
|
|
3226
|
+
}
|
|
3227
|
+
if (isBareReceipt && (truncation.truncated || collectedLines.length < totalFileLines)) {
|
|
3228
|
+
outputText += `\n\n${formatReceiptFooter(localReadPath, collectedLines.length, totalFileLines, collectedBytes)}`;
|
|
3229
|
+
}
|
|
3230
|
+
if (rawSelector && truncation.truncated) {
|
|
3231
|
+
outputText += `\n\n${formatRawReadCapNotice(maxBytesForRead, fileSize, localDirection)}`;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
if (capturedDisplayContent) {
|
|
3235
|
+
details.displayContent = capturedDisplayContent;
|
|
3236
|
+
}
|
|
2017
3237
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
3238
|
+
if (!firstLineExceedsLimit && collectedLines.length > 0) {
|
|
3239
|
+
const blocks = scanConflictLines(collectedLines, startLineDisplay);
|
|
3240
|
+
if (blocks.length > 0) {
|
|
3241
|
+
const history = getConflictHistory(this.session);
|
|
3242
|
+
const displayPathForWarning = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
3243
|
+
const entries = blocks.map(block =>
|
|
3244
|
+
history.register({
|
|
3245
|
+
absolutePath,
|
|
3246
|
+
displayPath: displayPathForWarning,
|
|
3247
|
+
...block,
|
|
3248
|
+
}),
|
|
3249
|
+
);
|
|
3250
|
+
// Cheap full-file scan only when the window already showed
|
|
3251
|
+
// at least one conflict — otherwise pay nothing on clean files.
|
|
3252
|
+
let totalInFile = entries.length;
|
|
3253
|
+
let scanTruncated = false;
|
|
3254
|
+
try {
|
|
3255
|
+
const fileScan = await scanFileForConflicts(absolutePath);
|
|
3256
|
+
totalInFile = Math.max(entries.length, fileScan.blocks.length);
|
|
3257
|
+
scanTruncated = fileScan.scanTruncated;
|
|
3258
|
+
} catch {
|
|
3259
|
+
// Best-effort enrichment; fall back to window-only count.
|
|
3260
|
+
}
|
|
3261
|
+
outputText += formatConflictWarning(entries, {
|
|
3262
|
+
totalInFile,
|
|
2026
3263
|
displayPath: displayPathForWarning,
|
|
2027
|
-
|
|
2028
|
-
})
|
|
2029
|
-
|
|
2030
|
-
// Cheap full-file scan only when the window already showed
|
|
2031
|
-
// at least one conflict — otherwise pay nothing on clean files.
|
|
2032
|
-
let totalInFile = entries.length;
|
|
2033
|
-
let scanTruncated = false;
|
|
2034
|
-
try {
|
|
2035
|
-
const fileScan = await scanFileForConflicts(absolutePath);
|
|
2036
|
-
totalInFile = Math.max(entries.length, fileScan.blocks.length);
|
|
2037
|
-
scanTruncated = fileScan.scanTruncated;
|
|
2038
|
-
} catch {
|
|
2039
|
-
// Best-effort enrichment; fall back to window-only count.
|
|
3264
|
+
scanTruncated,
|
|
3265
|
+
});
|
|
3266
|
+
details.conflictCount = entries.length;
|
|
2040
3267
|
}
|
|
2041
|
-
outputText += formatConflictWarning(entries, {
|
|
2042
|
-
totalInFile,
|
|
2043
|
-
displayPath: displayPathForWarning,
|
|
2044
|
-
scanTruncated,
|
|
2045
|
-
});
|
|
2046
|
-
details.conflictCount = entries.length;
|
|
2047
3268
|
}
|
|
2048
|
-
}
|
|
2049
3269
|
|
|
2050
|
-
|
|
3270
|
+
content = [{ type: "text", text: outputText }];
|
|
3271
|
+
}
|
|
2051
3272
|
}
|
|
2052
3273
|
}
|
|
2053
3274
|
}
|
|
@@ -2070,6 +3291,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2070
3291
|
if (truncationInfo) {
|
|
2071
3292
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
2072
3293
|
}
|
|
3294
|
+
if (directionalWindow) {
|
|
3295
|
+
resultBuilder.truncationWindows(directionalWindow, {
|
|
3296
|
+
noticeOwner: directionalNoticeOwner,
|
|
3297
|
+
rangeBase: directionalRangeBase,
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
2073
3300
|
if (columnTruncated > 0) {
|
|
2074
3301
|
resultBuilder.limits({ columnMax: columnTruncated });
|
|
2075
3302
|
}
|
|
@@ -2115,8 +3342,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2115
3342
|
absolutePath: string,
|
|
2116
3343
|
suffixResolution: { from: string; to: string } | undefined,
|
|
2117
3344
|
signal: AbortSignal | undefined,
|
|
3345
|
+
truncationParam?: TruncationDirection,
|
|
2118
3346
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2119
3347
|
throwIfAborted(signal);
|
|
3348
|
+
if (truncationParam !== undefined && truncationParam !== "head") {
|
|
3349
|
+
throw new ToolError(
|
|
3350
|
+
"Explicit truncation for :conflicts indexes is not yet supported; the conflict preamble and bulk-resolve notice will be preserved when a structural index window is added.",
|
|
3351
|
+
);
|
|
3352
|
+
}
|
|
3353
|
+
|
|
2120
3354
|
const scan = await scanFileForConflicts(absolutePath);
|
|
2121
3355
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
2122
3356
|
const history = getConflictHistory(this.session);
|
|
@@ -2149,6 +3383,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2149
3383
|
url: string,
|
|
2150
3384
|
parsedSel: ParsedSelector,
|
|
2151
3385
|
signal?: AbortSignal,
|
|
3386
|
+
truncationParam?: TruncationDirection,
|
|
2152
3387
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2153
3388
|
const internalRouter = InternalUrlRouter.instance();
|
|
2154
3389
|
|
|
@@ -2178,6 +3413,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2178
3413
|
const resource = await internalRouter.resolve(url, {
|
|
2179
3414
|
cwd: this.session.cwd,
|
|
2180
3415
|
getArtifactsDir: this.session.getArtifactsDir,
|
|
3416
|
+
getAuthorizedArtifactsDirs: this.session.getAuthorizedArtifactsDirs,
|
|
2181
3417
|
settings: this.session.settings,
|
|
2182
3418
|
signal,
|
|
2183
3419
|
});
|
|
@@ -2185,8 +3421,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2185
3421
|
|
|
2186
3422
|
// If extraction was used, return directly (no pagination)
|
|
2187
3423
|
if (hasExtraction) {
|
|
3424
|
+
if (truncationParam !== undefined && truncationParam !== "head") {
|
|
3425
|
+
throw new ToolError("Explicit truncation for internal URL query extraction is not yet supported.");
|
|
3426
|
+
}
|
|
2188
3427
|
return toolResult(details).text(resource.content).sourceInternal(url).done();
|
|
2189
3428
|
}
|
|
3429
|
+
const direction = resolveEffectiveDirection(truncationParam, "converted", this.session.settings);
|
|
2190
3430
|
|
|
2191
3431
|
const raw = isRawSelector(parsedSel);
|
|
2192
3432
|
if (isMultiRange(parsedSel) && parsedSel.kind === "lines") {
|
|
@@ -2197,6 +3437,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2197
3437
|
entityLabel: "resource",
|
|
2198
3438
|
immutable: resource.immutable,
|
|
2199
3439
|
raw,
|
|
3440
|
+
truncationDirection: direction,
|
|
2200
3441
|
});
|
|
2201
3442
|
}
|
|
2202
3443
|
|
|
@@ -2209,6 +3450,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2209
3450
|
ignoreResultLimits: scheme === "skill",
|
|
2210
3451
|
immutable: resource.immutable,
|
|
2211
3452
|
raw,
|
|
3453
|
+
truncationDirection: direction,
|
|
3454
|
+
bodyFooterPath: parsedSel.kind === "none" && !raw ? url : undefined,
|
|
2212
3455
|
});
|
|
2213
3456
|
}
|
|
2214
3457
|
|
|
@@ -2216,6 +3459,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2216
3459
|
async #readDirectory(
|
|
2217
3460
|
absolutePath: string,
|
|
2218
3461
|
limit: number | undefined,
|
|
3462
|
+
direction: TruncationDirection,
|
|
2219
3463
|
signal?: AbortSignal,
|
|
2220
3464
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2221
3465
|
const READ_DIRECTORY_MAX_DEPTH = 2;
|
|
@@ -2239,20 +3483,26 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2239
3483
|
const receiptLines = this.session.settings.get("read.receiptBudgetLines");
|
|
2240
3484
|
const receiptBytes = this.session.settings.get("read.receiptBudgetBytes") * 1024;
|
|
2241
3485
|
const sourceLines = tree.totalLines <= 1 ? ["(empty directory)"] : tree.rendered.split("\n");
|
|
2242
|
-
const
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
3486
|
+
const source = sourceLines.join("\n");
|
|
3487
|
+
const window = selectReadWindow(source, sourceLines, direction, receiptLines, receiptBytes);
|
|
3488
|
+
const shownLines = windowOutputLines(window);
|
|
3489
|
+
const shownSegments = windowSegments(window);
|
|
3490
|
+
const shownBytes =
|
|
3491
|
+
shownSegments.reduce((sum, segment) => sum + segment.bytes, 0) + Math.max(0, shownSegments.length - 1);
|
|
3492
|
+
let output = renderReadWindow(window, false, false);
|
|
3493
|
+
if (window.kind !== "full" || tree.truncated) {
|
|
3494
|
+
output += `\n\n${formatListingTruncationNotice(shownLines, shownBytes, absolutePath, direction)}`;
|
|
3495
|
+
}
|
|
3496
|
+
const details: ReadToolDetails = {
|
|
3497
|
+
isDirectory: true,
|
|
3498
|
+
spillEligible: false,
|
|
3499
|
+
resolvedPath: tree.rootPath,
|
|
3500
|
+
...(window.kind !== "full" && direction !== "head" ? { truncation: makeWindowResult(window, source) } : {}),
|
|
3501
|
+
};
|
|
3502
|
+
const resultBuilder = toolResult(details).text(output).sourcePath(tree.rootPath);
|
|
3503
|
+
if (window.kind !== "full" && direction !== "head")
|
|
3504
|
+
resultBuilder.truncationWindows(window, { noticeOwner: "body", maxBytes: receiptBytes });
|
|
3505
|
+
return resultBuilder.done();
|
|
2256
3506
|
}
|
|
2257
3507
|
}
|
|
2258
3508
|
|