Merge remote-tracking branch 'origin/main' into refresh
ci / web (push) Successful in 55s
ci / rust (push) Successful in 3m27s
ci / flake (push) Successful in 6m42s

This commit is contained in:
Miguel Palhas
2026-08-31 09:58:18 +01:00
2 changed files with 100 additions and 1 deletions
+96 -1
View File
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { emptyLayout, leaves, type Layout } from "./layout.js";
import { addTab, emptyLayout, leaves, type Layout } from "./layout.js";
import { splitLeaf } from "./layout.js";
import { createMultiView, type MultiView } from "./multiview.js";
import type { ConnectionPool } from "./connections.js";
@@ -333,3 +333,98 @@ describe("openDraft newTab (#100)", () => {
expect(leaves(tab.root)).toHaveLength(2);
});
});
describe("multiview closeSession tab cleanup (#137)", () => {
beforeEach(() => {
stubFetch();
document.body.replaceChildren();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("closes the tab when its last session goes away", () => {
const layout = emptyLayout();
const first = layout.tabs[0];
first.focusedLeafId = splitLeaf(first, "", "s1", "row", true);
const second = addTab(layout);
second.focusedLeafId = splitLeaf(second, "", "s2", "row", true);
const mv = makeMv(layout);
document.body.appendChild(mv.el);
mv.render();
mv.closeSession("s2");
expect(layout.tabs).toHaveLength(1);
expect(layout.tabs[0].id).toBe(first.id);
expect(layout.activeTabId).toBe(first.id);
});
it("closes a background tab without stealing focus from the active one", () => {
const layout = emptyLayout();
const background = layout.tabs[0];
background.focusedLeafId = splitLeaf(background, "", "s1", "row", true);
const active = addTab(layout);
active.focusedLeafId = splitLeaf(active, "", "s2", "row", true);
const mv = makeMv(layout);
document.body.appendChild(mv.el);
mv.render();
mv.closeSession("s1");
expect(layout.tabs).toHaveLength(1);
expect(layout.tabs[0].id).toBe(active.id);
expect(layout.activeTabId).toBe(active.id);
});
it("keeps a tab that still holds another pane", () => {
const layout = emptyLayout();
const tab = layout.tabs[0];
const first = splitLeaf(tab, "", "s1", "row", true);
tab.focusedLeafId = splitLeaf(tab, first, "s2", "row", true);
addTab(layout);
layout.activeTabId = tab.id;
const mv = makeMv(layout);
document.body.appendChild(mv.el);
mv.render();
mv.closeSession("s2");
expect(layout.tabs).toHaveLength(2);
expect(leaves(tab.root)).toHaveLength(1);
});
it("keeps a tab holding a draft pane", async () => {
const layout = emptyLayout();
const tab = layout.tabs[0];
const s1 = splitLeaf(tab, "", "s1", "row", true);
splitLeaf(tab, s1, null, "row", true);
addTab(layout);
layout.activeTabId = tab.id;
const mv = makeMv(layout);
document.body.appendChild(mv.el);
mv.render();
await flush();
mv.closeSession("s1");
expect(layout.tabs).toHaveLength(2);
expect(leaves(tab.root)).toHaveLength(1);
});
it("never closes the only tab", () => {
const layout = emptyLayout();
const tab = layout.tabs[0];
tab.focusedLeafId = splitLeaf(tab, "", "s1", "row", true);
const mv = makeMv(layout);
document.body.appendChild(mv.el);
mv.render();
mv.closeSession("s1");
expect(layout.tabs).toHaveLength(1);
expect(layout.tabs[0].root).toBeNull();
expect(layout.activeTabId).toBe(tab.id);
});
});
+4
View File
@@ -553,6 +553,10 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
if (loc.tab.focusedLeafId === loc.leaf.id) {
loc.tab.focusedLeafId = leaves(loc.tab.root)[0]?.id ?? null;
}
// A tab that held nothing but the dead session goes with it (#137): a killed
// session should not leave an empty tab behind for the operator to close by
// hand. `closeTab` keeps the last tab (emptied) so there is always one.
if (!loc.tab.root) closeTab(layout, loc.tab.id);
render();
}