l0ng-aiandl0ng-ai 6f842c3007 fix(file-tree): stop a watcher event repainting a window with nothing to draw (#249)
* fix(file-tree): stop a watcher event repainting a window with nothing to draw

Issue #243 made two claims. The flicker was fixed independently on main by
4814d94 "fix(remote): keep file-tree listings on screen while they refresh",
which reached the same mechanism separately and went further on two axes. This
is only the other claim — the one in the issue's title, and the one still true
on main: `file_tree_apply_fs_events` ended in an unconditional `cx.notify()`, so
every batch from a watcher that is recursive over the root repainted the whole
window. The overwhelming majority of those batches name a directory the tree has
never listed and does not show: `.git` internals, build output, `node_modules`.
A window with nothing new to draw was being asked to draw, several times a
second, for as long as anything under the root was being written.

* `invalidate_dir` reports whether it reached anything the tree holds, and a
  batch that reached nothing returns without repainting.
* What it did reach is re-read from the callback itself, rather than by
  notifying in order to buy the paint that would have re-read it.
* `land_load` returns `Landed { superseded, changed }` and the caller repaints
  only on `changed`, so a file rewritten in a directory on screen costs no
  frames. `TreeEntry` gains `PartialEq` for that comparison.
* The `.gitignore` whole-cache branch is scoped by `gitignore_reaches_tree`:
  patterns at `D/.gitignore` govern `D` and below, so unless a tracked directory
  sits under `D` there is nothing to refresh. Its `cx.notify()` is deliberately
  kept when the branch *is* taken — `invalidate_all` restarts the search and
  only a paint re-walks it. `InFlight::pending_keys` is new, for the in-flight
  half of that test.

Measured, not asserted. `Tty7App::render` feeds a per-thread draw counter under
`cfg(test)`, and gpui's test build redraws dirty windows from inside
`flush_effects` — so a headless window plus that counter answers "does this
reach render idle?" with no compositor and without the reporter's Wayland
session. Supporting seams: `test_window::harness` / `harness_with_pane` and
`terminal::view::quiet_test_pane`. `render_probe::arm` takes a draw budget so a
repaint-loop regression fails the test instead of hanging the suite — the loop
lives inside one `flush_effects` call and nothing outside it can interrupt it.

Render idle is measured as "stops drawing", not "never draws again": settling
legitimately costs a last frame as the final listing lands, so the count is
taken over a second interval once the first has absorbed that tail. Confirmed
rather than assumed — the count is 1 at 3s and still 1 at 12s.

Numbers, each taken by reverting the hunk it belongs to and re-running:

    five writes under an unlisted directory      5 draws -> 0
    five rewrites of a displayed file           10 draws -> 0
    five .gitignore writes under node_modules   10 draws -> 0
    marks left unread after a gitignore refresh  1      -> 0

The four controls — a settled panel on a non-empty, an empty and a hidden-only
directory, and a real change still arriving — pass either way, which is the
point: they were never the bug. `untracked_paths_leave_no_bookkeeping_behind`
also passes either way, because main's `invalidate_dir` already only marks what
is cached; it is a guard, not a fix.

Two things stated rather than faked. The end-to-end through a real OS watcher
did not survive: the watcher moved into the host layer, which a synthetic
`file_tree_apply_fs_events` cannot drive. For the same reason the gitignore test
asserts the marking and the re-read rather than the recomputed `ignored` flags,
which the host owns — and says so where it stops.

This commit replaces four earlier ones on this branch, squashed because they
were written against the single-host `file_tree` that no longer exists and could
not be replayed onto it. What each contributed:

* 744a08d fix(file-tree): stop the watcher blanking the panel and repainting for
  nothing — the original. Its flicker half is dropped in favour of 4814d94; its
  render-idle half is what this commit is.
* a88544c no-mistakes(review): scope gitignore refresh to the tree and cover
  in-flight loads — the `gitignore_reaches_tree` scoping, kept. Its other half,
  covering in-flight loads in the whole-cache refresh, is not carried: main's
  `invalidate_all` already stales them via `InFlight::invalidate_all`.
* 0ff3b64 no-mistakes(document): correct stale watcher comment in file_tree docs
  — folded into the doc comments here.
* f0e33a4 no-mistakes(document): drop tracked AGENTS.md, move render-probe caveat
  into source — the deletion is moot on this base (the file was never added),
  and the caveat it relocated is in `render_probe`'s doc comment.

Separately and deliberately not fixed here: on macOS the watcher reports paths
through `/private/var` while the cache is keyed by the root as handed in, so a
root reached via a symlink never matches and its changes are missed. Pre-existing
and invisible on Linux; the tests canonicalize around it and say so.

1484 tests pass across the workspace, fmt clean, clippy unchanged from
origin/main's baseline at 57 warnings. The app was not built, launched or driven;
visual acceptance is the owner's.

Refs #243.

* no-mistakes(review): correct watch scope claims, repaint on moved root, skip hidden-panel reads

* no-mistakes(review): exclude the SFTP column from the tree-drawn gate, fix stale docs

Also carries the correction the earlier messages on this branch owe the reader.

The first commit's message (and the CHANGELOG entry it shipped with) asserted
that the file tree watches its root **recursively**, and justified the whole
change on the traffic that supposedly produced: `.git` internals, build output,
everything under `node_modules`. That was wrong, not merely imprecise. The watch
is non-recursive — `sync_watch`'s own doc says so, it covers roots plus expanded
directories, and `WatchedDirs::translate` enforces it per backend. The recursive
watcher belonged to the older single-host design; the premise was carried across
the port to the host-keyed tree without being re-checked.

What is actually reachable, and all this now claims: the tree hears about a
change in a directory it is *displaying*, and a file's contents being rewritten
reports exactly as loudly as a file appearing. Comparing the re-read against
what is already on screen is what stops that repainting a window with nothing
new to draw.

Measured against that reachable case only, by reverting the comparison and
re-running: five rewrites of a file in a displayed directory cost 10 frames and
now cost 0. The figures the first message quoted for writes under an unlisted
directory and for `.gitignore` writes under `node_modules` are withdrawn — the
tests behind them synthesised watcher events this watch cannot deliver, so they
described scenarios the system cannot produce. Those tests have been removed or
reframed as guards on the predicate rather than evidence of a live symptom.

The other corrections in this round:

* A `.git` create or delete cleared the repo-root cache and then took the new
  early return, so nothing re-resolved it and a moved repository root no longer
  re-rooted the tree on an idle window. That regression came in with this change
  and is closed.
* The watcher-driven re-read was not gated on the tree being drawn, so a hidden
  panel did filesystem work it never used to do. It is gated now.
* That gate then assumed the Files tab always draws the local tree, which it
  does not: a connected native-SSH pane substitutes the SFTP browser and the
  local tree is never rendered. The predicate accounts for that too.
* Two stale statements of the recursive premise survived the first sweep, in
  `assets.rs` and `code_editor.rs`, and are corrected.

The SFTP-substitution case is covered by a test at the predicate; the
substitution itself is a render-path branch with no headless seam, so what is
asserted is the predicate's answer rather than the panel's output.

* no-mistakes(review): gate watcher re-read on listings drawn, fix stale docs

* no-mistakes(review): delete withdrawn recursive-watch CHANGELOG entry duplicated by rebases

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-29 16:09:04 +08:00

tty7

tty7

A terminal workbench: shells, sessions, SSH, coding agents.

Pure Rust · GPU rendering on Zed's gpui · VT core from Alacritty


CI Version License Discord

English · 简体中文

Why

  • Fast — ~2× the throughput of Alacritty, Ghostty, or Kitty (benchmarks)
  • Sessions persist — quit or reboot; your shells keep running, no tmux
  • Editor-grade input — completion, syntax highlighting, history search built in; zero config for zsh, bash, fish, PowerShell
  • Agent-aware — recognizes Claude Code & co. in a pane: status, notifications, session resume

Install

Native builds for each platform on Releases:

macOS …-macos-arm64.dmg · …-x86_64.dmg drag into Applications
Windows …-setup.exe · portable ….zip
Linux …-x86_64.AppImage chmod +x and run — x11/wayland libs bundled

What's inside

Input ghost suggestions from history · explained tab completion · syntax highlighting · multi-line editing · click places the caret · ⌃ R fuzzy history
Window tabs & splits · ⌘ P palette · ⌘ F scrollback search · nine themes · IME
Coding agents per-pane agent detection (~17 CLIs): status dot, notifications, branch + diff, resume after reboot, tray icon that signals "needs your input"
SSH native russh stack: profiles with keychain secrets, SFTP panel, port forwarding, jump hosts

Details for every row: docs/features.md. Keybindings: ⌘ , opens Settings — browse and remap everything, tmux preset included (full list).

Benchmarks

Same machine, same day, same 155×40 grid — Apple M1 Pro, macOS 26.3.1, five-run averages (2026-07-04):

tty7 Alacritty Ghostty Kitty
Plaintext IO — 11 MB cat (lower = better) 95 ms 239 ms 179 ms 185 ms
DOOM-fire frame rate (higher = better) 888 fps 485 fps 552 fps 617 fps
Cold-launch memory 116 MB¹ 105 MB 128 MB 130 MB

¹ GUI 105 MB + the persistent daemon 11 MB.

Methodology and one-command reproduction: scripts/bench/.


S
Description
A terminal workbench in pure Rust: shells, persistent sessions, SSH, coding agents. GPU-rendered on Zed's gpui, VT core from Alacritty.
Readme Apache-2.0
34 MiB
Languages
Rust 99.1%
Shell 0.5%
PowerShell 0.2%
Inno Setup 0.1%