feat(maestro): spawn form rerolls from a fetched name batch (#53)

This commit is contained in:
Miguel Palhas
2026-08-23 21:15:33 +01:00
parent a970ba89c2
commit 818e0cc2db
7 changed files with 207 additions and 37 deletions
+55 -6
View File
@@ -1,8 +1,7 @@
import { escapeHtml } from "./html.js";
import { createSession, getDaemonInfo, projects as fetchProjects, gitRoot } from "./api.js";
import { createSession, fetchNames, getDaemonInfo, projects as fetchProjects, gitRoot } from "./api.js";
import { attachDirAutocomplete } from "./dirAutocomplete.js";
import { filterProjects, groupProjects, isStale, sortProjects } from "./projectList.js";
import { generateName } from "./names.js";
import type { CreateSessionRequest, Project, Session, SpawnPreset } from "./types.js";
import { abbreviateCwd, abbreviateFolderName } from "./types.js";
@@ -197,8 +196,46 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
const syncBranchToName = () => {
if (!branchEdited) branchInput.value = nameInput.value;
};
nameInput.value = prefill.name ?? generateName();
// Handles come from `GET /api/names` in batches (#53): the first one seeds
// the field, rerolls consume the rest locally so they stay instant, and a
// new batch is fetched when (and only when) the queue runs dry. One refill
// at a time keeps rapid rerolls from storming the daemon.
const NAME_BATCH_SIZE = 20;
let nameQueue: string[] = [];
let nameFetch: Promise<void> | null = null;
// Guards against a late batch clobbering something the operator already
// typed or rerolled.
let nameTouched = Boolean(prefill.name);
const refillNames = (): Promise<void> => {
if (!nameFetch) {
nameFetch = fetchNames(NAME_BATCH_SIZE)
.then((batch) => {
if (!destroyed) nameQueue.push(...batch.names);
})
.catch(() => {
/* reroll just stays on the current name */
})
.finally(() => {
nameFetch = null;
});
}
return nameFetch;
};
nameInput.value = prefill.name ?? "";
if (!nameTouched) {
void refillNames().then(() => {
if (destroyed || nameTouched || nameInput.value) return;
const next = nameQueue.shift();
if (next !== undefined) {
nameInput.value = next;
syncBranchToName();
emit();
}
});
}
nameInput.addEventListener("input", () => {
nameTouched = true;
syncBranchToName();
emit();
});
@@ -207,9 +244,21 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
emit();
});
(form.querySelector(".reroll") as HTMLButtonElement).addEventListener("click", () => {
nameInput.value = generateName();
syncBranchToName();
emit();
nameTouched = true;
const apply = () => {
if (destroyed) return;
const next = nameQueue.shift();
if (next === undefined) return;
nameInput.value = next;
syncBranchToName();
emit();
};
// Only hit the daemon when the local batch is exhausted.
if (nameQueue.length === 0) {
void refillNames().then(apply);
} else {
apply();
}
});
syncBranchToName(); // seed the branch from the initial name