use super::{api_helpers::pane_agent_status, App}; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct TerminalTarget { pub ws_idx: usize, pub tab_idx: usize, pub pane_id: crate::layout::PaneId, pub terminal_id: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct TerminalTargetCandidate { pub terminal_id: String, pub pane_id: String, pub workspace_id: String, pub tab_id: String, pub cwd: Option, pub agent_status: crate::api::schema::AgentStatus, } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum TerminalTargetError { NotFound { target: String, }, Ambiguous { target: String, candidates: Vec, }, } impl App { pub(crate) fn resolve_terminal_target( &self, target: &str, ) -> Result { let terminal_matches: Vec<_> = self .terminal_targets() .into_iter() .filter(|candidate| candidate.terminal_id == target) .collect(); if let Some(resolved) = self.single_terminal_match(target, terminal_matches)? { return Ok(resolved); } if let Some((ws_idx, pane_id)) = self.parse_current_public_pane_id(target) { if let Some(resolved) = self.terminal_target_for_pane(ws_idx, pane_id) { return Ok(resolved); } } let agent_matches: Vec<_> = self .terminal_targets() .into_iter() .filter(|candidate| { self.state .terminals .get(candidate.terminal_id.as_str()) .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, agent_matches)? { return Ok(resolved); } Err(TerminalTargetError::NotFound { target: target.to_string(), }) } pub(crate) fn resolve_agent_target( &self, target: &str, ) -> Result { if let Some((ws_idx, pane_id)) = self.parse_current_public_pane_id(target) { if let Some(resolved) = self .terminal_target_for_pane(ws_idx, pane_id) .filter(|resolved| self.target_is_agent(resolved)) { return Ok(resolved); } } let name_matches: Vec<_> = self .terminal_targets() .into_iter() .filter(|candidate| { self.state .terminals .get(candidate.terminal_id.as_str()) .is_some_and(|terminal| terminal.agent_name.as_deref() == Some(target)) }) .collect(); if let Some(resolved) = self.single_terminal_match(target, name_matches)? { return Ok(resolved); } Err(TerminalTargetError::NotFound { target: target.to_string(), }) } fn target_is_agent(&self, target: &TerminalTarget) -> bool { self.state .terminals .get(target.terminal_id.as_str()) .is_some_and(|terminal| terminal.is_agent_terminal()) } fn single_terminal_match( &self, target: &str, matches: Vec, ) -> Result, TerminalTargetError> { match matches.len() { 0 => Ok(None), 1 => Ok(matches.into_iter().next()), _ => Err(TerminalTargetError::Ambiguous { target: target.to_string(), candidates: matches .into_iter() .filter_map(|candidate| { self.terminal_target_candidate(candidate.ws_idx, candidate.pane_id) }) .collect(), }), } } fn terminal_targets(&self) -> Vec { self.state .workspaces .iter() .enumerate() .flat_map(|(ws_idx, ws)| { ws.tabs.iter().enumerate().flat_map(move |(tab_idx, tab)| { tab.layout .pane_ids() .into_iter() .filter_map(move |pane_id| { tab.terminal_id(pane_id).map(|terminal_id| TerminalTarget { ws_idx, tab_idx, pane_id, terminal_id: terminal_id.to_string(), }) }) }) }) .collect() } fn terminal_target_for_pane( &self, ws_idx: usize, pane_id: crate::layout::PaneId, ) -> Option { let ws = self.state.workspaces.get(ws_idx)?; let tab_idx = ws.find_tab_index_for_pane(pane_id)?; let terminal_id = ws.terminal_id(pane_id)?.to_string(); Some(TerminalTarget { ws_idx, tab_idx, pane_id, terminal_id, }) } fn terminal_target_candidate( &self, ws_idx: usize, pane_id: crate::layout::PaneId, ) -> Option { let ws = self.state.workspaces.get(ws_idx)?; let tab_idx = ws.find_tab_index_for_pane(pane_id)?; let pane = ws.pane_state(pane_id)?; let terminal = self.state.terminals.get(&pane.attached_terminal_id)?; Some(TerminalTargetCandidate { terminal_id: terminal.id.to_string(), pane_id: self.public_pane_id(ws_idx, pane_id)?, workspace_id: self.public_workspace_id(ws_idx), tab_id: self.public_tab_id(ws_idx, tab_idx)?, cwd: ws.tabs[tab_idx] .cwd_for_pane(pane_id, &self.state.terminals, &self.terminal_runtimes) .map(|cwd| cwd.display().to_string()), agent_status: pane_agent_status(terminal.state, pane.seen), }) } } #[cfg(test)] mod tests { use super::*; use crate::app::{AppPolicy, AppState}; fn test_app() -> App { let (_, api_rx) = tokio::sync::mpsc::unbounded_channel(); App::new( &crate::config::Config::default(), AppPolicy::TEST, None, api_rx, crate::api::EventHub::default(), ) } #[test] fn named_targets_follow_terminal_identity_after_pane_and_tab_reordering() { let mut app = test_app(); app.state = AppState::test_with_adversarial_identity_state(); let targets = app.terminal_targets(); for (index, target) in targets.iter().enumerate() { app.state .terminals .get_mut(target.terminal_id.as_str()) .unwrap() .set_agent_name(format!("worker-{index}")); } for (index, target) in targets.iter().enumerate() { let name = format!("worker-{index}"); assert_eq!(app.resolve_terminal_target(&name).unwrap(), *target); assert_eq!(app.resolve_agent_target(&name).unwrap(), *target); let pane = app.public_pane_id(target.ws_idx, target.pane_id).unwrap(); assert_eq!(app.resolve_agent_target(&pane).unwrap(), *target); } app.state.assert_invariants_for_test(); } #[test] fn name_lookup_keeps_pane_order_and_ignores_detached_terminals() { let mut app = test_app(); app.state = AppState::test_with_adversarial_identity_state(); let targets = app.terminal_targets(); for target in &targets { app.state .terminals .get_mut(target.terminal_id.as_str()) .unwrap() .set_agent_name("shared".into()); } let detached_id = crate::terminal::TerminalId::alloc(); let mut detached = crate::terminal::TerminalState::new(detached_id.clone(), std::env::temp_dir()); detached.set_agent_name("detached".into()); app.state.terminals.insert(detached_id, detached); assert!(matches!( app.resolve_agent_target("detached"), Err(TerminalTargetError::NotFound { .. }) )); for result in [ app.resolve_terminal_target("shared"), app.resolve_agent_target("shared"), ] { let Err(TerminalTargetError::Ambiguous { candidates, .. }) = result else { panic!("expected all attached panes to remain ambiguous"); }; assert_eq!( candidates .iter() .map(|candidate| candidate.terminal_id.as_str()) .collect::>(), targets .iter() .map(|target| target.terminal_id.as_str()) .collect::>(), ); } } #[test] #[ignore = "manual terminal target lookup scaling profile"] fn terminal_target_lookup_profile() { use std::hint::black_box; use std::time::{Duration, Instant}; for count in [1, 15, 128, 512] { let mut app = test_app(); let mut workspace = crate::workspace::Workspace::test_new("lookup-profile"); for _ in 1..count { workspace.test_split(ratatui::layout::Direction::Horizontal); } app.state.workspaces = vec![workspace]; app.state.ensure_test_terminals(); let id = app.state.workspaces[0] .terminal_id(app.state.workspaces[0].tabs[0].root_pane) .unwrap() .clone(); app.state .terminals .get_mut(&id) .unwrap() .set_agent_name("profile-target".into()); for (label, agent, target) in [ ("terminal-name", false, "profile-target"), ("agent-name", true, "profile-target"), ("missing", false, "missing-target"), ] { let lookup = || { if agent { app.resolve_agent_target(target) } else { app.resolve_terminal_target(target) } }; for _ in 0..32 { black_box(lookup()).ok(); } let mut samples = Vec::new(); for _ in 0..7 { let start = Instant::now(); let mut iterations = 0; while start.elapsed() < Duration::from_millis(20) { black_box(lookup()).ok(); iterations += 1; } samples.push(start.elapsed().as_secs_f64() * 1e6 / f64::from(iterations)); } samples.sort_by(f64::total_cmp); println!( "terminal-target panes={count} case={label} median_us={:.3}", samples[3] ); } } } }