import assert from "node:assert/strict"; import { test } from "node:test"; import { command, mainStack, parseLayout, stacks, stackOf, titleOf, Workbench, type Layout, type LayoutNode } from "../src/shell/index.ts"; function workbench(layout?: Layout) { let n = 0; return new Workbench({ id: () => `id${++n}`, ...(layout && { layout }) }); } /** A compact picture of the tree: `row(primary[explorer] main[a.md])`. */ function shape(wb: Workbench, node: LayoutNode = wb.layout.root): string { if (node.type === "split") return `${node.axis}(${node.children.map((child) => shape(wb, child)).join(" ")})`; const titles = node.panels.map((id) => { const panel = wb.layout.panels[id]!; return panel.kind === "editor" || panel.kind === "preview" ? panel.input.path : panel.kind; }); return `${node.slot ?? "stack"}[${titles.join(",")}]`; } function vscode() { const wb = workbench(); wb.open({ kind: "explorer", input: { root: "/" } }); wb.open({ kind: "editor", input: { path: "a.md" } }); wb.open({ kind: "terminal", input: { cwd: "/", session: "t1" } }); wb.open({ kind: "agent", input: { conversation: "c1" } }); return wb; } test("kinds open into their slots, which are created where VS Code keeps them", () => { const wb = vscode(); assert.equal(shape(wb), "row(primary[explorer] column(main[a.md] panel[terminal]) secondary[agent])"); }); test("the panel slot spans the whole editor area, not just the main stack", () => { const wb = workbench(); wb.open({ kind: "explorer", input: { root: "/" } }); wb.open({ kind: "editor", input: { path: "a.md" } }); wb.open({ kind: "preview", input: { path: "a.md" } }, { beside: mainStack(wb.layout.root).id, edge: "right" }); wb.open({ kind: "terminal", input: { cwd: "/", session: "t1" } }); assert.equal(shape(wb), "row(primary[explorer] column(row(main[a.md] stack[a.md]) panel[terminal]))"); }); test("opening a view that is already open activates it instead of duplicating it", () => { const wb = vscode(); const a = wb.open({ kind: "editor", input: { path: "a.md" } }); wb.open({ kind: "editor", input: { path: "b.md" } }); assert.equal(wb.open({ kind: "editor", input: { path: "a.md" } }), a); assert.equal(mainStack(wb.layout.root).active, a); assert.equal(wb.panels("editor").length, 2); }); test("closing the last panel of a side stack removes it, and the main stack always survives", () => { const wb = vscode(); const terminal = wb.panels("terminal")[0]!.id; const editor = wb.panels("editor")[0]!.id; wb.dispatch({ type: "close", panel: terminal }); wb.dispatch({ type: "close", panel: editor }); assert.equal(shape(wb), "row(primary[explorer] main[] secondary[agent])"); assert.equal(Object.keys(wb.layout.panels).length, 2); }); test("closing the active tab activates its neighbour", () => { const wb = workbench(); wb.open({ kind: "editor", input: { path: "a.md" } }); const b = wb.open({ kind: "editor", input: { path: "b.md" } }); const c = wb.open({ kind: "editor", input: { path: "c.md" } }); wb.dispatch({ type: "activate", panel: b }); wb.dispatch({ type: "close", panel: b }); assert.equal(mainStack(wb.layout.root).active, c); }); test("a closed slot's stack is recreated on its edge with its default share", () => { const wb = vscode(); wb.dispatch({ type: "close", panel: wb.panels("explorer")[0]!.id }); wb.open({ kind: "explorer", input: { root: "/" } }); assert.equal(shape(wb), "row(primary[explorer] column(main[a.md] panel[terminal]) secondary[agent])"); const root = wb.layout.root; assert.equal(root.type, "split"); assert.ok(root.type === "split" && Math.abs(root.sizes[0]! - 0.2) < 1e-9); }); test("moving a panel beside its stack splits it, and a same-axis split takes a sibling instead of nesting", () => { const wb = workbench(); wb.open({ kind: "editor", input: { path: "a.md" } }); const b = wb.open({ kind: "editor", input: { path: "b.md" } }); const c = wb.open({ kind: "editor", input: { path: "c.md" } }); const main = mainStack(wb.layout.root).id; wb.dispatch({ type: "move", panel: b, to: { beside: main, edge: "right" } }); wb.dispatch({ type: "move", panel: c, to: { beside: main, edge: "right" } }); assert.equal(shape(wb), "row(main[a.md] stack[c.md] stack[b.md])"); const root = wb.layout.root; assert.ok(root.type === "split"); assert.deepEqual(root.sizes, [0.25, 0.25, 0.5]); }); test("moving the only panel of a stack beside that stack leaves one stack, not an empty one", () => { const wb = workbench(); wb.open({ kind: "editor", input: { path: "a.md" } }); const b = wb.open({ kind: "editor", input: { path: "b.md" } }); wb.dispatch({ type: "move", panel: b, to: { beside: mainStack(wb.layout.root).id, edge: "bottom" } }); const lone = stackOf(wb.layout.root, b)!.id; wb.dispatch({ type: "move", panel: b, to: { beside: lone, edge: "right" } }); assert.equal(shape(wb), "column(main[a.md] stack[b.md])"); }); test("moving a panel into another stack joins it, and an emptied split collapses", () => { const wb = workbench(); wb.open({ kind: "editor", input: { path: "a.md" } }); const b = wb.open({ kind: "editor", input: { path: "b.md" } }); const main = mainStack(wb.layout.root).id; wb.dispatch({ type: "move", panel: b, to: { beside: main, edge: "right" } }); wb.dispatch({ type: "move", panel: b, to: { stack: main, index: 0 } }); assert.equal(shape(wb), "main[b.md,a.md]"); }); test("moving within one stack reorders its tabs", () => { const wb = workbench(); const a = wb.open({ kind: "editor", input: { path: "a.md" } }); wb.open({ kind: "editor", input: { path: "b.md" } }); wb.open({ kind: "editor", input: { path: "c.md" } }); wb.dispatch({ type: "move", panel: a, to: { stack: mainStack(wb.layout.root).id, index: 2 } }); assert.equal(shape(wb), "main[b.md,c.md,a.md]"); }); /** Each floating window's tabs, by path or kind. */ function windows(wb: Workbench): string[][] { return wb.layout.floating.map((stack) => stack.panels.map((id) => { const panel = wb.layout.panels[id]!; return panel.kind === "editor" || panel.kind === "preview" ? panel.input.path : panel.kind; })); } test("side bars split only top and bottom; beside one's side edge joins it", () => { const wb = vscode(); const primary = stacks(wb.layout.root).find((stack) => stack.slot === "primary")!.id; const b = wb.open({ kind: "editor", input: { path: "b.md" } }); wb.dispatch({ type: "move", panel: b, to: { beside: primary, edge: "right" } }); assert.equal(shape(wb), "row(primary[explorer,b.md] column(main[a.md] panel[terminal]) secondary[agent])"); wb.dispatch({ type: "move", panel: b, to: { beside: primary, edge: "bottom" } }); assert.equal(shape(wb), "row(column(primary[explorer] stack[b.md]) column(main[a.md] panel[terminal]) secondary[agent])"); }); test("floating a panel opens a window stack for it, and its emptied stack collapses", () => { const wb = vscode(); const terminal = wb.panels("terminal")[0]!.id; wb.dispatch({ type: "float", panel: terminal }); assert.equal(shape(wb), "row(primary[explorer] main[a.md] secondary[agent])"); assert.deepEqual(windows(wb), [["terminal"]]); assert.ok(terminal in wb.layout.panels); }); test("panels move into a floating window, and beside one joins it, since a window doesn't split", () => { const wb = workbench(); const a = wb.open({ kind: "editor", input: { path: "a.md" } }); const b = wb.open({ kind: "editor", input: { path: "b.md" } }); const c = wb.open({ kind: "editor", input: { path: "c.md" } }); wb.dispatch({ type: "float", panel: a }); const window = wb.layout.floating[0]!.id; const focus = wb.layout.focus; wb.dispatch({ type: "move", panel: b, to: { stack: window, index: 0 } }); wb.dispatch({ type: "move", panel: c, to: { beside: window, edge: "right" } }); assert.deepEqual(windows(wb), [["b.md", "a.md", "c.md"]]); assert.equal(wb.layout.floating[0]!.active, c); assert.equal(wb.layout.focus, focus, "a window never takes layout focus"); wb.dispatch({ type: "activate", panel: a }); assert.equal(wb.layout.floating[0]!.active, a); }); test("a floating panel docks back with move, and an emptied window goes away", () => { const wb = workbench(); wb.open({ kind: "editor", input: { path: "a.md" } }); const b = wb.open({ kind: "editor", input: { path: "b.md" } }); wb.dispatch({ type: "float", panel: b }); wb.dispatch({ type: "move", panel: b, to: { beside: mainStack(wb.layout.root).id, edge: "right" } }); assert.equal(shape(wb), "row(main[a.md] stack[b.md])"); assert.deepEqual(windows(wb), []); wb.dispatch({ type: "float", panel: b }); wb.dispatch({ type: "close", panel: b }); assert.deepEqual(windows(wb), []); assert.ok(!(b in wb.layout.panels)); }); test("floating the only panel of a window opens another window", () => { const wb = workbench(); const a = wb.open({ kind: "editor", input: { path: "a.md" } }); wb.dispatch({ type: "float", panel: a }); const first = wb.layout.floating[0]!.id; wb.dispatch({ type: "float", panel: a }); assert.equal(wb.layout.floating.length, 1); assert.notEqual(wb.layout.floating[0]!.id, first); }); test("a stored layout's floating panels come back to their slots, since its windows are gone", () => { const wb = vscode(); wb.dispatch({ type: "float", panel: wb.panels("terminal")[0]!.id }); wb.dispatch({ type: "move", panel: wb.panels("agent")[0]!.id, to: { stack: wb.layout.floating[0]!.id } }); const parsed = parseLayout(JSON.parse(JSON.stringify(wb.layout))); assert.deepEqual(parsed?.floating, wb.layout.floating); const restored = workbench(parsed); assert.equal(shape(restored), "row(primary[explorer] column(main[a.md] panel[terminal]) secondary[agent])"); assert.deepEqual(windows(restored), []); }); test("parsing keeps floating stacks unslotted and drops panels a stack already shows", () => { const layout = JSON.parse(JSON.stringify(vscode().layout)); const shown = Object.keys(layout.panels)[0]; const floating = [{ type: "stack", id: "w", slot: "main", panels: [shown, "nope"], active: shown }]; assert.deepEqual(parseLayout({ ...layout, floating })?.floating, []); const { floating: _, ...older } = layout; assert.deepEqual(parseLayout(older)?.floating, []); }); test("nested splits of one axis flatten when the split between them collapses", () => { const wb = workbench(); wb.open({ kind: "editor", input: { path: "a.md" } }); const b = wb.open({ kind: "editor", input: { path: "b.md" } }); const c = wb.open({ kind: "editor", input: { path: "c.md" } }); const main = mainStack(wb.layout.root).id; wb.dispatch({ type: "move", panel: b, to: { beside: main, edge: "right" } }); wb.dispatch({ type: "move", panel: c, to: { beside: stackOf(wb.layout.root, b)!.id, edge: "bottom" } }); assert.equal(shape(wb), "row(main[a.md] column(stack[b.md] stack[c.md]))"); wb.dispatch({ type: "close", panel: b }); assert.equal(shape(wb), "row(main[a.md] stack[c.md])"); }); test("toggling a slot hides its stack, moves focus off it, and opening into it reveals it", () => { const wb = vscode(); wb.dispatch({ type: "focus", stack: stackOf(wb.layout.root, wb.panels("explorer")[0]!.id)!.id }); command("layout.toggle.primary")!.run(wb); assert.deepEqual(wb.layout.hidden, ["primary"]); assert.equal(wb.layout.focus, mainStack(wb.layout.root).id); assert.match(shape(wb), /primary\[explorer\]/, "a hidden stack keeps its place and size"); wb.open({ kind: "explorer", input: { root: "/" } }); assert.deepEqual(wb.layout.hidden, []); }); test("resize normalizes sizes and ignores a size list of the wrong length", () => { const wb = vscode(); const root = wb.layout.root; assert.ok(root.type === "split"); wb.dispatch({ type: "resize", split: root.id, sizes: [1, 2, 1] }); assert.deepEqual((wb.layout.root as typeof root).sizes, [0.25, 0.5, 0.25]); const before = wb.layout; wb.dispatch({ type: "resize", split: root.id, sizes: [1, 1] }); assert.equal(wb.layout, before); }); test("subscribers hear about changes, not no-ops", () => { const wb = vscode(); let calls = 0; wb.subscribe(() => calls++); wb.dispatch({ type: "close", panel: "missing" as never }); wb.dispatch({ type: "toggle", slot: "panel" }); assert.equal(calls, 1); }); test("split commands move the active panel into a new group", () => { const wb = workbench(); wb.open({ kind: "editor", input: { path: "a.md" } }); wb.open({ kind: "editor", input: { path: "b.md" } }); command("panel.split.right")!.run(wb); assert.equal(shape(wb), "row(main[a.md] stack[b.md])"); assert.equal(wb.layout.focus, stacks(wb.layout.root)[1]!.id); }); test("processes open beside the terminal; a browser is one panel per origin, titled by its host", () => { const wb = vscode(); wb.open({ kind: "processes", input: { sandbox: "default" } }); const browser = wb.open({ kind: "browser", input: { url: "http://localhost:3000/" } }); assert.equal(wb.open({ kind: "browser", input: { url: "http://localhost:3000/about" } }), browser); wb.open({ kind: "browser", input: { url: "http://localhost:5173/" } }); assert.equal(shape(wb), "row(primary[explorer] column(main[a.md,browser,browser] panel[terminal,processes]) secondary[agent])"); assert.deepEqual(wb.panels("browser").map((p) => titleOf(p)), ["localhost:3000", "localhost:5173"]); assert.equal(titleOf({ kind: "processes", input: { sandbox: "default" } }), "Processes"); const parsed = parseLayout(JSON.parse(JSON.stringify(wb.layout))); assert.deepEqual(parsed, wb.layout); const panels = { ...wb.layout.panels, [browser]: { id: browser, kind: "browser", input: { url: 3000 } } }; assert.equal(parseLayout({ ...JSON.parse(JSON.stringify(wb.layout)), panels }), undefined); }); test("a preview bound to no server is titled Preview, survives a stored layout, and retargets in place", () => { const wb = vscode(); const preview = wb.open({ kind: "browser", input: { url: null } }); const other = wb.open({ kind: "browser", input: { url: "http://localhost:5173/" } }); assert.equal(titleOf(wb.layout.panels[preview]!), "Preview"); const stored = JSON.parse(JSON.stringify(wb.layout)); assert.deepEqual(parseLayout(stored), wb.layout); const panels = { ...stored.panels, [preview]: { id: preview, kind: "browser", input: {} } }; assert.equal(parseLayout({ ...stored, panels }), undefined); assert.equal(parseLayout({ ...stored, panels: { ...stored.panels, x: { id: "x", kind: "editor", input: { path: null } } } }), undefined); wb.dispatch({ type: "retarget", panel: preview, kind: "browser", input: { url: "http://localhost:5173/about" } }); assert.deepEqual(wb.layout.panels[preview]!.input, { url: null }); wb.dispatch({ type: "retarget", panel: preview, kind: "editor", input: { path: "b.md" } }); assert.deepEqual(wb.layout.panels[preview]!.input, { url: null }); wb.dispatch({ type: "retarget", panel: preview, kind: "browser", input: { url: "http://localhost:3000/" } }); assert.deepEqual(wb.panels("browser").map((p) => [p.id, titleOf(p)]), [[preview, "localhost:3000"], [other, "localhost:5173"]]); assert.equal(shape(wb), "row(primary[explorer] column(main[a.md,browser,browser] panel[terminal]) secondary[agent])"); }); test("a stored layout round-trips through JSON", () => { const wb = vscode(); const parsed = parseLayout(JSON.parse(JSON.stringify(wb.layout))); assert.deepEqual(parsed, wb.layout); }); test("parsing rejects a layout without exactly one main stack, and drops unknown or unshown panels", () => { const layout = JSON.parse(JSON.stringify(vscode().layout)); assert.equal(parseLayout({ ...layout, root: { type: "stack", id: "x", panels: [], active: null } }), undefined); assert.equal(parseLayout({ ...layout, version: 2 }), undefined); const orphan = { id: "orphan", kind: "editor", input: { path: "z.md" } }; const parsed = parseLayout({ ...layout, panels: { ...layout.panels, orphan } }); assert.ok(parsed && !("orphan" in parsed.panels)); const badInput = { ...layout.panels, orphan: { ...orphan, input: { path: 1 } } }; assert.equal(parseLayout({ ...layout, panels: badInput }), undefined); });