use std::path::PathBuf; use crate::api::schema::{EventData, EventEnvelope, EventKind}; use tracing::error; use super::{ api_helpers::{pane_agent_status, tab_attention_priority}, App, Mode, }; use crate::{config::NewTerminalCwdConfig, workspace::Workspace}; pub(crate) fn resolve_new_terminal_cwd( policy: &NewTerminalCwdConfig, follow_cwd: Option, ) -> PathBuf { match policy { NewTerminalCwdConfig::Follow => follow_cwd .or_else(|| std::env::var_os("HOME").map(PathBuf::from)) .or_else(|| std::env::current_dir().ok()) .unwrap_or_else(|| PathBuf::from("/")), NewTerminalCwdConfig::Home => std::env::var_os("HOME") .map(PathBuf::from) .or_else(|| std::env::current_dir().ok()) .unwrap_or_else(|| PathBuf::from("/")), NewTerminalCwdConfig::Current => { std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")) } NewTerminalCwdConfig::Path(path) => crate::worktree::expand_tilde_path(path), } } impl App { pub(super) fn seed_cwd_from_workspace(&self, ws_idx: usize) -> Option { self.state .workspaces .get(ws_idx)? .resolved_identity_cwd_from(&self.state.terminals, &self.terminal_runtimes) } pub(super) fn resolve_new_terminal_cwd(&self, follow_cwd: Option) -> PathBuf { resolve_new_terminal_cwd(&self.state.new_terminal_cwd, follow_cwd) } pub(super) fn workspace_creation_source(&self) -> Option { if self.state.mode == Mode::Navigate && self.state.workspaces.get(self.state.selected).is_some() { return Some(self.state.selected); } self.state.active.or_else(|| { self.state .workspaces .get(self.state.selected) .map(|_| self.state.selected) }) } /// Create a workspace with a real PTY (needs event_tx). pub(crate) fn create_workspace(&mut self) { let follow_cwd = self .workspace_creation_source() .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx)); let initial_cwd = self.resolve_new_terminal_cwd(follow_cwd); if let Err(e) = self.create_workspace_with_events(initial_cwd, true) { error!(err = %e, "failed to create workspace"); self.state.mode = Mode::Navigate; } } pub(crate) fn create_tab(&mut self) { let custom_name = self.state.requested_new_tab_name.take(); let active_before = self.state.active; let follow_cwd = self .state .active .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx)); let initial_cwd = self.resolve_new_terminal_cwd(follow_cwd); match self.create_tab_with_options(initial_cwd, true) { Ok(created_idx) => { let created_workspace = active_before.is_none(); let ws_idx = if created_workspace { Some(created_idx) } else { self.state.active }; let tab_idx = if created_workspace { 0 } else { created_idx }; if let Some(name) = custom_name { if let Some(ws) = ws_idx.and_then(|ws_idx| self.state.workspaces.get_mut(ws_idx)) { if let Some(tab) = ws.tabs.get_mut(tab_idx) { tab.set_custom_name(name); } self.schedule_session_save(); } } if let Some(ws_idx) = ws_idx { if created_workspace { self.emit_workspace_open_events(ws_idx); } else { self.emit_tab_created_events(ws_idx, tab_idx); } } } Err(e) => { error!(err = %e, "failed to create tab"); } } } pub(super) fn create_tab_with_options( &mut self, initial_cwd: PathBuf, focus: bool, ) -> std::io::Result { let Some(ws_idx) = self.state.active else { return self.create_workspace_with_options(initial_cwd, focus); }; let (rows, cols) = self.state.estimate_pane_size(); let ws = &mut self.state.workspaces[ws_idx]; let (idx, terminal, runtime) = ws.create_tab( rows, cols, initial_cwd, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode), Vec::new(), )?; let root_pane = ws.tabs[idx].root_pane; self.terminal_runtimes.insert(terminal.id.clone(), runtime); self.state.terminals.insert(terminal.id.clone(), terminal); self.state.remove_alias_shadowed_by_new_pane(root_pane); if focus { self.state.switch_workspace_tab(ws_idx, idx); self.state.mode = Mode::Terminal; } let workspace_id = self.state.workspaces[ws_idx].id.clone(); let tab_id = self .public_tab_id(ws_idx, idx) .unwrap_or_else(|| crate::workspace::public_tab_id_for_number(&workspace_id, idx + 1)); let root_pane = self.state.workspaces[ws_idx].tabs[idx].root_pane.raw(); crate::logging::tab_created(&workspace_id, &tab_id, root_pane); self.schedule_session_save(); Ok(idx) } pub(crate) fn create_workspace_with_options( &mut self, initial_cwd: PathBuf, focus: bool, ) -> std::io::Result { self.create_workspace_with_launch_env(initial_cwd, focus, Vec::new()) } pub(crate) fn create_workspace_with_events( &mut self, initial_cwd: PathBuf, focus: bool, ) -> std::io::Result<()> { let ws_idx = self.create_workspace_with_options(initial_cwd, focus)?; self.emit_workspace_open_events(ws_idx); Ok(()) } pub(crate) fn create_workspace_with_launch_env( &mut self, initial_cwd: PathBuf, focus: bool, extra_env: Vec<(String, String)>, ) -> std::io::Result { let (rows, cols) = self.state.estimate_pane_size(); let (ws, terminal, runtime) = Workspace::new_with_extra_env( initial_cwd, rows, cols, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode), self.event_tx.clone(), self.render_notify.clone(), self.render_dirty.clone(), extra_env, )?; self.terminal_runtimes.insert(terminal.id.clone(), runtime); self.state.terminals.insert(terminal.id.clone(), terminal); self.state.workspaces.push(ws); let idx = self.state.workspaces.len() - 1; self.state .remove_alias_shadowed_by_new_pane(self.state.workspaces[idx].tabs[0].root_pane); let workspace_id = self.state.workspaces[idx].id.clone(); let root_pane = self.state.workspaces[idx].tabs[0].root_pane.raw(); crate::logging::workspace_created(&workspace_id, root_pane); if focus || self.state.active.is_none() { self.state.switch_workspace(idx); self.state.mode = Mode::Terminal; } self.schedule_session_save(); Ok(idx) } pub(super) fn collect_panes_for_workspace( &self, workspace_id: Option<&str>, ) -> Result, (String, String)> { if let Some(workspace_id) = workspace_id { let Some(ws_idx) = self.parse_workspace_id(workspace_id) else { return Err(( "workspace_not_found".into(), format!("workspace {workspace_id} not found"), )); }; let Some(ws) = self.state.workspaces.get(ws_idx) else { return Err(( "workspace_not_found".into(), format!("workspace {workspace_id} not found"), )); }; Ok(ws .tabs .iter() .flat_map(|tab| tab.layout.pane_ids().into_iter()) .filter_map(|pane_id| self.pane_info(ws_idx, pane_id)) .collect()) } else { Ok(self .state .workspaces .iter() .enumerate() .flat_map(|(ws_idx, ws)| { ws.tabs .iter() .flat_map(|tab| tab.layout.pane_ids().into_iter()) .filter_map(move |pane_id| self.pane_info(ws_idx, pane_id)) }) .collect()) } } pub(super) fn tab_info( &self, ws_idx: usize, tab_idx: usize, ) -> Option { let ws = self.state.workspaces.get(ws_idx)?; let tab = ws.tabs.get(tab_idx)?; let (agg_state, seen) = tab .panes .values() .filter_map(|pane| { self.state .terminals .get(&pane.attached_terminal_id) .map(|terminal| (terminal.state, pane.seen)) }) .max_by_key(|(state, seen)| tab_attention_priority(*state, *seen)) .unwrap_or((crate::detect::AgentState::Unknown, true)); Some(crate::api::schema::TabInfo { tab_id: self.public_tab_id(ws_idx, tab_idx)?, workspace_id: self.public_workspace_id(ws_idx), number: tab.number, label: ws.tab_display_name(tab_idx)?, focused: self.state.active == Some(ws_idx) && ws.active_tab == tab_idx, pane_count: tab.panes.len(), agent_status: pane_agent_status(agg_state, seen), }) } pub(crate) fn emit_workspace_open_events(&mut self, ws_idx: usize) { let workspace_info = self.workspace_info(ws_idx); let Some(tab) = self.tab_info(ws_idx, 0) else { return; }; let Some(root_pane) = self.root_pane_info(ws_idx, 0) else { return; }; self.emit_event(EventEnvelope { event: EventKind::WorkspaceCreated, data: EventData::WorkspaceCreated { workspace: workspace_info, }, }); self.emit_tab_and_pane_created_events(tab, root_pane); } pub(crate) fn emit_tab_created_events(&mut self, ws_idx: usize, tab_idx: usize) { let Some(tab) = self.tab_info(ws_idx, tab_idx) else { return; }; let Some(root_pane) = self.root_pane_info(ws_idx, tab_idx) else { return; }; self.emit_tab_and_pane_created_events(tab, root_pane); } fn emit_tab_and_pane_created_events( &mut self, tab: crate::api::schema::TabInfo, root_pane: crate::api::schema::PaneInfo, ) { self.emit_event(EventEnvelope { event: EventKind::TabCreated, data: EventData::TabCreated { tab }, }); self.emit_event(EventEnvelope { event: EventKind::PaneCreated, data: EventData::PaneCreated { pane: root_pane }, }); } pub(super) fn workspace_created_result( &self, ws_idx: usize, ) -> Option { Some(crate::api::schema::ResponseResult::WorkspaceCreated { workspace: self.workspace_info(ws_idx), tab: self.tab_info(ws_idx, 0)?, root_pane: self.root_pane_info(ws_idx, 0)?, }) } pub(super) fn tab_created_result( &self, ws_idx: usize, tab_idx: usize, ) -> Option { Some(crate::api::schema::ResponseResult::TabCreated { tab: self.tab_info(ws_idx, tab_idx)?, root_pane: self.root_pane_info(ws_idx, tab_idx)?, }) } pub(super) fn root_pane_info( &self, ws_idx: usize, tab_idx: usize, ) -> Option { let ws = self.state.workspaces.get(ws_idx)?; let tab = ws.tabs.get(tab_idx)?; self.pane_info(ws_idx, tab.root_pane) } pub(super) fn pane_info( &self, ws_idx: usize, pane_id: crate::layout::PaneId, ) -> Option { let ws = self.state.workspaces.get(ws_idx)?; let pane = ws.pane_state(pane_id)?; let terminal = self.state.terminals.get(&pane.attached_terminal_id)?; let tab_idx = ws.find_tab_index_for_pane(pane_id)?; let focused = self.state.active == Some(ws_idx) && ws.active_tab == tab_idx && ws .focused_pane_id() .is_some_and(|focused| focused == pane_id); let presentation = terminal.effective_presentation(); Some(crate::api::schema::PaneInfo { pane_id: self.public_pane_id(ws_idx, pane_id)?, terminal_id: terminal.id.to_string(), workspace_id: self.public_workspace_id(ws_idx), tab_id: self.public_tab_id(ws_idx, tab_idx)?, focused, cwd: ws.tabs[tab_idx] .cwd_for_pane(pane_id, &self.state.terminals, &self.terminal_runtimes) .map(|cwd| cwd.display().to_string()), foreground_cwd: ws.tabs[tab_idx] .foreground_cwd_for_pane(pane_id, &self.terminal_runtimes) .map(|cwd| cwd.display().to_string()), label: terminal.manual_label.clone(), agent: terminal.effective_agent_label().map(str::to_string), title: presentation.title, display_agent: presentation.display_agent, agent_status: pane_agent_status(terminal.state, pane.seen), custom_status: presentation.custom_status, state_labels: presentation.state_labels, agent_session: terminal_agent_session_info(terminal), revision: terminal.revision, }) } pub(super) fn lookup_runtime( &self, ws_idx: usize, pane_id: crate::layout::PaneId, ) -> Option<(&crate::terminal::TerminalRuntime, String)> { let runtime = self.state .runtime_for_pane_in_workspace(&self.terminal_runtimes, ws_idx, pane_id)?; Some((runtime, self.public_workspace_id(ws_idx))) } pub(super) fn lookup_runtime_sender( &self, ws_idx: usize, pane_id: crate::layout::PaneId, ) -> Option<&crate::terminal::TerminalRuntime> { self.state .runtime_for_pane_in_workspace(&self.terminal_runtimes, ws_idx, pane_id) } pub(super) fn workspace_info(&self, index: usize) -> crate::api::schema::WorkspaceInfo { let ws = &self.state.workspaces[index]; let (agg_state, seen) = ws.aggregate_state(&self.state.terminals); crate::api::schema::WorkspaceInfo { workspace_id: self.public_workspace_id(index), number: index + 1, label: ws.display_name_from(&self.state.terminals, &self.terminal_runtimes), focused: self.state.active == Some(index), pane_count: ws.public_pane_numbers.len(), tab_count: ws.tabs.len(), active_tab_id: self.public_tab_id(index, ws.active_tab).unwrap_or_else(|| { crate::workspace::public_tab_id_for_number(&ws.id, ws.active_tab + 1) }), agent_status: pane_agent_status(agg_state, seen), worktree: ws .worktree_space() .map(|space| crate::api::schema::WorkspaceWorktreeInfo { repo_key: space.key.clone(), repo_name: space.label.clone(), repo_root: space.repo_root.display().to_string(), checkout_path: space.checkout_path.display().to_string(), is_linked_worktree: space.is_linked_worktree, }), } } } fn terminal_agent_session_info( terminal: &crate::terminal::TerminalState, ) -> Option { if let Some(authority) = terminal.hook_authority.as_ref() { if let Some(session_ref) = authority.session_ref.as_ref() { return Some(crate::api::schema::AgentSessionInfo { source: authority.source.clone(), agent: authority.agent_label.clone(), kind: session_ref.kind, value: session_ref.value.clone(), }); } } terminal .persisted_agent_session .as_ref() .map(|session| crate::api::schema::AgentSessionInfo { source: session.source.clone(), agent: session.agent.clone(), kind: session.session_ref.kind, value: session.session_ref.value.clone(), }) }