Skip to content

feat(tui): add render diff debug log#2332

Merged
Hmbown merged 2 commits into
Hmbown:mainfrom
cyq1017:codex/fix-2085-tui-render-debug
May 31, 2026
Merged

feat(tui): add render diff debug log#2332
Hmbown merged 2 commits into
Hmbown:mainfrom
cyq1017:codex/fix-2085-tui-render-debug

Conversation

@cyq1017
Copy link
Copy Markdown
Contributor

@cyq1017 cyq1017 commented May 29, 2026

Summary

  • add opt-in CODEWHALE_TUI_DEBUG=1 render-diff logging to the TUI backend
  • append per-frame size, diff cell count, and sampled coordinates to tui-render.log
  • default new installs to the existing CodeWhale log directory while preserving legacy .deepseek/logs fallback
  • avoid logging cell text/content

Notes

Testing

  • cargo test -p codewhale-tui tui::color_compat::tests --all-features --locked
  • cargo test -p codewhale-tui runtime_log::tests::log_directory --all-features --locked
  • cargo check -p codewhale-tui --all-features --locked
  • git diff --check

Part of #2085.

Greptile Summary

This PR adds an opt-in render-diff debug log (CODEWHALE_TUI_DEBUG=1) to the TUI backend and migrates the log_directory() fallback from .deepseek/logs to .codewhale/logs when neither directory exists yet.

  • runtime_log.rs: log_directory() is promoted to pub(crate), the default-fallback path is now .codewhale/logs, and a new test covers the legacy .deepseek/logs detection branch.
  • color_compat.rs: A new RenderDebugLog struct is wired into ColorCompatBackend::draw(). When the env var is truthy, each frame appends a structured line (frame number, viewport size, diff cell count, up to 24 sampled coordinates) to tui-render.log in the existing TUI log directory. Cell text/content is deliberately omitted.

Confidence Score: 5/5

Safe to merge; the feature is fully opt-in behind an env var and the log-directory migration is backwards-compatible with existing .deepseek/logs installs.

The render-diff logging is gated behind CODEWHALE_TUI_DEBUG=1, making it invisible to normal users. The log_directory() migration is backward-compatible — existing .deepseek/logs directories are detected and preserved. All error paths fail gracefully. The sole concern is log usability across sessions, not correctness.

No files require special attention beyond the session-boundary suggestion on color_compat.rs.

Important Files Changed

Filename Overview
crates/tui/src/runtime_log.rs Made log_directory() pub(crate), changed fallback path from .deepseek/logs to .codewhale/logs when neither directory exists, added legacy-directory test, and cleaned up doc comments.
crates/tui/src/tui/color_compat.rs Added opt-in RenderDebugLog that appends per-frame size, diff cell count, and sampled coordinates to tui-render.log; no session boundary markers written between sessions, making multi-session log analysis difficult.

Sequence Diagram

sequenceDiagram
    participant Env as Environment
    participant Backend as ColorCompatBackend
    participant RDL as RenderDebugLog
    participant LogDir as log_directory()
    participant File as tui-render.log

    Backend->>Env: var("CODEWHALE_TUI_DEBUG")
    Env-->>Backend: "1" (enabled)
    Backend->>LogDir: log_directory()
    LogDir-->>Backend: ~/.codewhale/logs
    Backend->>File: OpenOptions::append(true).open()
    File-->>Backend: "RenderDebugLog { file, frame: 0 }"

    loop Each draw() call
        Backend->>Backend: adapt cell colors
        Backend->>Backend: "self.size() -> viewport"
        Backend->>RDL: "record(viewport, &adapted_cells)"
        RDL->>RDL: "frame += 1, sample first 24 (x,y)"
        RDL->>File: "write frame=N size=WxH diff_cells=M sample=..."
        Backend->>Backend: inner.draw(adapted_cells)
    end
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (2): Last reviewed commit: "fix(tui): keep render debug logs in code..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a render debugging feature to the TUI color compatibility backend, allowing frame rendering metrics and cell diff samples to be logged when the CODEWHALE_TUI_DEBUG environment variable is enabled. The feedback recommends addressing a directory resolution issue to ensure new installations default to the .codewhale directory instead of the legacy .deepseek directory. Additionally, the reviewer suggests optimizing the debug line formatting to avoid intermediate allocations and using an RAII guard in tests to guarantee environment variables are restored even in the event of a panic.

