fix: polish terminal-core agent semantics
This commit is contained in:
parent
3ecacfddae
commit
f7fe0f203e
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)? {
|
||||
|
|
|
|||
139
src/cli.rs
139
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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue