Preserve existing panes when applying a preset

Previously: clicking 1 / 2H / 3H / 2V / 2×2 in the titlebar replaced
the whole tree with brand-new empty leaves, killing every shell — and
the only safeguard was a window.confirm() that's easy to miss-click.
The user lost work whenever they reached for a preset.

New behaviour via `reshapeToPreset`:
- The preset's shape is built fresh (1, 2, 3, or 4 slots), then existing
  leaves are spliced into those slots in DFS order. Their id / distro /
  cwd / label / broadcast all carry over, so the same PaneId is still
  mapped — the PTY keeps running.
- If the preset has MORE slots than existing leaves (e.g. 1 pane → 2×2),
  the extra slots stay as fresh empty leaves and new shells spawn there.
  No prompt — pure additive change.
- If the preset has FEWER slots than existing leaves (e.g. 8 panes →
  2×2), the overflow leaves are returned in `dropped`. We confirm with
  the user, and if they accept, kill those PTYs explicitly.

Tradeoff: split ratios reset to 0.5 (the whole point of "apply preset"
is to use its layout). That's an acceptable cost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-22 19:27:50 +01:00
parent 2a0c096095
commit 8c3af8f9ee
2 changed files with 68 additions and 10 deletions

View file

@ -22,6 +22,7 @@ import {
changeLabel,
toggleBroadcast as toggleBroadcastInTree,
setAllBroadcast,
reshapeToPreset,
serialize,
deserialize,
presetSingle,
@ -264,18 +265,33 @@ export default function App() {
const applyPreset = useCallback(
(make: (d: { distro?: string }) => TreeNode) => {
const count = leafCount(tree);
if (
count > 1 &&
!window.confirm(
`Replace current layout (${count} panes)? This kills all open shells.`,
)
) {
return;
const { tree: nextTree, dropped } = reshapeToPreset(tree, make, {
distro: defaultDistro,
});
if (dropped.length > 0) {
const ok = window.confirm(
`This preset has fewer slots than your current ${leafCount(tree)} panes. ${dropped.length} pane${dropped.length === 1 ? "" : "s"} will be closed (their shells will be killed). Continue?`,
);
if (!ok) return;
for (const id of dropped) {
const paneId = paneIdByLeafRef.current.get(id);
if (paneId != null) {
void killPane(paneId).catch((e) =>
console.warn("killPane failed:", e),
);
paneIdByLeafRef.current.delete(id);
}
}
if (activeLeafId && dropped.includes(activeLeafId)) {
setActiveLeafId(null);
}
}
setTree(make({ distro: defaultDistro }));
setTree(nextTree);
},
[tree, defaultDistro],
[tree, defaultDistro, activeLeafId],
);
const paletteLeaves = useMemo<LeafNode[]>(