diff --git a/web/src/api.ts b/web/src/api.ts index b3857fd..7ba95b6 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -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(res: Response): Promise { if (res.ok) { @@ -43,6 +50,14 @@ export async function killSession(id: Uuid): Promise { ); } +/** 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 { + 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`; diff --git a/web/src/dirAutocomplete.ts b/web/src/dirAutocomplete.ts new file mode 100644 index 0000000..36a0328 Binary files /dev/null and b/web/src/dirAutocomplete.ts differ diff --git a/web/src/spawn.ts b/web/src/spawn.ts index 6a4d3d5..e625aba 100644 --- a/web/src/spawn.ts +++ b/web/src/spawn.ts @@ -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): void { @@ -97,7 +98,21 @@ export function openSpawnModal(daemons: Map): 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) => { diff --git a/web/src/styles.css b/web/src/styles.css index 601c230..c8f5dc8 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -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 { diff --git a/web/src/types.ts b/web/src/types.ts index 280e440..b14560d 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -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;