Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
525dc9a04a | ||
|
|
37338def71 |
@@ -122,6 +122,31 @@ describe("multiview openDraft (#62)", () => {
|
||||
expect(tab.focusedLeafId).toBe(empty);
|
||||
});
|
||||
|
||||
it("keeps the draft form instance when an unrelated session closes (#68)", async () => {
|
||||
const layout = emptyLayout();
|
||||
const tab = layout.tabs[0];
|
||||
const s1 = splitLeaf(tab, "", "s1", "row", true);
|
||||
const draftLeaf = splitLeaf(tab, s1, null, "row", true);
|
||||
tab.focusedLeafId = draftLeaf;
|
||||
const mv = makeMv(layout);
|
||||
document.body.appendChild(mv.el);
|
||||
mv.render();
|
||||
await flush();
|
||||
|
||||
const nameInput = mv.el.querySelector(".mv-draft input[name=name]") as HTMLInputElement;
|
||||
nameInput.value = "wip";
|
||||
nameInput.dispatchEvent(new Event("input"));
|
||||
await flush();
|
||||
expect(leaves(tab.root).find((l) => l.id === draftLeaf)?.draft?.name).toBe("wip");
|
||||
|
||||
const formBefore = mv.el.querySelector(".mv-draft");
|
||||
mv.closeSession("s1");
|
||||
await flush();
|
||||
|
||||
expect(mv.el.querySelector(".mv-draft")).toBe(formBefore);
|
||||
expect((mv.el.querySelector(".mv-draft input[name=name]") as HTMLInputElement).value).toBe("wip");
|
||||
});
|
||||
|
||||
it("seeds a clone's cwd and command into the draft", async () => {
|
||||
const layout = emptyLayout();
|
||||
const mv = makeMv(layout);
|
||||
|
||||
+55
-29
@@ -70,9 +70,11 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
const { pool, layout, sidebar, infoFor, onDisconnect, renderWelcome } = opts;
|
||||
const persist = () => opts.onLayoutChange?.();
|
||||
|
||||
// Draft-pane forms (#57) carry global listeners (dir autocomplete), so each
|
||||
// render tears down the previous generation before rebuilding the canvas.
|
||||
const draftForms = new Set<SpawnFormHandle>();
|
||||
// Draft-pane forms (#57) carry global listeners (dir autocomplete), so forms
|
||||
// whose leaves are gone are destroyed on each render. Forms for draft leaves
|
||||
// that survive the render are reused in place (#68): rebuilding one mid-edit
|
||||
// would drop focus, caret and selection, and re-fetch info/projects/names.
|
||||
const draftForms = new Map<string, SpawnFormHandle>();
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.className = "mv";
|
||||
@@ -150,30 +152,35 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
// Draft pane (#57): an empty pane is a spawn form. Values hang off the
|
||||
// leaf (never persisted) so each draft keeps its own half-filled state;
|
||||
// submitting fills this leaf with the new session in place.
|
||||
const form = createSpawnForm({
|
||||
seed: leaf.draft ?? undefined,
|
||||
class: "mv-draft",
|
||||
onChange: (values) => {
|
||||
leaf.draft = values;
|
||||
},
|
||||
onSuccess: (session) => {
|
||||
// The pane can be closed while the POST is in flight. Mounting the
|
||||
// session onto a leaf no longer in the tree would strand it in no
|
||||
// pane at all, so only claim the leaf while it is still there —
|
||||
// searching every tab, since the operator may have switched tabs
|
||||
// while the request was in flight.
|
||||
const host = layout.tabs.find((t) => leaves(t.root).includes(leaf));
|
||||
if (host) {
|
||||
leaf.sessionId = session.id;
|
||||
leaf.draft = null;
|
||||
host.focusedLeafId = leaf.id;
|
||||
}
|
||||
render();
|
||||
opts.onOpenSessionsChange?.();
|
||||
},
|
||||
});
|
||||
draftForms.add(form);
|
||||
host.appendChild(form.el);
|
||||
const form = draftForms.get(leaf.id);
|
||||
if (form) {
|
||||
host.appendChild(form.el);
|
||||
} else {
|
||||
const created = createSpawnForm({
|
||||
seed: leaf.draft ?? undefined,
|
||||
class: "mv-draft",
|
||||
onChange: (values) => {
|
||||
leaf.draft = values;
|
||||
},
|
||||
onSuccess: (session) => {
|
||||
// The pane can be closed while the POST is in flight. Mounting the
|
||||
// session onto a leaf no longer in the tree would strand it in no
|
||||
// pane at all, so only claim the leaf while it is still there —
|
||||
// searching every tab, since the operator may have switched tabs
|
||||
// while the request was in flight.
|
||||
const host = layout.tabs.find((t) => leaves(t.root).includes(leaf));
|
||||
if (host) {
|
||||
leaf.sessionId = session.id;
|
||||
leaf.draft = null;
|
||||
host.focusedLeafId = leaf.id;
|
||||
}
|
||||
render();
|
||||
opts.onOpenSessionsChange?.();
|
||||
},
|
||||
});
|
||||
draftForms.set(leaf.id, created);
|
||||
host.appendChild(created.el);
|
||||
}
|
||||
}
|
||||
|
||||
const tools = document.createElement("div");
|
||||
@@ -395,9 +402,25 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
|
||||
// ---- top-level render ----
|
||||
|
||||
/** Destroy forms whose leaf is no longer a draft (leaf removed or filled by
|
||||
* a session). Surviving draft leaves keep their form instance (#68). */
|
||||
function pruneForms(tab: { root: Node | null }): void {
|
||||
const alive = new Set(
|
||||
tab.root
|
||||
? leaves(tab.root)
|
||||
.filter((l) => !l.sessionId)
|
||||
.map((l) => l.id)
|
||||
: [],
|
||||
);
|
||||
for (const [id, f] of draftForms) {
|
||||
if (!alive.has(id)) {
|
||||
f.destroy();
|
||||
draftForms.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
for (const f of draftForms) f.destroy();
|
||||
draftForms.clear();
|
||||
renderTabs();
|
||||
canvas.replaceChildren();
|
||||
const tab = activeTab(layout);
|
||||
@@ -406,6 +429,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
el.classList.toggle("mv-mobile-solo", isMobile && layout.tabs.length === 1);
|
||||
|
||||
if (!tab.root) {
|
||||
pruneForms(tab);
|
||||
pool.setVisible([]);
|
||||
pool.setFocus(null);
|
||||
canvas.appendChild(renderWelcome());
|
||||
@@ -421,12 +445,14 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
|
||||
if (isMobile) {
|
||||
renderMobile(tab);
|
||||
pruneForms(tab);
|
||||
persist();
|
||||
return;
|
||||
}
|
||||
|
||||
const hosts: { id: string; host: HTMLElement; info?: AttachInfo }[] = [];
|
||||
canvas.appendChild(buildTree(tab.root, hosts));
|
||||
pruneForms(tab);
|
||||
pool.setVisible(hosts.map((h) => ({ id: h.id, pane: h.host, info: h.info })));
|
||||
syncFocus();
|
||||
persist();
|
||||
|
||||
Reference in New Issue
Block a user