feat(maestro): live directory autocomplete in spawn form (NAP-329)
ci / rust (push) Failing after 5m3s

Adds api.listDir + a reusable dirAutocomplete widget (live dropdown,
Tab/Enter completion, /-drill-in, per-(daemon,dir) cache) wired onto the
cwd and worktree base-repo fields, with matching dropdown styling.
This commit is contained in:
2026-06-15 21:20:36 +00:00
parent 498cb471b7
commit 3aedd7a0a5
5 changed files with 88 additions and 2 deletions
+16 -1
View File
@@ -1,4 +1,11 @@
import type { ApiError, CreateSessionRequest, DaemonInfo, Session, Uuid } from "./types.js";
import type {
ApiError,
CreateSessionRequest,
DaemonInfo,
DirListing,
Session,
Uuid,
} from "./types.js";
async function jsonOrError<T>(res: Response): Promise<T> {
if (res.ok) {
@@ -43,6 +50,14 @@ export async function killSession(id: Uuid): Promise<void> {
);
}
/** List one directory's immediate subdirectories on `daemonId`'s host (NAP-329).
* `path` may be empty, `~`, relative, or absolute — the daemon resolves it
* against its own home dir. */
export async function listDir(daemonId: string, path: string): Promise<DirListing> {
const url = `/api/daemons/${encodeURIComponent(daemonId)}/dirs?path=${encodeURIComponent(path)}`;
return jsonOrError(await fetch(url, { credentials: "same-origin" }));
}
export function eventsWsUrl(): string {
const proto = location.protocol === "https:" ? "wss:" : "ws:";
return `${proto}//${location.host}/api/events`;
Binary file not shown.
+16 -1
View File
@@ -1,4 +1,5 @@
import { createSession } from "./api.js";
import { attachDirAutocomplete } from "./dirAutocomplete.js";
import type { CreateSessionRequest, DaemonInfo } from "./types.js";
export function openSpawnModal(daemons: Map<string, DaemonInfo>): void {
@@ -97,7 +98,21 @@ export function openSpawnModal(daemons: Map<string, DaemonInfo>): void {
select.addEventListener("change", applyHomeDir);
applyHomeDir();
const close = () => host.remove();
// Live directory autocomplete (NAP-329) on the path fields, scoped to the
// currently-selected daemon — re-reading `select.value` per query means it
// tracks host changes for free.
const baseRepoInput = host.querySelector(
"input[name=worktree_base_repo]",
) as HTMLInputElement;
const autocompletes = [
attachDirAutocomplete(cwdInput, () => select.value),
attachDirAutocomplete(baseRepoInput, () => select.value),
];
const close = () => {
autocompletes.forEach((a) => a.destroy());
host.remove();
};
host.querySelector(".close")?.addEventListener("click", close);
host.querySelector(".cancel")?.addEventListener("click", close);
host.addEventListener("click", (e) => {
+46
View File
@@ -1052,6 +1052,52 @@ textarea {
border-radius: var(--r-2);
}
/* ---------- directory autocomplete (NAP-329) ---------- */
.dir-ac {
position: fixed;
z-index: var(--z-toast); /* above the modal it completes within */
margin: 0;
padding: var(--sp-1);
list-style: none;
max-height: 280px;
overflow-y: auto;
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--r-2);
box-shadow: var(--shadow-2);
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.dir-ac-item {
padding: 6px var(--sp-2);
border-radius: var(--r-1);
color: var(--ink);
font-family: var(--font-mono);
font-size: var(--fs-14);
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dir-ac-item:hover {
background: var(--surface-2);
}
.dir-ac-item.is-active {
background: var(--accent-tint);
color: var(--ink);
}
.dir-ac-more {
padding: 6px var(--sp-2);
color: var(--faint);
font-size: var(--fs-12);
font-style: italic;
}
/* ---------- not-found ---------- */
.not-found {
+10
View File
@@ -27,6 +27,16 @@ export interface DaemonInfo {
home_dir?: string;
}
/** One directory's immediate subdirectories on a daemon's host (NAP-329). */
export interface DirListing {
/** Resolved absolute directory that was listed (after ~/relative expansion). */
path: string;
/** Immediate subdirectory names, sorted case-insensitively. */
entries: string[];
/** True when the listing was capped server-side. */
truncated?: boolean;
}
export interface WorktreeSpec {
base_repo: string;
branch: string;