aboutsummaryrefslogtreecommitdiff
path: root/src/app/api.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/api.rs')
-rw-r--r--src/app/api.rs709
1 files changed, 454 insertions, 255 deletions
diff --git a/src/app/api.rs b/src/app/api.rs
index 2ed224c7..e47eb452 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -1,131 +1,230 @@
-//! API request handling and response building for [`App`].
+use std::time::{Duration, Instant};
use super::{
- detect_state_from_api, encode_api_keys, encode_api_text, pane_agent_status,
- tab_attention_priority, App, Mode,
+ api_helpers::{
+ detect_state_from_api, encode_api_keys, encode_api_text, normalize_reported_agent_label,
+ pane_agent_status,
+ },
+ App, Mode, OverlayPaneState, ToastKind,
};
+use crate::events::AppEvent;
-fn normalize_reported_agent_label(agent: &str) -> Option<String> {
- let trimmed = agent.trim();
- if trimmed.is_empty() {
- return None;
- }
- if let Some(agent) = crate::detect::parse_agent_label(trimmed) {
- return Some(crate::detect::agent_label(agent).to_string());
- }
- Some(trimmed.to_string())
-}
+impl App {
+ pub(crate) fn handle_internal_event(&mut self, ev: AppEvent) {
+ if let AppEvent::ClipboardWrite { content } = ev {
+ crate::selection::write_osc52_bytes(&content);
+ return;
+ }
-pub(super) fn api_request_changes_ui(request: &crate::api::schema::Request) -> bool {
- use crate::api::schema::Method;
+ let overlay_state = if let AppEvent::PaneDied { pane_id } = &ev {
+ self.overlay_panes.remove(pane_id)
+ } else {
+ None
+ };
- matches!(
- &request.method,
- Method::WorkspaceCreate(_)
- | Method::WorkspaceFocus(_)
- | Method::WorkspaceRename(_)
- | Method::WorkspaceClose(_)
- | Method::TabCreate(_)
- | Method::TabFocus(_)
- | Method::TabRename(_)
- | Method::TabClose(_)
- | Method::PaneSplit(_)
- | Method::PaneReportAgent(_)
- | Method::PaneClearAgentAuthority(_)
- | Method::PaneReleaseAgent(_)
- | Method::PaneClose(_)
- )
-}
+ if let AppEvent::PaneDied { pane_id } = &ev {
+ if let Some((ws_idx, _)) = self.find_pane(*pane_id) {
+ if let Some(public_pane_id) = self.public_pane_id(ws_idx, *pane_id) {
+ self.emit_event(crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::PaneExited,
+ data: crate::api::schema::EventData::PaneExited {
+ pane_id: public_pane_id,
+ workspace_id: self.public_workspace_id(ws_idx),
+ },
+ });
+ }
+ }
+ }
-impl App {
- pub(super) fn handle_api_request_message(
- &mut self,
- msg: crate::api::ApiRequestMessage,
- ) -> bool {
- let changed = api_request_changes_ui(&msg.request);
- let response = self.handle_api_request(msg.request);
- let _ = msg.respond_to.send(response);
- changed
- }
+ let released_agent = if let AppEvent::HookAgentReleased {
+ pane_id,
+ known_agent,
+ ..
+ } = &ev
+ {
+ known_agent.map(|agent| (*pane_id, agent))
+ } else {
+ None
+ };
- pub(super) fn public_workspace_id(&self, ws_idx: usize) -> String {
- self.state.workspaces[ws_idx].id.clone()
+ let previous_toast = self.state.toast.clone();
+ let pane_updates = self.state.handle_app_event(ev);
+ for update in &pane_updates {
+ self.emit_pane_state_update(update);
+ }
+ if let Some((pane_id, agent)) = released_agent {
+ if pane_updates.iter().any(|update| update.pane_id == pane_id) {
+ if let Some((ws_idx, _)) = self.find_pane(pane_id) {
+ if let Some(runtime) = self.state.workspaces[ws_idx].runtimes.get(&pane_id) {
+ runtime.begin_graceful_release(agent);
+ }
+ }
+ }
+ }
+ if let Some(overlay) = overlay_state {
+ self.restore_overlay_after_exit(overlay);
+ }
+ self.sync_toast_deadline(previous_toast);
}
- pub(super) fn public_tab_id(&self, ws_idx: usize, tab_idx: usize) -> Option<String> {
- let ws = self.state.workspaces.get(ws_idx)?;
- ws.tabs.get(tab_idx)?;
- Some(format!("{}:{}", ws.id, tab_idx + 1))
- }
+ fn restore_overlay_after_exit(&mut self, overlay: OverlayPaneState) {
+ let Some(ws) = self.state.workspaces.get_mut(overlay.ws_idx) else {
+ return;
+ };
+ if overlay.tab_idx >= ws.tabs.len() {
+ return;
+ }
- pub(super) fn public_pane_id(
- &self,
- ws_idx: usize,
- pane_id: crate::layout::PaneId,
- ) -> Option<String> {
- let ws = self.state.workspaces.get(ws_idx)?;
- let pane_number = ws.public_pane_number(pane_id)?;
- Some(format!("{}-{pane_number}", ws.id))
+ ws.active_tab = overlay.tab_idx;
+ let tab = &mut ws.tabs[overlay.tab_idx];
+ if tab.panes.contains_key(&overlay.previous_focus) {
+ tab.layout.focus_pane(overlay.previous_focus);
+ }
+ tab.zoomed = overlay.previous_zoomed;
+
+ if self.state.active == Some(overlay.ws_idx) {
+ self.state.mode = Mode::Terminal;
+ }
}
- pub(super) fn parse_workspace_id(&self, id: &str) -> Option<usize> {
- self.state
- .workspaces
- .iter()
- .position(|workspace| workspace.id == id)
- .or_else(|| id.strip_prefix("w_")?.parse::<usize>().ok()?.checked_sub(1))
- .or_else(|| id.parse::<usize>().ok()?.checked_sub(1))
+ fn emit_pane_state_update(&self, update: &crate::app::actions::PaneStateUpdate) {
+ let Some(pane_id) = self.public_pane_id(update.ws_idx, update.pane_id) else {
+ return;
+ };
+ let workspace_id = self.public_workspace_id(update.ws_idx);
+
+ if update.previous_agent_label != update.agent_label {
+ self.emit_event(crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::PaneAgentDetected,
+ data: crate::api::schema::EventData::PaneAgentDetected {
+ pane_id: pane_id.clone(),
+ workspace_id: workspace_id.clone(),
+ agent: update.agent_label.clone(),
+ },
+ });
+ }
+
+ if update.previous_state != update.state {
+ let agent_status = self
+ .state
+ .workspaces
+ .get(update.ws_idx)
+ .and_then(|ws| ws.pane_state(update.pane_id))
+ .map(|pane| pane_agent_status(pane.state, pane.seen))
+ .unwrap_or_else(|| pane_agent_status(update.state, true));
+ self.emit_event(crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::PaneAgentStatusChanged,
+ data: crate::api::schema::EventData::PaneAgentStatusChanged {
+ pane_id,
+ workspace_id,
+ agent_status,
+ },
+ });
+ }
}
- pub(super) fn parse_tab_id(&self, id: &str) -> Option<(usize, usize)> {
- if let Some(rest) = id.strip_prefix("t_") {
- let (ws_raw, tab_raw) = rest.rsplit_once('_')?;
- let ws_idx = self.parse_workspace_id(ws_raw)?;
- let tab_idx = tab_raw.parse::<usize>().ok()?.checked_sub(1)?;
- self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?;
- return Some((ws_idx, tab_idx));
+ pub(super) fn sync_toast_deadline(
+ &mut self,
+ previous_toast: Option<crate::app::state::ToastNotification>,
+ ) {
+ if self.state.toast != previous_toast {
+ self.toast_deadline = self.state.toast.as_ref().map(|toast| {
+ let duration = match toast.kind {
+ ToastKind::NeedsAttention => Duration::from_secs(8),
+ ToastKind::Finished => Duration::from_secs(5),
+ ToastKind::UpdateInstalled => Duration::from_secs(3),
+ };
+ Instant::now() + duration
+ });
}
+ }
- let (ws_raw, tab_raw) = id.rsplit_once(':')?;
- let ws_idx = self.parse_workspace_id(ws_raw)?;
- let tab_idx = tab_raw.parse::<usize>().ok()?.checked_sub(1)?;
- self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?;
- Some((ws_idx, tab_idx))
+ pub(super) fn emit_event(&self, event: crate::api::schema::EventEnvelope) {
+ self.event_hub.push(event);
}
- pub(super) fn parse_pane_id(&self, id: &str) -> Option<(usize, crate::layout::PaneId)> {
- if let Some(rest) = id.strip_prefix("p_") {
- if let Some((ws_raw, pane_raw)) = rest.rsplit_once('_') {
- let ws_idx = self.parse_workspace_id(ws_raw)?;
- let pane_id = crate::layout::PaneId::from_raw(pane_raw.parse::<u32>().ok()?);
- return Some((ws_idx, pane_id));
- }
+ pub(crate) fn sync_focus_events(&mut self) {
+ let current_focus = self.state.active.and_then(|idx| {
+ self.state
+ .workspaces
+ .get(idx)
+ .and_then(|ws| ws.focused_pane_id().map(|pane_id| (idx, pane_id)))
+ });
+ if current_focus == self.last_focus {
+ return;
+ }
- let pane_id = crate::layout::PaneId::from_raw(rest.parse::<u32>().ok()?);
- return self.find_pane(pane_id).map(|(ws_idx, _)| (ws_idx, pane_id));
+ if let Some((ws_idx, pane_id)) = self.last_focus {
+ self.send_pane_focus_event(ws_idx, pane_id, crate::ghostty::FocusEvent::Lost);
+ }
+ if let Some((ws_idx, pane_id)) = current_focus {
+ self.send_pane_focus_event(ws_idx, pane_id, crate::ghostty::FocusEvent::Gained);
+ self.emit_event(crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::WorkspaceFocused,
+ data: crate::api::schema::EventData::WorkspaceFocused {
+ workspace_id: self.public_workspace_id(ws_idx),
+ },
+ });
+ if let Some(tab_id) =
+ self.public_tab_id(ws_idx, self.state.workspaces[ws_idx].active_tab)
+ {
+ self.emit_event(crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::TabFocused,
+ data: crate::api::schema::EventData::TabFocused {
+ tab_id,
+ workspace_id: self.public_workspace_id(ws_idx),
+ },
+ });
+ }
+ if let Some(public_pane_id) = self.public_pane_id(ws_idx, pane_id) {
+ self.emit_event(crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::PaneFocused,
+ data: crate::api::schema::EventData::PaneFocused {
+ pane_id: public_pane_id,
+ workspace_id: self.public_workspace_id(ws_idx),
+ },
+ });
+ }
}
- let (ws_raw, pane_number_raw) = id.rsplit_once('-')?;
- let ws_idx = self.parse_workspace_id(ws_raw)?;
- let pane_number = pane_number_raw.parse::<usize>().ok()?;
- let ws = self.state.workspaces.get(ws_idx)?;
- let pane_id = ws
- .public_pane_numbers
- .iter()
- .find_map(|(pane_id, number)| (*number == pane_number).then_some(*pane_id))?;
- Some((ws_idx, pane_id))
+ self.last_focus = current_focus;
+ }
+
+ fn send_pane_focus_event(
+ &self,
+ ws_idx: usize,
+ pane_id: crate::layout::PaneId,
+ event: crate::ghostty::FocusEvent,
+ ) {
+ let Some(runtime) = self
+ .state
+ .workspaces
+ .get(ws_idx)
+ .and_then(|ws| ws.runtime(pane_id))
+ else {
+ return;
+ };
+ runtime.try_send_focus_event(event);
}
- pub(super) fn handle_api_request(&mut self, request: crate::api::schema::Request) -> String {
+ pub(crate) fn handle_api_request(&mut self, request: crate::api::schema::Request) -> String {
self.drain_internal_events();
use bytes::Bytes;
use crate::api::schema::{
- ErrorBody, ErrorResponse, Method, PaneListParams, PaneReadResult, ReadSource,
+ ErrorBody, ErrorResponse, IntegrationInstallResult, IntegrationTarget,
+ IntegrationUninstallResult, Method, PaneListParams, PaneReadResult, ReadSource,
ResponseResult, SuccessResponse, TabListParams,
};
let response = match request.method {
+ Method::ServerStop(_) => {
+ self.state.should_quit = true;
+ SuccessResponse {
+ id: request.id,
+ result: ResponseResult::Ok {},
+ }
+ }
Method::WorkspaceList(_) => SuccessResponse {
id: request.id,
result: ResponseResult::WorkspaceList {
@@ -1065,6 +1164,265 @@ impl App {
result: ResponseResult::Ok {},
}
}
+ Method::IntegrationInstall(params) => {
+ let target = params.target;
+ let messages = match target {
+ IntegrationTarget::Pi => {
+ let path = crate::integration::install_pi().map_err(|err| {
+ serde_json::to_string(&ErrorResponse {
+ id: request.id.clone(),
+ error: ErrorBody {
+ code: "integration_install_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap()
+ });
+ match path {
+ Ok(path) => {
+ vec![format!("installed pi integration to {}", path.display())]
+ }
+ Err(response) => return response,
+ }
+ }
+ IntegrationTarget::Claude => {
+ let installed = crate::integration::install_claude().map_err(|err| {
+ serde_json::to_string(&ErrorResponse {
+ id: request.id.clone(),
+ error: ErrorBody {
+ code: "integration_install_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap()
+ });
+ match installed {
+ Ok(installed) => vec![
+ format!(
+ "installed claude integration hook to {}",
+ installed.hook_path.display()
+ ),
+ format!(
+ "ensured claude settings at {}",
+ installed.settings_path.display()
+ ),
+ ],
+ Err(response) => return response,
+ }
+ }
+ IntegrationTarget::Codex => {
+ let installed = crate::integration::install_codex().map_err(|err| {
+ serde_json::to_string(&ErrorResponse {
+ id: request.id.clone(),
+ error: ErrorBody {
+ code: "integration_install_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap()
+ });
+ match installed {
+ Ok(installed) => vec![
+ format!(
+ "installed codex integration hook to {}",
+ installed.hook_path.display()
+ ),
+ format!(
+ "ensured codex hooks at {}",
+ installed.hooks_path.display()
+ ),
+ format!(
+ "ensured codex config at {}",
+ installed.config_path.display()
+ ),
+ ],
+ Err(response) => return response,
+ }
+ }
+ IntegrationTarget::Opencode => {
+ let installed = crate::integration::install_opencode().map_err(|err| {
+ serde_json::to_string(&ErrorResponse {
+ id: request.id.clone(),
+ error: ErrorBody {
+ code: "integration_install_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap()
+ });
+ match installed {
+ Ok(installed) => vec![format!(
+ "installed opencode integration plugin to {}",
+ installed.plugin_path.display()
+ )],
+ Err(response) => return response,
+ }
+ }
+ };
+
+ SuccessResponse {
+ id: request.id,
+ result: ResponseResult::IntegrationInstall {
+ target,
+ details: IntegrationInstallResult { messages },
+ },
+ }
+ }
+ Method::IntegrationUninstall(params) => {
+ let target = params.target;
+ let messages = match target {
+ IntegrationTarget::Pi => {
+ let result = crate::integration::uninstall_pi().map_err(|err| {
+ serde_json::to_string(&ErrorResponse {
+ id: request.id.clone(),
+ error: ErrorBody {
+ code: "integration_uninstall_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap()
+ });
+ match result {
+ Ok(result) => {
+ if result.removed_extension {
+ vec![format!(
+ "removed pi integration extension at {}",
+ result.extension_path.display()
+ )]
+ } else {
+ vec![format!(
+ "no pi integration extension found at {}",
+ result.extension_path.display()
+ )]
+ }
+ }
+ Err(response) => return response,
+ }
+ }
+ IntegrationTarget::Claude => {
+ let result = crate::integration::uninstall_claude().map_err(|err| {
+ serde_json::to_string(&ErrorResponse {
+ id: request.id.clone(),
+ error: ErrorBody {
+ code: "integration_uninstall_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap()
+ });
+ match result {
+ Ok(result) => {
+ let mut messages = Vec::new();
+ if result.removed_hook_file {
+ messages.push(format!(
+ "removed claude hook at {}",
+ result.hook_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no claude hook found at {}",
+ result.hook_path.display()
+ ));
+ }
+ if result.updated_settings {
+ messages.push(format!(
+ "removed herdr claude hook entries from {}",
+ result.settings_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no herdr claude hook entries found in {}",
+ result.settings_path.display()
+ ));
+ }
+ messages
+ }
+ Err(response) => return response,
+ }
+ }
+ IntegrationTarget::Codex => {
+ let result = crate::integration::uninstall_codex().map_err(|err| {
+ serde_json::to_string(&ErrorResponse {
+ id: request.id.clone(),
+ error: ErrorBody {
+ code: "integration_uninstall_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap()
+ });
+ match result {
+ Ok(result) => {
+ let mut messages = Vec::new();
+ if result.removed_hook_file {
+ messages.push(format!(
+ "removed codex hook at {}",
+ result.hook_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no codex hook found at {}",
+ result.hook_path.display()
+ ));
+ }
+ if result.updated_hooks {
+ messages.push(format!(
+ "removed herdr codex hook entries from {}",
+ result.hooks_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no herdr codex hook entries found in {}",
+ result.hooks_path.display()
+ ));
+ }
+ messages.push(format!(
+ "left codex config unchanged at {}",
+ result.config_path.display()
+ ));
+ messages
+ }
+ Err(response) => return response,
+ }
+ }
+ IntegrationTarget::Opencode => {
+ let result = crate::integration::uninstall_opencode().map_err(|err| {
+ serde_json::to_string(&ErrorResponse {
+ id: request.id.clone(),
+ error: ErrorBody {
+ code: "integration_uninstall_failed".into(),
+ message: err.to_string(),
+ },
+ })
+ .unwrap()
+ });
+ match result {
+ Ok(result) => {
+ if result.removed_plugin {
+ vec![format!(
+ "removed opencode integration plugin at {}",
+ result.plugin_path.display()
+ )]
+ } else {
+ vec![format!(
+ "no opencode integration plugin found at {}",
+ result.plugin_path.display()
+ )]
+ }
+ }
+ Err(response) => return response,
+ }
+ }
+ };
+
+ SuccessResponse {
+ id: request.id,
+ result: ResponseResult::IntegrationUninstall {
+ target,
+ details: IntegrationUninstallResult { messages },
+ },
+ }
+ }
_ => {
return serde_json::to_string(&ErrorResponse {
id: request.id,
@@ -1079,163 +1437,4 @@ impl App {
serde_json::to_string(&response).unwrap()
}
-
- pub(super) fn collect_panes_for_workspace(
- &self,
- workspace_id: Option<&str>,
- ) -> Result<Vec<crate::api::schema::PaneInfo>, (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<crate::api::schema::TabInfo> {
- let ws = self.state.workspaces.get(ws_idx)?;
- let tab = ws.tabs.get(tab_idx)?;
- let (agg_state, seen) = tab
- .panes
- .values()
- .map(|pane| (pane.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_idx + 1,
- label: tab.display_name(),
- 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(super) fn workspace_created_result(
- &self,
- ws_idx: usize,
- ) -> Option<crate::api::schema::ResponseResult> {
- 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<crate::api::schema::ResponseResult> {
- 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<crate::api::schema::PaneInfo> {
- 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<crate::api::schema::PaneInfo> {
- let ws = self.state.workspaces.get(ws_idx)?;
- let pane = ws.pane_state(pane_id)?;
- let runtime = ws.runtime(pane_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);
- Some(crate::api::schema::PaneInfo {
- 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)?,
- focused,
- cwd: runtime
- .and_then(|rt| rt.cwd())
- .map(|cwd| cwd.display().to_string()),
- agent: pane.effective_agent_label().map(str::to_string),
- agent_status: pane_agent_status(pane.state, pane.seen),
- revision: 0,
- })
- }
-
- pub(super) fn lookup_runtime(
- &self,
- ws_idx: usize,
- pane_id: crate::layout::PaneId,
- ) -> Option<(&crate::pane::PaneRuntime, String)> {
- let ws = self.state.workspaces.get(ws_idx)?;
- let runtime = ws.runtime(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::pane::PaneRuntime> {
- let ws = self.state.workspaces.get(ws_idx)?;
- ws.runtime(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();
- crate::api::schema::WorkspaceInfo {
- workspace_id: self.public_workspace_id(index),
- number: index + 1,
- label: ws.display_name(),
- 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(|| format!("{}:{}", ws.id, ws.active_tab + 1)),
- agent_status: pane_agent_status(agg_state, seen),
- }
- }
}