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:
@@ -188,12 +188,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: "vm1".into(),
|
||||
hostname: "vm1.lan".into(),
|
||||
version: "0.1.0".into(),
|
||||
daemon_url: "ws://10.0.0.2:7070".into(),
|
||||
home_dir: String::new(),
|
||||
},
|
||||
];
|
||||
let mut buf = Vec::new();
|
||||
@@ -222,6 +224,7 @@ mod tests {
|
||||
hostname: "naps".into(),
|
||||
version: "0.1.0".into(),
|
||||
daemon_url: "ws://x".into(),
|
||||
home_dir: String::new(),
|
||||
}];
|
||||
let mut buf = Vec::new();
|
||||
super::print_session_table(&mut buf, &[], &daemons);
|
||||
|
||||
@@ -131,6 +131,7 @@ mod tests {
|
||||
hostname: format!("{daemon_id}-host"),
|
||||
version: "0.1.0".into(),
|
||||
daemon_url: format!("ws://{daemon_id}:7070"),
|
||||
home_dir: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
|
||||
config.daemon_id.clone(),
|
||||
config.hostname.clone(),
|
||||
config.daemon_url.clone(),
|
||||
config.home_dir.clone(),
|
||||
config.token.clone(),
|
||||
manager.clone(),
|
||||
Some(events.clone()),
|
||||
@@ -138,6 +139,7 @@ async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
|
||||
manager.clone(),
|
||||
config.hostname.clone(),
|
||||
config.daemon_url.clone(),
|
||||
config.home_dir.clone(),
|
||||
catalog.clone(),
|
||||
events,
|
||||
client.broker(),
|
||||
|
||||
@@ -72,6 +72,10 @@ pub enum ConfigError {
|
||||
pub struct DaemonConfig {
|
||||
pub daemon_id: String,
|
||||
pub hostname: String,
|
||||
/// Home directory of the OS user the daemon runs as, advertised to clients
|
||||
/// so they can prefill a spawn form's working directory. Empty when `$HOME`
|
||||
/// can't be resolved (NAP-328).
|
||||
pub home_dir: String,
|
||||
/// Registry WS URL the daemon dials. Derived from `[shared].registry_url`
|
||||
/// via [`maestro_config::derive_daemon_ws_url`].
|
||||
pub registry_url: String,
|
||||
@@ -189,6 +193,9 @@ pub fn load_config(path: &Path, env: EnvOverrides) -> Result<DaemonConfig, Confi
|
||||
let daemon = file.daemon;
|
||||
|
||||
let hostname = daemon.hostname.unwrap_or_else(system_hostname);
|
||||
let home_dir = dirs::home_dir()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let daemon_id = env
|
||||
.daemon_id
|
||||
.or(daemon.daemon_id)
|
||||
@@ -248,6 +255,7 @@ pub fn load_config(path: &Path, env: EnvOverrides) -> Result<DaemonConfig, Confi
|
||||
Ok(DaemonConfig {
|
||||
daemon_id,
|
||||
hostname,
|
||||
home_dir,
|
||||
registry_url,
|
||||
bind_addr,
|
||||
daemon_url,
|
||||
|
||||
@@ -54,6 +54,9 @@ pub struct LocalApiState {
|
||||
pub manager: SessionManager,
|
||||
pub hostname: Arc<String>,
|
||||
pub daemon_url: Arc<String>,
|
||||
/// Home directory of the OS user this daemon runs as, advertised so clients
|
||||
/// can prefill a spawn form's working directory. Empty if unresolved.
|
||||
pub home_dir: Arc<String>,
|
||||
/// Peer-only mirror of the global catalog. Empty until the daemon's
|
||||
/// registry client connects and receives the snapshot.
|
||||
pub catalog: CatalogMirror,
|
||||
@@ -87,6 +90,7 @@ async fn list_daemons(State(s): State<LocalApiState>) -> Json<Vec<DaemonInfo>> {
|
||||
hostname: (*s.hostname).clone(),
|
||||
version: PROTOCOL_VERSION.into(),
|
||||
daemon_url: (*s.daemon_url).clone(),
|
||||
home_dir: (*s.home_dir).clone(),
|
||||
};
|
||||
let mut out = vec![self_info];
|
||||
out.extend(s.catalog.daemons().await);
|
||||
@@ -206,6 +210,7 @@ pub fn state_for(
|
||||
manager: SessionManager,
|
||||
hostname: impl Into<String>,
|
||||
daemon_url: impl Into<String>,
|
||||
home_dir: impl Into<String>,
|
||||
catalog: CatalogMirror,
|
||||
events: LocalEventBus,
|
||||
broker: BrokerHandle,
|
||||
@@ -214,6 +219,7 @@ pub fn state_for(
|
||||
manager,
|
||||
hostname: Arc::new(hostname.into()),
|
||||
daemon_url: Arc::new(daemon_url.into()),
|
||||
home_dir: Arc::new(home_dir.into()),
|
||||
catalog,
|
||||
events,
|
||||
broker,
|
||||
@@ -254,6 +260,7 @@ async fn pump_events(
|
||||
hostname: (*s.hostname).clone(),
|
||||
version: PROTOCOL_VERSION.into(),
|
||||
daemon_url: (*s.daemon_url).clone(),
|
||||
home_dir: (*s.home_dir).clone(),
|
||||
};
|
||||
let mut daemons = vec![self_info];
|
||||
daemons.extend(s.catalog.daemons().await);
|
||||
|
||||
@@ -48,6 +48,7 @@ pub struct RegistryClient {
|
||||
daemon_id: String,
|
||||
hostname: String,
|
||||
daemon_url: String,
|
||||
home_dir: String,
|
||||
token: String,
|
||||
/// Operator-set perimeter (id + reachable address) advertised in heartbeats
|
||||
/// so peer daemons can dial this one over T2 when they share the same
|
||||
@@ -68,11 +69,13 @@ pub struct RegistryClient {
|
||||
}
|
||||
|
||||
impl RegistryClient {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
registry_url: impl Into<String>,
|
||||
daemon_id: impl Into<String>,
|
||||
hostname: impl Into<String>,
|
||||
daemon_url: impl Into<String>,
|
||||
home_dir: impl Into<String>,
|
||||
token: impl Into<String>,
|
||||
manager: SessionManager,
|
||||
) -> Self {
|
||||
@@ -81,6 +84,7 @@ impl RegistryClient {
|
||||
daemon_id,
|
||||
hostname,
|
||||
daemon_url,
|
||||
home_dir,
|
||||
token,
|
||||
manager,
|
||||
None,
|
||||
@@ -89,11 +93,13 @@ impl RegistryClient {
|
||||
|
||||
/// Variant that wires a [`LocalEventBus`](crate::LocalEventBus) into the
|
||||
/// catalog mirror so peer deltas reach `WS /api/events` (NAP-248).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_with_bus(
|
||||
registry_url: impl Into<String>,
|
||||
daemon_id: impl Into<String>,
|
||||
hostname: impl Into<String>,
|
||||
daemon_url: impl Into<String>,
|
||||
home_dir: impl Into<String>,
|
||||
token: impl Into<String>,
|
||||
manager: SessionManager,
|
||||
bus: Option<crate::LocalEventBus>,
|
||||
@@ -108,6 +114,7 @@ impl RegistryClient {
|
||||
daemon_id: daemon_id.into(),
|
||||
hostname: hostname.into(),
|
||||
daemon_url: daemon_url.into(),
|
||||
home_dir: home_dir.into(),
|
||||
token: token.into(),
|
||||
perimeter: None,
|
||||
manager,
|
||||
@@ -171,6 +178,7 @@ impl RegistryClient {
|
||||
version: PROTOCOL_VERSION.into(),
|
||||
token: self.token.clone(),
|
||||
daemon_url: self.daemon_url.clone(),
|
||||
home_dir: self.home_dir.clone(),
|
||||
};
|
||||
sink.send(Message::Text(serde_json::to_string(&hello)?))
|
||||
.await?;
|
||||
|
||||
@@ -117,6 +117,7 @@ async fn list_daemons_includes_self_and_mirrored_peers() {
|
||||
hostname: "konishi-host".into(),
|
||||
version: "0.1.0".into(),
|
||||
daemon_url: "ws://10.0.0.2:7070".into(),
|
||||
home_dir: String::new(),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -58,6 +58,7 @@ async fn daemon_registers_and_streams_snapshot() {
|
||||
"laptop",
|
||||
"naps-laptop",
|
||||
"ws://127.0.0.1:8081",
|
||||
"/home/naps62",
|
||||
TOKEN,
|
||||
fx.manager.clone(),
|
||||
);
|
||||
@@ -115,6 +116,7 @@ async fn daemon_reconnects_after_registry_drop() {
|
||||
"laptop",
|
||||
"naps-laptop",
|
||||
"ws://127.0.0.1:8081",
|
||||
"/home/naps62",
|
||||
TOKEN,
|
||||
fx.manager.clone(),
|
||||
);
|
||||
@@ -156,6 +158,7 @@ async fn registry_create_session_round_trips() {
|
||||
"laptop",
|
||||
"naps-laptop",
|
||||
"ws://127.0.0.1:8081",
|
||||
"/home/naps62",
|
||||
TOKEN,
|
||||
fx.manager.clone(),
|
||||
);
|
||||
|
||||
@@ -159,6 +159,15 @@ async fn daemon_local_api_mirrors_peer_catalog() {
|
||||
assert!(ids.contains(&"alpha"));
|
||||
assert!(ids.contains(&"beta"));
|
||||
|
||||
// NAP-328: home_dir is advertised end-to-end — for a peer relayed through
|
||||
// the registry (beta, via Hello → register → catalog mirror), so the web
|
||||
// spawn form can prefill the working directory.
|
||||
let beta = list.iter().find(|d| d.daemon_id == "beta").unwrap();
|
||||
assert!(
|
||||
!beta.home_dir.is_empty(),
|
||||
"peer home_dir should propagate through the registry: {beta:?}"
|
||||
);
|
||||
|
||||
client.kill_session(b.id).await.expect("kill b");
|
||||
}
|
||||
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
|
||||
@@ -38,14 +38,15 @@ pub async fn handle_socket(
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let (daemon_id, hostname, version, token, daemon_url) = match hello {
|
||||
let (daemon_id, hostname, version, token, daemon_url, home_dir) = match hello {
|
||||
DaemonOutbound::Hello {
|
||||
daemon_id,
|
||||
hostname,
|
||||
version,
|
||||
token,
|
||||
daemon_url,
|
||||
} => (daemon_id, hostname, version, token, daemon_url),
|
||||
home_dir,
|
||||
} => (daemon_id, hostname, version, token, daemon_url, home_dir),
|
||||
_ => {
|
||||
let _ = send_error(&mut sink, "expected_hello", "first message must be Hello").await;
|
||||
return None;
|
||||
@@ -66,6 +67,7 @@ pub async fn handle_socket(
|
||||
hostname,
|
||||
version,
|
||||
daemon_url,
|
||||
home_dir,
|
||||
out_tx.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -83,6 +83,7 @@ pub struct DaemonView {
|
||||
pub hostname: String,
|
||||
pub version: String,
|
||||
pub daemon_url: String,
|
||||
pub home_dir: String,
|
||||
}
|
||||
|
||||
impl From<DaemonView> for DaemonInfo {
|
||||
@@ -92,6 +93,7 @@ impl From<DaemonView> for DaemonInfo {
|
||||
hostname: v.hostname,
|
||||
version: v.version,
|
||||
daemon_url: v.daemon_url,
|
||||
home_dir: v.home_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +122,7 @@ struct DaemonEntry {
|
||||
hostname: String,
|
||||
version: String,
|
||||
daemon_url: String,
|
||||
home_dir: String,
|
||||
outbound: mpsc::Sender<DaemonInbound>,
|
||||
/// Latest perimeter advertised by this daemon in its heartbeats (id + addr
|
||||
/// together, or absent). `None` until the daemon advertises one (NAP-247 T2
|
||||
@@ -139,6 +142,7 @@ impl DaemonEntry {
|
||||
hostname: self.hostname.clone(),
|
||||
version: self.version.clone(),
|
||||
daemon_url: self.daemon_url.clone(),
|
||||
home_dir: self.home_dir.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,6 +197,7 @@ impl Registry {
|
||||
hostname: String,
|
||||
version: String,
|
||||
daemon_url: String,
|
||||
home_dir: String,
|
||||
outbound: mpsc::Sender<DaemonInbound>,
|
||||
) -> Uuid {
|
||||
let connection_token = Uuid::new_v4();
|
||||
@@ -204,6 +209,7 @@ impl Registry {
|
||||
hostname: hostname.clone(),
|
||||
version: version.clone(),
|
||||
daemon_url: daemon_url.clone(),
|
||||
home_dir: home_dir.clone(),
|
||||
outbound,
|
||||
perimeter: None,
|
||||
connection_token,
|
||||
@@ -222,6 +228,7 @@ impl Registry {
|
||||
hostname,
|
||||
version,
|
||||
daemon_url,
|
||||
home_dir,
|
||||
},
|
||||
});
|
||||
connection_token
|
||||
@@ -874,7 +881,10 @@ mod tests {
|
||||
|
||||
// The home daemon gets the relayed result.
|
||||
match home_rx.recv().await.expect("home got ForwardCreateResult") {
|
||||
DaemonInbound::ForwardCreateResult { reply_to: got, result } => {
|
||||
DaemonInbound::ForwardCreateResult {
|
||||
reply_to: got,
|
||||
result,
|
||||
} => {
|
||||
assert_eq!(got, reply_to);
|
||||
match result {
|
||||
RpcResult::Ok { value } => {
|
||||
@@ -924,7 +934,10 @@ mod tests {
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
let r = reg.clone();
|
||||
let task = tokio::spawn(async move { r.forward_kill("laptop", Uuid::new_v4(), "konishi", id).await });
|
||||
let task = tokio::spawn(async move {
|
||||
r.forward_kill("laptop", Uuid::new_v4(), "konishi", id)
|
||||
.await
|
||||
});
|
||||
|
||||
let owner_reply_to = match target_rx.recv().await.expect("owner got KillSession") {
|
||||
DaemonInbound::KillSession { reply_to, id: got } => {
|
||||
|
||||
@@ -89,6 +89,7 @@ fn hello(token: &str) -> DaemonOutbound {
|
||||
version: "0.1.0".into(),
|
||||
token: token.into(),
|
||||
daemon_url: "ws://127.0.0.1:8081".into(),
|
||||
home_dir: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ impl MockDaemon {
|
||||
version: "0.1.0".into(),
|
||||
token: TOKEN.into(),
|
||||
daemon_url: self.daemon_url.clone(),
|
||||
home_dir: String::new(),
|
||||
};
|
||||
ws.send(WsMessage::Text(serde_json::to_string(&hello).unwrap()))
|
||||
.await
|
||||
|
||||
@@ -514,6 +514,7 @@ mod tests {
|
||||
hostname: "konishi.lan".into(),
|
||||
version: "test".into(),
|
||||
daemon_url: "ws://k:7070".into(),
|
||||
home_dir: String::new(),
|
||||
};
|
||||
app.catalog.apply_event(RegistryEvent::Snapshot {
|
||||
daemons: vec![d.clone()],
|
||||
@@ -537,6 +538,7 @@ mod tests {
|
||||
hostname: "k".into(),
|
||||
version: "t".into(),
|
||||
daemon_url: "ws://k".into(),
|
||||
home_dir: String::new(),
|
||||
}],
|
||||
sessions: vec![Session {
|
||||
id: sid,
|
||||
|
||||
@@ -205,6 +205,7 @@ mod tests {
|
||||
hostname: format!("{id}.lan"),
|
||||
version: "test".into(),
|
||||
daemon_url: format!("ws://{id}.lan:7070"),
|
||||
home_dir: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -312,6 +312,7 @@ mod tests {
|
||||
hostname: "konishi".into(),
|
||||
version: "0".into(),
|
||||
daemon_url: "ws://x".into(),
|
||||
home_dir: String::new(),
|
||||
}],
|
||||
daemon_idx: 0,
|
||||
cwd: "/x".into(),
|
||||
|
||||
@@ -93,6 +93,7 @@ mod tests {
|
||||
hostname: id.into(),
|
||||
version: "test".into(),
|
||||
daemon_url: format!("ws://{id}:7070"),
|
||||
home_dir: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ fn daemon(id: &str) -> DaemonInfo {
|
||||
hostname: format!("{id}.lan"),
|
||||
version: "test".into(),
|
||||
daemon_url: format!("ws://{id}.lan:7070"),
|
||||
home_dir: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -25,7 +25,7 @@ export function openSpawnModal(daemons: Map<string, DaemonInfo>): void {
|
||||
</label>
|
||||
<label>
|
||||
Command
|
||||
<input name="command" type="text" required placeholder="bash" autocomplete="off" />
|
||||
<input name="command" type="text" required value="claude" placeholder="bash" autocomplete="off" />
|
||||
</label>
|
||||
<label>
|
||||
Name (optional)
|
||||
@@ -82,6 +82,21 @@ export function openSpawnModal(daemons: Map<string, DaemonInfo>): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Prefill the working directory with the selected daemon's home dir, and keep
|
||||
// it in sync when the daemon changes — unless the user has typed their own.
|
||||
const cwdInput = host.querySelector("input[name=cwd]") as HTMLInputElement;
|
||||
let cwdEdited = false;
|
||||
cwdInput.addEventListener("input", () => {
|
||||
cwdEdited = true;
|
||||
});
|
||||
const applyHomeDir = () => {
|
||||
if (cwdEdited) return;
|
||||
const home = daemons.get(select.value)?.home_dir;
|
||||
if (home) cwdInput.value = home;
|
||||
};
|
||||
select.addEventListener("change", applyHomeDir);
|
||||
applyHomeDir();
|
||||
|
||||
const close = () => host.remove();
|
||||
host.querySelector(".close")?.addEventListener("click", close);
|
||||
host.querySelector(".cancel")?.addEventListener("click", close);
|
||||
|
||||
@@ -22,6 +22,9 @@ export interface DaemonInfo {
|
||||
hostname: string;
|
||||
version: string;
|
||||
daemon_url: string;
|
||||
/** Home dir of the user the daemon runs as; used to prefill the spawn form's
|
||||
* working directory. Empty/absent for older daemons. */
|
||||
home_dir?: string;
|
||||
}
|
||||
|
||||
export interface WorktreeSpec {
|
||||
|
||||
Reference in New Issue
Block a user