sinelaw/fresh · error
active window present
Error message
active window present
What it means
Panic from `Option::expect` after `self.windows.get_mut(&self.active_window)` in `App::handle_terminal_exited`. The map lookup itself returns `None` when the recorded `active_window` id has no entry in `windows`. The code assumes an active window always exists while async messages (here: a terminal process exiting) are processed, so a missing entry is an invariant breach.
Solutions
- Drop the expect: `if let Some(w) = self.windows.get_mut(&self.active_window)` — the subsequent `get_mut(&buffer_id)` already tolerates absence.
- When closing a window, kill/detach its terminal processes and drain pending async messages for that window first.
- Re-resolve the window that owns the buffer (search all windows by buffer_id) instead of assuming the active window.
- Clear or repoint `active_window` synchronously with window removal.
Example fix
// before
if let Some(state) = self
.windows
.get_mut(&self.active_window)
.map(|w| &mut w.buffers)
.expect("active window present")
.get_mut(&buffer_id)
// after
if let Some(state) = self
.windows
.get_mut(&self.active_window)
.map(|w| &mut w.buffers)
.and_then(|b| b.get_mut(&buffer_id))
{ state.editing_disabled = true; } Defensive patterns
Strategy: validation
Validate before calling
if !app.windows.contains_key(&app.active_window) {
return; // window already closed; terminal state update is moot
} Type guard
fn active_buffers_mut(app: &mut App) -> Option<&mut BufferMap> {
app.windows.get_mut(&app.active_window).map(|w| &mut w.buffers)
} Try / catch
// Chain Option instead of expecting:
if let Some(state) = app.windows.get_mut(&app.active_window)
.map(|w| &mut w.buffers)
.and_then(|b| b.get_mut(&buffer_id))
{
state.editing_disabled = true;
} Prevention
- Kill terminal processes and drain their async messages before closing the owning window.
- Never assume the active window in async message handlers; resolve ownership per buffer.
- Model 'no active window' explicitly (Option<WindowId>) and handle it in every handler.
- Add tests for terminal exit after window close.
When it happens
Trigger: A terminal buffer's process exits and `handle_terminal_exited` runs while `self.active_window` points at a window id that was closed/removed — e.g. the terminal pane's window was closed but the async exit message still queued.
Common situations: Closing a window containing a running terminal and then the process exits; stale `active_window` after workspace/window teardown; processing a backlog of async messages after window removal.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- active window present
- has_other_tab
- active window must have a populated split layout
- active window present
- active window present
AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13).
Data as JSON: /api/errors/63e6976520cbc3a6.
Report an issue: GitHub.
Appendix: source
Thrown at crates/fresh-editor/src/app/async_dispatch.rs:1451
// Sync terminal content to buffer (final screen state). This pins
// the viewport to the start of the visible screen, so the dead
// terminal is pixel-identical to its last live frame.
//
// Nothing is appended after it, deliberately. This used to write a
// "[Terminal process exited]" line into the backing file and then
// scroll past the pin to reveal it — which pushed the top of the
// screen out of view, and the first line of an agent's last answer
// is often the part you wanted. The exit is reported on the tab
// title and by the status-bar restart indicator instead, neither of
// which costs a row of output.
self.active_window_mut().sync_terminal_to_buffer(buffer_id);
// Ensure buffer remains read-only with no line numbers
if let Some(state) = self
.windows
.get_mut(&self.active_window)
.map(|w| &mut w.buffers)
.expect("active window present")
.get_mut(&buffer_id)
{
state.editing_disabled = true;
state.margins.configure_for_line_numbers(false);
state.buffer.set_modified(false);
}
// Remove from terminal_buffers so it's no longer treated
// as a terminal — unless we're holding it for a remote
// reconnect to respawn in place (see above).
if !preserve_for_reconnect {
self.active_window_mut().terminal_buffers.remove(&buffer_id);
// Snapshot everything a restart needs *before* the handle is
// closed below, so the buffer can be brought back live in
// place (palette command / status-bar indicator) with the
// same argv precedence a workspace restore would use. The
// reconnect path doesn't need this: it keeps the binding and
// respawns from the still-intact terminal-id-keyed maps.View on GitHub (pinned to 67894ca546)