Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ for GitHub Release notes, so every published version must have a matching
showing follow controls, while existing follow timelines retain append,
detach, pause, and reset behavior.

### Fixed

- Keep forward and backward search sessions aware of committed records appended
after the search began, including when repeating a completed miss.

## [0.6.0] - 2026-07-22

### Added
Expand Down
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,14 @@ committed tail:
fmtview --follow service.jsonl
```

The first frame reverse-scans only enough source data to build the last
viewport; it does not format or index the file from the beginning. A final
record is committed only after its newline arrives, so a writer's partial EOF
record is never displayed early. While the viewport remains at the bottom,
The first frame reverse-scans from EOF to the newest committed record boundary;
that discovery work is proportional to the incomplete EOF suffix, so a
newline-free file is the intentional worst case. It then loads the initial
committed tail under a separate 128-record/4 MiB budget; one record may exceed
the byte budget so it can remain intact. Earlier committed history is not
formatted or indexed from the beginning. A final record is committed only after
its newline arrives, so a writer's partial EOF record is never displayed early.
While the viewport remains at the bottom,
committed appends advance it automatically. Scrolling up changes the footer to
`follow:detached`; scrolling back to the physical bottom reattaches. Press `f`
to pause or resume follow explicitly. Search plus structure, chat-role, and tool
Expand All @@ -286,7 +290,11 @@ after opening.

Follow mode requires a real file and an interactive stdout terminal. Ordinary
viewing and redirected output are unchanged; `--follow` never turns redirected
stdout into an endless stream.
stdout into an endless stream. The built-in file source is intended for
append-oriented logs and detects normal truncation, rotation, replacement, and
copytruncate signals. A producer that performs arbitrary in-place rewrites
without a distinguishable identity or metadata change should provide its own
`RecordTimeline` implementation.

## Embedding a Record Timeline

Expand Down Expand Up @@ -617,9 +625,12 @@ rendered output in memory for browsing.
- Record-like TTY previews, such as JSONL logs, use a lazy path: `fmtview`
sniffs a small prefix to confirm that the input is independent records, then
formats only the records needed for the visible window.
- Follow-mode JSONL opens from a bounded reverse tail scan. Refresh locates the
- Follow-mode JSONL opens from a chunked reverse tail scan. Refresh locates the
newest committed delimiter from EOF, and record/byte budgets bound subsequent
older and newer loads; no persistent full-file index is required.
older and newer loads; no persistent full-file index is required. Committed
boundary discovery work is proportional to the incomplete EOF suffix. The
separate initial committed-tail load is capped at 128 records and 4 MiB,
except that one record may exceed the byte budget.
- Lazy preview writes transformed records into a temporary spool and keeps compact
offsets, not formatted strings, in memory. The title shows `N+` lines while
the session index is still incomplete and idle time extends the index.
Expand Down
5 changes: 5 additions & 0 deletions crates/fmtview-core/src/viewer/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@ impl FileViewer {
self.state
.shift_for_insert(change.inserted_at, change.inserted_lines);
}
if change.appended_lines > 0 {
let end = self.file.line_count();
self.state
.extend_for_append(end.saturating_sub(change.appended_lines), end);
}
if change.appended_lines > 0 && self.state.follow == Some(FollowState::Following) {
set_file_end(&mut self.state, self.file.line_count());
}
Expand Down
Loading