docs(maestro): design for spawn-form dir autocomplete + home prefill (NAP-329)

This commit is contained in:
2026-06-15 21:10:57 +00:00
parent 23594a5f00
commit 8842147164
@@ -0,0 +1,107 @@
# Spawn form: home-dir prefill + live directory autocomplete (NAP-329)
## Problem
The web spawn form's **Working directory** field is `required` but offers no
help filling it: no reliable prefill, no completion. The user wants
1. the field prefilled with the **selected host's home dir**, updating when the
host changes; and
2. **shell-style directory completion** (`cd dir/sub<tab>`) driven by **real-time
queries to each host's daemon**, so it works across every federated host.
Applies to both the **cwd** field and the worktree **Base repo** field.
## Architecture context
The web SPA talks to a single **home daemon** (served by `maestro-web`, which
reverse-proxies `/api/*`) — or, in dev, directly to the **registry** (vite
proxy). The home daemon owns its sessions and mirrors peers' catalogs; it
**brokers** cross-daemon operations (`create`/`kill`) to the owning daemon over
the registry control link (NAP-327). The registry can also drive a daemon RPC
directly (`create_session`/`kill_session`) for its own `client_api` front door.
Directory listing must reach **the daemon whose host you're spawning on**, which
may be the home daemon itself or any peer. So we mirror the create/kill plumbing
end to end rather than only the registry leg.
## Design
### Listing strategy
The daemon lists **one directory's immediate subdirectories**; the web caches
the result per `(daemonId, dir)` and filters the trailing path segment
client-side, re-querying only when the user crosses a `/`. Fewer round-trips,
instant filtering, still live. Directories only (cwd / base-repo are always
dirs); hidden dirs included (shell parity); sorted case-insensitively; capped.
### Wire protocol (`maestro-protocol`)
- `DirListing { path: PathBuf, entries: Vec<String>, truncated: bool }` — `path`
is the **resolved absolute** dir (after `~`/relative expansion); `entries` are
immediate subdirectory names.
- `DaemonInbound::ListDir { reply_to, path: String }` — registry → owner daemon.
- `DaemonOutbound::ListDirResult { reply_to, result: RpcResult<DirListing> }` —
owner → registry.
- `DaemonOutbound::ForwardListDir { reply_to, daemon_id, path }` — home daemon →
registry (relay to owner).
- `DaemonInbound::ForwardListDirResult { reply_to, result: RpcResult<DirListing> }`
— registry → home daemon.
Carried by the existing `RpcResult` + `#[serde(other)] Unknown` forward-compat
machinery, so older peers degrade gracefully.
### Daemon (`maestro-daemon`)
- New pure module `dirlist`: `list_dir(home_dir, requested) -> Result<DirListing, ErrorInfo>`.
Expansion: empty → home; leading `~` → home; relative → joined onto home.
Reads the dir, keeps directories (resolving symlinks), sorts, caps (`MAX_ENTRIES`).
Error codes: `not_found`, `permission_denied`, `not_a_directory`, `io_error`.
- `registry_client` handles `ListDir` (calls `dirlist::list_dir`, replies
`ListDirResult`) and `ForwardListDirResult` (routes to broker).
- `broker` gains `forward_list_dir(daemon_id, path)` + `complete_forward_list_dir`,
mirroring `forward_kill` (fail-fast while disconnected, RAII pending cleanup).
- `local_api`: `GET /api/daemons/:id/dirs?path=<p>` — self → `dirlist::list_dir`;
peer → `broker.forward_list_dir`. Maps error codes to HTTP status.
### Registry (`maestro-registry`)
- `Registry::list_dir(daemon_id, path)` mirroring `create_session` (new
`pending_list_dir` map + `PendingGuard`, `complete_list_dir`); no catalog
side-effects.
- `forward_list_dir(home, reply_to, target, path)` mirroring `forward_kill`.
- `handler` dispatches `ListDirResult` → `complete_list_dir` and `ForwardListDir`
→ `forward_list_dir`.
- `client_api`: `GET /api/daemons/:id/dirs?path=<p>` → `registry.list_dir`, same
timeout/error mapping as `create_session`.
### Web (`web/src`)
- `types.ts`: `DirListing`.
- `api.ts`: `listDir(daemonId, path) -> Promise<DirListing>` →
`GET /api/daemons/:id/dirs?path=…`.
- New `dirAutocomplete.ts`: attaches to an input + a daemon-id getter. Debounced
fetch of the current dir, `(daemon,dir)` cache, fixed-position dropdown with
arrow-nav; **Tab** completes the longest common prefix / accepts the highlighted
entry; **Enter** accepts (suppressing submit) or submits when closed; **`/`**
drills into the next level; **Esc** closes. Failures degrade to no suggestions.
- `spawn.ts`: keep host-change home-dir prefill; wire the widget onto cwd +
worktree base repo.
## Testing
- Protocol: serde round-trip for the four new variants + `DirListing`, and an
unknown-variant decode (already covered generically).
- Daemon `dirlist`: temp-dir listing (dirs-only, hidden included, sorted), `~`
and relative + empty expansion, and `not_found` / `not_a_directory` codes.
- Daemon `broker`: `forward_list_dir` round-trip + fail-fast-while-disconnected,
mirroring the existing forward_create/kill tests.
- Registry: `list_dir` RPC pairing + `complete_list_dir`; `forward_list_dir`
relay, reusing the create/kill in-process channel harness.
- Web: no test runner present; rely on `tsc` typecheck + manual verification.
## Scope
In: the full web→(home daemon|registry)→owner daemon path so completion works
for any host. Out: listing files (dirs only); fuzzy matching (prefix only);
persisting recently-used paths.