docs(maestro): design for spawn-form dir autocomplete + home prefill (NAP-329)
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user