Files

37 lines
3.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TECH.md — Fix read_files with out-of-bounds line ranges
## Context
When an LLM requests a `read_files` tool call with line ranges entirely beyond EOF (e.g., lines 18912090 of a 1237-line file), the client returns `ReadFilesResult::Success { files: [] }` — the file silently vanishes from the result. This renders as an empty box with a green checkmark in the blocklist UI, and the server receives `anyFilesSuccess: {}` with zero files.
The root cause is in `TextFileAccumulator::flush_range` (`crates/warp_files/src/text_file_reader.rs:131`). The condition `let should_emit = !self.buf.is_empty() || (final_flush && self.whole_file)` only emits a segment on final flush for whole-file reads. For ranged reads where no lines fall within the requested range, `buf` stays empty and no segment is emitted. The caller (`read_local_file_context` in `app/src/ai/blocklist/action_model/execute.rs:10291045`) extends `file_contexts` from an empty segments iterator, then `continue`s past the binary fallback — the file ends up in neither `file_contexts` nor `missing_files`.
Relevant files:
- `crates/warp_files/src/text_file_reader.rs:131``flush_range` with the `should_emit` guard
- `app/src/ai/blocklist/action_model/execute.rs:10291045``Segments` branch in `read_local_file_context`
- `app/src/ai/blocklist/block/view_impl/output.rs:424450` — rendering logic for `ReadFilesResult::Success`
## Proposed changes
### 1. `TextFileAccumulator::flush_range` — always emit on final flush
Change the `should_emit` condition from `!self.buf.is_empty() || (final_flush && self.whole_file)` to `!self.buf.is_empty() || final_flush`. This makes the behavior consistent: every requested range always produces a `TextFileSegment`, even when the range is entirely past EOF. The emitted segment has `content: ""`, the original requested `line_range`, and `line_count` set to the total file lines (populated by `finalize`).
This is a one-line change. The `whole_file` field remains used for trailing-newline preservation logic, so it is not removed.
### 2. Defensive rendering for empty `file_contexts`
In the `ReadFilesResult::Success` rendering arm, add a match guard `if !file_contexts.is_empty()` for the normal path. Add a second arm `if file_contexts.is_empty()` that renders an error-styled action box with a red X icon and "Failed to read files" message, then `continue`s. This prevents an empty box regardless of upstream cause.
### 3. Tests
- Update existing `empty_file_with_ranges_produces_no_segment` → renamed to `empty_file_with_ranges_produces_empty_segment`, now expects 1 segment with empty content and the original range.
- Add `range_past_eof_produces_empty_segment` — 5-line file, range 10..15, expects 1 segment with empty content, `line_count: 5`.
- Add `multiple_ranges_some_past_eof` — 5-line file, ranges [1..3, 10..15], expects 2 segments: first with content, second empty.
## Testing and validation
The fix is verified by the three unit tests above, all in `crates/warp_files/src/text_file_reader_tests.rs`. They cover:
- Empty file with ranges (previously zero segments, now one)
- Non-empty file with range entirely past EOF (the exact bug scenario)
- Mix of valid and out-of-bounds ranges in the same request
The defensive rendering change is not unit-tested (it requires the full UI framework context) but prevents the user-visible symptom for any future edge case that produces empty `file_contexts`.