aboutsummaryrefslogtreecommitdiff
path: root/src/app/agents.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-17 01:41:16 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-17 01:41:16 +0300
commit86adf88351e17c6a11cac253714b169eac0db233 (patch)
tree883d32b0126e0b69449e0e9c0a7017d279c23935 /src/app/agents.rs
parentd2aa8865911ded97a67054fb2392a1c12fae018c (diff)
feat: add terminal identity and agent cli facade
Diffstat (limited to 'src/app/agents.rs')
-rw-r--r--src/app/agents.rs213
1 files changed, 213 insertions, 0 deletions
diff --git a/src/app/agents.rs b/src/app/agents.rs
new file mode 100644
index 00000000..88f7ec97
--- /dev/null
+++ b/src/app/agents.rs
@@ -0,0 +1,213 @@
+use super::{terminal_targets::TerminalTargetError, App, Mode};
+
+impl App {
+ pub(super) fn collect_agent_infos(&self) -> Vec<crate::api::schema::AgentInfo> {
+ self.state
+ .workspaces
+ .iter()
+ .enumerate()
+ .flat_map(|(ws_idx, ws)| {
+ ws.tabs.iter().flat_map(move |tab| {
+ tab.layout
+ .pane_ids()
+ .into_iter()
+ .filter_map(move |pane_id| self.agent_info(ws_idx, pane_id))
+ })
+ })
+ .filter(|agent| agent.name.is_some() || agent.agent.is_some())
+ .collect()
+ }
+
+ pub(super) fn agent_info_for_target(
+ &self,
+ target: &str,
+ ) -> Result<crate::api::schema::AgentInfo, TerminalTargetError> {
+ let resolved = self.resolve_terminal_target(target)?;
+ self.agent_info(resolved.ws_idx, resolved.pane_id)
+ .ok_or_else(|| TerminalTargetError::NotFound {
+ target: target.to_string(),
+ })
+ }
+
+ pub(super) fn focus_agent_target(
+ &mut self,
+ target: &str,
+ ) -> Result<crate::api::schema::AgentInfo, TerminalTargetError> {
+ let resolved = self.resolve_terminal_target(target)?;
+ self.state.switch_workspace(resolved.ws_idx);
+ self.state.switch_tab(resolved.tab_idx);
+ if let Some(tab) = self
+ .state
+ .workspaces
+ .get_mut(resolved.ws_idx)
+ .and_then(|ws| ws.tabs.get_mut(resolved.tab_idx))
+ {
+ tab.layout.focus_pane(resolved.pane_id);
+ }
+ self.state.mode = Mode::Terminal;
+ self.agent_info(resolved.ws_idx, resolved.pane_id)
+ .ok_or_else(|| TerminalTargetError::NotFound {
+ target: target.to_string(),
+ })
+ }
+
+ pub(super) fn rename_agent_target(
+ &mut self,
+ target: &str,
+ name: Option<String>,
+ ) -> Result<crate::api::schema::AgentInfo, AgentRenameError> {
+ let resolved = self
+ .resolve_terminal_target(target)
+ .map_err(AgentRenameError::Target)?;
+ let normalized_name = name.and_then(|name| {
+ let trimmed = name.trim().to_string();
+ (!trimmed.is_empty()).then_some(trimmed)
+ });
+
+ if let Some(name) = normalized_name.as_deref() {
+ let conflicts = self.agent_name_conflicts(name, &resolved.terminal_id);
+ if !conflicts.is_empty() {
+ return Err(AgentRenameError::DuplicateName {
+ name: name.to_string(),
+ candidates: conflicts,
+ });
+ }
+ }
+
+ let Some(ws) = self.state.workspaces.get_mut(resolved.ws_idx) else {
+ return Err(AgentRenameError::Target(TerminalTargetError::NotFound {
+ target: target.to_string(),
+ }));
+ };
+ let Some(pane_state) = ws.pane_state_mut(resolved.pane_id) else {
+ return Err(AgentRenameError::Target(TerminalTargetError::NotFound {
+ target: target.to_string(),
+ }));
+ };
+ match normalized_name {
+ Some(name) => pane_state.set_manual_label(name),
+ None => pane_state.clear_manual_label(),
+ }
+ self.state.mark_session_dirty();
+ self.agent_info(resolved.ws_idx, resolved.pane_id)
+ .ok_or_else(|| {
+ AgentRenameError::Target(TerminalTargetError::NotFound {
+ target: target.to_string(),
+ })
+ })
+ }
+
+ pub(super) fn agent_target_error_body(
+ &self,
+ err: TerminalTargetError,
+ ) -> crate::api::schema::ErrorBody {
+ match err {
+ TerminalTargetError::NotFound { target } => crate::api::schema::ErrorBody {
+ code: "agent_not_found".into(),
+ message: format!("agent target {target} not found"),
+ },
+ TerminalTargetError::Ambiguous { target, candidates } => {
+ crate::api::schema::ErrorBody {
+ code: "agent_target_ambiguous".into(),
+ message: format!(
+ "agent target {target} is ambiguous; candidates: {}",
+ candidates
+ .into_iter()
+ .map(|candidate| format!(
+ "terminal_id={} pane_id={} workspace_id={} tab_id={} cwd={} status={:?}",
+ candidate.terminal_id,
+ candidate.pane_id,
+ candidate.workspace_id,
+ candidate.tab_id,
+ candidate.cwd.unwrap_or_else(|| "unknown".into()),
+ candidate.agent_status,
+ ))
+ .collect::<Vec<_>>()
+ .join("; ")
+ ),
+ }
+ }
+ }
+ }
+
+ pub(super) fn agent_rename_error_body(
+ &self,
+ err: AgentRenameError,
+ ) -> crate::api::schema::ErrorBody {
+ match err {
+ AgentRenameError::Target(err) => self.agent_target_error_body(err),
+ AgentRenameError::DuplicateName { name, candidates } => crate::api::schema::ErrorBody {
+ code: "agent_name_taken".into(),
+ message: format!(
+ "agent name {name} is already used; candidates: {}",
+ candidates
+ .into_iter()
+ .map(|candidate| format!(
+ "terminal_id={} pane_id={} workspace_id={} tab_id={} cwd={} status={:?}",
+ candidate.terminal_id,
+ candidate.pane_id,
+ candidate.workspace_id,
+ candidate.tab_id,
+ candidate.cwd.unwrap_or_else(|| "unknown".into()),
+ candidate.agent_status,
+ ))
+ .collect::<Vec<_>>()
+ .join("; ")
+ ),
+ },
+ }
+ }
+
+ fn agent_info(
+ &self,
+ ws_idx: usize,
+ pane_id: crate::layout::PaneId,
+ ) -> Option<crate::api::schema::AgentInfo> {
+ let pane = self.pane_info(ws_idx, pane_id)?;
+ Some(crate::api::schema::AgentInfo {
+ terminal_id: pane.terminal_id,
+ name: pane.label,
+ agent: pane.agent,
+ agent_status: pane.agent_status,
+ workspace_id: pane.workspace_id,
+ tab_id: pane.tab_id,
+ pane_id: pane.pane_id,
+ focused: pane.focused,
+ cwd: pane.cwd,
+ revision: pane.revision,
+ })
+ }
+
+ fn agent_name_conflicts(
+ &self,
+ name: &str,
+ except_terminal_id: &str,
+ ) -> Vec<crate::api::schema::AgentInfo> {
+ self.state
+ .workspaces
+ .iter()
+ .enumerate()
+ .flat_map(|(ws_idx, ws)| {
+ ws.tabs.iter().flat_map(move |tab| {
+ tab.layout
+ .pane_ids()
+ .into_iter()
+ .filter_map(move |pane_id| {
+ let pane_state = ws.pane_state(pane_id)?;
+ (pane_state.manual_label.as_deref() == Some(name))
+ .then(|| self.agent_info(ws_idx, pane_id))?
+ })
+ })
+ })
+ .filter(|agent| agent.terminal_id != except_terminal_id)
+ .collect()
+ }
+}
+
+pub(super) enum AgentRenameError {
+ Target(TerminalTargetError),
+ DuplicateName {
+ name: String,
+ candidates: Vec<crate::api::schema::AgentInfo>,
+ },
+}