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

  1. Drop the expect: `if let Some(w) = self.windows.get_mut(&self.active_window)` — the subsequent `get_mut(&buffer_id)` already tolerates absence.
  2. When closing a window, kill/detach its terminal processes and drain pending async messages for that window first.
  3. Re-resolve the window that owns the buffer (search all windows by buffer_id) instead of assuming the active window.
  4. 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

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


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)