sinelaw/fresh · error
active window must have a populated split layout
Error message
active window must have a populated split layout
What it means
Panic from `.expect("active window must have a populated split layout")` in `close_buffer_internal` (buffer_close.rs:132). After resolving which buffer to close, the code fetches the active window's split manager to find the active split for the replacement buffer. The expect asserts two invariants: the active window exists AND its `buffers.splits()` layout is populated. It panics when either the window is gone or the split tree is empty/uninitialized at that point.
Solutions
- Handle the None case explicitly: if there is no active split layout, fall back to a plain buffer close without replacement bookkeeping.
- Guarantee every window always has a populated split layout (initialize splits at window creation; only dismantle after close completes).
- Defer the replacement/split-update logic until after confirming the window and its splits are alive.
- Add an assertion/test that `buffers.splits()` is Some whenever a buffer close is processed.
Example fix
// before
.and_then(|w| w.buffers.splits())
.map(|(mgr, _)| mgr)
.expect("active window must have a populated split layout")
.active_split()
// after
let Some(active_split) = self
.windows
.get(&self.active_window)
.and_then(|w| w.buffers.splits())
.map(|(mgr, _)| mgr)
.map(|mgr| mgr.active_split())
else {
tracing::debug!("no active split layout; skipping replacement targeting");
return self.close_without_replacement(id);
}; Defensive patterns
Strategy: validation
Validate before calling
if app.windows.get(&app.active_window).and_then(|w| w.buffers.splits()).is_none() {
// no split layout: fall back to a simple close without replacement logic
return app.close_without_replacement(id);
} Type guard
fn active_split_manager(app: &App) -> Option<&SplitManager> {
app.windows.get(&app.active_window)
.and_then(|w| w.buffers.splits())
.map(|(mgr, _)| mgr)
} Try / catch
if std::panic::catch_unwind(AssertUnwindSafe(|| app.close_buffer_internal(id))).is_err() {
tracing::error!("close_buffer_internal panicked: missing split layout");
} Prevention
- Initialize the split layout eagerly at window creation so splits() is never None.
- Tear down splits only after all close flows complete.
- Handle Option from splits() explicitly with a fallback instead of expect.
- Assert split-layout presence in tests covering force_close_buffer and quit-time closes.
When it happens
Trigger: `close_buffer`, `force_close_buffer`, or `close_tab_keeping_editor_leaf_empty` leads to `close_buffer_internal` while `self.windows.get(&self.active_window)` is None or `w.buffers.splits()` returns None (window with no split layout yet, e.g. a freshly created or torn-down window).
Common situations: Force-closing buffers during window close/quit flow before splits are built; closing the last tab in a window whose split tree was already dismantled; UI close events arriving after window teardown.
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/67b68d8d48f2532d.
Report an issue: GitHub.
Appendix: source
Thrown at crates/fresh-editor/src/app/buffer_close.rs:132
// If closing a terminal buffer, tear down its terminal-side state.
// Removing the entry drops the buffer's remembered mode with it.
if let Some(tb) = self.active_window_mut().terminal_buffers.remove(&id) {
self.cleanup_closed_terminal(id, tb.terminal_id);
}
// Capture before resolving the replacement: the last-resort
// `new_buffer()` path calls `set_active_buffer`, which would change
// `active_buffer()` out from under this check.
let closing_active = self.active_buffer() == id;
// The split the replacement lands in.
let active_split = self
.windows
.get(&self.active_window)
.and_then(|w| w.buffers.splits())
.map(|(mgr, _)| mgr)
.expect("active window must have a populated split layout")
.active_split();
let CloseReplacement {
buffer: replacement_buffer,
created_empty: created_empty_buffer,
return_to_group,
} = self.resolve_close_replacement(id, active_split, force_empty_placeholder);
// Switch to replacement buffer BEFORE updating splits.
// Only needed when the closing buffer is the one the user is
// looking at — otherwise the current active buffer stays.
if closing_active {
self.set_active_buffer(replacement_buffer);
// If we landed on a hidden panel buffer to fill the Group-case
// housekeeping slot, scrub the *visible* side effects
// (`open_buffers`, `focus_history`) so the panel buffer doesn't
// appear as a tab. The `keyed_states` entry `switch_buffer`View on GitHub (pinned to 67894ca546)