{"record":{"id":"400f68024aff37db","repo":"sinelaw/fresh","slug":"has-other-tab","errorCode":null,"errorMessage":"has_other_tab","messagePattern":"has_other_tab","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/fresh-editor/src/app/buffer_close.rs","lineNumber":765,"sourceCode":"            if !has_other_tab {\n                // This is genuinely the only tab in this split — close it.\n                self.handle_close_split(split_id.into());\n                self.sync_terminal_mode_to_active_buffer();\n                return true;\n            }\n\n            // Pick the tab to activate after removal: the one before the\n            // closed tab (or the next one if we closed the first). This\n            // mirrors the previous buffer-only behaviour but can also land\n            // on a remaining group tab.\n            let replacement = if closing_idx > 0 {\n                targets[closing_idx - 1]\n            } else {\n                // First remaining target after the closed one.\n                *targets\n                    .iter()\n                    .find(|t| **t != closing)\n                    .expect(\"has_other_tab\")\n            };\n\n            // Activate the replacement tab and drop the closed one. The buffer\n            // case must move the split tree AND the `SplitViewState.active_buffer`\n            // together: routing it through `set_pane_buffer` (not the tree-only\n            // `set_split_buffer`) is the fix for the cursor desync — updating\n            // only the tree stranded the view-state on the just-closed buffer,\n            // so the cursor and render read its zeroed view-state while edits\n            // applied to the tree's (different) buffer.\n            match replacement {\n                TabTarget::Buffer(replacement_buffer) => {\n                    self.active_window_mut()\n                        .set_pane_buffer(split_id, replacement_buffer);\n                    // The replacement is active now, so removing the closed\n                    // buffer also frees its keyed view-state (`remove_buffer`\n                    // refuses to drop the state of whatever is still active).\n                    if let Some(view_state) = self\n                        .windows","sourceCodeStart":747,"sourceCodeEnd":783,"githubUrl":"https://github.com/sinelaw/fresh/blob/67894ca5463dbd7a89bb31add4627c27d6b79d83/crates/fresh-editor/src/app/buffer_close.rs#L747-L783","documentation":"Panic from `.expect(\"has_other_tab\")` in `close_tab_in_split` (crates/fresh-editor/src/app/buffer_close.rs:765). After removing the closing target from `targets`, the code assumes at least one other tab remains in the split to activate; when the closed tab was the only distinct target, `find` returns None and the assert fires.","triggerScenarios":"Closing a tab whose split contains only that one target while the earlier `closing_idx == 0 && targets.len() == 1` branch is reached (i.e. `targets` filtered to `!= closing` is empty).","commonSituations":"Closing the last remaining tab in a split where the tab-closing logic already removed the split (ordering mismatch with the `split_tabs.len() <= 1` branch); duplicated TabTarget entries that make `position`/dedupe logic select an unexpected index; calling close on an already-closing buffer.","solutions":["Check `targets.len() > 1` / existence of a non-closing target before entering the replacement-selection branch and route single-tab splits to the split-close path","Guard with `if let Some(next) = ... else { return }` and close the split instead of panicking","Ensure the earlier `handle_close_split` branch catches all single-target splits so this line is only reached with leftovers","Add a unit test: close the sole tab of a split"],"exampleFix":"// before\n*targets.iter().find(|t| **t != closing).expect(\"has_other_tab\")\n// after\nmatch targets.iter().find(|t| **t != closing) {\n    Some(t) => *t,\n    None => return self.handle_close_split(split_id.into()),\n}","handlingStrategy":"type-guard","validationCode":"let other = targets.iter().find(|t| *t != closing);\nif other.is_none() { /* route to handle_close_split instead of continuing */ }","typeGuard":"fn replacement_target<'a>(targets: &'a [TabTarget], closing: &TabTarget) -> Option<&'a TabTarget> {\n    targets.iter().find(|t| *t != closing)\n}","tryCatchPattern":"match targets.iter().find(|t| *t != closing) { Some(t) => activate(t), None => close_split(split_id) }","preventionTips":["Always handle the single-tab split case explicitly before selecting a replacement","Deduplicate TabTarget entries before index math","Unit-test closing the only tab in a split","Never .expect on iterator .find over user-mutable collections"],"tags":["rust","panic","invariant","tab-management"],"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-16T04:17:20.429Z"}