@sayknow-cli/coding-agent 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +9 -1
- package/dist/types/commands/daemon.d.ts +2 -2
- 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/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-contract.d.ts +40 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +5 -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 +156 -10
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +33 -2
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +5 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +13 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
- package/dist/types/runtime-mcp/manager.d.ts +9 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +52 -4
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/query/handlers.d.ts +14 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +9 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +83 -24
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
- package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +89 -18
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/skc-runtime/state-writer.d.ts +5 -1
- package/dist/types/skc-runtime/team-launch.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
- package/dist/types/skc-runtime/team-store.d.ts +12 -0
- package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +133 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +22 -9
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +7 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -12
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/compile-args.ts +0 -1
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-docs-index.ts +4 -1
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +58 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +16 -100
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +6 -38
- package/src/cli/read-cli.ts +2 -1
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +133 -11
- package/src/commands/daemon.ts +22 -5
- package/src/commands/deep-interview.ts +26 -1
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +134 -17
- package/src/commands/team.ts +1 -1
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-contract.ts +174 -0
- package/src/config/model-profiles.ts +51 -10
- package/src/config/model-registry.ts +24 -4
- package/src/config/model-resolver.ts +2 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +157 -10
- package/src/config/settings.ts +254 -108
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
- package/src/defaults/skc/skills/team/SKILL.md +1 -1
- package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/eval/py/executor.ts +37 -13
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/extensibility/shared-events.ts +5 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/internal-urls/docs-index.generated.ts +17 -16
- package/src/internal-urls/local-protocol.ts +87 -25
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +120 -42
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +52 -0
- package/src/modes/acp/acp-agent.ts +647 -83
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/btw-panel.ts +82 -16
- 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 +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/sayknow-pet-widget.ts +20 -8
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +26 -8
- package/src/modes/components/welcome.ts +50 -23
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +101 -54
- package/src/modes/controllers/event-controller.ts +119 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +100 -8
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +201 -60
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +6 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +99 -4
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +17 -3
- package/src/prompts/agents/critic.md +17 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -3
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
- package/src/runtime-mcp/json-rpc.ts +28 -11
- package/src/runtime-mcp/manager.ts +77 -59
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +293 -43
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +22 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +10 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +793 -121
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +87 -25
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/prompt-reconciliation.ts +240 -0
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
- package/src/sdk/bus/telegram-daemon-control.ts +161 -40
- package/src/sdk/bus/telegram-daemon.ts +3649 -593
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +8 -2
- package/src/sdk/host/control/operations.ts +3 -2
- package/src/sdk/host/host.ts +27 -9
- package/src/sdk/host/query/handlers.ts +125 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +145 -0
- package/src/sdk/protocol/operation-registry.ts +24 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +69 -8
- package/src/sdk/startup-capability.ts +15 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1112 -314
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +1426 -259
- package/src/session/internal/managed-session-storage.ts +414 -238
- package/src/session/internal/native-publish-outcome.ts +271 -0
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2079 -469
- package/src/session/session-storage.ts +151 -11
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +49 -5
- package/src/setup/provider-presets.json +2 -1
- package/src/skc-runtime/deep-interview-runtime.ts +303 -36
- package/src/skc-runtime/deep-interview-stage.ts +1016 -0
- package/src/skc-runtime/deep-interview-state.ts +8 -5
- package/src/skc-runtime/gc-render.ts +1 -0
- package/src/skc-runtime/gc-runtime.ts +11 -1
- package/src/skc-runtime/linux-proc.ts +70 -36
- package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/skc-runtime/ralplan-runtime.ts +1057 -75
- package/src/skc-runtime/repository-binding.ts +267 -0
- package/src/skc-runtime/session-state-sidecar.ts +25 -9
- package/src/skc-runtime/state-runtime.ts +50 -12
- package/src/skc-runtime/state-writer.ts +68 -56
- package/src/skc-runtime/team-launch.ts +34 -5
- package/src/skc-runtime/team-runtime.ts +41 -1
- package/src/skc-runtime/team-store.ts +32 -0
- package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/skc-runtime/team-workers.ts +1 -0
- package/src/skc-runtime/tmux-common.ts +13 -2
- package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
- package/src/skc-runtime/tmux-sessions.ts +45 -6
- package/src/skc-runtime/ultragoal-guard.ts +93 -15
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
- package/src/skc-runtime/ultragoal-runtime.ts +469 -38
- package/src/skc-runtime/workflow-manifest.generated.json +195 -5
- package/src/skc-runtime/workflow-manifest.ts +67 -3
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +104 -12
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/executor.ts +165 -47
- package/src/task/index.ts +123 -15
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/receipt.ts +3 -0
- package/src/task/render.ts +45 -26
- package/src/task/skc-command.ts +2 -2
- package/src/task/types.ts +55 -10
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +127 -154
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +7 -1
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1544 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
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,8 +74,10 @@ 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 {
|
|
80
|
+
enforceSqliteQueryOnly,
|
|
76
81
|
executeReadQuery,
|
|
77
82
|
getRowByKey,
|
|
78
83
|
getRowByRowId,
|
|
@@ -98,6 +103,12 @@ const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
|
98
103
|
const MAX_SUMMARY_LINES = 20_000;
|
|
99
104
|
const RAW_COLLECTOR_MAX_BYTES = MAX_SUMMARY_BYTES;
|
|
100
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
|
+
|
|
101
112
|
/**
|
|
102
113
|
* Per-line column cap for file reads. Lines wider than the value of
|
|
103
114
|
* `tools.outputMaxColumns` are ellipsis-truncated at display time; the file
|
|
@@ -184,6 +195,259 @@ function countTextLines(text: string): number {
|
|
|
184
195
|
return text.split("\n").length;
|
|
185
196
|
}
|
|
186
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
|
+
|
|
187
451
|
/**
|
|
188
452
|
* Footer appended to summarized reads telling the model how to recover the
|
|
189
453
|
* elided body. Without this hint, agents either ignore the `...`/`{ .. }`
|
|
@@ -201,8 +465,15 @@ function formatReceiptFooter(readPath: string, shownLines: number, totalLines: n
|
|
|
201
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]`;
|
|
202
466
|
}
|
|
203
467
|
|
|
204
|
-
function formatSummaryCapFooter(
|
|
205
|
-
|
|
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]`;
|
|
206
477
|
}
|
|
207
478
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
208
479
|
|
|
@@ -240,13 +511,15 @@ function expandRangeWithContext(
|
|
|
240
511
|
};
|
|
241
512
|
}
|
|
242
513
|
|
|
243
|
-
async function streamLinesFromFile(
|
|
514
|
+
export async function streamLinesFromFile(
|
|
244
515
|
filePath: string,
|
|
245
516
|
startLine: number,
|
|
246
517
|
maxLinesToCollect: number,
|
|
247
518
|
maxBytes: number,
|
|
248
519
|
selectedLineLimit: number | null,
|
|
249
520
|
signal?: AbortSignal,
|
|
521
|
+
collect: "head" | "tail" | "both" = "head",
|
|
522
|
+
collectEndExclusive: number | null = null,
|
|
250
523
|
): Promise<{
|
|
251
524
|
lines: string[];
|
|
252
525
|
totalFileLines: number;
|
|
@@ -255,6 +528,22 @@ async function streamLinesFromFile(
|
|
|
255
528
|
firstLinePreview?: { text: string; bytes: number };
|
|
256
529
|
firstLineByteLength?: number;
|
|
257
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;
|
|
258
547
|
}> {
|
|
259
548
|
const bufferChunk = Buffer.allocUnsafe(READ_CHUNK_SIZE);
|
|
260
549
|
const collectedLines: string[] = [];
|
|
@@ -272,30 +561,72 @@ async function streamLinesFromFile(
|
|
|
272
561
|
let firstLineByteLength: number | undefined;
|
|
273
562
|
let selectedBytesTotal = 0;
|
|
274
563
|
let selectedLinesSeen = 0;
|
|
564
|
+
let windowBytesTotal = 0;
|
|
565
|
+
let windowLinesTotal = 0;
|
|
275
566
|
let captureLine = false;
|
|
276
567
|
let discardLineChunks = false;
|
|
277
568
|
let lineCaptureLimit = 0;
|
|
278
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
|
+
|
|
279
612
|
const setupLineState = () => {
|
|
280
|
-
captureLine = !doneCollecting && lineIndex >= startLine;
|
|
613
|
+
captureLine = collect === "head" && !doneCollecting && lineIndex >= startLine;
|
|
281
614
|
discardLineChunks = !captureLine;
|
|
282
615
|
if (captureLine) {
|
|
283
616
|
const separatorBytes = collectedLines.length > 0 ? 1 : 0;
|
|
284
617
|
lineCaptureLimit = maxBytes - collectedBytes - separatorBytes;
|
|
285
|
-
if (lineCaptureLimit <= 0)
|
|
286
|
-
discardLineChunks = true;
|
|
287
|
-
}
|
|
618
|
+
if (lineCaptureLimit <= 0) discardLineChunks = true;
|
|
288
619
|
} else {
|
|
289
620
|
lineCaptureLimit = 0;
|
|
290
621
|
}
|
|
291
622
|
};
|
|
292
623
|
|
|
293
624
|
const decodeLine = (): string => {
|
|
294
|
-
if (
|
|
295
|
-
if (currentLineChunks.length === 1 && currentLineChunks[0]?.length ===
|
|
625
|
+
if (currentLineKept === 0) return "";
|
|
626
|
+
if (currentLineChunks.length === 1 && currentLineChunks[0]?.length === currentLineKept) {
|
|
296
627
|
return currentLineChunks[0].toString("utf-8");
|
|
297
628
|
}
|
|
298
|
-
return Buffer.concat(currentLineChunks,
|
|
629
|
+
return Buffer.concat(currentLineChunks, currentLineKept).toString("utf-8");
|
|
299
630
|
};
|
|
300
631
|
|
|
301
632
|
const maybeCapturePreview = (segment: Uint8Array) => {
|
|
@@ -308,15 +639,123 @@ async function streamLinesFromFile(
|
|
|
308
639
|
firstLinePreviewBytes += slice.length;
|
|
309
640
|
};
|
|
310
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
|
+
|
|
311
657
|
const appendSegment = (segment: Uint8Array) => {
|
|
312
658
|
currentLineLength += segment.length;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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;
|
|
319
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;
|
|
745
|
+
}
|
|
746
|
+
};
|
|
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);
|
|
320
759
|
};
|
|
321
760
|
|
|
322
761
|
const finalizeLine = () => {
|
|
@@ -324,41 +763,65 @@ async function streamLinesFromFile(
|
|
|
324
763
|
selectedBytesTotal += currentLineLength + (selectedLinesSeen > 0 ? 1 : 0);
|
|
325
764
|
selectedLinesSeen++;
|
|
326
765
|
}
|
|
766
|
+
if (inWindow()) {
|
|
767
|
+
windowBytesTotal += currentLineLength + (windowLinesTotal > 0 ? 1 : 0);
|
|
768
|
+
windowLinesTotal++;
|
|
769
|
+
}
|
|
327
770
|
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
doneCollecting = true;
|
|
335
|
-
if (firstLineByteLength === undefined) {
|
|
336
|
-
firstLineByteLength = currentLineLength;
|
|
337
|
-
}
|
|
338
|
-
} else if (collectedLines.length > 0 && collectedBytes + separatorBytes + currentLineLength > maxBytes) {
|
|
339
|
-
stoppedByByteLimit = true;
|
|
340
|
-
doneCollecting = true;
|
|
341
|
-
} else {
|
|
342
|
-
const lineText = decodeLine();
|
|
343
|
-
collectedLines.push(lineText);
|
|
344
|
-
collectedBytes += separatorBytes + currentLineLength;
|
|
345
|
-
if (firstLineByteLength === undefined) {
|
|
346
|
-
firstLineByteLength = currentLineLength;
|
|
347
|
-
}
|
|
348
|
-
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) {
|
|
349
777
|
stoppedByByteLimit = true;
|
|
350
778
|
doneCollecting = true;
|
|
351
|
-
|
|
779
|
+
if (firstLineByteLength === undefined) firstLineByteLength = currentLineLength;
|
|
780
|
+
} else if (collectedLines.length > 0 && collectedBytes + separatorBytes + currentLineLength > maxBytes) {
|
|
781
|
+
stoppedByByteLimit = true;
|
|
352
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
|
+
}
|
|
353
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());
|
|
354
817
|
}
|
|
355
|
-
} else if (lineIndex >= startLine && firstLineByteLength === undefined) {
|
|
356
|
-
firstLineByteLength = currentLineLength;
|
|
357
818
|
}
|
|
358
819
|
|
|
359
820
|
lineIndex++;
|
|
360
821
|
currentLineLength = 0;
|
|
822
|
+
currentLineKept = 0;
|
|
361
823
|
currentLineChunks = [];
|
|
824
|
+
currentLineDropped = false;
|
|
362
825
|
setupLineState();
|
|
363
826
|
};
|
|
364
827
|
|
|
@@ -366,7 +829,6 @@ async function streamLinesFromFile(
|
|
|
366
829
|
|
|
367
830
|
try {
|
|
368
831
|
fileHandle = await fs.open(filePath, "r");
|
|
369
|
-
|
|
370
832
|
while (true) {
|
|
371
833
|
throwIfAborted(signal);
|
|
372
834
|
const { bytesRead } = await fileHandle.read(bufferChunk, 0, bufferChunk.length, null);
|
|
@@ -380,27 +842,18 @@ async function streamLinesFromFile(
|
|
|
380
842
|
for (let i = 0; i < chunk.length; i++) {
|
|
381
843
|
if (chunk[i] === 0x0a) {
|
|
382
844
|
const segment = chunk.subarray(start, i);
|
|
383
|
-
if (segment.length > 0)
|
|
384
|
-
appendSegment(segment);
|
|
385
|
-
}
|
|
845
|
+
if (segment.length > 0) appendSegment(segment);
|
|
386
846
|
finalizeLine();
|
|
387
847
|
start = i + 1;
|
|
388
848
|
}
|
|
389
849
|
}
|
|
390
|
-
|
|
391
|
-
if (start < chunk.length) {
|
|
392
|
-
appendSegment(chunk.subarray(start));
|
|
393
|
-
}
|
|
850
|
+
if (start < chunk.length) appendSegment(chunk.subarray(start));
|
|
394
851
|
}
|
|
395
852
|
} finally {
|
|
396
|
-
if (fileHandle)
|
|
397
|
-
await fileHandle.close();
|
|
398
|
-
}
|
|
853
|
+
if (fileHandle) await fileHandle.close();
|
|
399
854
|
}
|
|
400
855
|
|
|
401
|
-
if (endedWithNewline || currentLineLength > 0 || !sawAnyByte)
|
|
402
|
-
finalizeLine();
|
|
403
|
-
}
|
|
856
|
+
if (endedWithNewline || currentLineLength > 0 || !sawAnyByte) finalizeLine();
|
|
404
857
|
|
|
405
858
|
let firstLinePreview: { text: string; bytes: number } | undefined;
|
|
406
859
|
if (firstLinePreviewBytes > 0) {
|
|
@@ -408,17 +861,271 @@ async function streamLinesFromFile(
|
|
|
408
861
|
firstLinePreview = { text, bytes };
|
|
409
862
|
}
|
|
410
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;
|
|
411
893
|
return {
|
|
412
|
-
lines: collectedLines,
|
|
894
|
+
lines: collect === "head" ? collectedLines : ringLines,
|
|
413
895
|
totalFileLines: lineIndex,
|
|
414
896
|
collectedBytes,
|
|
415
|
-
stoppedByByteLimit,
|
|
897
|
+
stoppedByByteLimit: collect === "head" ? stoppedByByteLimit : ringEvictedForBytes,
|
|
416
898
|
firstLinePreview,
|
|
417
899
|
firstLineByteLength,
|
|
418
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,
|
|
419
917
|
};
|
|
420
918
|
}
|
|
421
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
|
+
|
|
422
1129
|
// Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
|
|
423
1130
|
const MAX_IMAGE_SIZE = MAX_IMAGE_INPUT_BYTES;
|
|
424
1131
|
const GLOB_TIMEOUT_MS = 5000;
|
|
@@ -494,6 +1201,12 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
|
|
|
494
1201
|
const readSchema = z
|
|
495
1202
|
.object({
|
|
496
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
|
+
),
|
|
497
1210
|
})
|
|
498
1211
|
.strict();
|
|
499
1212
|
|
|
@@ -729,6 +1442,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
729
1442
|
this.description = prompt.render(readDescription, {
|
|
730
1443
|
DEFAULT_LIMIT: String(this.#defaultLimit),
|
|
731
1444
|
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
|
|
1445
|
+
TRUNCATION_DEFAULT: String(this.session.settings.get("read.truncation") ?? "last"),
|
|
1446
|
+
RECEIPT_LINES: String(this.session.settings.get("read.receiptBudgetLines")),
|
|
1447
|
+
RECEIPT_KIB: String(this.session.settings.get("read.receiptBudgetBytes")),
|
|
732
1448
|
IS_HL_MODE: displayMode.hashLines,
|
|
733
1449
|
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
734
1450
|
});
|
|
@@ -832,6 +1548,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
832
1548
|
sourcePath?: string;
|
|
833
1549
|
spillEligible?: boolean;
|
|
834
1550
|
receiptPath?: string;
|
|
1551
|
+
bodyFooterPath?: string;
|
|
835
1552
|
sourceUrl?: string;
|
|
836
1553
|
sourceInternal?: string;
|
|
837
1554
|
entityLabel: string;
|
|
@@ -840,19 +1557,21 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
840
1557
|
raw?: boolean;
|
|
841
1558
|
immutable?: boolean;
|
|
842
1559
|
wrapUntrusted?: boolean;
|
|
1560
|
+
truncationDirection?: TruncationDirection;
|
|
1561
|
+
/** Truncation metadata stays relative to the selected body, not a protected prefix. */
|
|
1562
|
+
truncationMetadataBodyRelative?: boolean;
|
|
1563
|
+
/** Preserve this leading URL preamble while truncating only the selected body. */
|
|
1564
|
+
protectedPrefixChars?: number;
|
|
1565
|
+
cacheLinesFor?: string;
|
|
843
1566
|
},
|
|
844
1567
|
): AgentToolResult<ReadToolDetails> {
|
|
845
1568
|
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
846
1569
|
const details = options.details ?? {};
|
|
847
1570
|
const allLines = text.split("\n");
|
|
848
1571
|
const totalLines = allLines.length;
|
|
849
|
-
// User-requested 0-indexed range start. Lines BEFORE this are leading
|
|
850
|
-
// context (added below if offset is explicit).
|
|
851
1572
|
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
852
1573
|
const ignoreResultLimits = options.ignoreResultLimits ?? false;
|
|
853
1574
|
const requestedEnd = limit !== undefined ? Math.min(requestedStart + limit, allLines.length) : allLines.length;
|
|
854
|
-
// Expand only on sides the user actually constrained: leading context
|
|
855
|
-
// when offset>1, trailing context when a finite limit was set.
|
|
856
1575
|
const expanded = expandRangeWithContext(
|
|
857
1576
|
requestedStart,
|
|
858
1577
|
requestedEnd,
|
|
@@ -861,19 +1580,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
861
1580
|
limit !== undefined,
|
|
862
1581
|
);
|
|
863
1582
|
const startLine = expanded.startLine;
|
|
864
|
-
const
|
|
1583
|
+
const endLine = expanded.endLine;
|
|
865
1584
|
const startLineDisplay = startLine + 1;
|
|
866
1585
|
|
|
867
1586
|
const resultBuilder = toolResult(details);
|
|
868
|
-
if (options.sourcePath)
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
if (options.sourceUrl) {
|
|
872
|
-
resultBuilder.sourceUrl(options.sourceUrl);
|
|
873
|
-
}
|
|
874
|
-
if (options.sourceInternal) {
|
|
875
|
-
resultBuilder.sourceInternal(options.sourceInternal);
|
|
876
|
-
}
|
|
1587
|
+
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
1588
|
+
if (options.sourceUrl) resultBuilder.sourceUrl(options.sourceUrl);
|
|
1589
|
+
if (options.sourceInternal) resultBuilder.sourceInternal(options.sourceInternal);
|
|
877
1590
|
|
|
878
1591
|
if (requestedStart >= allLines.length) {
|
|
879
1592
|
const suggestion =
|
|
@@ -887,92 +1600,217 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
887
1600
|
.done();
|
|
888
1601
|
}
|
|
889
1602
|
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
?
|
|
909
|
-
:
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1603
|
+
const direction = options.truncationDirection ?? "head";
|
|
1604
|
+
const receipt = options.receiptPath !== undefined;
|
|
1605
|
+
const exposeReceiptHeadMeta = receipt && options.cacheLinesFor !== undefined;
|
|
1606
|
+
const bodyFooterPath = options.receiptPath ?? options.bodyFooterPath;
|
|
1607
|
+
const preservePrefix =
|
|
1608
|
+
direction !== "head" && options.protectedPrefixChars !== undefined && options.protectedPrefixChars > 0;
|
|
1609
|
+
const protectedPrefix = preservePrefix ? text.slice(0, options.protectedPrefixChars) : "";
|
|
1610
|
+
const protectedPrefixLines = preservePrefix ? protectedPrefix.split("\n").length - 1 : 0;
|
|
1611
|
+
const rangeStart = preservePrefix ? Math.max(startLine, protectedPrefixLines) : startLine;
|
|
1612
|
+
|
|
1613
|
+
const rangeLines = allLines.slice(rangeStart, endLine);
|
|
1614
|
+
const rangeText = rangeLines.join("\n");
|
|
1615
|
+
const budgetLines = receipt
|
|
1616
|
+
? this.session.settings.get("read.receiptBudgetLines")
|
|
1617
|
+
: options.resultByteCeiling === undefined
|
|
1618
|
+
? DEFAULT_MAX_LINES
|
|
1619
|
+
: Number.MAX_SAFE_INTEGER;
|
|
1620
|
+
const budgetBytes = receipt
|
|
1621
|
+
? this.session.settings.get("read.receiptBudgetBytes") * 1024
|
|
1622
|
+
: (options.resultByteCeiling ?? DEFAULT_MAX_BYTES);
|
|
1623
|
+
const relativeWindow = ignoreResultLimits
|
|
1624
|
+
? fullReadWindow(rangeText, rangeLines.length, Buffer.byteLength(rangeText, "utf-8"))
|
|
1625
|
+
: selectReadWindow(
|
|
1626
|
+
receipt ? text : rangeText,
|
|
1627
|
+
receipt ? allLines : rangeLines,
|
|
1628
|
+
direction,
|
|
1629
|
+
budgetLines,
|
|
1630
|
+
budgetBytes,
|
|
914
1631
|
);
|
|
915
|
-
|
|
1632
|
+
// A directional head window cannot render anything when its first source line
|
|
1633
|
+
// exceeds the byte budget. Fall back to the historical head truncator so ACP
|
|
1634
|
+
// still emits the same UTF-8 preview and metadata as the pre-direction code.
|
|
1635
|
+
const oversizedHeadWindow =
|
|
1636
|
+
!receipt &&
|
|
1637
|
+
direction === "head" &&
|
|
1638
|
+
options.resultByteCeiling === undefined &&
|
|
1639
|
+
relativeWindow.kind === "head-only" &&
|
|
1640
|
+
relativeWindow.head === undefined;
|
|
1641
|
+
const legacyHeadByteCeiling = options.resultByteCeiling ?? (oversizedHeadWindow ? budgetBytes : undefined);
|
|
1642
|
+
const window = rebaseWindow(relativeWindow, receipt ? 0 : rangeStart);
|
|
1643
|
+
// Keep explicit ranges in the selected-window coordinate system for truncation metadata.
|
|
1644
|
+
// Rendered output still uses `window`, whose anchors retain absolute file line numbers.
|
|
1645
|
+
const metadataWindow =
|
|
1646
|
+
options.truncationMetadataBodyRelative || (offset !== undefined && direction !== "head")
|
|
1647
|
+
? relativeWindow
|
|
1648
|
+
: window;
|
|
1649
|
+
const metadataRangeBase =
|
|
1650
|
+
options.truncationMetadataBodyRelative || (offset !== undefined && direction !== "head")
|
|
1651
|
+
? "window"
|
|
1652
|
+
: undefined;
|
|
916
1653
|
const shouldAddHashLines = displayMode.hashLines;
|
|
917
|
-
const shouldAddLineNumbers = shouldAddHashLines
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
1654
|
+
const shouldAddLineNumbers = !shouldAddHashLines && displayMode.lineNumbers;
|
|
1655
|
+
const maxColumns = options.raw ? 0 : resolveOutputMaxColumns(this.session.settings);
|
|
1656
|
+
const bodyRawWindow = renderReadWindow(window, false, false, maxColumns);
|
|
1657
|
+
const bodyRenderedWindow = renderReadWindow(window, shouldAddHashLines, shouldAddLineNumbers, maxColumns);
|
|
1658
|
+
const rawWindow = preservePrefix ? `${protectedPrefix}${bodyRawWindow}` : bodyRawWindow;
|
|
1659
|
+
const renderedWindow = preservePrefix ? `${protectedPrefix}${bodyRenderedWindow}` : bodyRenderedWindow;
|
|
1660
|
+
const partialTail = partialTailOf(window);
|
|
1661
|
+
const partialTailWarning = partialTail !== undefined && shouldAddHashLines && bodyFooterPath !== undefined;
|
|
1662
|
+
if (partialTailWarning) {
|
|
1663
|
+
Object.defineProperty(metadataWindow, "outputLinesOverride", { value: 0, enumerable: false });
|
|
1664
|
+
Object.defineProperty(metadataWindow, "outputBytesOverride", { value: 0, enumerable: false });
|
|
1665
|
+
}
|
|
1666
|
+
const selectedContent = windowSegments(window)
|
|
1667
|
+
.map(segment => segment.content)
|
|
1668
|
+
.join("\n");
|
|
1669
|
+
details.displayContent = {
|
|
1670
|
+
text: rawWindow,
|
|
1671
|
+
startLine: preservePrefix
|
|
1672
|
+
? 1
|
|
1673
|
+
: (window.head?.origin.startLine ?? window.tail?.origin.startLine ?? startLineDisplay),
|
|
921
1674
|
};
|
|
922
1675
|
|
|
923
|
-
let
|
|
924
|
-
let
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1676
|
+
let usedLegacyHeadTruncation = false;
|
|
1677
|
+
let outputText = partialTailWarning
|
|
1678
|
+
? formatOversizedLineWarning(
|
|
1679
|
+
bodyFooterPath!,
|
|
1680
|
+
partialTail!.origin.startLine,
|
|
1681
|
+
partialTail!.sourceLineBytes,
|
|
1682
|
+
budgetBytes,
|
|
1683
|
+
totalLines,
|
|
1684
|
+
)
|
|
1685
|
+
: renderedWindow;
|
|
1686
|
+
let truncationResult: TruncationResult | undefined;
|
|
1687
|
+
if (window.kind !== "full" && (!receipt || direction !== "head" || exposeReceiptHeadMeta)) {
|
|
1688
|
+
const windowResult = makeWindowResult(metadataWindow, selectedContent);
|
|
1689
|
+
const baseTruncation =
|
|
1690
|
+
!receipt && direction === "head"
|
|
1691
|
+
? {
|
|
1692
|
+
...windowResult,
|
|
1693
|
+
truncated: true,
|
|
1694
|
+
}
|
|
1695
|
+
: windowResult;
|
|
1696
|
+
truncationResult = partialTailWarning
|
|
1697
|
+
? { ...baseTruncation, outputLines: 0, outputBytes: 0, lastLineExceedsLimit: true }
|
|
1698
|
+
: baseTruncation;
|
|
1699
|
+
details.truncation = truncationResult;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// Preserve the historical head byte-cap oversized-line behavior. The
|
|
1703
|
+
// directional window model is used for tail/both; head remains the old
|
|
1704
|
+
// oracle so existing hashline and snippet contracts stay byte-identical.
|
|
1705
|
+
if (!receipt && direction === "head" && legacyHeadByteCeiling !== undefined) {
|
|
1706
|
+
const legacy = truncateHead(rangeText, {
|
|
1707
|
+
maxBytes: legacyHeadByteCeiling,
|
|
1708
|
+
maxLines: Number.MAX_SAFE_INTEGER,
|
|
1709
|
+
});
|
|
1710
|
+
truncationResult = legacy;
|
|
1711
|
+
usedLegacyHeadTruncation = legacy.truncated === true;
|
|
1712
|
+
details.truncation = legacy.truncated ? legacy : undefined;
|
|
1713
|
+
if (legacy.firstLineExceedsLimit) {
|
|
1714
|
+
const firstLine = allLines[startLine] ?? "";
|
|
1715
|
+
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
1716
|
+
const snippet = truncateHeadBytes(firstLine, legacyHeadByteCeiling);
|
|
1717
|
+
if (shouldAddHashLines) {
|
|
1718
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(legacyHeadByteCeiling)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
1719
|
+
} else {
|
|
1720
|
+
outputText = formatTextWithMode(snippet.text, startLineDisplay, false, shouldAddLineNumbers);
|
|
1721
|
+
}
|
|
1722
|
+
if (snippet.text.length === 0) {
|
|
1723
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(legacyHeadByteCeiling)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1724
|
+
}
|
|
1725
|
+
details.displayContent = { text: snippet.text, startLine: startLineDisplay };
|
|
1726
|
+
} else if (legacy.truncated) {
|
|
1727
|
+
outputText = formatTextWithMode(legacy.content, startLineDisplay, shouldAddHashLines, shouldAddLineNumbers);
|
|
937
1728
|
} else {
|
|
938
|
-
outputText =
|
|
1729
|
+
outputText = formatTextWithMode(rangeText, startLineDisplay, shouldAddHashLines, shouldAddLineNumbers);
|
|
939
1730
|
}
|
|
1731
|
+
}
|
|
940
1732
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1733
|
+
const userLimitedLines = limit !== undefined ? endLine - startLine : undefined;
|
|
1734
|
+
if (bodyFooterPath !== undefined && window.kind !== "full" && !partialTailWarning) {
|
|
1735
|
+
// A head window only owns a body footer on the historical receipt paths.
|
|
1736
|
+
// Archive members and internal URLs reach here through `bodyFooterPath`
|
|
1737
|
+
// alone; adding a head footer there would emit a second notice beside the
|
|
1738
|
+
// wrapper's, changing output that must stay byte-identical when the
|
|
1739
|
+
// direction is rolled back to head.
|
|
1740
|
+
const footer =
|
|
1741
|
+
direction === "head"
|
|
1742
|
+
? receipt
|
|
1743
|
+
? formatReceiptFooter(
|
|
1744
|
+
bodyFooterPath,
|
|
1745
|
+
metadataWindow.head?.lines ?? 0,
|
|
1746
|
+
metadataWindow.totalLines,
|
|
1747
|
+
windowSegments(metadataWindow).reduce((sum, segment) => sum + segment.bytes, 0),
|
|
1748
|
+
)
|
|
1749
|
+
: undefined
|
|
1750
|
+
: formatDirectionalFooter(bodyFooterPath, metadataWindow);
|
|
1751
|
+
if (footer) {
|
|
1752
|
+
outputText += `\n\n${footer}`;
|
|
1753
|
+
if (direction !== "head") rememberBodyTruncationFooter(details, footer);
|
|
945
1754
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
} else if (truncation.truncated) {
|
|
953
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
954
|
-
details.truncation = truncation;
|
|
955
|
-
truncationInfo = {
|
|
956
|
-
result: truncation,
|
|
957
|
-
options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
|
|
958
|
-
};
|
|
959
|
-
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
|
1755
|
+
} else if (
|
|
1756
|
+
!receipt &&
|
|
1757
|
+
window.kind === "full" &&
|
|
1758
|
+
userLimitedLines !== undefined &&
|
|
1759
|
+
startLine + userLimitedLines < allLines.length
|
|
1760
|
+
) {
|
|
960
1761
|
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
961
1762
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
962
|
-
|
|
963
|
-
outputText = formatText(selectedContent, startLineDisplay);
|
|
964
1763
|
outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`;
|
|
965
|
-
} else {
|
|
966
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
967
|
-
}
|
|
968
|
-
if (options.receiptPath && (truncation.truncated || countTextLines(selectedContent) < totalLines)) {
|
|
969
|
-
outputText += `\n\n${formatReceiptFooter(options.receiptPath, countTextLines(selectedContent), totalLines, Buffer.byteLength(selectedContent, "utf-8"))}`;
|
|
970
1764
|
}
|
|
971
1765
|
|
|
972
|
-
details.spillEligible =
|
|
1766
|
+
details.spillEligible =
|
|
1767
|
+
options.spillEligible === true && selectedContent.length > 0 && !windowHasPartialSegment(window);
|
|
1768
|
+
if (options.cacheLinesFor && !windowHasPartialSegment(window)) {
|
|
1769
|
+
for (const segment of windowSegments(window)) {
|
|
1770
|
+
if (segment.kind === "lines" && segment.lines > 0) {
|
|
1771
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1772
|
+
options.cacheLinesFor,
|
|
1773
|
+
segment.origin.startLine,
|
|
1774
|
+
linesOfSegment(segment),
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
973
1779
|
resultBuilder.text(options.wrapUntrusted ? wrapUntrustedContent(outputText) : outputText);
|
|
974
|
-
if (
|
|
975
|
-
|
|
1780
|
+
if (window.kind !== "full" && (!receipt || direction !== "head" || exposeReceiptHeadMeta)) {
|
|
1781
|
+
const receiptHead = receipt && direction === "head";
|
|
1782
|
+
if (receiptHead && truncationResult?.truncated) {
|
|
1783
|
+
const diskLike = {
|
|
1784
|
+
...truncationResult,
|
|
1785
|
+
totalBytes: Buffer.byteLength(selectedContent, "utf-8"),
|
|
1786
|
+
};
|
|
1787
|
+
resultBuilder.truncation(diskLike, {
|
|
1788
|
+
direction: "head",
|
|
1789
|
+
startLine: startLineDisplay,
|
|
1790
|
+
totalFileLines: totalLines,
|
|
1791
|
+
});
|
|
1792
|
+
} else if (usedLegacyHeadTruncation && truncationResult?.truncated) {
|
|
1793
|
+
resultBuilder.truncation(truncationResult, {
|
|
1794
|
+
direction: "head",
|
|
1795
|
+
startLine: startLineDisplay,
|
|
1796
|
+
totalFileLines: totalLines,
|
|
1797
|
+
});
|
|
1798
|
+
} else {
|
|
1799
|
+
// Only a directional window moves notice ownership into the body: its
|
|
1800
|
+
// footer names the kept range and the re-read path. A head window keeps
|
|
1801
|
+
// the wrapper notice, which carries the `:N to continue` hint instead.
|
|
1802
|
+
resultBuilder.truncationWindows(metadataWindow, {
|
|
1803
|
+
noticeOwner: bodyFooterPath !== undefined && direction !== "head" ? "body" : undefined,
|
|
1804
|
+
maxBytes: direction === "head" ? undefined : budgetBytes,
|
|
1805
|
+
rangeBase: metadataRangeBase,
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
} else if (truncationResult?.truncated) {
|
|
1809
|
+
resultBuilder.truncation(truncationResult, {
|
|
1810
|
+
direction: "head",
|
|
1811
|
+
startLine: startLineDisplay,
|
|
1812
|
+
totalFileLines: totalLines,
|
|
1813
|
+
});
|
|
976
1814
|
}
|
|
977
1815
|
return resultBuilder.done();
|
|
978
1816
|
}
|
|
@@ -995,6 +1833,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
995
1833
|
entityLabel: string;
|
|
996
1834
|
raw?: boolean;
|
|
997
1835
|
immutable?: boolean;
|
|
1836
|
+
truncationDirection?: TruncationDirection;
|
|
1837
|
+
cacheLinesFor?: string;
|
|
998
1838
|
},
|
|
999
1839
|
): AgentToolResult<ReadToolDetails> {
|
|
1000
1840
|
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
@@ -1003,13 +1843,54 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1003
1843
|
const totalLines = allLines.length;
|
|
1004
1844
|
const shouldAddHashLines = displayMode.hashLines;
|
|
1005
1845
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1846
|
+
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1847
|
+
const direction = options.truncationDirection ?? "head";
|
|
1848
|
+
let columnTruncated = 0;
|
|
1006
1849
|
|
|
1007
1850
|
const resultBuilder = toolResult(details);
|
|
1008
1851
|
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
1009
1852
|
if (options.sourceUrl) resultBuilder.sourceUrl(options.sourceUrl);
|
|
1010
1853
|
if (options.sourceInternal) resultBuilder.sourceInternal(options.sourceInternal);
|
|
1011
1854
|
|
|
1855
|
+
const renderBlockWindow = (
|
|
1856
|
+
window: ReadWindow,
|
|
1857
|
+
): { text: string; partialTail?: Extract<ReadSegment, { kind: "partial-line" }> } => {
|
|
1858
|
+
const renderSegment = (segment: ReadSegment): string => {
|
|
1859
|
+
const hash = shouldAddHashLines && segment.kind === "lines";
|
|
1860
|
+
// A partial line is a byte suffix, not a complete source line. Never
|
|
1861
|
+
// give it an anchor that could make the preview look fully readable.
|
|
1862
|
+
const numbers = !hash && shouldAddLineNumbers && segment.kind === "lines";
|
|
1863
|
+
const renderContent =
|
|
1864
|
+
!options.raw && !hash && maxColumns > 0
|
|
1865
|
+
? segment.content
|
|
1866
|
+
.split("\n")
|
|
1867
|
+
.map(line => {
|
|
1868
|
+
const result = truncateLine(line, maxColumns);
|
|
1869
|
+
if (result.wasTruncated) columnTruncated = maxColumns;
|
|
1870
|
+
return result.text;
|
|
1871
|
+
})
|
|
1872
|
+
.join("\n")
|
|
1873
|
+
: segment.content;
|
|
1874
|
+
return formatTextWithMode(renderContent, segment.origin.startLine, hash, numbers);
|
|
1875
|
+
};
|
|
1876
|
+
let text = "";
|
|
1877
|
+
if (window.kind === "full") text = window.head ? renderSegment(window.head) : "";
|
|
1878
|
+
else if (window.kind === "head-only") text = window.head ? renderSegment(window.head) : "";
|
|
1879
|
+
else if (window.kind === "tail-only") text = window.tail ? renderSegment(window.tail) : "";
|
|
1880
|
+
else {
|
|
1881
|
+
const marker = formatMiddleElisionMarker(window.elidedLines, window.elidedBytes);
|
|
1882
|
+
if (window.head && window.tail) {
|
|
1883
|
+
text =
|
|
1884
|
+
window.elidedLines <= 0
|
|
1885
|
+
? `${renderSegment(window.head)}\n${renderSegment(window.tail)}`
|
|
1886
|
+
: `${renderSegment(window.head)}\n${marker}\n${renderSegment(window.tail)}`;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return { text, partialTail: partialTailOf(window) };
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1012
1892
|
const parts: string[] = [];
|
|
1893
|
+
let hasPartialSegment = false;
|
|
1013
1894
|
const outOfBounds: LineRange[] = [];
|
|
1014
1895
|
for (const range of ranges) {
|
|
1015
1896
|
if (range.startLine > totalLines) {
|
|
@@ -1017,8 +1898,41 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1017
1898
|
continue;
|
|
1018
1899
|
}
|
|
1019
1900
|
const effectiveEnd = Math.min(range.endLine ?? totalLines, totalLines);
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1901
|
+
const blockLines = allLines.slice(range.startLine - 1, effectiveEnd);
|
|
1902
|
+
const blockText = blockLines.join("\n");
|
|
1903
|
+
const requestedLength = range.endLine !== undefined ? range.endLine - range.startLine + 1 : this.#defaultLimit;
|
|
1904
|
+
const blockMaxLines = Math.min(requestedLength, DEFAULT_MAX_LINES);
|
|
1905
|
+
const blockMaxBytes = Math.max(DEFAULT_MAX_BYTES, blockMaxLines * 512);
|
|
1906
|
+
const relative = selectReadWindow(blockText, blockLines, direction, blockMaxLines, blockMaxBytes);
|
|
1907
|
+
const window = rebaseWindow(relative, range.startLine - 1);
|
|
1908
|
+
hasPartialSegment ||= windowHasPartialSegment(window);
|
|
1909
|
+
const rendered = renderBlockWindow(window);
|
|
1910
|
+
let blockOutput = rendered.text;
|
|
1911
|
+
if (rendered.partialTail) {
|
|
1912
|
+
const disclosurePath =
|
|
1913
|
+
options.sourceInternal ?? options.sourceUrl ?? options.sourcePath ?? options.entityLabel;
|
|
1914
|
+
blockOutput = shouldAddHashLines
|
|
1915
|
+
? formatOversizedLineWarning(
|
|
1916
|
+
disclosurePath,
|
|
1917
|
+
rendered.partialTail.origin.startLine,
|
|
1918
|
+
rendered.partialTail.sourceLineBytes,
|
|
1919
|
+
blockMaxBytes,
|
|
1920
|
+
totalLines,
|
|
1921
|
+
)
|
|
1922
|
+
: `${blockOutput}\n\n${formatDirectionalFooter(disclosurePath, window)}`;
|
|
1923
|
+
}
|
|
1924
|
+
parts.push(blockOutput);
|
|
1925
|
+
if (options.cacheLinesFor && !windowHasPartialSegment(window)) {
|
|
1926
|
+
for (const segment of windowSegments(window)) {
|
|
1927
|
+
if (segment.kind === "lines" && segment.lines > 0) {
|
|
1928
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1929
|
+
options.cacheLinesFor,
|
|
1930
|
+
segment.origin.startLine,
|
|
1931
|
+
linesOfSegment(segment),
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1022
1936
|
}
|
|
1023
1937
|
|
|
1024
1938
|
const outputText = parts.length > 0 ? parts.join("\n\n…\n\n") : "";
|
|
@@ -1029,9 +1943,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1029
1943
|
}
|
|
1030
1944
|
const finalText =
|
|
1031
1945
|
notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
|
|
1032
|
-
details.spillEligible = parts.length > 0;
|
|
1033
|
-
|
|
1946
|
+
details.spillEligible = parts.length > 0 && !hasPartialSegment;
|
|
1034
1947
|
resultBuilder.text(finalText);
|
|
1948
|
+
if (columnTruncated > 0) resultBuilder.limits({ columnMax: columnTruncated });
|
|
1035
1949
|
return resultBuilder.done();
|
|
1036
1950
|
}
|
|
1037
1951
|
|
|
@@ -1048,6 +1962,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1048
1962
|
displayMode: { hashLines: boolean; lineNumbers: boolean },
|
|
1049
1963
|
suffixResolution: { from: string; to: string } | undefined,
|
|
1050
1964
|
signal: AbortSignal | undefined,
|
|
1965
|
+
direction: TruncationDirection,
|
|
1051
1966
|
): Promise<{
|
|
1052
1967
|
outputText: string;
|
|
1053
1968
|
contentLineCount: number;
|
|
@@ -1066,6 +1981,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1066
1981
|
sourcePath: absolutePath,
|
|
1067
1982
|
entityLabel: "file",
|
|
1068
1983
|
raw: rawSelector,
|
|
1984
|
+
truncationDirection: direction,
|
|
1985
|
+
cacheLinesFor: absolutePath,
|
|
1069
1986
|
});
|
|
1070
1987
|
if (suffixResolution) {
|
|
1071
1988
|
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
@@ -1078,6 +1995,27 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1078
1995
|
}
|
|
1079
1996
|
}
|
|
1080
1997
|
|
|
1998
|
+
// The legacy stream collector remains the head/default implementation.
|
|
1999
|
+
// Directional multi-range reads use the same materialized window oracle as
|
|
2000
|
+
// ACP so each block has identical bytes, anchors, and cache entries.
|
|
2001
|
+
if (direction !== "head") {
|
|
2002
|
+
const diskText = await Bun.file(absolutePath).text();
|
|
2003
|
+
const result = this.#buildInMemoryMultiRangeResult(diskText, ranges, {
|
|
2004
|
+
details: { resolvedPath: absolutePath, suffixResolution },
|
|
2005
|
+
sourcePath: absolutePath,
|
|
2006
|
+
entityLabel: "file",
|
|
2007
|
+
raw: rawSelector,
|
|
2008
|
+
truncationDirection: direction,
|
|
2009
|
+
cacheLinesFor: absolutePath,
|
|
2010
|
+
});
|
|
2011
|
+
const firstText = result.content.find((content): content is TextContent => content.type === "text");
|
|
2012
|
+
if (suffixResolution && firstText) {
|
|
2013
|
+
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
2014
|
+
firstText.text = `${notice}\n${firstText.text}`;
|
|
2015
|
+
}
|
|
2016
|
+
return { outputText: "", contentLineCount: 0, columnTruncated: 0, bridgeResult: result };
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1081
2019
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1082
2020
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1083
2021
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
@@ -1110,6 +2048,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1110
2048
|
}
|
|
1111
2049
|
|
|
1112
2050
|
const collectedLines = streamResult.lines;
|
|
2051
|
+
const cacheLines = collectedLines.slice();
|
|
1113
2052
|
if (!rawSelector && !shouldAddHashLines && maxColumns > 0) {
|
|
1114
2053
|
for (let i = 0; i < collectedLines.length; i++) {
|
|
1115
2054
|
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
@@ -1121,8 +2060,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1121
2060
|
}
|
|
1122
2061
|
|
|
1123
2062
|
contentLineCount += collectedLines.length;
|
|
1124
|
-
if (
|
|
1125
|
-
getFileReadCache(this.session).recordContiguous(absolutePath, range.startLine,
|
|
2063
|
+
if (cacheLines.length > 0) {
|
|
2064
|
+
getFileReadCache(this.session).recordContiguous(absolutePath, range.startLine, cacheLines);
|
|
1126
2065
|
}
|
|
1127
2066
|
|
|
1128
2067
|
const blockText = collectedLines.join("\n");
|
|
@@ -1142,6 +2081,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1142
2081
|
subPath: string,
|
|
1143
2082
|
limit: number | undefined,
|
|
1144
2083
|
details: ReadToolDetails,
|
|
2084
|
+
direction: TruncationDirection,
|
|
1145
2085
|
signal?: AbortSignal,
|
|
1146
2086
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1147
2087
|
const DEFAULT_LIMIT = 500;
|
|
@@ -1165,14 +2105,21 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1165
2105
|
}
|
|
1166
2106
|
|
|
1167
2107
|
const output = results.length > 0 ? results.join("\n") : "(empty archive directory)";
|
|
1168
|
-
const
|
|
1169
|
-
const
|
|
1170
|
-
const directoryDetails: ReadToolDetails = {
|
|
1171
|
-
|
|
2108
|
+
const window = selectReadWindow(output, results, direction, Number.MAX_SAFE_INTEGER, DEFAULT_MAX_BYTES);
|
|
2109
|
+
const rendered = prependSuffixResolutionNotice(renderReadWindow(window, false, false), details.suffixResolution);
|
|
2110
|
+
const directoryDetails: ReadToolDetails = {
|
|
2111
|
+
...details,
|
|
2112
|
+
isDirectory: true,
|
|
2113
|
+
...(window.kind !== "full" ? { truncation: makeWindowResult(window, output) } : {}),
|
|
2114
|
+
};
|
|
2115
|
+
const resultBuilder = toolResult<ReadToolDetails>(directoryDetails).text(rendered);
|
|
1172
2116
|
resultBuilder.sourcePath(archivePath).limits({ resultLimit: limitMeta.resultLimit?.reached });
|
|
1173
|
-
if (
|
|
1174
|
-
|
|
1175
|
-
|
|
2117
|
+
if (window.kind !== "full") {
|
|
2118
|
+
if (direction === "head") {
|
|
2119
|
+
resultBuilder.truncation(makeWindowResult(window, output), { direction: "head" });
|
|
2120
|
+
} else {
|
|
2121
|
+
resultBuilder.truncationWindows(window, { maxBytes: DEFAULT_MAX_BYTES });
|
|
2122
|
+
}
|
|
1176
2123
|
}
|
|
1177
2124
|
return resultBuilder.done();
|
|
1178
2125
|
}
|
|
@@ -1182,6 +2129,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1182
2129
|
parsedSel: ParsedSelector,
|
|
1183
2130
|
resolvedArchivePath: ResolvedArchiveReadPath,
|
|
1184
2131
|
signal?: AbortSignal,
|
|
2132
|
+
truncationParam?: TruncationDirection,
|
|
1185
2133
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1186
2134
|
throwIfAborted(signal);
|
|
1187
2135
|
const archive = await openArchive(resolvedArchivePath.absolutePath);
|
|
@@ -1196,6 +2144,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1196
2144
|
if (!node) {
|
|
1197
2145
|
throw new ToolError(`Path '${readPath}' not found inside archive`);
|
|
1198
2146
|
}
|
|
2147
|
+
const route: ReadRoute = node.isDirectory
|
|
2148
|
+
? "dir-archive"
|
|
2149
|
+
: isMultiRange(parsedSel)
|
|
2150
|
+
? "local-multi-range"
|
|
2151
|
+
: parsedSel.kind === "none"
|
|
2152
|
+
? "archive-member-bare"
|
|
2153
|
+
: "local-range";
|
|
2154
|
+
const direction = resolveEffectiveDirection(truncationParam, route, this.session.settings);
|
|
1199
2155
|
|
|
1200
2156
|
if (node.isDirectory) {
|
|
1201
2157
|
if (isMultiRange(parsedSel)) {
|
|
@@ -1208,12 +2164,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1208
2164
|
resolvedArchivePath.archiveSubPath,
|
|
1209
2165
|
limit,
|
|
1210
2166
|
details,
|
|
2167
|
+
direction,
|
|
1211
2168
|
signal,
|
|
1212
2169
|
);
|
|
1213
2170
|
}
|
|
1214
2171
|
|
|
1215
2172
|
const entry = await archive.readFile(resolvedArchivePath.archiveSubPath);
|
|
1216
2173
|
const text = decodeUtf8Text(entry.bytes);
|
|
2174
|
+
if (text === null && truncationParam !== undefined && truncationParam !== "head") {
|
|
2175
|
+
throw new ToolError("Explicit truncation for binary archive entries is not yet supported.");
|
|
2176
|
+
}
|
|
1217
2177
|
if (text === null) {
|
|
1218
2178
|
return toolResult<ReadToolDetails>(details)
|
|
1219
2179
|
.text(
|
|
@@ -1234,6 +2194,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1234
2194
|
sourcePath: resolvedArchivePath.absolutePath,
|
|
1235
2195
|
entityLabel: "archive entry",
|
|
1236
2196
|
raw,
|
|
2197
|
+
truncationDirection: direction,
|
|
1237
2198
|
})
|
|
1238
2199
|
: this.#buildInMemoryTextResult(
|
|
1239
2200
|
text,
|
|
@@ -1244,6 +2205,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1244
2205
|
sourcePath: resolvedArchivePath.absolutePath,
|
|
1245
2206
|
entityLabel: "archive entry",
|
|
1246
2207
|
raw,
|
|
2208
|
+
truncationDirection: direction,
|
|
2209
|
+
bodyFooterPath: parsedSel.kind === "none" ? readPath : undefined,
|
|
1247
2210
|
},
|
|
1248
2211
|
);
|
|
1249
2212
|
const firstText = result.content.find((content): content is TextContent => content.type === "text");
|
|
@@ -1256,6 +2219,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1256
2219
|
async #readSqlite(
|
|
1257
2220
|
resolvedSqlitePath: ResolvedSqliteReadPath,
|
|
1258
2221
|
signal?: AbortSignal,
|
|
2222
|
+
truncationParam?: TruncationDirection,
|
|
1259
2223
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1260
2224
|
throwIfAborted(signal);
|
|
1261
2225
|
|
|
@@ -1268,28 +2232,47 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1268
2232
|
resolvedPath: resolvedSqlitePath.absolutePath,
|
|
1269
2233
|
suffixResolution: resolvedSqlitePath.suffixResolution,
|
|
1270
2234
|
};
|
|
2235
|
+
const sqliteRoute: ReadRoute = selector.kind === "list" ? "sqlite-list" : "sqlite-rows";
|
|
2236
|
+
const direction = resolveEffectiveDirection(truncationParam, sqliteRoute, this.session.settings);
|
|
2237
|
+
// SQLite row/schema/query/raw selectors are row-selection routes: queryRows and
|
|
2238
|
+
// executeReadQuery apply their SQL row limits, while renderRow/renderTable only
|
|
2239
|
+
// cap individual cell width. No rendered-output truncator runs on these paths.
|
|
2240
|
+
// Keep explicit direction as a deliberate no-op there; sqlite-list below
|
|
2241
|
+
// truncates only its rendered table listing.
|
|
1271
2242
|
|
|
1272
2243
|
let db: Database | null = null;
|
|
1273
2244
|
try {
|
|
1274
2245
|
db = new Database(resolvedSqlitePath.absolutePath, { readonly: true, strict: true });
|
|
2246
|
+
enforceSqliteQueryOnly(db);
|
|
1275
2247
|
db.run("PRAGMA busy_timeout = 3000");
|
|
1276
2248
|
throwIfAborted(signal);
|
|
1277
2249
|
|
|
1278
2250
|
switch (selector.kind) {
|
|
1279
2251
|
case "list": {
|
|
1280
2252
|
const listLimit = applyListLimit(listTables(db), { limit: 500 });
|
|
1281
|
-
const
|
|
1282
|
-
|
|
2253
|
+
const body = renderTableList(listLimit.items);
|
|
2254
|
+
const window = selectReadWindow(
|
|
2255
|
+
body,
|
|
2256
|
+
body.split("\n"),
|
|
2257
|
+
direction,
|
|
2258
|
+
Number.MAX_SAFE_INTEGER,
|
|
2259
|
+
DEFAULT_MAX_BYTES,
|
|
2260
|
+
);
|
|
2261
|
+
const rendered = prependSuffixResolutionNotice(
|
|
2262
|
+
renderReadWindow(window, false, false),
|
|
1283
2263
|
resolvedSqlitePath.suffixResolution,
|
|
1284
2264
|
);
|
|
1285
|
-
|
|
1286
|
-
details.truncation = truncation.truncated ? truncation : undefined;
|
|
2265
|
+
details.truncation = window.kind !== "full" ? makeWindowResult(window, body) : undefined;
|
|
1287
2266
|
const resultBuilder = toolResult<ReadToolDetails>(details)
|
|
1288
|
-
.text(
|
|
2267
|
+
.text(rendered)
|
|
1289
2268
|
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
1290
2269
|
.limits({ resultLimit: listLimit.meta.resultLimit?.reached });
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
2270
|
+
if (window.kind !== "full") {
|
|
2271
|
+
if (direction === "head") {
|
|
2272
|
+
resultBuilder.truncation(makeWindowResult(window, body), { direction: "head" });
|
|
2273
|
+
} else {
|
|
2274
|
+
resultBuilder.truncationWindows(window, { maxBytes: DEFAULT_MAX_BYTES });
|
|
2275
|
+
}
|
|
1293
2276
|
}
|
|
1294
2277
|
return resultBuilder.done();
|
|
1295
2278
|
}
|
|
@@ -1551,6 +2534,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1551
2534
|
"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.",
|
|
1552
2535
|
);
|
|
1553
2536
|
}
|
|
2537
|
+
if (params.truncation !== undefined && params.truncation !== "head") {
|
|
2538
|
+
throw new ToolError("Explicit truncation for conflict:// regions is not yet supported.");
|
|
2539
|
+
}
|
|
1554
2540
|
return this.#readConflictRegion(conflictUri.id, conflictUri.scope);
|
|
1555
2541
|
}
|
|
1556
2542
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
@@ -1570,15 +2556,36 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1570
2556
|
preferCached: true,
|
|
1571
2557
|
},
|
|
1572
2558
|
);
|
|
1573
|
-
|
|
2559
|
+
const requestedDirection = resolveEffectiveDirection(
|
|
2560
|
+
params.truncation,
|
|
2561
|
+
"url-cache-page",
|
|
2562
|
+
this.session.settings,
|
|
2563
|
+
);
|
|
2564
|
+
const direction =
|
|
2565
|
+
requestedDirection !== "head" && cached.wrappedPreambleChars === undefined ? "head" : requestedDirection;
|
|
2566
|
+
const selectorInput = prepareReadUrlSelectorInput(
|
|
2567
|
+
cached.output,
|
|
2568
|
+
cached.preambleChars,
|
|
2569
|
+
cached.wrappedPreambleChars,
|
|
2570
|
+
);
|
|
2571
|
+
|
|
2572
|
+
return this.#buildInMemoryTextResult(selectorInput.text, parsedUrlTarget.offset, parsedUrlTarget.limit, {
|
|
1574
2573
|
details: { ...cached.details },
|
|
1575
2574
|
sourceUrl: cached.details.finalUrl,
|
|
1576
2575
|
entityLabel: "URL output",
|
|
1577
2576
|
immutable: true,
|
|
1578
2577
|
wrapUntrusted: true,
|
|
2578
|
+
truncationDirection: direction,
|
|
2579
|
+
protectedPrefixChars: selectorInput.preambleChars,
|
|
2580
|
+
truncationMetadataBodyRelative: true,
|
|
1579
2581
|
});
|
|
1580
2582
|
}
|
|
1581
|
-
return executeReadUrl(
|
|
2583
|
+
return executeReadUrl(
|
|
2584
|
+
this.session,
|
|
2585
|
+
{ path: parsedUrlTarget.path, raw: parsedUrlTarget.raw },
|
|
2586
|
+
signal,
|
|
2587
|
+
resolveEffectiveDirection(params.truncation, "url-reader", this.session.settings),
|
|
2588
|
+
);
|
|
1582
2589
|
}
|
|
1583
2590
|
|
|
1584
2591
|
// Handle internal URLs (agent://, artifact://, memory://, rule://, local://, skc://, issue://, pr://).
|
|
@@ -1593,7 +2600,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1593
2600
|
if (internalTarget.sel !== undefined && parsed.kind === "none") {
|
|
1594
2601
|
throw new ToolError(`Invalid internal URL selector "${internalTarget.sel}".`);
|
|
1595
2602
|
}
|
|
1596
|
-
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
2603
|
+
return this.#handleInternalUrl(internalTarget.path, parsed, signal, params.truncation);
|
|
1597
2604
|
}
|
|
1598
2605
|
|
|
1599
2606
|
const archivePath = await this.#resolveArchiveReadPath(readPath, signal);
|
|
@@ -1605,12 +2612,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1605
2612
|
archiveParsed,
|
|
1606
2613
|
{ ...archivePath, archiveSubPath: archiveSubPath.path },
|
|
1607
2614
|
signal,
|
|
2615
|
+
params.truncation,
|
|
1608
2616
|
);
|
|
1609
2617
|
}
|
|
1610
2618
|
|
|
1611
2619
|
const sqlitePath = await this.#resolveSqliteReadPath(readPath, signal);
|
|
1612
2620
|
if (sqlitePath) {
|
|
1613
|
-
return this.#readSqlite(sqlitePath, signal);
|
|
2621
|
+
return this.#readSqlite(sqlitePath, signal, params.truncation);
|
|
1614
2622
|
}
|
|
1615
2623
|
|
|
1616
2624
|
const localTarget = splitPathAndSel(readPath);
|
|
@@ -1656,7 +2664,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1656
2664
|
if (isMultiRange(parsed)) {
|
|
1657
2665
|
throw new ToolError("Multi-range line selectors are not supported for directory listings.");
|
|
1658
2666
|
}
|
|
1659
|
-
const
|
|
2667
|
+
const dirDirection = resolveEffectiveDirection(params.truncation, "dir-local", this.session.settings);
|
|
2668
|
+
const dirResult = await this.#readDirectory(
|
|
2669
|
+
absolutePath,
|
|
2670
|
+
selToOffsetLimit(parsed).limit,
|
|
2671
|
+
dirDirection,
|
|
2672
|
+
signal,
|
|
2673
|
+
);
|
|
2674
|
+
|
|
1660
2675
|
if (suffixResolution) {
|
|
1661
2676
|
dirResult.details ??= {};
|
|
1662
2677
|
dirResult.details.suffixResolution = suffixResolution;
|
|
@@ -1665,7 +2680,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1665
2680
|
}
|
|
1666
2681
|
|
|
1667
2682
|
if (parsed.kind === "conflicts") {
|
|
1668
|
-
return this.#readFileConflicts(absolutePath, suffixResolution, signal);
|
|
2683
|
+
return this.#readFileConflicts(absolutePath, suffixResolution, signal, params.truncation);
|
|
1669
2684
|
}
|
|
1670
2685
|
|
|
1671
2686
|
const imageMetadata = await readImageMetadata(absolutePath);
|
|
@@ -1678,10 +2693,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1678
2693
|
let sourcePath: string | undefined;
|
|
1679
2694
|
let columnTruncated = 0;
|
|
1680
2695
|
let truncationInfo:
|
|
1681
|
-
| {
|
|
2696
|
+
| {
|
|
2697
|
+
result: TruncationResult;
|
|
2698
|
+
options: {
|
|
2699
|
+
direction: "head";
|
|
2700
|
+
startLine?: number;
|
|
2701
|
+
totalFileLines?: number;
|
|
2702
|
+
noticeOwner?: "body";
|
|
2703
|
+
maxBytes?: number;
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
1682
2706
|
| undefined;
|
|
2707
|
+
let directionalWindow: ReadWindow | undefined;
|
|
2708
|
+
let directionalNoticeOwner: "body" | undefined;
|
|
2709
|
+
let directionalRangeBase: "file" | "window" | undefined;
|
|
1683
2710
|
|
|
1684
2711
|
if (mimeType) {
|
|
2712
|
+
if (params.truncation !== undefined && params.truncation !== "head") {
|
|
2713
|
+
throw new ToolError("Explicit truncation for image reads is not yet supported.");
|
|
2714
|
+
}
|
|
1685
2715
|
if (fileSize > MAX_IMAGE_SIZE) {
|
|
1686
2716
|
const sizeStr = formatBytes(fileSize);
|
|
1687
2717
|
const maxStr = formatBytes(MAX_IMAGE_SIZE);
|
|
@@ -1713,11 +2743,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1713
2743
|
}
|
|
1714
2744
|
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
1715
2745
|
const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
|
|
2746
|
+
const direction = resolveEffectiveDirection(params.truncation, "converted", this.session.settings);
|
|
1716
2747
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
1717
2748
|
return this.#buildInMemoryMultiRangeResult(notebookText, parsed.ranges, {
|
|
1718
2749
|
details: { resolvedPath: absolutePath },
|
|
1719
2750
|
sourcePath: absolutePath,
|
|
1720
2751
|
entityLabel: "notebook",
|
|
2752
|
+
truncationDirection: direction,
|
|
1721
2753
|
});
|
|
1722
2754
|
}
|
|
1723
2755
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
@@ -1734,10 +2766,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1734
2766
|
)
|
|
1735
2767
|
: undefined,
|
|
1736
2768
|
receiptPath: parsed.kind === "none" ? localReadPath : undefined,
|
|
2769
|
+
truncationDirection: direction,
|
|
1737
2770
|
});
|
|
1738
2771
|
} else if (shouldConvertWithMarkit) {
|
|
1739
2772
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1740
2773
|
if (result.ok) {
|
|
2774
|
+
const direction = resolveEffectiveDirection(params.truncation, "converted", this.session.settings);
|
|
1741
2775
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1742
2776
|
return this.#buildInMemoryTextResult(result.content, offset, limit, {
|
|
1743
2777
|
details: { resolvedPath: absolutePath },
|
|
@@ -1753,6 +2787,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1753
2787
|
)
|
|
1754
2788
|
: undefined,
|
|
1755
2789
|
receiptPath: parsed.kind === "none" ? localReadPath : undefined,
|
|
2790
|
+
truncationDirection: direction,
|
|
1756
2791
|
});
|
|
1757
2792
|
}
|
|
1758
2793
|
content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
|
|
@@ -1764,6 +2799,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1764
2799
|
(this.session.settings.get("read.summarize.prose") || !PROSE_SUMMARY_EXTENSIONS.has(ext))
|
|
1765
2800
|
) {
|
|
1766
2801
|
const summary = await this.#trySummarize(absolutePath, fileSize, signal);
|
|
2802
|
+
if (params.truncation !== undefined && params.truncation !== "head" && summary?.parsed) {
|
|
2803
|
+
throw new ToolError(
|
|
2804
|
+
"Explicit truncation for structural summaries is not yet supported; summary direction will be applied after summary units become directional.",
|
|
2805
|
+
);
|
|
2806
|
+
}
|
|
1767
2807
|
if (summary?.parsed && summary.elided) {
|
|
1768
2808
|
const renderedSummary = this.#renderSummary(summary);
|
|
1769
2809
|
const footer = formatSummaryElisionFooter(
|
|
@@ -1802,6 +2842,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1802
2842
|
|
|
1803
2843
|
if (!content) {
|
|
1804
2844
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
2845
|
+
const multiDirection = resolveEffectiveDirection(
|
|
2846
|
+
params.truncation,
|
|
2847
|
+
"local-multi-range",
|
|
2848
|
+
this.session.settings,
|
|
2849
|
+
);
|
|
1805
2850
|
const multiResult = await this.#readLocalFileMultiRange(
|
|
1806
2851
|
absolutePath,
|
|
1807
2852
|
parsed.ranges,
|
|
@@ -1809,6 +2854,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1809
2854
|
displayMode,
|
|
1810
2855
|
suffixResolution,
|
|
1811
2856
|
signal,
|
|
2857
|
+
multiDirection,
|
|
1812
2858
|
);
|
|
1813
2859
|
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
1814
2860
|
content = [{ type: "text", text: multiResult.outputText }];
|
|
@@ -1820,6 +2866,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1820
2866
|
} else {
|
|
1821
2867
|
// Raw text or line-range mode
|
|
1822
2868
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
2869
|
+
const bareEligible = parsed.kind === "none";
|
|
2870
|
+
const localDirection = resolveEffectiveDirection(
|
|
2871
|
+
params.truncation,
|
|
2872
|
+
bareEligible ? "local-bare-stream" : isRawSelector(parsed) ? "local-raw" : "local-range",
|
|
2873
|
+
this.session.settings,
|
|
2874
|
+
);
|
|
1823
2875
|
// Try ACP bridge first — editor's in-memory buffer is source of truth.
|
|
1824
2876
|
// Request full text so local range rendering keeps normal context and line numbers.
|
|
1825
2877
|
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
@@ -1831,6 +2883,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1831
2883
|
sourcePath: absolutePath,
|
|
1832
2884
|
entityLabel: "file",
|
|
1833
2885
|
raw: isRawSelector(parsed),
|
|
2886
|
+
cacheLinesFor: absolutePath,
|
|
2887
|
+
truncationDirection: localDirection,
|
|
1834
2888
|
});
|
|
1835
2889
|
if (suffixResolution) {
|
|
1836
2890
|
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
@@ -1872,6 +2926,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1872
2926
|
: isBareReceipt
|
|
1873
2927
|
? receiptBudgetBytes
|
|
1874
2928
|
: Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
2929
|
+
const collectMode = localDirection === "head" ? "head" : localDirection === "last" ? "tail" : "both";
|
|
2930
|
+
const collectEndExclusive =
|
|
2931
|
+
rawSelector || isBareReceipt ? null : startLine + effectiveLimit + leadingContext + trailingContext;
|
|
1875
2932
|
|
|
1876
2933
|
const streamResult = await streamLinesFromFile(
|
|
1877
2934
|
absolutePath,
|
|
@@ -1880,6 +2937,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1880
2937
|
maxBytesForRead,
|
|
1881
2938
|
selectedLineLimit,
|
|
1882
2939
|
signal,
|
|
2940
|
+
collectMode,
|
|
2941
|
+
collectEndExclusive,
|
|
1883
2942
|
);
|
|
1884
2943
|
|
|
1885
2944
|
const {
|
|
@@ -1904,148 +2963,295 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1904
2963
|
.done();
|
|
1905
2964
|
}
|
|
1906
2965
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2966
|
+
if (collectMode !== "head") {
|
|
2967
|
+
const window = streamResultWindow(streamResult, collectMode, startLine, collectEndExclusive);
|
|
2968
|
+
const metadataWindow = rebaseWindow(window, -startLine);
|
|
2969
|
+
directionalWindow = metadataWindow;
|
|
2970
|
+
directionalNoticeOwner = isBareReceipt ? "body" : undefined;
|
|
2971
|
+
directionalRangeBase = offset !== undefined && localDirection !== "head" ? "window" : undefined;
|
|
2972
|
+
const directionalPartial = windowHasPartialSegment(window);
|
|
2973
|
+
const shouldAddDirectionalHashLines = !rawSelector && displayMode.hashLines;
|
|
2974
|
+
const shouldAddDirectionalLineNumbers = rawSelector
|
|
2975
|
+
? false
|
|
2976
|
+
: !shouldAddDirectionalHashLines && displayMode.lineNumbers;
|
|
2977
|
+
const directionalMaxColumns = rawSelector ? 0 : resolveOutputMaxColumns(this.session.settings);
|
|
2978
|
+
if (!rawSelector && !shouldAddDirectionalHashLines && directionalMaxColumns > 0) {
|
|
2979
|
+
for (const segment of windowSegments(window)) {
|
|
2980
|
+
if (
|
|
2981
|
+
segment.content
|
|
2982
|
+
.split("\n")
|
|
2983
|
+
.some(line => truncateLine(line, directionalMaxColumns).wasTruncated)
|
|
2984
|
+
) {
|
|
2985
|
+
columnTruncated = directionalMaxColumns;
|
|
2986
|
+
break;
|
|
2987
|
+
}
|
|
1920
2988
|
}
|
|
1921
2989
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1963
|
-
firstLineBytes,
|
|
1964
|
-
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
1965
|
-
} else {
|
|
1966
|
-
outputText = formatText(snippet.text, startLineDisplay);
|
|
2990
|
+
const selectedContent = windowSegments(window)
|
|
2991
|
+
.map(segment => segment.content)
|
|
2992
|
+
.join("\n");
|
|
2993
|
+
const partialTail = partialTailOf(window);
|
|
2994
|
+
const partialTailWarning = partialTail !== undefined && shouldAddDirectionalHashLines;
|
|
2995
|
+
if (partialTailWarning) {
|
|
2996
|
+
Object.defineProperty(window, "outputLinesOverride", { value: 0, enumerable: false });
|
|
2997
|
+
Object.defineProperty(window, "outputBytesOverride", { value: 0, enumerable: false });
|
|
2998
|
+
Object.defineProperty(metadataWindow, "outputLinesOverride", { value: 0, enumerable: false });
|
|
2999
|
+
Object.defineProperty(metadataWindow, "outputBytesOverride", { value: 0, enumerable: false });
|
|
3000
|
+
}
|
|
3001
|
+
const baseWindowResult =
|
|
3002
|
+
window.kind === "full" ? undefined : makeWindowResult(metadataWindow, selectedContent);
|
|
3003
|
+
const windowResult =
|
|
3004
|
+
baseWindowResult && partialTailWarning
|
|
3005
|
+
? { ...baseWindowResult, outputLines: 0, outputBytes: 0, lastLineExceedsLimit: true }
|
|
3006
|
+
: baseWindowResult;
|
|
3007
|
+
const rawWindow = renderReadWindow(window, false, false, directionalMaxColumns);
|
|
3008
|
+
let outputText = partialTailWarning
|
|
3009
|
+
? formatOversizedLineWarning(
|
|
3010
|
+
localReadPath,
|
|
3011
|
+
partialTail.origin.startLine,
|
|
3012
|
+
partialTail.sourceLineBytes,
|
|
3013
|
+
maxBytesForRead,
|
|
3014
|
+
totalFileLines,
|
|
3015
|
+
)
|
|
3016
|
+
: renderReadWindow(
|
|
3017
|
+
window,
|
|
3018
|
+
shouldAddDirectionalHashLines,
|
|
3019
|
+
shouldAddDirectionalLineNumbers,
|
|
3020
|
+
directionalMaxColumns,
|
|
3021
|
+
);
|
|
3022
|
+
const windowEnd = Math.min(collectEndExclusive ?? totalFileLines, totalFileLines);
|
|
3023
|
+
let bodyFooter: string | undefined;
|
|
3024
|
+
if (isBareReceipt && window.kind !== "full" && !partialTailWarning) {
|
|
3025
|
+
const footer = formatDirectionalFooter(localReadPath, metadataWindow);
|
|
3026
|
+
if (footer) {
|
|
3027
|
+
outputText += `\n\n${footer}`;
|
|
3028
|
+
bodyFooter = footer;
|
|
3029
|
+
}
|
|
1967
3030
|
}
|
|
1968
|
-
if (
|
|
1969
|
-
outputText
|
|
1970
|
-
|
|
1971
|
-
|
|
3031
|
+
if (rawSelector && window.kind !== "full") {
|
|
3032
|
+
outputText += `\n\n${formatRawReadCapNotice(maxBytesForRead, fileSize, localDirection)}`;
|
|
3033
|
+
}
|
|
3034
|
+
if (
|
|
3035
|
+
!isBareReceipt &&
|
|
3036
|
+
window.kind === "full" &&
|
|
3037
|
+
limit !== undefined &&
|
|
3038
|
+
startLine < windowEnd &&
|
|
3039
|
+
windowEnd < totalFileLines
|
|
3040
|
+
) {
|
|
3041
|
+
const remaining = totalFileLines - windowEnd;
|
|
3042
|
+
outputText += `\n\n[${remaining} more lines in file. Use :${windowEnd + 1} to continue]`;
|
|
3043
|
+
}
|
|
3044
|
+
details = {
|
|
3045
|
+
resolvedPath: absolutePath,
|
|
3046
|
+
suffixResolution,
|
|
3047
|
+
displayContent: {
|
|
3048
|
+
text: rawWindow,
|
|
3049
|
+
startLine: window.head?.origin.startLine ?? window.tail?.origin.startLine ?? startLineDisplay,
|
|
3050
|
+
},
|
|
3051
|
+
...(windowResult ? { truncation: windowResult } : {}),
|
|
3052
|
+
spillEligible: !isBareReceipt && selectedContent.length > 0 && !directionalPartial,
|
|
3053
|
+
};
|
|
3054
|
+
if (bodyFooter) rememberBodyTruncationFooter(details, bodyFooter);
|
|
3055
|
+
if (!directionalPartial) {
|
|
3056
|
+
for (const segment of windowSegments(window)) {
|
|
3057
|
+
if (segment.kind === "lines" && segment.lines > 0) {
|
|
3058
|
+
getFileReadCache(this.session).recordContiguous(
|
|
3059
|
+
absolutePath,
|
|
3060
|
+
segment.origin.startLine,
|
|
3061
|
+
linesOfSegment(segment),
|
|
3062
|
+
);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
if (!partialTailWarning) {
|
|
3067
|
+
const conflictBlocks = windowSegments(window).flatMap(segment =>
|
|
3068
|
+
segment.kind === "lines"
|
|
3069
|
+
? scanConflictLines(linesOfSegment(segment), segment.origin.startLine)
|
|
3070
|
+
: [],
|
|
3071
|
+
);
|
|
3072
|
+
if (conflictBlocks.length > 0) {
|
|
3073
|
+
const history = getConflictHistory(this.session);
|
|
3074
|
+
const displayPathForWarning = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
3075
|
+
const entries = conflictBlocks.map(block =>
|
|
3076
|
+
history.register({
|
|
3077
|
+
absolutePath,
|
|
3078
|
+
displayPath: displayPathForWarning,
|
|
3079
|
+
...block,
|
|
3080
|
+
}),
|
|
3081
|
+
);
|
|
3082
|
+
// Cheap full-file scan only when the window already showed
|
|
3083
|
+
// at least one conflict — otherwise pay nothing on clean files.
|
|
3084
|
+
let totalInFile = entries.length;
|
|
3085
|
+
let scanTruncated = false;
|
|
3086
|
+
try {
|
|
3087
|
+
const fileScan = await scanFileForConflicts(absolutePath);
|
|
3088
|
+
totalInFile = Math.max(entries.length, fileScan.blocks.length);
|
|
3089
|
+
scanTruncated = fileScan.scanTruncated;
|
|
3090
|
+
} catch {
|
|
3091
|
+
// Best-effort enrichment; fall back to window-only count.
|
|
3092
|
+
}
|
|
3093
|
+
outputText += formatConflictWarning(entries, {
|
|
3094
|
+
totalInFile,
|
|
3095
|
+
displayPath: displayPathForWarning,
|
|
3096
|
+
scanTruncated,
|
|
3097
|
+
});
|
|
3098
|
+
details.conflictCount = entries.length;
|
|
3099
|
+
}
|
|
1972
3100
|
}
|
|
1973
|
-
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1974
|
-
|
|
1975
3101
|
sourcePath = absolutePath;
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
3102
|
+
content = [{ type: "text", text: outputText }];
|
|
3103
|
+
} else {
|
|
3104
|
+
// Per-line column cap. Skipped in raw mode so `:raw` always returns
|
|
3105
|
+
// verbatim bytes for paste-back-into-tool workflows. Total byte/line
|
|
3106
|
+
// counts in `truncation` keep reflecting the source, not the trimmed
|
|
3107
|
+
// view — column truncation surfaces separately via `.limits()`.
|
|
3108
|
+
|
|
3109
|
+
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
3110
|
+
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
3111
|
+
if (!rawSelector && !shouldAddHashLines && maxColumns > 0) {
|
|
3112
|
+
for (let i = 0; i < collectedLines.length; i++) {
|
|
3113
|
+
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
3114
|
+
if (wasTruncated) {
|
|
3115
|
+
collectedLines[i] = text;
|
|
3116
|
+
columnTruncated = maxColumns;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
const selectedContent = collectedLines.join("\n");
|
|
3122
|
+
const userLimitedLines = collectedLines.length;
|
|
3123
|
+
|
|
3124
|
+
const totalSelectedLines = totalFileLines - startLine;
|
|
3125
|
+
const totalSelectedBytes = collectedBytes;
|
|
3126
|
+
const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
|
|
3127
|
+
const firstLineExceedsLimit =
|
|
3128
|
+
firstLineByteLength !== undefined && firstLineByteLength > maxBytesForRead;
|
|
3129
|
+
|
|
3130
|
+
const truncation: TruncationResult = {
|
|
3131
|
+
content: selectedContent,
|
|
3132
|
+
truncated: wasTruncated,
|
|
3133
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
3134
|
+
totalLines: totalSelectedLines,
|
|
3135
|
+
totalBytes: totalSelectedBytes,
|
|
3136
|
+
outputLines: collectedLines.length,
|
|
3137
|
+
outputBytes: collectedBytes,
|
|
3138
|
+
lastLinePartial: false,
|
|
3139
|
+
firstLineExceedsLimit,
|
|
1979
3140
|
};
|
|
1980
|
-
} else if (truncation.truncated) {
|
|
1981
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
1982
|
-
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1983
3141
|
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
3142
|
+
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
3143
|
+
getFileReadCache(this.session).recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
const shouldAddLineNumbers = rawSelector
|
|
3147
|
+
? false
|
|
3148
|
+
: shouldAddHashLines
|
|
3149
|
+
? false
|
|
3150
|
+
: displayMode.lineNumbers;
|
|
3151
|
+
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
3152
|
+
const formatText = (text: string, startNum: number): string => {
|
|
3153
|
+
capturedDisplayContent = { text, startLine: startNum };
|
|
3154
|
+
return formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1988
3155
|
};
|
|
1989
|
-
} else if (startLine + userLimitedLines < totalFileLines) {
|
|
1990
|
-
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
1991
|
-
const nextOffset = startLine + userLimitedLines + 1;
|
|
1992
3156
|
|
|
1993
|
-
outputText
|
|
1994
|
-
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
1995
|
-
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3157
|
+
let outputText: string;
|
|
1996
3158
|
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
outputText = formatText(truncation.content, startLineDisplay);
|
|
2001
|
-
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3159
|
+
if (truncation.firstLineExceedsLimit) {
|
|
3160
|
+
const firstLineBytes = firstLineByteLength ?? 0;
|
|
3161
|
+
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
2002
3162
|
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
3163
|
+
if (shouldAddHashLines) {
|
|
3164
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
3165
|
+
firstLineBytes,
|
|
3166
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
3167
|
+
} else {
|
|
3168
|
+
outputText = formatText(snippet.text, startLineDisplay);
|
|
3169
|
+
}
|
|
3170
|
+
if (snippet.text.length === 0) {
|
|
3171
|
+
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
3172
|
+
firstLineBytes,
|
|
3173
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
3174
|
+
}
|
|
3175
|
+
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3176
|
+
|
|
3177
|
+
sourcePath = absolutePath;
|
|
3178
|
+
truncationInfo = {
|
|
3179
|
+
result: truncation,
|
|
3180
|
+
// Head receipts keep the wrapper notice: it carries the `:N to
|
|
3181
|
+
// continue` hint, which the body footer does not duplicate.
|
|
3182
|
+
// Only directional windows transfer notice ownership to the body.
|
|
3183
|
+
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
3184
|
+
};
|
|
3185
|
+
} else if (truncation.truncated) {
|
|
3186
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
3187
|
+
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3188
|
+
|
|
3189
|
+
sourcePath = absolutePath;
|
|
3190
|
+
truncationInfo = {
|
|
3191
|
+
result: truncation,
|
|
3192
|
+
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
3193
|
+
};
|
|
3194
|
+
} else if (startLine + userLimitedLines < totalFileLines) {
|
|
3195
|
+
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
3196
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
3197
|
+
|
|
3198
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
3199
|
+
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
3200
|
+
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
3201
|
+
|
|
3202
|
+
sourcePath = absolutePath;
|
|
3203
|
+
} else {
|
|
3204
|
+
// No truncation, no user limit exceeded
|
|
3205
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
3206
|
+
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
2011
3207
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
3208
|
+
sourcePath = absolutePath;
|
|
3209
|
+
}
|
|
3210
|
+
if (isBareReceipt && (truncation.truncated || collectedLines.length < totalFileLines)) {
|
|
3211
|
+
outputText += `\n\n${formatReceiptFooter(localReadPath, collectedLines.length, totalFileLines, collectedBytes)}`;
|
|
3212
|
+
}
|
|
3213
|
+
if (rawSelector && truncation.truncated) {
|
|
3214
|
+
outputText += `\n\n${formatRawReadCapNotice(maxBytesForRead, fileSize, localDirection)}`;
|
|
3215
|
+
}
|
|
2015
3216
|
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
3217
|
+
if (capturedDisplayContent) {
|
|
3218
|
+
details.displayContent = capturedDisplayContent;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
if (!firstLineExceedsLimit && collectedLines.length > 0) {
|
|
3222
|
+
const blocks = scanConflictLines(collectedLines, startLineDisplay);
|
|
3223
|
+
if (blocks.length > 0) {
|
|
3224
|
+
const history = getConflictHistory(this.session);
|
|
3225
|
+
const displayPathForWarning = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
3226
|
+
const entries = blocks.map(block =>
|
|
3227
|
+
history.register({
|
|
3228
|
+
absolutePath,
|
|
3229
|
+
displayPath: displayPathForWarning,
|
|
3230
|
+
...block,
|
|
3231
|
+
}),
|
|
3232
|
+
);
|
|
3233
|
+
// Cheap full-file scan only when the window already showed
|
|
3234
|
+
// at least one conflict — otherwise pay nothing on clean files.
|
|
3235
|
+
let totalInFile = entries.length;
|
|
3236
|
+
let scanTruncated = false;
|
|
3237
|
+
try {
|
|
3238
|
+
const fileScan = await scanFileForConflicts(absolutePath);
|
|
3239
|
+
totalInFile = Math.max(entries.length, fileScan.blocks.length);
|
|
3240
|
+
scanTruncated = fileScan.scanTruncated;
|
|
3241
|
+
} catch {
|
|
3242
|
+
// Best-effort enrichment; fall back to window-only count.
|
|
3243
|
+
}
|
|
3244
|
+
outputText += formatConflictWarning(entries, {
|
|
3245
|
+
totalInFile,
|
|
2024
3246
|
displayPath: displayPathForWarning,
|
|
2025
|
-
|
|
2026
|
-
})
|
|
2027
|
-
|
|
2028
|
-
// Cheap full-file scan only when the window already showed
|
|
2029
|
-
// at least one conflict — otherwise pay nothing on clean files.
|
|
2030
|
-
let totalInFile = entries.length;
|
|
2031
|
-
let scanTruncated = false;
|
|
2032
|
-
try {
|
|
2033
|
-
const fileScan = await scanFileForConflicts(absolutePath);
|
|
2034
|
-
totalInFile = Math.max(entries.length, fileScan.blocks.length);
|
|
2035
|
-
scanTruncated = fileScan.scanTruncated;
|
|
2036
|
-
} catch {
|
|
2037
|
-
// Best-effort enrichment; fall back to window-only count.
|
|
3247
|
+
scanTruncated,
|
|
3248
|
+
});
|
|
3249
|
+
details.conflictCount = entries.length;
|
|
2038
3250
|
}
|
|
2039
|
-
outputText += formatConflictWarning(entries, {
|
|
2040
|
-
totalInFile,
|
|
2041
|
-
displayPath: displayPathForWarning,
|
|
2042
|
-
scanTruncated,
|
|
2043
|
-
});
|
|
2044
|
-
details.conflictCount = entries.length;
|
|
2045
3251
|
}
|
|
2046
|
-
}
|
|
2047
3252
|
|
|
2048
|
-
|
|
3253
|
+
content = [{ type: "text", text: outputText }];
|
|
3254
|
+
}
|
|
2049
3255
|
}
|
|
2050
3256
|
}
|
|
2051
3257
|
}
|
|
@@ -2068,6 +3274,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2068
3274
|
if (truncationInfo) {
|
|
2069
3275
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
2070
3276
|
}
|
|
3277
|
+
if (directionalWindow) {
|
|
3278
|
+
resultBuilder.truncationWindows(directionalWindow, {
|
|
3279
|
+
noticeOwner: directionalNoticeOwner,
|
|
3280
|
+
rangeBase: directionalRangeBase,
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
2071
3283
|
if (columnTruncated > 0) {
|
|
2072
3284
|
resultBuilder.limits({ columnMax: columnTruncated });
|
|
2073
3285
|
}
|
|
@@ -2113,8 +3325,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2113
3325
|
absolutePath: string,
|
|
2114
3326
|
suffixResolution: { from: string; to: string } | undefined,
|
|
2115
3327
|
signal: AbortSignal | undefined,
|
|
3328
|
+
truncationParam?: TruncationDirection,
|
|
2116
3329
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2117
3330
|
throwIfAborted(signal);
|
|
3331
|
+
if (truncationParam !== undefined && truncationParam !== "head") {
|
|
3332
|
+
throw new ToolError(
|
|
3333
|
+
"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.",
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
2118
3337
|
const scan = await scanFileForConflicts(absolutePath);
|
|
2119
3338
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
2120
3339
|
const history = getConflictHistory(this.session);
|
|
@@ -2147,6 +3366,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2147
3366
|
url: string,
|
|
2148
3367
|
parsedSel: ParsedSelector,
|
|
2149
3368
|
signal?: AbortSignal,
|
|
3369
|
+
truncationParam?: TruncationDirection,
|
|
2150
3370
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2151
3371
|
const internalRouter = InternalUrlRouter.instance();
|
|
2152
3372
|
|
|
@@ -2183,8 +3403,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2183
3403
|
|
|
2184
3404
|
// If extraction was used, return directly (no pagination)
|
|
2185
3405
|
if (hasExtraction) {
|
|
3406
|
+
if (truncationParam !== undefined && truncationParam !== "head") {
|
|
3407
|
+
throw new ToolError("Explicit truncation for internal URL query extraction is not yet supported.");
|
|
3408
|
+
}
|
|
2186
3409
|
return toolResult(details).text(resource.content).sourceInternal(url).done();
|
|
2187
3410
|
}
|
|
3411
|
+
const direction = resolveEffectiveDirection(truncationParam, "converted", this.session.settings);
|
|
2188
3412
|
|
|
2189
3413
|
const raw = isRawSelector(parsedSel);
|
|
2190
3414
|
if (isMultiRange(parsedSel) && parsedSel.kind === "lines") {
|
|
@@ -2195,6 +3419,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2195
3419
|
entityLabel: "resource",
|
|
2196
3420
|
immutable: resource.immutable,
|
|
2197
3421
|
raw,
|
|
3422
|
+
truncationDirection: direction,
|
|
2198
3423
|
});
|
|
2199
3424
|
}
|
|
2200
3425
|
|
|
@@ -2207,6 +3432,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2207
3432
|
ignoreResultLimits: scheme === "skill",
|
|
2208
3433
|
immutable: resource.immutable,
|
|
2209
3434
|
raw,
|
|
3435
|
+
truncationDirection: direction,
|
|
3436
|
+
bodyFooterPath: parsedSel.kind === "none" && !raw ? url : undefined,
|
|
2210
3437
|
});
|
|
2211
3438
|
}
|
|
2212
3439
|
|
|
@@ -2214,6 +3441,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2214
3441
|
async #readDirectory(
|
|
2215
3442
|
absolutePath: string,
|
|
2216
3443
|
limit: number | undefined,
|
|
3444
|
+
direction: TruncationDirection,
|
|
2217
3445
|
signal?: AbortSignal,
|
|
2218
3446
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2219
3447
|
const READ_DIRECTORY_MAX_DEPTH = 2;
|
|
@@ -2237,20 +3465,26 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2237
3465
|
const receiptLines = this.session.settings.get("read.receiptBudgetLines");
|
|
2238
3466
|
const receiptBytes = this.session.settings.get("read.receiptBudgetBytes") * 1024;
|
|
2239
3467
|
const sourceLines = tree.totalLines <= 1 ? ["(empty directory)"] : tree.rendered.split("\n");
|
|
2240
|
-
const
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
3468
|
+
const source = sourceLines.join("\n");
|
|
3469
|
+
const window = selectReadWindow(source, sourceLines, direction, receiptLines, receiptBytes);
|
|
3470
|
+
const shownLines = windowOutputLines(window);
|
|
3471
|
+
const shownSegments = windowSegments(window);
|
|
3472
|
+
const shownBytes =
|
|
3473
|
+
shownSegments.reduce((sum, segment) => sum + segment.bytes, 0) + Math.max(0, shownSegments.length - 1);
|
|
3474
|
+
let output = renderReadWindow(window, false, false);
|
|
3475
|
+
if (window.kind !== "full" || tree.truncated) {
|
|
3476
|
+
output += `\n\n${formatListingTruncationNotice(shownLines, shownBytes, absolutePath, direction)}`;
|
|
3477
|
+
}
|
|
3478
|
+
const details: ReadToolDetails = {
|
|
3479
|
+
isDirectory: true,
|
|
3480
|
+
spillEligible: false,
|
|
3481
|
+
resolvedPath: tree.rootPath,
|
|
3482
|
+
...(window.kind !== "full" && direction !== "head" ? { truncation: makeWindowResult(window, source) } : {}),
|
|
3483
|
+
};
|
|
3484
|
+
const resultBuilder = toolResult(details).text(output).sourcePath(tree.rootPath);
|
|
3485
|
+
if (window.kind !== "full" && direction !== "head")
|
|
3486
|
+
resultBuilder.truncationWindows(window, { noticeOwner: "body", maxBytes: receiptBytes });
|
|
3487
|
+
return resultBuilder.done();
|
|
2254
3488
|
}
|
|
2255
3489
|
}
|
|
2256
3490
|
|