Fix workspace accumulation, tab-close popover, scrollbars, drag ghost

- window_state.rs: persist only the main window's workspaces. The aggregator
  flattened every window's tabs into the saved file; main then adopted the
  whole blob on launch, so detached windows' ephemeral tabs (and Pane N
  drag-out artifacts) accumulated without bound.
- TabStrip: portal the close-confirm popover to <body> with fixed,
  viewport-clamped positioning so the horizontally-scrolling strip can't clip
  it and it never runs off a window edge.
- styles.css: make themed ::-webkit-scrollbar global, not just xterm viewport.
- LeafPane: B1 drag-out ghost chip (portal, edge-pinned, orange detach state).
- App.tsx: moveToNewWindow waits briefly for pane registration instead of
  failing instantly on an in-flight spawn/adopt.
- gitignore cargo-test.lo*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-28 20:24:09 +01:00
parent bea6cf2977
commit e6d0040021
9 changed files with 224 additions and 95 deletions

View file

@ -66,29 +66,20 @@ impl WindowsState {
}
}
/// Build the on-disk envelope by concatenating every window's
/// workspaces in stable label order (main first when present, then
/// the rest sorted alphabetically by label — deterministic so the
/// file diff stays stable across no-op saves).
/// Build the on-disk envelope from ONLY the main window's workspaces.
///
/// Detached windows are ephemeral — their tabs are discarded on close
/// (Chrome-style), and only the main window's tabs are meant to survive
/// a restart. Persisting every window's workspaces (the original design)
/// let detached windows' tabs — and the `Pane N` adopt-targets from
/// drag-out — leak into the saved file; on the next launch the main
/// window loaded the whole blob and adopted them all, so they
/// accumulated without bound. Keying the persisted set to the main label
/// makes detached state structurally unable to pollute it.
fn build_envelope(&self) -> Value {
let map = self.per_window.lock();
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_by(|a, b| {
// main first, then alpha
match (a.as_str(), b.as_str()) {
(MAIN_WINDOW_LABEL, _) => std::cmp::Ordering::Less,
(_, MAIN_WINDOW_LABEL) => std::cmp::Ordering::Greater,
(x, y) => x.cmp(y),
}
});
let mut workspaces: Vec<Value> = Vec::new();
for k in keys {
if let Some(list) = map.get(k) {
for w in list {
workspaces.push(w.clone());
}
}
}
let workspaces: Vec<Value> =
map.get(MAIN_WINDOW_LABEL).cloned().unwrap_or_default();
serde_json::json!({
"version": 2,
"workspaces": workspaces,