feat(maestro): spawn form defaults command to claude + prefills cwd to daemon home dir (NAP-328)

Two web spawn QoL improvements plus the wire support they need:

- Command field now prefills `claude` instead of an empty box.
- Working directory prefills the selected daemon's home dir, updating on
  daemon change unless the user has typed their own value.

The daemon resolves its `$HOME` via `dirs::home_dir()` at startup and
advertises it end-to-end: a new `home_dir` field on `DaemonOutbound::Hello`
flows through the registry's `register` into `DaemonInfo` (served on
`/api/daemons` and `/api/events`). The field is `#[serde(default)]`, so older
daemons that omit it deserialize to an empty string — backward compatible, and
the UI simply skips prefill when it's empty.

Hierarchical `/`-separated labels in the web sidebar already worked
(web/src/catalog.ts); no change there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 18:07:53 +00:00
co-authored by Claude Opus 4.8
parent 906538c776
commit 6d7cd0b4c8
21 changed files with 119 additions and 5 deletions
+31
View File
@@ -100,6 +100,11 @@ pub enum DaemonOutbound {
/// in `GET /api/sessions/:id/connect-info` so the client can connect
/// directly. Example: `ws://10.0.0.1:8081`.
daemon_url: String,
/// Absolute home directory of the OS user this daemon runs as, used by
/// clients to prefill a spawn form's working directory. Empty when the
/// daemon could not resolve `$HOME`. (NAP-328)
#[serde(default)]
home_dir: String,
},
SessionsSnapshot {
sessions: Vec<Session>,
@@ -234,6 +239,11 @@ pub struct DaemonInfo {
pub hostname: String,
pub version: String,
pub daemon_url: String,
/// Absolute home directory of the OS user the daemon runs as. Clients use
/// it to prefill the spawn form's working directory. Empty when unknown
/// (older daemon, or `$HOME` unresolved). (NAP-328)
#[serde(default)]
pub home_dir: String,
}
/// Response body for `GET /api/sessions/:id/connect-info`.
@@ -528,6 +538,7 @@ mod tests {
version: PROTOCOL_VERSION.into(),
token: "shhh".into(),
daemon_url: "ws://10.0.0.1:8081".into(),
home_dir: String::new(),
};
let v = serde_json::to_value(&hello).unwrap();
assert_eq!(v["type"], "Hello");
@@ -656,10 +667,25 @@ mod tests {
hostname: "naps-laptop".into(),
version: "0.1.0".into(),
daemon_url: "ws://10.0.0.1:8081".into(),
home_dir: "/home/naps62".into(),
};
assert_eq!(d, round_trip(&d));
}
#[test]
fn daemon_info_home_dir_defaults_when_absent() {
// Older daemons / pre-NAP-328 payloads omit `home_dir`; it must
// deserialize to an empty string rather than failing.
let json = r#"{
"daemon_id": "laptop",
"hostname": "naps-laptop",
"version": "0.1.0",
"daemon_url": "ws://10.0.0.1:8081"
}"#;
let d: DaemonInfo = serde_json::from_str(json).unwrap();
assert_eq!(d.home_dir, "");
}
#[test]
fn connect_info_round_trips() {
let c = ConnectInfo {
@@ -676,6 +702,7 @@ mod tests {
hostname: "naps-laptop".into(),
version: "0.1.0".into(),
daemon_url: "ws://10.0.0.1:8081".into(),
home_dir: String::new(),
};
let session = sample_session();
let variants = vec![
@@ -882,12 +909,14 @@ mod tests {
hostname: "naps-laptop".into(),
version: "0.1.0".into(),
daemon_url: "ws://10.0.0.1:7070".into(),
home_dir: String::new(),
},
DaemonInfo {
daemon_id: "konishi".into(),
hostname: "konishi".into(),
version: "0.1.0".into(),
daemon_url: "ws://10.0.0.2:7070".into(),
home_dir: String::new(),
},
],
sessions: vec![mine.clone(), peer_session.clone()],
@@ -929,6 +958,7 @@ mod tests {
hostname: "x".into(),
version: "y".into(),
daemon_url: "z".into(),
home_dir: String::new(),
}
}
.for_peer("laptop")
@@ -966,6 +996,7 @@ mod tests {
hostname: "peer-host".into(),
version: "0.1.0".into(),
daemon_url: "ws://peer:7070".into(),
home_dir: String::new(),
}],
sessions: vec![sample_session()],
},