The picker now leads the form in both clients and takes focus on open, so Tab walks from it into the command tiles. Opening the form also preselects the project of the session you were last on — a worktree session seeds its base repository — so 'n' then Enter spawns another session there.
100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
// The spawn modal: dialog chrome around the shared spawn form (spawnForm.ts).
|
|
//
|
|
// One modal at a time — opening while one is up replaces it. Escape, the ✕ and
|
|
// a click on the backdrop all close it; the form's own Escape handlers (the
|
|
// directory autocomplete) swallow the key first, so dismissing a dropdown never
|
|
// also discards the form.
|
|
|
|
import { createSpawnForm, type SpawnFormSeed } from "./spawnForm.js";
|
|
import type { Session } from "./types.js";
|
|
|
|
/** Seed values for the spawn form — used by "Spawn similar" to open the dialog
|
|
* with the same cwd, command and name as an existing session. */
|
|
export type SpawnPrefill = SpawnFormSeed;
|
|
|
|
export interface SpawnModalOptions {
|
|
seed?: SpawnPrefill;
|
|
/** Called with the created session after a successful submit; the modal has
|
|
* already closed by then, so the caller decides where it lands. */
|
|
onSuccess: (session: Session) => void;
|
|
}
|
|
|
|
/** Closer for the modal currently up, so opening a second one tears the first
|
|
* one down properly — dropping its node alone would strand the form's global
|
|
* listeners (dir autocomplete) and the Escape handler. */
|
|
let closeCurrent: (() => void) | null = null;
|
|
|
|
export function openSpawnModal(opts: SpawnModalOptions): void {
|
|
closeSpawnModal();
|
|
|
|
const host = document.createElement("div");
|
|
host.id = "modal-host";
|
|
host.className = "modal-backdrop";
|
|
const dialog = document.createElement("div");
|
|
dialog.className = "modal spawn-modal";
|
|
dialog.setAttribute("role", "dialog");
|
|
dialog.setAttribute("aria-modal", "true");
|
|
dialog.setAttribute("aria-labelledby", "spawn-title");
|
|
const header = document.createElement("header");
|
|
header.className = "modal-header";
|
|
const title = document.createElement("h2");
|
|
title.id = "spawn-title";
|
|
// "Spawn similar" arrives with a seed; the title says which act this is.
|
|
title.textContent = opts.seed ? "Spawn similar" : "Spawn session";
|
|
const closeBtn = document.createElement("button");
|
|
closeBtn.type = "button";
|
|
closeBtn.className = "btn btn-icon close";
|
|
closeBtn.setAttribute("aria-label", "Close");
|
|
closeBtn.textContent = "✕";
|
|
header.append(title, closeBtn);
|
|
dialog.appendChild(header);
|
|
host.appendChild(dialog);
|
|
document.body.appendChild(host);
|
|
|
|
const form = createSpawnForm({
|
|
seed: opts.seed,
|
|
onSuccess: (session) => {
|
|
close();
|
|
opts.onSuccess(session);
|
|
},
|
|
});
|
|
dialog.appendChild(form.el);
|
|
// The project filter owns the keyboard on open: typing narrows the list and
|
|
// Tab from here reaches the command tiles, matching the TUI's draft pane.
|
|
form.el.querySelector<HTMLInputElement>(".project-filter")?.focus();
|
|
|
|
function close(): void {
|
|
if (closeCurrent === close) closeCurrent = null;
|
|
window.removeEventListener("keydown", onKeydown);
|
|
form.destroy();
|
|
host.remove();
|
|
}
|
|
closeCurrent = close;
|
|
|
|
function onKeydown(e: KeyboardEvent): void {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
close();
|
|
}
|
|
}
|
|
|
|
// The modal keeps its Cancel affordance; the shared form ships only Submit.
|
|
const cancel = document.createElement("button");
|
|
cancel.type = "button";
|
|
cancel.className = "btn btn-ghost cancel";
|
|
cancel.textContent = "Cancel";
|
|
cancel.addEventListener("click", close);
|
|
form.el.querySelector(".form-actions")?.prepend(cancel);
|
|
|
|
closeBtn.addEventListener("click", close);
|
|
host.addEventListener("click", (e) => {
|
|
if (e.target === host) close();
|
|
});
|
|
window.addEventListener("keydown", onKeydown);
|
|
}
|
|
|
|
/** Close whatever spawn modal is up. No-op when none is. */
|
|
export function closeSpawnModal(): void {
|
|
closeCurrent?.();
|
|
}
|