fix(daemon): reseed id counter on restore + heal duplicate leaves
Root cause of the multi-focus/multi-search/linked-terminal bug: the in-memory id counter resets to 0 each daemon start, but restore() never advanced it past restored ids. After a restart new_surface_id() re-minted existing ids → the same surface_id appeared twice in a layout tree (rendered as two panels sharing focus, search bar, and output channel — one ends up blank). Session-persistence made restarts routine, surfacing the latent bug. - restore() now reseeds the counter to max(restored id)+1 - ops::dedupe_leaves heals an already-corrupted persisted tree on load Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,32 @@ pub fn remove_leaf(root: LayoutNode, target: &SurfaceId) -> Option<LayoutNode> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop duplicate leaves, keeping the first (left-to-right) occurrence of each
|
||||
/// surface id; collapses now-single-child splits. Returns None if empty.
|
||||
///
|
||||
/// Heals a tree corrupted by a duplicate surface id (e.g. an id re-minted after
|
||||
/// a daemon restart before the counter fix), which otherwise renders the same
|
||||
/// panel twice and confuses focus/search/output routing keyed by surface id.
|
||||
pub fn dedupe_leaves(root: LayoutNode) -> Option<LayoutNode> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
dedupe(root, &mut seen)
|
||||
}
|
||||
fn dedupe(node: LayoutNode, seen: &mut std::collections::HashSet<SurfaceId>) -> Option<LayoutNode> {
|
||||
match node {
|
||||
LayoutNode::Leaf { surface_id } => {
|
||||
if seen.insert(surface_id.clone()) { Some(LayoutNode::Leaf { surface_id }) } else { None }
|
||||
}
|
||||
LayoutNode::Split { orient, children, .. } => {
|
||||
let kept: Vec<LayoutNode> = children.into_iter().filter_map(|c| dedupe(c, seen)).collect();
|
||||
match kept.len() {
|
||||
0 => None,
|
||||
1 => Some(kept.into_iter().next().unwrap()),
|
||||
n => Some(LayoutNode::Split { orient, ratios: even(n), children: kept }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set ratios on the split node addressed by `path` (child indices from root).
|
||||
/// Normalizes to sum 1.0 and clamps each to >= MIN_RATIO. Returns false if the
|
||||
/// path is invalid or the length does not match the node's child count.
|
||||
@@ -247,6 +273,27 @@ mod tests {
|
||||
assert_eq!(leaves(&after), vec![sid("s_2"), sid("s_1")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedupe_removes_duplicate_leaf_keeping_first() {
|
||||
// s_1 appears twice (the production corruption): heal to one occurrence.
|
||||
let root = LayoutNode::Split {
|
||||
orient: Orient::H, ratios: vec![1.0/3.0; 3],
|
||||
children: vec![LayoutNode::leaf(sid("s_0")), LayoutNode::leaf(sid("s_1")), LayoutNode::leaf(sid("s_1"))],
|
||||
};
|
||||
let healed = dedupe_leaves(root).unwrap();
|
||||
assert_eq!(leaves(&healed), vec![sid("s_0"), sid("s_1")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedupe_clean_tree_is_unchanged() {
|
||||
let root = LayoutNode::Split {
|
||||
orient: Orient::H, ratios: vec![0.5, 0.5],
|
||||
children: vec![LayoutNode::leaf(sid("s_0")), LayoutNode::leaf(sid("s_1"))],
|
||||
};
|
||||
let out = dedupe_leaves(root.clone()).unwrap();
|
||||
assert_eq!(leaves(&out), vec![sid("s_0"), sid("s_1")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_onto_self_is_noop() {
|
||||
let root = LayoutNode::leaf(sid("s_1"));
|
||||
|
||||
Reference in New Issue
Block a user