e4255539fc
Issue #1085 ("TUI viewport drifts down inside alt-screen at end of turn, leaving top rows blank, esp. after sub-agents") was closed in v0.8.18 by adding `reset_terminal_viewport()` to home the cursor on TurnComplete / focus / resize. v0.8.27's flicker fix (`abf3fa66f`) dropped the `\x1b[2J\x1b[3J` deep-clear from that path to stop the double-clear flicker on Ghostty / VSCode / Win10 conhost. That left ratatui's incremental-diff renderer relying on its internal model matching reality — which only holds while nothing else writes to the terminal. Two latent `eprintln!` sites had been quietly emitting raw bytes into the alt-screen for the entire v0.8.x cycle: * `tools/subagent/mod.rs::persist_state_best_effort` (fires whenever the per-step sub-agent state save hits an error; under parallel sub-agents this can fire dozens of times per turn) * `tools/subagent/mod.rs::new_shared_subagent_manager` (fires once on init if the prior state file fails to load) Plus a third found during this fix: * `network_policy.rs::record` (fires every time a network-policy audit write fails) Each eprintln advanced the alt-screen cursor by one row and scrolled the buffer up by one row, but ratatui's renderer didn't know — it kept writing to absolute row positions, which now meant "one row higher than visible." After ~30 leaks the TUI content appeared to drift downward, with a blank band growing above the header. v0.8.18's periodic full-clear had been masking it; v0.8.27's flicker fix unmasked it. Three layers of defence so this class of bug "isn't an option anymore": 1. **`crates/tui/src/runtime_log.rs` — file-backed tracing subscriber + Unix fd-level stderr redirect.** A daily-rolling log file at `~/.deepseek/logs/tui-YYYY-MM-DD.log` is created at TUI startup (right after `EnterAlternateScreen`). A `tracing-subscriber` registry routes `tracing::warn!` / `tracing::error!` calls to it. On Unix, the process's stderr fd is `dup2`'d to the same file for the lifetime of the `TuiLogGuard`. Any future raw `eprintln!` — ours, a panic message, a third-party crate's verbose output — lands in the log file instead of the alt-screen. The guard restores the original stderr fd on drop so shutdown messages still reach the user's terminal. 2. **`tracing::warn!` replacements** for the three known leak sites (`subagent/mod.rs` ×2, `network_policy.rs` ×1). With (1) in place these messages now go to the log file with structured fields (`?err`, `host`, `tool`) instead of opaque text rows in the alt-screen. 3. **Module-level `#![deny(clippy::print_stdout, clippy::print_stderr)]`** on `tools/`, `core/`, `tui/`, `runtime_threads.rs`, and `network_policy.rs`. Any future `eprintln!` / `println!` added to a TUI runtime path fails the lint at compile time. Legitimate CLI-print paths (`main.rs` eval / init / doctor, `runtime_api.rs` server banners, `logging.rs` verbose helpers, `skills/mod.rs` listing utilities, `execpolicy/execpolicycheck.rs` JSON output, `ui::run_event_loop` post-`LeaveAlternateScreen` resume hint, two `#[test] #[ignore]` perf benches in `tui/transcript.rs` / `tui/widgets/mod.rs` / `core/capacity.rs`) keep their existing prints — they all run outside the alt-screen lifetime. The dup2 redirect is Unix-only because there's no equivalent stable Rust API for fd-redirecting `STDERR_FILENO` on Windows; on Windows the tracing-subscriber layer + the clippy denies still apply, and ratatui's own use of crossterm avoids the worst leakage classes. Cross-platform stderr redirect via `SetStdHandle` is a follow-up. The new `runtime_log` module ships with one test (`log_directory_prefers_home`) that pins the `HOME` / `USERPROFILE` / `dirs::home_dir()` resolution order — uses the process-wide `test_support::lock_test_env()` lock for env-mutation safety. Two `#[test] #[ignore]` benches in `tui/transcript.rs` (rail-prefix memory) and `tui/widgets/mod.rs` (transcript scroll bench) and one in `core/capacity.rs` (`bench_compute_profile`) keep their stdout prints via `#[allow(clippy::print_stdout)]` on the individual test. New dependencies: `tracing-subscriber 0.3` (env-filter + fmt features) and `tracing-appender 0.2` at the workspace root, both pulled into `crates/tui` only. Closes the v0.8.28 regression Hunter reported in screenshots: parallel sub-agents running `exec_shell` triggered the scroll demon with the TUI content squeezed into the bottom third of the terminal and ~30 rows of blank above the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>