diff --git a/web/src/main.ts b/web/src/main.ts index aa5fd48..7e81f41 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -236,6 +236,9 @@ mv = createMultiView({ infoFor, onDisconnect: closePane, renderWelcome, + // A draft-pane submit (#57) fills its leaf in place; the shell route may flip + // from catalog (no sessions) to attach. + onOpenSessionsChange: updateShellRoute, onLayoutChange: scheduleSave, }); let contentShowsMv = false; diff --git a/web/src/multiview.ts b/web/src/multiview.ts index 777865b..725fd53 100644 --- a/web/src/multiview.ts +++ b/web/src/multiview.ts @@ -1,8 +1,9 @@ // The multi-view renderer/controller (issues #30 splits, #31 tabs, #33 // highlights). Owns the tab bar + canvas inside the content pane, builds the // active tab's split tree into DOM, and mounts a live AttachConnection into each -// session leaf via the pool. Empty leaves (freshly split, not yet filled) show a -// placeholder. Drag-to-dock is a later issue (#32); mobile projection is #35. +// session leaf via the pool. Empty leaves are draft panes (#57): they render +// the spawn form inline, and submitting replaces the draft with that session's +// terminal in place. Drag-to-dock is issue #32; mobile projection is #35. import type { SidebarHandle } from "./catalog.js"; import type { ConnectionPool } from "./connections.js"; @@ -22,6 +23,7 @@ import { } from "./layout.js"; import { sessionColor } from "./sessioncolor.js"; import type { AttachInfo } from "./attach.js"; +import { createSpawnForm, type SpawnFormHandle } from "./spawnForm.js"; export interface MultiViewOptions { pool: ConnectionPool; @@ -33,6 +35,9 @@ export interface MultiViewOptions { onDisconnect: (sessionId: string) => void; /** Welcome content for an empty tab. */ renderWelcome: () => HTMLElement; + /** Called when a draft-pane submit turns an empty pane into a session pane, + * so the shell can re-evaluate its route (sidebar vs attach layout). */ + onOpenSessionsChange?: () => void; /** Called after any layout change (structure, focus, or divider resize) so the * shell can persist it (issue #34). */ onLayoutChange?: () => void; @@ -60,6 +65,10 @@ 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(); + const el = document.createElement("div"); el.className = "mv"; const tabBar = document.createElement("div"); @@ -133,7 +142,26 @@ export function createMultiView(opts: MultiViewOptions): MultiView { const host = document.createElement("div"); host.className = "mv-pane-host"; if (!leaf.sessionId) { - host.innerHTML = `
Empty pane — pick a session from the sidebar.
`; + // 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) => { + leaf.sessionId = session.id; + leaf.draft = null; + const t = activeTab(layout); + if (leaves(t.root).includes(leaf)) t.focusedLeafId = leaf.id; + render(); + opts.onOpenSessionsChange?.(); + }, + }); + draftForms.add(form); + host.appendChild(form.el); } const tools = document.createElement("div"); @@ -326,6 +354,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { // Fill the focused pane (VS Code style: open replaces the active group). const target = (tab.focusedLeafId && findLeaf(tab.root, tab.focusedLeafId)) || leaves(tab.root)[0]; target.sessionId = sessionId; + target.draft = null; tab.focusedLeafId = target.id; render(); } @@ -333,6 +362,8 @@ export function createMultiView(opts: MultiViewOptions): MultiView { // ---- top-level render ---- function render(): void { + for (const f of draftForms) f.destroy(); + draftForms.clear(); renderTabs(); canvas.replaceChildren(); const tab = activeTab(layout); @@ -569,6 +600,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { if (source.kind === "pane" && self) { const dragged = findLeaf(tab.root, self)!; [dragged.sessionId, target.sessionId] = [target.sessionId, dragged.sessionId]; + [dragged.draft, target.draft] = [target.draft, dragged.draft]; tab.focusedLeafId = target.id; } else if (sessionId) { const loc = locateSession(layout, sessionId); @@ -576,6 +608,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { const t2 = findLeaf(tab.root, targetLeafId); if (t2) { t2.sessionId = sessionId; + t2.draft = null; tab.focusedLeafId = t2.id; } }