Files
codewhale/Cargo.toml
T
Hunter Bown e4255539fc fix(tui): close the scroll-demon class structurally (#1085 regression)
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>
2026-05-10 20:20:25 -05:00

53 lines
1.6 KiB
TOML

[workspace]
members = [
"crates/agent",
"crates/app-server",
"crates/cli",
"crates/config",
"crates/core",
"crates/execpolicy",
"crates/hooks",
"crates/mcp",
"crates/protocol",
"crates/secrets",
"crates/state",
"crates/tools",
"crates/tui",
"crates/tui-core",
]
default-members = ["crates/cli", "crates/app-server", "crates/tui"]
resolver = "2"
[workspace.package]
version = "0.8.28"
edition = "2024"
# Rust 1.88 stabilized `let_chains` in `if`/`while` conditions, which the
# codebase relies on extensively. Cargo enforces this so users on older
# toolchains get a clear "package requires rustc 1.88+" error instead of a
# confusing E0658 from rustc.
rust-version = "1.88"
license = "MIT"
repository = "https://github.com/Hmbown/DeepSeek-TUI"
[workspace.dependencies]
anyhow = "1.0.100"
async-trait = "0.1.89"
axum = { version = "0.8.5", features = ["json"] }
chrono = { version = "0.4.43", features = ["serde"] }
clap = { version = "4.5.54", features = ["derive"] }
clap_complete = "4.5"
dirs = "6.0.0"
reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls"] }
rusqlite = { version = "0.32.1", features = ["bundled"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
thiserror = "2.0"
tokio = { version = "1.49.0", features = ["full"] }
toml = "0.9.7"
sha2 = "0.10"
tower-http = { version = "0.6", features = ["cors"] }
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
uuid = { version = "1.11", features = ["v4"] }