aboutsummaryrefslogtreecommitdiff
path: root/src/app
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
parentd2aa8865911ded97a67054fb2392a1c12fae018c (diff)
feat: add terminal identity and agent cli facade
Diffstat (limited to 'src/app')
-rw-r--r--src/app/agents.rs213
-rw-r--r--src/app/api.rs147
-rw-r--r--src/app/creation.rs1
-rw-r--r--src/app/mod.rs101
-rw-r--r--src/app/terminal_targets.rs165
5 files changed, 627 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>,
+ },
+}
diff --git a/src/app/api.rs b/src/app/api.rs
index 5f959365..666e8a20 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -828,6 +828,153 @@ impl App {
result: ResponseResult::Ok {},
}
}
+ Method::AgentList(_) => SuccessResponse {
+ id: request.id,
+ result: ResponseResult::AgentList {
+ agents: self.collect_agent_infos(),
+ },
+ },
+ Method::AgentGet(target) => {
+ let agent = match self.agent_info_for_target(&target.target) {
+ Ok(agent) => agent,
+ Err(err) => {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: self.agent_target_error_body(err),
+ })
+ .unwrap();
+ }
+ };
+ SuccessResponse {
+ id: request.id,
+ result: ResponseResult::AgentInfo { agent },
+ }
+ }
+ Method::AgentFocus(target) => {
+ let agent = match self.focus_agent_target(&target.target) {
+ Ok(agent) => agent,
+ Err(err) => {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: self.agent_target_error_body(err),
+ })
+ .unwrap();
+ }
+ };
+ SuccessResponse {
+ id: request.id,
+ result: ResponseResult::AgentInfo { agent },
+ }
+ }
+ Method::AgentRename(params) => {
+ let agent = match self.rename_agent_target(&params.target, params.name) {
+ Ok(agent) => agent,
+ Err(err) => {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: self.agent_rename_error_body(err),
+ })
+ .unwrap();
+ }
+ };
+ SuccessResponse {
+ id: request.id,
+ result: ResponseResult::AgentInfo { agent },
+ }
+ }
+ Method::AgentRead(params) => {
+ let resolved = match self.resolve_terminal_target(&params.target) {
+ Ok(resolved) => resolved,
+ Err(err) => {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: self.agent_target_error_body(err),
+ })
+ .unwrap();
+ }
+ };
+ let Some((pane, workspace_id)) =
+ self.lookup_runtime(resolved.ws_idx, resolved.pane_id)
+ else {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: ErrorBody {
+ code: "agent_not_found".into(),
+ message: format!("agent target {} not found", params.target),
+ },
+ })
+ .unwrap();
+ };
+ let requested_lines = params.lines.unwrap_or(80).min(1000) as usize;
+ let text = match params.format {
+ ReadFormat::Text => match params.source {
+ ReadSource::Visible => pane.visible_text(),
+ ReadSource::Recent => pane.recent_text(requested_lines),
+ ReadSource::RecentUnwrapped => pane.recent_unwrapped_text(requested_lines),
+ },
+ ReadFormat::Ansi => match params.source {
+ ReadSource::Visible => pane.visible_ansi(),
+ ReadSource::Recent => pane.recent_ansi(requested_lines),
+ ReadSource::RecentUnwrapped => pane.recent_unwrapped_ansi(requested_lines),
+ },
+ };
+ SuccessResponse {
+ id: request.id,
+ result: ResponseResult::PaneRead {
+ read: PaneReadResult {
+ pane_id: self
+ .public_pane_id(resolved.ws_idx, resolved.pane_id)
+ .unwrap_or_else(|| params.target.clone()),
+ workspace_id,
+ tab_id: self
+ .public_tab_id(resolved.ws_idx, resolved.tab_idx)
+ .unwrap(),
+ source: params.source,
+ format: params.format,
+ text,
+ revision: 0,
+ truncated: false,
+ },
+ },
+ }
+ }
+ Method::AgentSend(params) => {
+ let resolved = match self.resolve_terminal_target(&params.target) {
+ Ok(resolved) => resolved,
+ Err(err) => {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: self.agent_target_error_body(err),
+ })
+ .unwrap();
+ }
+ };
+ let Some(runtime) = self.lookup_runtime_sender(resolved.ws_idx, resolved.pane_id)
+ else {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: ErrorBody {
+ code: "agent_not_found".into(),
+ message: format!("agent target {} not found", params.target),
+ },
+ })
+ .unwrap();
+ };
+ if let Err(err) = runtime.try_send_bytes(Bytes::from(params.text)) {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: ErrorBody {
+ code: "agent_send_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap();
+ }
+ SuccessResponse {
+ id: request.id,
+ result: ResponseResult::Ok {},
+ }
+ }
Method::PaneSplit(params) => {
let Some((ws_idx, target_pane_id)) = self.parse_pane_id(&params.target_pane_id)
else {
diff --git a/src/app/creation.rs b/src/app/creation.rs
index a2758d7a..f1ee41d8 100644
--- a/src/app/creation.rs
+++ b/src/app/creation.rs
@@ -239,6 +239,7 @@ impl App {
.is_some_and(|focused| focused == pane_id);
Some(crate::api::schema::PaneInfo {
pane_id: self.public_pane_id(ws_idx, pane_id)?,
+ terminal_id: ws.terminal_id(pane_id)?.to_string(),
workspace_id: self.public_workspace_id(ws_idx),
tab_id: self.public_tab_id(ws_idx, tab_idx)?,
focused,
diff --git a/src/app/mod.rs b/src/app/mod.rs
index d9f666ed..d37bcb5d 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -5,6 +5,7 @@
//! - `input.rs` — key/mouse → action translation
pub(crate) mod actions;
+mod agents;
mod api;
mod api_helpers;
mod config_io;
@@ -14,6 +15,7 @@ mod input;
mod runtime;
mod session;
pub mod state;
+mod terminal_targets;
mod theme_sync;
use std::collections::{HashMap, HashSet};
@@ -1512,6 +1514,8 @@ mod tests {
assert_eq!(tab.workspace_id, workspace.workspace_id);
assert_eq!(root_pane.workspace_id, workspace.workspace_id);
assert_eq!(root_pane.tab_id, tab.tab_id);
+ assert!(root_pane.terminal_id.starts_with("term_"));
+ assert_ne!(root_pane.terminal_id, root_pane.pane_id);
}
#[test]
@@ -1624,6 +1628,103 @@ mod tests {
.is_none());
}
+ #[test]
+ fn terminal_target_resolves_terminal_id() {
+ let mut app = test_app();
+ let workspace = Workspace::test_new("terminal-target-id");
+ let pane = workspace.tabs[0].root_pane;
+ let terminal_id = workspace.terminal_id(pane).unwrap().to_string();
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+
+ let resolved = app.resolve_terminal_target(&terminal_id).unwrap();
+
+ assert_eq!(resolved.ws_idx, 0);
+ assert_eq!(resolved.pane_id, pane);
+ assert_eq!(resolved.terminal_id, terminal_id);
+ }
+
+ #[test]
+ fn terminal_target_resolves_legacy_pane_id() {
+ let mut app = test_app();
+ let workspace = Workspace::test_new("terminal-target-pane");
+ let pane = workspace.tabs[0].root_pane;
+ let terminal_id = workspace.terminal_id(pane).unwrap().to_string();
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ let pane_id = app.public_pane_id(0, pane).unwrap();
+
+ let resolved = app.resolve_terminal_target(&pane_id).unwrap();
+
+ assert_eq!(resolved.pane_id, pane);
+ assert_eq!(resolved.terminal_id, terminal_id);
+ }
+
+ #[test]
+ fn terminal_target_resolves_unique_manual_name() {
+ let mut app = test_app();
+ let mut workspace = Workspace::test_new("terminal-target-name");
+ let pane = workspace.tabs[0].root_pane;
+ workspace.pane_state_mut(pane).unwrap().manual_label = Some("reviewer".into());
+ let terminal_id = workspace.terminal_id(pane).unwrap().to_string();
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+
+ let resolved = app.resolve_terminal_target("reviewer").unwrap();
+
+ assert_eq!(resolved.pane_id, pane);
+ assert_eq!(resolved.terminal_id, terminal_id);
+ }
+
+ #[test]
+ fn terminal_target_reports_missing_target() {
+ let mut app = test_app();
+ app.state.workspaces = vec![Workspace::test_new("terminal-target-missing")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+
+ let err = app.resolve_terminal_target("missing-agent").unwrap_err();
+
+ assert_eq!(
+ err,
+ crate::app::terminal_targets::TerminalTargetError::NotFound {
+ target: "missing-agent".into()
+ }
+ );
+ }
+
+ #[test]
+ fn terminal_target_reports_ambiguous_duplicate_manual_name() {
+ let mut app = test_app();
+ let mut workspace = Workspace::test_new("terminal-target-ambiguous");
+ let first = workspace.tabs[0].root_pane;
+ let second = workspace.test_split(ratatui::layout::Direction::Horizontal);
+ workspace.pane_state_mut(first).unwrap().manual_label = Some("worker".into());
+ workspace.pane_state_mut(second).unwrap().manual_label = Some("worker".into());
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+
+ let err = app.resolve_terminal_target("worker").unwrap_err();
+
+ let crate::app::terminal_targets::TerminalTargetError::Ambiguous { target, candidates } =
+ err
+ else {
+ panic!("expected ambiguous terminal target");
+ };
+ assert_eq!(target, "worker");
+ assert_eq!(candidates.len(), 2);
+ assert!(candidates.iter().all(|candidate| {
+ candidate.terminal_id.starts_with("term_")
+ && candidate.pane_id.starts_with(&app.state.workspaces[0].id)
+ && candidate.workspace_id == app.state.workspaces[0].id
+ && candidate.cwd.is_some()
+ }));
+ }
+
#[tokio::test]
async fn pane_split_request_targets_pane_in_background_tab() {
let _guard = config_env_lock().lock().unwrap();
diff --git a/src/app/terminal_targets.rs b/src/app/terminal_targets.rs
new file mode 100644
index 00000000..cf90717d
--- /dev/null
+++ b/src/app/terminal_targets.rs
@@ -0,0 +1,165 @@
+use super::{api_helpers::pane_agent_status, App};
+
+// Staged for #00f: the agent CLI will use this resolver once #00e provides terminal ids.
+#[allow(dead_code)]
+#[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,
+}
+
+// Staged for #00f: ambiguity details are produced before the agent facade consumes them.
+#[allow(dead_code)]
+#[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<String>,
+ pub agent_status: crate::api::schema::AgentStatus,
+}
+
+// Staged for #00f: target errors are specified now and surfaced by agent commands next.
+#[allow(dead_code)]
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) enum TerminalTargetError {
+ NotFound {
+ target: String,
+ },
+ Ambiguous {
+ target: String,
+ candidates: Vec<TerminalTargetCandidate>,
+ },
+}
+
+impl App {
+ // Staged for #00f: current pane APIs stay pane-targeted while agent APIs will call this.
+ #[allow(dead_code)]
+ pub(crate) fn resolve_terminal_target(
+ &self,
+ target: &str,
+ ) -> Result<TerminalTarget, TerminalTargetError> {
+ 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_pane_id(target) {
+ if let Some(resolved) = self.terminal_target_for_pane(ws_idx, pane_id) {
+ return Ok(resolved);
+ }
+ }
+
+ let name_matches: Vec<_> = self
+ .terminal_targets()
+ .into_iter()
+ .filter(|candidate| {
+ self.state.workspaces[candidate.ws_idx]
+ .pane_state(candidate.pane_id)
+ .and_then(|pane| pane.manual_label.as_deref())
+ .is_some_and(|label| label == target)
+ })
+ .collect();
+ if let Some(resolved) = self.single_terminal_match(target, name_matches)? {
+ return Ok(resolved);
+ }
+
+ Err(TerminalTargetError::NotFound {
+ target: target.to_string(),
+ })
+ }
+
+ // Staged for #00f with resolve_terminal_target.
+ #[allow(dead_code)]
+ fn single_terminal_match(
+ &self,
+ target: &str,
+ matches: Vec<TerminalTarget>,
+ ) -> Result<Option<TerminalTarget>, 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(),
+ }),
+ }
+ }
+
+ // Staged for #00f with resolve_terminal_target.
+ #[allow(dead_code)]
+ fn terminal_targets(&self) -> Vec<TerminalTarget> {
+ 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()
+ }
+
+ // Staged for #00f with resolve_terminal_target.
+ #[allow(dead_code)]
+ fn terminal_target_for_pane(
+ &self,
+ ws_idx: usize,
+ pane_id: crate::layout::PaneId,
+ ) -> Option<TerminalTarget> {
+ 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,
+ })
+ }
+
+ // Staged for #00f with resolve_terminal_target.
+ #[allow(dead_code)]
+ fn terminal_target_candidate(
+ &self,
+ ws_idx: usize,
+ pane_id: crate::layout::PaneId,
+ ) -> Option<TerminalTargetCandidate> {
+ 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)?;
+ Some(TerminalTargetCandidate {
+ terminal_id: ws.terminal_id(pane_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)
+ .map(|cwd| cwd.display().to_string()),
+ agent_status: pane_agent_status(pane.state, pane.seen),
+ })
+ }
+}