fix(tui): forward resize dimensions to ratatui viewport
@imakid reported on Windows PowerShell that clicking the OS restore
button (maximize → windowed) during a long task turns the entire
terminal black, unrecoverable until Ctrl+C. The "refreshing context"
chip in the footer indicates the freeze coincides with a compaction
summary call (engine state `CoherenceState::RefreshingContext`).
Hypothesis (no Windows dev box, awaiting @imakid confirmation):
* Symptom 1 — "transcript stops refreshing" — is the expected
no-stream window during the side-channel compaction summary call.
Tokens don't stream until the summary returns and the next assistant
turn resumes. That's not a bug, but the UI doesn't currently surface
the distinction well.
* Symptom 2 — "black screen on restore-from-maximized" — most likely
comes from a Windows ConHost transient: `crossterm::terminal::size()`
briefly returns stale (maximized) dimensions during the
maximize→windowed transition, while the `Event::Resize(w, h)`
payload itself already carries the correct post-restore size. The
current handler does `terminal.clear() + terminal.draw()` and
relies on ratatui's internal autoresize, which calls
`crossterm::terminal::size()` — meaning we paint a frame sized to
the stale dimensions into the post-restore viewport, leaving most
of the visible area unpainted (the user-reported black screen).
The change forwards the event-reported `(w, h)` to ratatui via
`Terminal::resize(Rect)` before the clear+draw, so the viewport
commit always uses the OS-truthful new size regardless of whether
`crossterm::terminal::size()` has caught up. This is a defensive
change everywhere — the event payload is authoritative on every
platform — and it specifically addresses the Windows ConHost stale-
size race.
Also widens the resize tracing event to include `coherence_state`
and `use_alt_screen` so the next user bug report includes the
context we'd ask for in triage.
Tests
=====
`chat_widget_renders_cleanly_after_resize_during_refreshing_context`
pins the renderer-side invariant: a resize cycle that arrives while
`coherence_state == RefreshingContext` must produce non-empty frames
at every cycled width, and must not mutate the engine's
coherence_state. The actual fix lives in the event-handler size
forwarding; the test guards the renderer's no-empty-buffer contract
so a future regression that gates layout on coherence state would be
caught immediately.
What this PR does NOT change
============================
* No platform-specific code path. The fix is universal — passing the
event-reported size to ratatui is correct everywhere; Windows just
happens to be where the bug manifests today.
* No change to the freeze symptom directly. If symptom 1 is the
expected compaction-summary no-stream window, the right
follow-up is a UX cue ("compacting context, please hold") rather
than a bugfix.
@imakid — please test by installing this branch:
cargo install --git https://github.com/Hmbown/DeepSeek-TUI.git \\
--branch fix/582-powershell-resize
Then run a long task, click the OS restore button mid-task, and
confirm whether the black-screen symptom is gone. If it still
reproduces, please run with `RUST_LOG=deepseek_tui=debug` and post
the resize lines from the log so we can see the dimensions
crossterm/ratatui actually saw.
Verification
============
* `cargo fmt --all -- --check` clean.
* `cargo clippy -p deepseek-tui --bin deepseek-tui --all-features
--locked -- -D warnings` clean.
* `cargo test -p deepseek-tui --bin deepseek-tui --locked` →
2029 passed, 2 ignored.
Refs #582.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1519,7 +1519,13 @@ async fn run_event_loop(
|
||||
}
|
||||
|
||||
if let Event::Resize(width, height) = evt {
|
||||
tracing::debug!(width, height, "Event::Resize received; clearing terminal");
|
||||
tracing::debug!(
|
||||
width,
|
||||
height,
|
||||
coherence = ?app.coherence_state,
|
||||
use_alt_screen = app.use_alt_screen,
|
||||
"Event::Resize received; clearing terminal"
|
||||
);
|
||||
// Drain any further Resize events queued in this poll cycle so we
|
||||
// act on the final size only, then issue a single clear + redraw.
|
||||
// crossterm coalesces some resize events but rapid drag-resizes
|
||||
@@ -1548,6 +1554,28 @@ async fn run_event_loop(
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// #582: commit the event-reported size to ratatui's
|
||||
// viewport explicitly before the redraw, instead of
|
||||
// relying on `crossterm::terminal::size()` which gets
|
||||
// queried internally during `terminal.draw`. On
|
||||
// Windows ConHost specifically, `terminal::size()` has
|
||||
// been observed to return stale dimensions briefly
|
||||
// during a maximize→windowed transition; the next
|
||||
// `draw` then paints into a buffer that does not
|
||||
// match the post-restore viewport, producing the
|
||||
// unrecoverable black screen reported by @imakid.
|
||||
// The `Event::Resize` payload itself carries the
|
||||
// authoritative new size, so we forward it.
|
||||
if let Err(err) = terminal.resize(Rect::new(0, 0, final_w, final_h)) {
|
||||
tracing::warn!(
|
||||
?err,
|
||||
final_w,
|
||||
final_h,
|
||||
"terminal.resize during Resize event failed; falling back to clear+draw"
|
||||
);
|
||||
}
|
||||
|
||||
terminal.clear()?;
|
||||
app.handle_resize(final_w, final_h);
|
||||
// Draw immediately so the cleared screen gets repainted before
|
||||
|
||||
@@ -2587,6 +2587,74 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression for issue #582: a resize event arriving while the
|
||||
/// engine is in `CoherenceState::RefreshingContext` (i.e. running
|
||||
/// a compaction summary call) must NOT leave the chat widget with
|
||||
/// an empty viewport. The user-reported symptom on Windows
|
||||
/// PowerShell is that the screen turns black on the maximize→
|
||||
/// windowed transition during a long task; the post-resize render
|
||||
/// must produce a populated frame regardless of the active
|
||||
/// coherence intervention. Pins the invariant from the renderer
|
||||
/// side; the actual ConHost size-stale fix lives in
|
||||
/// `tui::ui::run_tui` (the `Event::Resize` handler now forwards
|
||||
/// the event-reported dimensions to ratatui's viewport before the
|
||||
/// redraw).
|
||||
#[test]
|
||||
fn chat_widget_renders_cleanly_after_resize_during_refreshing_context() {
|
||||
use crate::core::coherence::CoherenceState;
|
||||
|
||||
let mut app = create_test_app();
|
||||
for i in 0..30 {
|
||||
app.add_message(HistoryCell::User {
|
||||
content: format!("user message {i} during a long-running task"),
|
||||
});
|
||||
}
|
||||
|
||||
// Pretend the engine is mid-compaction when the resize arrives.
|
||||
app.coherence_state = CoherenceState::RefreshingContext;
|
||||
|
||||
// Drive the same shrink-then-grow cycle that maximize→windowed
|
||||
// transitions produce on Windows.
|
||||
for (width, height) in [(140u16, 40u16), (90, 28), (60, 20), (140, 40)] {
|
||||
app.handle_resize(width, height);
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
let mut buf = Buffer::empty(area);
|
||||
let widget = ChatWidget::new(&mut app, area);
|
||||
widget.render(area, &mut buf);
|
||||
|
||||
let mut non_empty = 0usize;
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let sym = buf[(x, y)].symbol();
|
||||
if sym != " " && !sym.is_empty() {
|
||||
non_empty += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
non_empty > 0,
|
||||
"resize-during-RefreshingContext at {width}x{height} produced an empty buffer; \
|
||||
render path must not gate on coherence state (#582)"
|
||||
);
|
||||
}
|
||||
|
||||
// The engine's coherence_state must survive a resize — it is
|
||||
// the engine's runtime decision, not a render-loop concern.
|
||||
// A future regression that bounced the state to `Healthy` on
|
||||
// resize would silently drop the "refreshing context" footer
|
||||
// chip while compaction is still in flight.
|
||||
assert_eq!(
|
||||
app.coherence_state,
|
||||
CoherenceState::RefreshingContext,
|
||||
"resize must not mutate engine-owned coherence_state"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression for issue #65: after `App::handle_resize`, the chat widget
|
||||
/// must produce a clean render at the new width — no stale wrapping,
|
||||
/// no panic, no content exceeding the requested width. Cycling through
|
||||
|
||||
Reference in New Issue
Block a user