Comment on lines +159 to 162
pub(crate) fn log_directory() -> Option<PathBuf> {
let resolve = |base: PathBuf| -> Option<PathBuf> {
let primary = base.join(".codewhale").join("logs");
if primary.exists() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Branding & Directory Creation Issue

Currently, log_directory() resolves to .deepseek/logs if .codewhale/logs does not exist, even if .deepseek/logs also does not exist. For a new user/installation, this will result in the creation of the legacy ~/.deepseek directory instead of the new ~/.codewhale directory.

To fix this and ensure that new installations correctly use the .codewhale directory while maintaining backward compatibility for existing users, the resolution logic should check if .deepseek/logs exists, and if not, default to .codewhale/logs.

Here is how the resolve closure can be updated:

    let resolve = |base: PathBuf| -> Option<PathBuf> {
        let primary = base.join(".codewhale").join("logs");
        if primary.exists() {
            return Some(primary);
        }
        let legacy = base.join(".deepseek").join("logs");
        if legacy.exists() {
            return Some(legacy);
        }
        Some(primary)
    };

Note: You will also need to update the corresponding tests (such as log_directory_prefers_home in runtime_log.rs and backend_writes_render_debug_log_when_enabled in color_compat.rs) to expect .codewhale instead of .deepseek for clean environments.

Comment on lines +219 to +234
fn render_debug_line(
frame: u64,
viewport: Option<Size>,
diff_cells: usize,
sample: &[(u16, u16)],
) -> String {
let size = viewport
.map(|size| format!("{}x{}", size.width, size.height))
.unwrap_or_else(|| "unknown".to_string());
let sample = sample
.iter()
.map(|(x, y)| format!("{x}:{y}"))
.collect::<Vec<_>>()
.join(",");
format!("frame={frame} size={size} diff_cells={diff_cells} sample={sample}\n")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performance Optimization: Avoid Intermediate Allocations

Since render_debug_line is called on every frame when debug logging is enabled, we should avoid allocating intermediate Strings and Vecs for the viewport size and coordinate samples.

Formatting directly into a single pre-allocated String buffer eliminates these allocations entirely, improving rendering performance and reducing GC/allocator pressure.

fn render_debug_line(
    frame: u64,
    viewport: Option<Size>,
    diff_cells: usize,
    sample: &[(u16, u16)],
) -> String {
    use std::fmt::Write;
    let mut line = String::new();
    write!(line, "frame={frame} size=").unwrap();
    if let Some(size) = viewport {
        write!(line, "{}x{}", size.width, size.height).unwrap();
    } else {
        line.push_str("unknown");
    }
    write!(line, " diff_cells={diff_cells} sample=").unwrap();
    for (i, (x, y)) in sample.iter().enumerate() {
        if i > 0 {
            line.push(',');
        }
        write!(line, "{x}:{y}").unwrap();
    }
    line.push('\n');
    line
}

Comment on lines +427 to +472
fn backend_writes_render_debug_log_when_enabled() {
let _lock = lock_test_env();
let tmp = tempfile::tempdir().expect("tempdir");
let previous_home = env::var_os("HOME");
let previous_userprofile = env::var_os("USERPROFILE");
let previous_debug = env::var_os(RENDER_DEBUG_ENV);

// SAFETY: environment mutation is serialized by lock_test_env.
unsafe {
env::set_var("HOME", tmp.path());
env::set_var("USERPROFILE", "");
env::set_var(RENDER_DEBUG_ENV, "1");
}

let writer = SharedWriter::default();
let mut backend = ColorCompatBackend::new(writer, ColorDepth::TrueColor, PaletteMode::Dark);
let mut cell = Cell::default();
cell.set_symbol("x");
backend.draw(std::iter::once((3, 4, &cell))).unwrap();

let log_path = tmp
.path()
.join(".deepseek")
.join("logs")
.join("tui-render.log");
let body = fs::read_to_string(log_path).expect("render debug log");
assert!(body.contains("frame=1"), "{body}");
assert!(body.contains("diff_cells=1"), "{body}");
assert!(body.contains("sample=3:4"), "{body}");

// SAFETY: environment mutation is serialized by lock_test_env.
unsafe {
match previous_home {
Some(value) => env::set_var("HOME", value),
None => env::remove_var("HOME"),
}
match previous_userprofile {
Some(value) => env::set_var("USERPROFILE", value),
None => env::remove_var("USERPROFILE"),
}
match previous_debug {
Some(value) => env::set_var(RENDER_DEBUG_ENV, value),
None => env::remove_var(RENDER_DEBUG_ENV),
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Robustness: Use RAII Guard for Environment Variable Cleanup

If this test panics or fails during execution (e.g., at the fs::read_to_string or assert! statements), the environment variables HOME, USERPROFILE, and CODEWHALE_TUI_DEBUG will not be restored because the cleanup block at the end of the function is skipped during stack unwinding. This can pollute the environment for other tests running in the same process, leading to flaky test suites.

Using an RAII guard (Drop implementation) guarantees that the environment variables are always restored, even if the test panics.

    fn backend_writes_render_debug_log_when_enabled() {
        let _lock = lock_test_env();
        let tmp = tempfile::tempdir().expect("tempdir");

        struct EnvGuard {
            previous_home: Option<std::ffi::OsString>,
            previous_userprofile: Option<std::ffi::OsString>,
            previous_debug: Option<std::ffi::OsString>,
        }

        impl Drop for EnvGuard {
            fn drop(&mut self) {
                // SAFETY: environment mutation is serialized by lock_test_env.
                unsafe {
                    match &self.previous_home {
                        Some(value) => env::set_var("HOME", value),
                        None => env::remove_var("HOME"),
                    }
                    match &self.previous_userprofile {
                        Some(value) => env::set_var("USERPROFILE", value),
                        None => env::remove_var("USERPROFILE"),
                    }
                    match &self.previous_debug {
                        Some(value) => env::set_var(RENDER_DEBUG_ENV, value),
                        None => env::remove_var(RENDER_DEBUG_ENV),
                    }
                }
            }
        }

        let _env_guard = EnvGuard {
            previous_home: env::var_os("HOME"),
            previous_userprofile: env::var_os("USERPROFILE"),
            previous_debug: env::var_os(RENDER_DEBUG_ENV),
        };

        // SAFETY: environment mutation is serialized by lock_test_env.
        unsafe {
            env::set_var("HOME", tmp.path());
            env::set_var("USERPROFILE", "");
            env::set_var(RENDER_DEBUG_ENV, "1");
        }

        let writer = SharedWriter::default();
        let mut backend = ColorCompatBackend::new(writer, ColorDepth::TrueColor, PaletteMode::Dark);
        let mut cell = Cell::default();
        cell.set_symbol("x");
        backend.draw(std::iter::once((3, 4, &cell))).unwrap();

        let log_path = tmp
            .path()
            .join(".deepseek")
            .join("logs")
            .join("tui-render.log");
        let body = fs::read_to_string(log_path).expect("render debug log");
        assert!(body.contains("frame=1"), "{body}");
        assert!(body.contains("diff_cells=1"), "{body}");
        assert!(body.contains("sample=3:4"), "{body}");
    }

tracing::debug!(?err, "failed to create TUI render debug log directory");
return None;
}
let path = log_dir.join("tui-render.log");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 tui-render.log falls under the 7-day pruning filter

is_tui_log_file_name in runtime_log.rs matches any file whose name starts with "tui-" and ends with ".log". tui-render.log satisfies both conditions, so prune_old_logs — called every time runtime_log::init() runs — will silently delete this file once its modification time is older than the configured retention window (default 7 days). Because RenderDebugLog opens the file in append mode, the expected use is accumulating data across many sessions; pruning defeats that intent and destroys the debugging history without any user-visible warning. The per-session rotating files (tui-YYYY-MM-DD-PID.log) follow a different access pattern and the pruning rule was written for them, not for this fixed-name accumulating file.

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/tui/src/tui/color_compat.rs
Comment on lines +187 to +195
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|err| {
tracing::debug!(?err, path = %path.display(), "failed to open TUI render debug log");
err
})
.ok()?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No growth cap or rotation for the accumulating debug log

tui-render.log is opened in append mode with no size limit or rotation. At typical ratatui render rates (up to 60 FPS), each frame line can be ~100–300 bytes, meaning several hundred MB per hour under heavy use. Since this is an opt-in debug flag, the risk is lower, but users who leave CODEWHALE_TUI_DEBUG=1 set for extended sessions could accumulate surprisingly large files with no warning.

Fix in Codex Fix in Claude Code Fix in Cursor

@cyq1017 cyq1017 marked this pull request as ready for review May 29, 2026 01:19
@Hmbown Hmbown merged commit 63be80d into Hmbown:main May 31, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants