Files

3.4 KiB
Raw Permalink Blame History

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 continues 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:131flush_range with the should_emit guard
  • app/src/ai/blocklist/action_model/execute.rs:10291045Segments 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 continues. 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.