diff --git a/src/app/actions.rs b/src/app/actions.rs index 27ca7474..faa5717d 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -693,18 +693,23 @@ impl AppState { pub fn apply_workspace_git_statuses(&mut self, results: Vec) -> bool { let mut changed = false; for result in results { - let Some(ws) = self + let Some(ws_idx) = self .workspaces - .iter_mut() - .find(|ws| ws.id == result.workspace_id) + .iter() + .position(|ws| ws.id == result.workspace_id) else { continue; }; - if ws.resolved_identity_cwd().as_ref() != Some(&result.resolved_identity_cwd) { + if self.workspaces[ws_idx] + .resolved_identity_cwd_from(&self.terminals, &self.terminal_runtimes) + .as_ref() + != Some(&result.resolved_identity_cwd) + { continue; } + let ws = &mut self.workspaces[ws_idx]; if ws.cached_git_branch != result.branch { ws.cached_git_branch = result.branch; changed = true; @@ -1050,6 +1055,7 @@ mod tests { } fn mark_agent(state: &mut AppState, ws_idx: usize, tab_idx: usize, pane_id: PaneId) { + state.ensure_test_terminals(); let terminal_id = state.workspaces[ws_idx].tabs[tab_idx] .panes .get(&pane_id) diff --git a/src/app/agents.rs b/src/app/agents.rs index f9e410f5..15b93674 100644 --- a/src/app/agents.rs +++ b/src/app/agents.rs @@ -17,7 +17,6 @@ impl App { .filter_map(move |pane_id| self.agent_info(ws_idx, pane_id)) }) }) - .filter(|agent| agent.name.is_some() || agent.agent.is_some()) .collect() } @@ -88,8 +87,11 @@ impl App { })); }; match normalized_name { - Some(name) => terminal.set_manual_label(name), - None => terminal.clear_manual_label(), + Some(name) => { + terminal.set_agent_name(name.clone()); + terminal.set_manual_label(name); + } + None => terminal.clear_agent_name(), } self.state.mark_session_dirty(); self.agent_info(resolved.ws_idx, resolved.pane_id) @@ -195,6 +197,7 @@ impl App { let Some(terminal) = self.state.terminals.get_mut(&terminal_id) else { return Err(AgentStartError::SpawnFailed("terminal disappeared".into())); }; + terminal.set_agent_name(name.clone()); terminal.set_manual_label(name); self.state.mark_session_dirty(); @@ -403,10 +406,16 @@ impl App { ws_idx: usize, pane_id: crate::layout::PaneId, ) -> Option { + let ws = self.state.workspaces.get(ws_idx)?; + let pane_state = ws.pane_state(pane_id)?; + let terminal = self.state.terminals.get(&pane_state.attached_terminal_id)?; + if !terminal.is_agent_terminal() { + return None; + } let pane = self.pane_info(ws_idx, pane_id)?; Some(crate::api::schema::AgentInfo { terminal_id: pane.terminal_id, - name: pane.label, + name: terminal.agent_name.clone(), agent: pane.agent, agent_status: pane.agent_status, workspace_id: pane.workspace_id, diff --git a/src/app/creation.rs b/src/app/creation.rs index 1832e637..f1638e15 100644 --- a/src/app/creation.rs +++ b/src/app/creation.rs @@ -8,7 +8,10 @@ use crate::workspace::Workspace; impl App { pub(super) fn seed_cwd_from_workspace(&self, ws_idx: usize) -> Option { - self.state.workspaces.get(ws_idx)?.resolved_identity_cwd() + self.state + .workspaces + .get(ws_idx)? + .resolved_identity_cwd_from(&self.state.terminals, &self.state.terminal_runtimes) } pub(super) fn workspace_creation_source(&self) -> Option { @@ -294,7 +297,7 @@ impl App { crate::api::schema::WorkspaceInfo { workspace_id: self.public_workspace_id(index), number: index + 1, - label: ws.display_name(), + label: ws.display_name_from(&self.state.terminals, &self.state.terminal_runtimes), focused: self.state.active == Some(index), pane_count: ws.public_pane_numbers.len(), tab_count: ws.tabs.len(), diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index fea88b55..7823de31 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -216,6 +216,8 @@ impl AppState { self.host_terminal_theme, ) { let new_id = new_pane.pane_id; + self.terminal_runtimes + .insert(new_pane.terminal.id.clone(), new_pane.runtime); self.terminals .insert(new_pane.terminal.id.clone(), new_pane.terminal); ws.layout.focus_pane(new_id); diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index 6445b6ab..798aafa8 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -330,6 +330,9 @@ impl App { self.state.host_terminal_theme, )?; let new_pane_id = new_pane.pane_id; + self.state + .terminal_runtimes + .insert(new_pane.terminal.id.clone(), new_pane.runtime); self.state .terminals .insert(new_pane.terminal.id.clone(), new_pane.terminal); @@ -1122,6 +1125,7 @@ mod tests { .await; assert_eq!(app.state.workspaces[0].tabs[0].layout.pane_count(), 2); + assert_eq!(app.state.terminal_runtimes.len(), 2); assert!(app.state.workspaces[0].tabs[0].zoomed); let _ = wait_for_file(&output_path); diff --git a/src/app/mod.rs b/src/app/mod.rs index c5654db2..1c818a9c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -412,8 +412,11 @@ impl App { state.terminals = restored_terminals; state.terminal_runtimes = restored_terminal_runtimes; - for ws in &mut state.workspaces { - ws.refresh_git_branch(); + for ws_idx in 0..state.workspaces.len() { + let cwd = state.workspaces[ws_idx] + .resolved_identity_cwd_from(&state.terminals, &state.terminal_runtimes); + state.workspaces[ws_idx].cached_git_branch = + cwd.as_deref().and_then(crate::workspace::git_branch); } // Background auto-update is disabled in monolithic no-session mode @@ -1702,7 +1705,7 @@ mod tests { } #[test] - fn terminal_target_resolves_unique_manual_name() { + fn terminal_target_resolves_unique_agent_name() { let mut app = test_app(); let workspace = Workspace::test_new("terminal-target-name"); let pane = workspace.tabs[0].root_pane; @@ -1718,7 +1721,7 @@ mod tests { .terminals .get_mut(&attached_terminal_id) .unwrap() - .manual_label = Some("reviewer".into()); + .set_agent_name("reviewer".into()); app.state.active = Some(0); app.state.selected = 0; @@ -1746,7 +1749,7 @@ mod tests { } #[test] - fn terminal_target_reports_ambiguous_duplicate_manual_name() { + fn terminal_target_reports_ambiguous_duplicate_agent_name() { let mut app = test_app(); let mut workspace = Workspace::test_new("terminal-target-ambiguous"); let first = workspace.tabs[0].root_pane; @@ -1762,7 +1765,7 @@ mod tests { .terminals .get_mut(&first_terminal_id) .unwrap() - .manual_label = Some("worker".into()); + .set_agent_name("worker".into()); let second_terminal_id = app.state.workspaces[0] .pane_state(second) .unwrap() @@ -1772,7 +1775,7 @@ mod tests { .terminals .get_mut(&second_terminal_id) .unwrap() - .manual_label = Some("worker".into()); + .set_agent_name("worker".into()); app.state.active = Some(0); app.state.selected = 0; diff --git a/src/app/runtime.rs b/src/app/runtime.rs index 98ae4e0e..55c89e73 100644 --- a/src/app/runtime.rs +++ b/src/app/runtime.rs @@ -247,7 +247,10 @@ impl App { .state .workspaces .iter() - .filter_map(|ws| ws.resolved_identity_cwd().map(|cwd| (ws.id.clone(), cwd))) + .filter_map(|ws| { + ws.resolved_identity_cwd_from(&self.state.terminals, &self.state.terminal_runtimes) + .map(|cwd| (ws.id.clone(), cwd)) + }) .collect(); if workspaces.is_empty() { diff --git a/src/app/terminal_targets.rs b/src/app/terminal_targets.rs index 94cf89d0..71ca83bb 100644 --- a/src/app/terminal_targets.rs +++ b/src/app/terminal_targets.rs @@ -65,8 +65,10 @@ impl App { .terminals .values() .find(|terminal| terminal.id.to_string() == candidate.terminal_id) - .and_then(|terminal| terminal.manual_label.as_deref()) - .is_some_and(|label| label == target) + .is_some_and(|terminal| { + terminal.agent_name.as_deref() == Some(target) + || terminal.effective_agent_label() == Some(target) + }) }) .collect(); if let Some(resolved) = self.single_terminal_match(target, name_matches)? { diff --git a/src/cli.rs b/src/cli.rs index ee73fb6f..a99149dc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -281,6 +281,7 @@ fn run_agent_command(args: &[String]) -> std::io::Result { "send" => agent_send(&args[1..]), "rename" => agent_rename(&args[1..]), "focus" => agent_focus(&args[1..]), + "wait" => agent_wait(&args[1..]), "attach" => agent_attach(&args[1..]), "start" => agent_start(&args[1..]), "help" | "--help" | "-h" => { @@ -934,14 +935,9 @@ fn agent_attach(args: &[String]) -> std::io::Result { Err(code) => return Ok(code), }; - let response = send_request(&Request { - id: "cli:agent:attach:resolve".into(), - method: Method::AgentGet(AgentTarget { - target: target.clone(), - }), - })?; - if let Some(error) = response.get("error") { - eprintln!("{}", serde_json::to_string(error).unwrap()); + let response = resolve_agent_target(&target, "cli:agent:attach:resolve")?; + if response.get("error").is_some() { + eprintln!("{}", serde_json::to_string(&response).unwrap()); return Ok(1); } let Some(terminal_id) = response["result"]["agent"]["terminal_id"].as_str() else { @@ -952,6 +948,107 @@ fn agent_attach(args: &[String]) -> std::io::Result { Ok(0) } +fn agent_wait(args: &[String]) -> std::io::Result { + let Some(target) = args.first() else { + eprintln!("usage: herdr agent wait --status [--timeout MS]"); + return Ok(2); + }; + + let mut timeout_ms = None; + let mut desired_status = None; + + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--status" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --status"); + return Ok(2); + }; + desired_status = Some(parse_agent_wait_status(value)?); + index += 2; + } + "--timeout" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --timeout"); + return Ok(2); + }; + timeout_ms = Some(parse_u64_flag("--timeout", value)?); + index += 2; + } + "help" | "--help" | "-h" => { + eprintln!("usage: herdr agent wait --status [--timeout MS]"); + return Ok(0); + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + let Some(agent_status) = desired_status else { + eprintln!("missing required --status"); + return Ok(2); + }; + + let response = resolve_agent_target(target, "cli:agent:wait:resolve")?; + if response.get("error").is_some() { + eprintln!("{}", serde_json::to_string(&response).unwrap()); + return Ok(1); + } + if response["result"]["agent"]["agent_status"] + .as_str() + .is_some_and(|current| agent_wait_status_satisfied(agent_status, current)) + { + println!("{}", serde_json::to_string(&response).unwrap()); + return Ok(0); + } + + let Some(pane_id) = response["result"]["agent"]["pane_id"].as_str() else { + eprintln!("agent wait failed: response did not include pane_id"); + return Ok(1); + }; + + let subscriptions = if agent_status == AgentStatus::Idle { + vec![ + Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(AgentStatus::Idle), + }, + Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(AgentStatus::Done), + }, + ] + } else { + vec![Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(agent_status), + }] + }; + + wait_for_agent_change( + Request { + id: "cli:agent:wait".into(), + method: Method::EventsSubscribe(crate::api::schema::EventsSubscribeParams { + subscriptions, + }), + }, + timeout_ms, + "timed out waiting for agent status change", + ) +} + +fn resolve_agent_target(target: &str, request_id: &str) -> std::io::Result { + send_request(&Request { + id: request_id.into(), + method: Method::AgentGet(AgentTarget { + target: target.to_owned(), + }), + }) +} + fn terminal_attach(args: &[String]) -> std::io::Result { let (terminal_id, takeover) = match parse_attach_target( args, @@ -1835,6 +1932,31 @@ fn parse_read_format(value: &str) -> std::io::Result { } } +fn agent_wait_status_satisfied(desired: AgentStatus, current: &str) -> bool { + match desired { + AgentStatus::Idle => matches!(current, "idle" | "done"), + AgentStatus::Working => current == "working", + AgentStatus::Blocked => current == "blocked", + AgentStatus::Unknown => current == "unknown", + AgentStatus::Done => false, + } +} + +fn parse_agent_wait_status(value: &str) -> std::io::Result { + match value { + "idle" => Ok(AgentStatus::Idle), + "working" => Ok(AgentStatus::Working), + "blocked" => Ok(AgentStatus::Blocked), + "unknown" => Ok(AgentStatus::Unknown), + "done" => Err(std::io::Error::other( + "done is a UI attention state; use idle for CLI agent completion waits", + )), + _ => Err(std::io::Error::other(format!( + "invalid agent status: {value} (expected idle, working, blocked, or unknown)" + ))), + } +} + fn parse_agent_status(value: &str) -> std::io::Result { match value { "idle" => Ok(AgentStatus::Idle), @@ -1978,6 +2100,7 @@ fn print_agent_help() { eprintln!(" herdr agent send "); eprintln!(" herdr agent rename |--clear"); eprintln!(" herdr agent focus "); + eprintln!(" herdr agent wait --status [--timeout MS]"); eprintln!(" herdr agent attach [--takeover]"); eprintln!(" herdr agent start [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- "); eprintln!(" targets accept terminal ids, unique agent names, and legacy pane ids"); diff --git a/src/persist/restore.rs b/src/persist/restore.rs index e53a61d9..0ac49e80 100644 --- a/src/persist/restore.rs +++ b/src/persist/restore.rs @@ -169,6 +169,10 @@ fn restore_tab( .get(id) .and_then(|old_id| snap.panes.get(old_id)) .and_then(|p| p.label.clone()); + let saved_agent_name = reverse_id_map + .get(id) + .and_then(|old_id| snap.panes.get(old_id)) + .and_then(|p| p.agent_name.clone()); match TerminalRuntime::spawn( *id, @@ -187,6 +191,9 @@ fn restore_tab( if let Some(label) = saved_label { terminal.set_manual_label(label); } + if let Some(agent_name) = saved_agent_name { + terminal.set_agent_name(agent_name); + } panes.insert(*id, PaneState::new(terminal_id.clone())); terminal_runtimes.insert(terminal_id, runtime); terminals.push(terminal); diff --git a/src/persist/snapshot.rs b/src/persist/snapshot.rs index 245fbb76..f9198e16 100644 --- a/src/persist/snapshot.rs +++ b/src/persist/snapshot.rs @@ -70,6 +70,8 @@ pub struct PaneSnapshot { pub cwd: PathBuf, #[serde(default, skip_serializing_if = "Option::is_none")] pub label: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_name: Option, } /// Serializable BSP tree. @@ -238,7 +240,7 @@ fn capture_workspace( id: Some(ws.id.clone()), custom_name: ws.custom_name.clone(), identity_cwd: ws - .resolved_identity_cwd() + .resolved_identity_cwd_from(terminals, terminal_runtimes) .unwrap_or_else(|| ws.identity_cwd.clone()), tabs: ws .tabs @@ -270,7 +272,19 @@ fn capture_tab( .get(id) .and_then(|pane| terminals.get(&pane.attached_terminal_id)) .and_then(|terminal| terminal.manual_label.clone()); - panes.insert(id.raw(), PaneSnapshot { cwd, label }); + let agent_name = tab + .panes + .get(id) + .and_then(|pane| terminals.get(&pane.attached_terminal_id)) + .and_then(|terminal| terminal.agent_name.clone()); + panes.insert( + id.raw(), + PaneSnapshot { + cwd, + label, + agent_name, + }, + ); } TabSnapshot { custom_name: tab.custom_name.clone(), @@ -427,6 +441,7 @@ mod tests { PaneSnapshot { cwd: PathBuf::from("/home/can/Projects/herdr"), label: None, + agent_name: None, }, ); panes.insert( @@ -434,6 +449,7 @@ mod tests { PaneSnapshot { cwd: PathBuf::from("/home/can/Projects/website"), label: Some("website".into()), + agent_name: None, }, ); @@ -747,6 +763,7 @@ mod tests { PaneSnapshot { cwd: PathBuf::from("/tmp/this-directory-does-not-exist-for-herdr-test"), label: None, + agent_name: None, }, ); panes.insert( @@ -756,6 +773,7 @@ mod tests { .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("/tmp")), label: None, + agent_name: None, }, ); diff --git a/src/terminal/state.rs b/src/terminal/state.rs index df89b688..f9d5b522 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -38,6 +38,7 @@ pub struct TerminalState { pub fallback_state: AgentState, pub hook_authority: Option, pub manual_label: Option, + pub agent_name: Option, hook_report_sequences: HashMap, pub state: AgentState, pub revision: u64, @@ -53,6 +54,7 @@ impl TerminalState { fallback_state: AgentState::Unknown, hook_authority: None, manual_label: None, + agent_name: None, hook_report_sequences: HashMap::new(), state: AgentState::Unknown, revision: 0, @@ -231,6 +233,21 @@ impl TerminalState { self.manual_label = None; } + pub fn set_agent_name(&mut self, name: String) { + let name = name.trim().to_string(); + self.agent_name = (!name.is_empty()).then_some(name); + } + + pub fn clear_agent_name(&mut self) { + self.agent_name = None; + } + + pub fn is_agent_terminal(&self) -> bool { + self.agent_name.is_some() + || self.effective_agent_label().is_some() + || self.launch_argv.is_some() + } + pub fn border_label(&self, show_agent_labels: bool) -> Option<&str> { self.manual_label.as_deref().or_else(|| { show_agent_labels diff --git a/src/ui/mobile.rs b/src/ui/mobile.rs index 8bcdf184..7b12177b 100644 --- a/src/ui/mobile.rs +++ b/src/ui/mobile.rs @@ -286,7 +286,10 @@ fn render_header_status(app: &AppState, frame: &mut Frame, area: Rect) { Span::styled(dot, dot_style.bg(p.panel_bg)), Span::raw(" "), Span::styled( - truncate(&ws.display_name(), name_w.saturating_sub(4) as usize), + truncate( + &ws.display_name_from(&app.terminals, &app.terminal_runtimes), + name_w.saturating_sub(4) as usize, + ), Style::default() .fg(p.text) .bg(p.panel_bg) @@ -437,7 +440,10 @@ fn render_mobile_switcher_content(app: &AppState, frame: &mut Frame, viewport: R Span::styled(dot, dot_style.bg(bg)), Span::styled(" ", Style::default().bg(bg)), Span::styled( - truncate(&ws.display_name(), content.width.saturating_sub(5) as usize), + truncate( + &ws.display_name_from(&app.terminals, &app.terminal_runtimes), + content.width.saturating_sub(5) as usize, + ), Style::default() .fg(p.text) .bg(bg) diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index eb6d6fab..32f9236f 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -137,7 +137,7 @@ pub(crate) fn agent_panel_entries(app: &AppState) -> Vec { .enumerate() .flat_map(|(ws_idx, ws)| { let multi_tab = ws.tabs.len() > 1; - let workspace_label = ws.display_name(); + let workspace_label = ws.display_name_from(&app.terminals, &app.terminal_runtimes); ws.pane_details(&app.terminals) .into_iter() .map(move |detail| AgentPanelEntry { @@ -646,7 +646,10 @@ fn render_workspace_list(app: &AppState, frame: &mut Frame, area: Rect, is_navig Span::styled(" ", Style::default()), Span::styled(icon, icon_style), Span::styled(" ", Style::default()), - Span::styled(ws.display_name(), name_style), + Span::styled( + ws.display_name_from(&app.terminals, &app.terminal_runtimes), + name_style, + ), ]; frame.render_widget( diff --git a/src/workspace.rs b/src/workspace.rs index 1d8842c8..69a83d75 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -517,6 +517,17 @@ impl Workspace { Some(self.identity_cwd.clone()) } + pub fn resolved_identity_cwd_from( + &self, + terminals: &HashMap, + terminal_runtimes: &HashMap, + ) -> Option { + self.tabs + .first() + .and_then(|tab| tab.cwd_for_pane(tab.root_pane, terminals, terminal_runtimes)) + .or_else(|| Some(self.identity_cwd.clone())) + } + pub fn display_name(&self) -> String { if let Some(name) = &self.custom_name { return name.clone(); @@ -527,6 +538,20 @@ impl Workspace { .unwrap_or_else(|| "workspace".into()) } + pub fn display_name_from( + &self, + terminals: &HashMap, + terminal_runtimes: &HashMap, + ) -> String { + if let Some(name) = &self.custom_name { + return name.clone(); + } + + self.resolved_identity_cwd_from(terminals, terminal_runtimes) + .map(|cwd| derive_label_from_cwd(&cwd)) + .unwrap_or_else(|| "workspace".into()) + } + pub fn branch(&self) -> Option { self.cached_git_branch.clone() } @@ -535,11 +560,6 @@ impl Workspace { self.cached_git_ahead_behind } - pub fn refresh_git_branch(&mut self) { - let cwd = self.resolved_identity_cwd(); - self.cached_git_branch = cwd.as_deref().and_then(git_branch); - } - #[cfg(test)] pub fn refresh_git_ahead_behind(&mut self) { let cwd = self.resolved_identity_cwd(); @@ -719,26 +739,23 @@ mod tests { use super::*; #[test] - fn workspace_identity_uses_identity_cwd() { - let root = std::env::temp_dir().join(format!( - "herdr-workspace-identity-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let identity_cwd = root.join("pion"); - std::fs::create_dir_all(identity_cwd.join(".git")).unwrap(); - + fn workspace_identity_follows_first_tab_root_pane_cwd() { let mut ws = Workspace::test_new("ignored"); ws.custom_name = None; - ws.identity_cwd = identity_cwd.clone(); + let root_pane = ws.tabs[0].root_pane; + let terminal_id = ws.tabs[0].terminal_id(root_pane).unwrap().clone(); + let mut terminals = HashMap::new(); + terminals.insert( + terminal_id.clone(), + TerminalState::new(terminal_id, PathBuf::from("/herdr-test/pion")), + ); + let terminal_runtimes = HashMap::new(); - assert_eq!(ws.display_name(), "pion"); - assert_eq!(ws.resolved_identity_cwd(), Some(identity_cwd)); - - std::fs::remove_dir_all(root).unwrap(); + assert_eq!(ws.display_name_from(&terminals, &terminal_runtimes), "pion"); + assert_eq!( + ws.resolved_identity_cwd_from(&terminals, &terminal_runtimes), + Some(PathBuf::from("/herdr-test/pion")) + ); } #[test] diff --git a/tests/api_ping.rs b/tests/api_ping.rs index 4300b54e..066c9179 100644 --- a/tests/api_ping.rs +++ b/tests/api_ping.rs @@ -661,20 +661,26 @@ fn agent_methods_round_trip_over_socket() { let agents = listed["result"]["agents"].as_array().unwrap(); assert_eq!(agents.len(), 1); assert_eq!(agents[0]["terminal_id"], terminal_id); - assert_eq!(agents[0]["name"], "worker"); + assert!(agents[0].get("name").is_none()); assert_eq!(agents[0]["agent"], "pi"); assert_eq!(agents[0]["agent_status"], "working"); assert_eq!(agents[0]["pane_id"], pane_id); - let fetched_by_name = send_request( + let fetched_by_detected_agent = send_request( &socket_path, - r#"{"id":"agent_get_name","method":"agent.get","params":{"target":"worker"}}"#, + r#"{"id":"agent_get_detected","method":"agent.get","params":{"target":"pi"}}"#, ); assert_eq!( - fetched_by_name["result"]["agent"]["terminal_id"], + fetched_by_detected_agent["result"]["agent"]["terminal_id"], terminal_id ); + let renamed_first_agent = send_request( + &socket_path, + r#"{"id":"agent_rename_first","method":"agent.rename","params":{"target":"pi","name":"worker"}}"#, + ); + assert_eq!(renamed_first_agent["result"]["agent"]["name"], "worker"); + let fetched_by_terminal = send_request( &socket_path, &format!( @@ -715,14 +721,23 @@ fn agent_methods_round_trip_over_socket() { .as_str() .unwrap(); - let second_renamed = send_request( + let second_reported = send_request( &socket_path, &format!( - r#"{{"id":"agent_second_rename","method":"pane.rename","params":{{"pane_id":"{}","label":"reviewer"}}}}"#, + r#"{{"id":"agent_second_report","method":"pane.report_agent","params":{{"pane_id":"{}","source":"test","agent":"codex","state":"idle"}}}}"#, second_pane_id ), ); - assert_eq!(second_renamed["result"]["pane"]["label"], "reviewer"); + assert_eq!(second_reported["result"]["type"], "ok"); + + let second_renamed = send_request( + &socket_path, + &format!( + r#"{{"id":"agent_second_rename","method":"agent.rename","params":{{"target":"{}","name":"reviewer"}}}}"#, + second_terminal_id + ), + ); + assert_eq!(second_renamed["result"]["agent"]["name"], "reviewer"); let duplicate = send_request( &socket_path, diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index 0cf60c5d..dd07f760 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -1301,7 +1301,7 @@ fn agent_commands_work() { .unwrap() .to_string(); - let renamed = run_cli(&socket_path, &["pane", "rename", &root_pane_id, "worker"]); + let renamed = run_cli(&socket_path, &["agent", "rename", &root_pane_id, "worker"]); assert!(renamed.status.success()); let listed = run_cli_json(&socket_path, &["agent", "list"]); @@ -1312,6 +1312,20 @@ fn agent_commands_work() { let fetched = run_cli_json(&socket_path, &["agent", "get", "worker"]); assert_eq!(fetched["result"]["agent"]["pane_id"], root_pane_id); + let waited = run_cli_json( + &socket_path, + &[ + "agent", + "wait", + "worker", + "--status", + "unknown", + "--timeout", + "100", + ], + ); + assert_eq!(waited["result"]["agent"]["pane_id"], root_pane_id); + let read = run_cli_json( &socket_path, &["agent", "read", &terminal_id, "--source", "visible"],