feat(maestro): "Spawn similar" catalog action + spawn-history/gitroot e2e (#28)
ci / rust (push) Successful in 4m15s
ci / rust (push) Successful in 4m15s
Web: rename the per-row clone affordance to "Spawn similar" — it reopens the revamped spawn form pre-seeded from the session's host, dir, command (matched back to a preset by the form), and label, but with a freshly generated name since it's a new session. Built entirely on the existing prefill machinery; the custom event is renamed `catalog-spawn-similar`. e2e (maestro-e2e): against real tmux + git, spawn into a git repo dir and assert (1) the daemon's recent-dirs endpoint surfaces that dir with its command — what #27's recents picker reads on the next form open — proving the durable spawn-history write, and (2) gitroot infers the enclosing repo as the worktree base_repo. Closes #28 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-11
@@ -118,16 +118,17 @@ function renderNode(node: TreeNode, depth: number): HTMLElement {
|
||||
${showCmd ? `<span class="leaf-meta"><span class="leaf-cmd">${escapeHtml(s.command)}</span></span>` : ""}
|
||||
${running ? "" : `<span class="leaf-badge">exited</span>`}
|
||||
<span class="leaf-actions">
|
||||
<button class="leaf-act leaf-clone" type="button" title="Clone into a new session" aria-label="Clone ${escapeHtml(name)}">⧉</button>
|
||||
<button class="leaf-act leaf-clone" type="button" title="Spawn similar" aria-label="Spawn similar to ${escapeHtml(name)}">⧉</button>
|
||||
<button class="leaf-act leaf-kill" type="button" title="Kill session" aria-label="Kill ${escapeHtml(name)}">✕</button>
|
||||
</span>
|
||||
`;
|
||||
const cloneBtn = row.querySelector(".leaf-clone") as HTMLButtonElement;
|
||||
cloneBtn.addEventListener("click", (ev) => {
|
||||
const similarBtn = row.querySelector(".leaf-clone") as HTMLButtonElement;
|
||||
similarBtn.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
// The sidebar (which has the daemon list) opens the prefilled spawn modal.
|
||||
document.dispatchEvent(new CustomEvent("catalog-clone", { detail: s }));
|
||||
// The sidebar (which has the daemon list) opens the spawn modal pre-seeded
|
||||
// from this session.
|
||||
document.dispatchEvent(new CustomEvent("catalog-spawn-similar", { detail: s }));
|
||||
});
|
||||
const killBtn = row.querySelector(".leaf-kill") as HTMLButtonElement;
|
||||
killBtn.addEventListener("click", async (ev) => {
|
||||
@@ -404,10 +405,11 @@ export function renderSidebar(store: CatalogStore): SidebarHandle {
|
||||
const rerenderHandler = () => refresh();
|
||||
document.addEventListener("catalog-rerender", rerenderHandler);
|
||||
|
||||
// Clone a session: reopen the spawn modal prefilled with its options.
|
||||
const cloneHandler = (ev: Event) => {
|
||||
// Spawn similar: reopen the spawn modal pre-seeded from a session's host, dir,
|
||||
// command (matched back to a preset by the form), and label — but with a
|
||||
// freshly generated name rather than the original's, since it's a new session.
|
||||
const spawnSimilarHandler = (ev: Event) => {
|
||||
const s = (ev as CustomEvent<Session>).detail;
|
||||
const rawName = typeof s.metadata?.name === "string" ? s.metadata.name : "";
|
||||
const snap = store.snapshot();
|
||||
openSpawnModal(
|
||||
snap.daemons,
|
||||
@@ -415,13 +417,12 @@ export function renderSidebar(store: CatalogStore): SidebarHandle {
|
||||
daemon_id: s.daemon_id,
|
||||
cwd: s.cwd,
|
||||
command: s.command,
|
||||
name: rawName,
|
||||
label: sessionLabel(s) ?? "",
|
||||
},
|
||||
snap.sessions,
|
||||
);
|
||||
};
|
||||
document.addEventListener("catalog-clone", cloneHandler);
|
||||
document.addEventListener("catalog-spawn-similar", spawnSimilarHandler);
|
||||
|
||||
const unsubscribe = store.subscribe(refresh);
|
||||
|
||||
@@ -433,7 +434,7 @@ export function renderSidebar(store: CatalogStore): SidebarHandle {
|
||||
},
|
||||
dispose() {
|
||||
document.removeEventListener("catalog-rerender", rerenderHandler);
|
||||
document.removeEventListener("catalog-clone", cloneHandler);
|
||||
document.removeEventListener("catalog-spawn-similar", spawnSimilarHandler);
|
||||
unsubscribe();
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ function leafRow(opts: {
|
||||
${opts.cmd !== opts.name ? `<span class="leaf-meta"><span class="leaf-cmd">${opts.cmd}</span></span>` : ""}
|
||||
${opts.status === "dead" ? `<span class="leaf-badge">exited</span>` : ""}
|
||||
<span class="leaf-actions">
|
||||
<button class="leaf-act leaf-clone" type="button" title="Clone" aria-label="Clone ${opts.name}">⧉</button>
|
||||
<button class="leaf-act leaf-clone" type="button" title="Spawn similar" aria-label="Spawn similar to ${opts.name}">⧉</button>
|
||||
<button class="leaf-act leaf-kill" type="button" title="Kill session" aria-label="Kill ${opts.name}">✕</button>
|
||||
</span>`;
|
||||
return row;
|
||||
|
||||
Reference in New Issue
Block a user