aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/app/actions.rs14
-rw-r--r--src/app/agents.rs17
-rw-r--r--src/app/creation.rs7
-rw-r--r--src/app/input/mod.rs2
-rw-r--r--src/app/input/navigate.rs4
-rw-r--r--src/app/mod.rs17
-rw-r--r--src/app/runtime.rs5
-rw-r--r--src/app/terminal_targets.rs6
-rw-r--r--src/cli.rs139
-rw-r--r--src/persist/restore.rs7
-rw-r--r--src/persist/snapshot.rs22
-rw-r--r--src/terminal/state.rs17
-rw-r--r--src/ui/mobile.rs10
-rw-r--r--src/ui/sidebar.rs7
-rw-r--r--src/workspace.rs63
-rw-r--r--tests/api_ping.rs29
-rw-r--r--tests/cli_wrapper.rs16
17 files changed, 317 insertions, 65 deletions
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<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)
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<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,
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<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(),
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
@@ -331,6 +331,9 @@ impl App {
)?;
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);
ws.active_tab_mut()
@@ -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<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");
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<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,
},
);
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<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
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<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(
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<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();
-
- assert_eq!(ws.display_name(), "pion");
- assert_eq!(ws.resolved_identity_cwd(), Some(identity_cwd));
-
- std::fs::remove_dir_all(root).unwrap();
+ 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_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"],