fix: polish terminal-core agent semantics

This commit is contained in:
Ogulcan Celik 2026-05-17 01:41:17 +03:00
parent 3ecacfddae
commit f7fe0f203e
17 changed files with 316 additions and 64 deletions

View File

@ -693,18 +693,23 @@ impl AppState {
pub fn apply_workspace_git_statuses(&mut self, results: Vec<WorkspaceGitStatus>) -> 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)

View File

@ -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<crate::api::schema::AgentInfo> {
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,

View File

@ -8,7 +8,10 @@ use crate::workspace::Workspace;
impl App {
pub(super) fn seed_cwd_from_workspace(&self, ws_idx: usize) -> Option<std::path::PathBuf> {
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<usize> {
@ -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(),

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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() {

View File

@ -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)? {

View File

@ -281,6 +281,7 @@ fn run_agent_command(args: &[String]) -> std::io::Result<i32> {
"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<i32> {
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<i32> {
Ok(0)
}
fn agent_wait(args: &[String]) -> std::io::Result<i32> {
let Some(target) = args.first() else {
eprintln!("usage: herdr agent wait <target> --status <idle|working|blocked|unknown> [--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 <target> --status <idle|working|blocked|unknown> [--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<serde_json::Value> {
send_request(&Request {
id: request_id.into(),
method: Method::AgentGet(AgentTarget {
target: target.to_owned(),
}),
})
}
fn terminal_attach(args: &[String]) -> std::io::Result<i32> {
let (terminal_id, takeover) = match parse_attach_target(
args,
@ -1835,6 +1932,31 @@ fn parse_read_format(value: &str) -> std::io::Result<ReadFormat> {
}
}
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<AgentStatus> {
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<AgentStatus> {
match value {
"idle" => Ok(AgentStatus::Idle),
@ -1978,6 +2100,7 @@ fn print_agent_help() {
eprintln!(" herdr agent send <target> <text>");
eprintln!(" herdr agent rename <target> <name>|--clear");
eprintln!(" herdr agent focus <target>");
eprintln!(" herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]");
eprintln!(" herdr agent attach <target> [--takeover]");
eprintln!(" herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>");
eprintln!(" targets accept terminal ids, unique agent names, and legacy pane ids");

View File

@ -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);

View File

@ -70,6 +70,8 @@ pub struct PaneSnapshot {
pub cwd: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_name: Option<String>,
}
/// 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,
},
);

View File

@ -38,6 +38,7 @@ pub struct TerminalState {
pub fallback_state: AgentState,
pub hook_authority: Option<HookAuthority>,
pub manual_label: Option<String>,
pub agent_name: Option<String>,
hook_report_sequences: HashMap<String, u64>,
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

View File

@ -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)

View File

@ -137,7 +137,7 @@ pub(crate) fn agent_panel_entries(app: &AppState) -> Vec<AgentPanelEntry> {
.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(

View File

@ -517,6 +517,17 @@ impl Workspace {
Some(self.identity_cwd.clone())
}
pub fn resolved_identity_cwd_from(
&self,
terminals: &HashMap<TerminalId, TerminalState>,
terminal_runtimes: &HashMap<TerminalId, TerminalRuntime>,
) -> Option<PathBuf> {
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<TerminalId, TerminalState>,
terminal_runtimes: &HashMap<TerminalId, TerminalRuntime>,
) -> 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<String> {
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]

View File

@ -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,

View File

@ -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"],