{"record":{"id":"63e6976520cbc3a6","repo":"sinelaw/fresh","slug":"active-window-present","errorCode":null,"errorMessage":"active window present","messagePattern":"active window present","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/fresh-editor/src/app/async_dispatch.rs","lineNumber":1451,"sourceCode":"            // Sync terminal content to buffer (final screen state). This pins\n            // the viewport to the start of the visible screen, so the dead\n            // terminal is pixel-identical to its last live frame.\n            //\n            // Nothing is appended after it, deliberately. This used to write a\n            // \"[Terminal process exited]\" line into the backing file and then\n            // scroll past the pin to reveal it — which pushed the top of the\n            // screen out of view, and the first line of an agent's last answer\n            // is often the part you wanted. The exit is reported on the tab\n            // title and by the status-bar restart indicator instead, neither of\n            // which costs a row of output.\n            self.active_window_mut().sync_terminal_to_buffer(buffer_id);\n\n            // Ensure buffer remains read-only with no line numbers\n            if let Some(state) = self\n                .windows\n                .get_mut(&self.active_window)\n                .map(|w| &mut w.buffers)\n                .expect(\"active window present\")\n                .get_mut(&buffer_id)\n            {\n                state.editing_disabled = true;\n                state.margins.configure_for_line_numbers(false);\n                state.buffer.set_modified(false);\n            }\n\n            // Remove from terminal_buffers so it's no longer treated\n            // as a terminal — unless we're holding it for a remote\n            // reconnect to respawn in place (see above).\n            if !preserve_for_reconnect {\n                self.active_window_mut().terminal_buffers.remove(&buffer_id);\n                // Snapshot everything a restart needs *before* the handle is\n                // closed below, so the buffer can be brought back live in\n                // place (palette command / status-bar indicator) with the\n                // same argv precedence a workspace restore would use. The\n                // reconnect path doesn't need this: it keeps the binding and\n                // respawns from the still-intact terminal-id-keyed maps.","sourceCodeStart":1433,"sourceCodeEnd":1469,"githubUrl":"https://github.com/sinelaw/fresh/blob/67894ca5463dbd7a89bb31add4627c27d6b79d83/crates/fresh-editor/src/app/async_dispatch.rs#L1433-L1469","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif let Some(state) = self\n    .windows\n    .get_mut(&self.active_window)\n    .map(|w| &mut w.buffers)\n    .expect(\"active window present\")\n    .get_mut(&buffer_id)\n// after\nif let Some(state) = self\n    .windows\n    .get_mut(&self.active_window)\n    .map(|w| &mut w.buffers)\n    .and_then(|b| b.get_mut(&buffer_id))\n{ state.editing_disabled = true; }","handlingStrategy":"validation","validationCode":"if !app.windows.contains_key(&app.active_window) {\n    return; // window already closed; terminal state update is moot\n}","typeGuard":"fn active_buffers_mut(app: &mut App) -> Option<&mut BufferMap> {\n    app.windows.get_mut(&app.active_window).map(|w| &mut w.buffers)\n}","tryCatchPattern":"// Chain Option instead of expecting:\nif let Some(state) = app.windows.get_mut(&app.active_window)\n    .map(|w| &mut w.buffers)\n    .and_then(|b| b.get_mut(&buffer_id))\n{\n    state.editing_disabled = true;\n}","preventionTips":["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."],"tags":["rust","panic","invariant","terminal","async"],"backgroundTag":"internal-invariant-violation","analyzedSha":"67894ca5463dbd7a89bb31add4627c27d6b79d83","analyzedAt":"2026-09-13T15:04:03.701Z","contentChangedAt":"2026-09-13T15:04:03.701Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}