aboutsummaryrefslogtreecommitdiff
path: root/src/app/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/mod.rs')
-rw-r--r--src/app/mod.rs1247
1 files changed, 8 insertions, 1239 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 3b3cdbc3..165a6ff3 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -2,10 +2,14 @@
//!
//! - `state.rs` — AppState, Mode, and pure data structs
//! - `actions.rs` — state mutations (testable without PTYs/async)
+//! - `api.rs` — API request handling and response building
//! - `input.rs` — key/mouse → action translation
+//! - `runtime_commands.rs` — custom command launching and overlay restoration
mod actions;
+mod api;
mod input;
+mod runtime_commands;
pub mod state;
use std::collections::{HashMap, HashSet};
@@ -33,17 +37,12 @@ use crate::config::Config;
use crate::events::AppEvent;
use crate::workspace::Workspace;
+#[cfg(test)]
+use self::api::api_request_changes_ui;
+
pub use state::{AppState, Mode, ToastKind, ViewState};
/// Full application: AppState + runtime concerns (event channels, async I/O).
-#[derive(Debug, Clone, Copy)]
-struct OverlayPaneState {
- ws_idx: usize,
- tab_idx: usize,
- previous_focus: crate::layout::PaneId,
- previous_zoomed: bool,
-}
-
pub struct App {
pub state: AppState,
pub event_tx: mpsc::Sender<AppEvent>,
@@ -66,7 +65,7 @@ pub struct App {
suppressed_repeat_keys: HashSet<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
render_notify: Arc<Notify>,
render_dirty: Arc<AtomicBool>,
- overlay_panes: HashMap<crate::layout::PaneId, OverlayPaneState>,
+ overlay_panes: HashMap<crate::layout::PaneId, runtime_commands::OverlayPaneState>,
}
enum LoopEvent {
@@ -100,27 +99,6 @@ fn repeat_key_identity(
(key.code, key.modifiers)
}
-fn api_request_changes_ui(request: &crate::api::schema::Request) -> bool {
- use crate::api::schema::Method;
-
- 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(_)
- )
-}
-
/// Resolve the palette from config: base theme + optional custom overrides.
fn resolve_palette(config: &crate::config::Config) -> state::Palette {
// Start with the named theme (default: catppuccin)
@@ -535,13 +513,6 @@ impl App {
changed
}
- 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
- }
-
async fn handle_raw_input_batch(&mut self, first: crate::raw_input::RawInputEvent) -> bool {
let mut changed = self.handle_raw_input_event(first).await;
@@ -845,26 +816,6 @@ impl App {
self.sync_toast_deadline(previous_toast);
}
- 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;
- }
-
- 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;
- }
- }
-
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;
@@ -996,1033 +947,6 @@ impl App {
.find_map(|(ws_idx, ws)| ws.pane_state(pane_id).map(|pane| (ws_idx, pane)))
}
- fn public_workspace_id(&self, ws_idx: usize) -> String {
- self.state.workspaces[ws_idx].id.clone()
- }
-
- 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 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))
- }
-
- 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 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));
- }
-
- 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))
- }
-
- 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));
- }
-
- 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));
- }
-
- 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))
- }
-
- 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,
- ResponseResult, SuccessResponse, TabListParams,
- };
-
- let response = match request.method {
- Method::WorkspaceList(_) => SuccessResponse {
- id: request.id,
- result: ResponseResult::WorkspaceList {
- workspaces: self
- .state
- .workspaces
- .iter()
- .enumerate()
- .map(|(idx, _)| self.workspace_info(idx))
- .collect(),
- },
- },
- Method::WorkspaceGet(target) => {
- let Some(index) = self.parse_workspace_id(&target.workspace_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", target.workspace_id),
- },
- })
- .unwrap();
- };
- let Some(_) = self.state.workspaces.get(index) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", target.workspace_id),
- },
- })
- .unwrap();
- };
- SuccessResponse {
- id: request.id,
- result: ResponseResult::WorkspaceInfo {
- workspace: self.workspace_info(index),
- },
- }
- }
- Method::WorkspaceCreate(params) => {
- let cwd = params
- .cwd
- .map(std::path::PathBuf::from)
- .or_else(|| std::env::current_dir().ok())
- .unwrap_or_else(|| std::path::PathBuf::from("/"));
- match self.create_workspace_with_options(cwd, params.focus) {
- Ok(index) => {
- if let Some(label) = params.label {
- if let Some(workspace) = self.state.workspaces.get_mut(index) {
- workspace.set_custom_name(label);
- }
- }
- let workspace = self.workspace_info(index);
- let tab = self
- .tab_info(index, 0)
- .expect("new workspace should have an initial tab");
- let root_pane = self
- .root_pane_info(index, 0)
- .expect("new workspace should have an initial root pane");
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::WorkspaceCreated,
- data: crate::api::schema::EventData::WorkspaceCreated {
- workspace: workspace.clone(),
- },
- });
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::TabCreated,
- data: crate::api::schema::EventData::TabCreated { tab: tab.clone() },
- });
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::PaneCreated,
- data: crate::api::schema::EventData::PaneCreated {
- pane: root_pane.clone(),
- },
- });
- SuccessResponse {
- id: request.id,
- result: self
- .workspace_created_result(index)
- .expect("new workspace should produce a complete create response"),
- }
- }
- Err(err) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_create_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- }
- }
- Method::WorkspaceFocus(target) => {
- let Some(index) = self.parse_workspace_id(&target.workspace_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", target.workspace_id),
- },
- })
- .unwrap();
- };
- if self.state.workspaces.get(index).is_none() {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", target.workspace_id),
- },
- })
- .unwrap();
- }
- self.state.switch_workspace(index);
- SuccessResponse {
- id: request.id,
- result: ResponseResult::WorkspaceInfo {
- workspace: self.workspace_info(index),
- },
- }
- }
- Method::WorkspaceRename(params) => {
- let Some(index) = self.parse_workspace_id(&params.workspace_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", params.workspace_id),
- },
- })
- .unwrap();
- };
- let Some(ws) = self.state.workspaces.get_mut(index) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", params.workspace_id),
- },
- })
- .unwrap();
- };
- ws.set_custom_name(params.label.clone());
- self.schedule_session_save();
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::WorkspaceRenamed,
- data: crate::api::schema::EventData::WorkspaceRenamed {
- workspace_id: self.public_workspace_id(index),
- label: params.label,
- },
- });
- SuccessResponse {
- id: request.id,
- result: ResponseResult::WorkspaceInfo {
- workspace: self.workspace_info(index),
- },
- }
- }
- Method::WorkspaceClose(target) => {
- let Some(index) = self.parse_workspace_id(&target.workspace_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", target.workspace_id),
- },
- })
- .unwrap();
- };
- if self.state.workspaces.get(index).is_none() {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", target.workspace_id),
- },
- })
- .unwrap();
- }
- self.state.selected = index;
- self.state.close_selected_workspace();
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::WorkspaceClosed,
- data: crate::api::schema::EventData::WorkspaceClosed {
- workspace_id: target.workspace_id,
- },
- });
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
- }
- Method::TabList(TabListParams { workspace_id }) => {
- let tabs = if let Some(workspace_id) = workspace_id {
- let Some(ws_idx) = self.parse_workspace_id(&workspace_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", workspace_id),
- },
- })
- .unwrap();
- };
- let Some(ws) = self.state.workspaces.get(ws_idx) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", workspace_id),
- },
- })
- .unwrap();
- };
- (0..ws.tabs.len())
- .filter_map(|tab_idx| self.tab_info(ws_idx, tab_idx))
- .collect()
- } else {
- let mut tabs = Vec::new();
- for (ws_idx, ws) in self.state.workspaces.iter().enumerate() {
- for tab_idx in 0..ws.tabs.len() {
- if let Some(tab) = self.tab_info(ws_idx, tab_idx) {
- tabs.push(tab);
- }
- }
- }
- tabs
- };
- SuccessResponse {
- id: request.id,
- result: ResponseResult::TabList { tabs },
- }
- }
- Method::TabGet(target) => {
- let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_not_found".into(),
- message: format!("tab {} not found", target.tab_id),
- },
- })
- .unwrap();
- };
- let Some(tab) = self.tab_info(ws_idx, tab_idx) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_not_found".into(),
- message: format!("tab {} not found", target.tab_id),
- },
- })
- .unwrap();
- };
- SuccessResponse {
- id: request.id,
- result: ResponseResult::TabInfo { tab },
- }
- }
- Method::TabCreate(params) => {
- let crate::api::schema::TabCreateParams {
- workspace_id,
- cwd,
- focus,
- label,
- } = params;
- let ws_idx = if let Some(workspace_id) = workspace_id {
- let Some(ws_idx) = self.parse_workspace_id(&workspace_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: format!("workspace {} not found", workspace_id),
- },
- })
- .unwrap();
- };
- ws_idx
- } else if let Some(active) = self.state.active {
- active
- } else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "workspace_not_found".into(),
- message: "no active workspace".into(),
- },
- })
- .unwrap();
- };
- let cwd = cwd
- .map(std::path::PathBuf::from)
- .or_else(|| {
- self.state.workspaces.get(ws_idx).and_then(|ws| {
- ws.active_tab()
- .and_then(|tab| tab.focused_runtime())
- .and_then(|rt| rt.cwd())
- })
- })
- .or_else(|| std::env::current_dir().ok())
- .unwrap_or_else(|| std::path::PathBuf::from("/"));
- let (rows, cols) = self.state.estimate_pane_size();
- let result = self
- .state
- .workspaces
- .get_mut(ws_idx)
- .ok_or_else(|| std::io::Error::other("workspace disappeared"))
- .and_then(|ws| {
- ws.create_tab(
- rows,
- cols,
- cwd,
- self.state.pane_scrollback_limit_bytes,
- self.state.host_terminal_theme,
- )
- });
- match result {
- Ok(tab_idx) => {
- if let Some(label) = label {
- if let Some(tab) = self
- .state
- .workspaces
- .get_mut(ws_idx)
- .and_then(|ws| ws.tabs.get_mut(tab_idx))
- {
- tab.set_custom_name(label);
- }
- }
- if focus {
- self.state.switch_workspace(ws_idx);
- self.state.switch_tab(tab_idx);
- self.state.mode = Mode::Terminal;
- }
- self.schedule_session_save();
- let tab = self.tab_info(ws_idx, tab_idx).unwrap();
- let root_pane = self
- .root_pane_info(ws_idx, tab_idx)
- .expect("new tab should have a root pane");
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::TabCreated,
- data: crate::api::schema::EventData::TabCreated { tab: tab.clone() },
- });
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::PaneCreated,
- data: crate::api::schema::EventData::PaneCreated {
- pane: root_pane.clone(),
- },
- });
- SuccessResponse {
- id: request.id,
- result: self
- .tab_created_result(ws_idx, tab_idx)
- .expect("new tab should produce a complete create response"),
- }
- }
- Err(err) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_create_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- }
- }
- Method::TabFocus(target) => {
- let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_not_found".into(),
- message: format!("tab {} not found", target.tab_id),
- },
- })
- .unwrap();
- };
- self.state.switch_workspace(ws_idx);
- self.state.switch_tab(tab_idx);
- let tab = self.tab_info(ws_idx, tab_idx).unwrap();
- SuccessResponse {
- id: request.id,
- result: ResponseResult::TabInfo { tab },
- }
- }
- Method::TabRename(params) => {
- let Some((ws_idx, tab_idx)) = self.parse_tab_id(&params.tab_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_not_found".into(),
- message: format!("tab {} not found", params.tab_id),
- },
- })
- .unwrap();
- };
- let Some(tab) = self
- .state
- .workspaces
- .get_mut(ws_idx)
- .and_then(|ws| ws.tabs.get_mut(tab_idx))
- else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_not_found".into(),
- message: format!("tab {} not found", params.tab_id),
- },
- })
- .unwrap();
- };
- tab.set_custom_name(params.label.clone());
- self.schedule_session_save();
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::TabRenamed,
- data: crate::api::schema::EventData::TabRenamed {
- tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(),
- workspace_id: self.public_workspace_id(ws_idx),
- label: params.label,
- },
- });
- let tab = self.tab_info(ws_idx, tab_idx).unwrap();
- SuccessResponse {
- id: request.id,
- result: ResponseResult::TabInfo { tab },
- }
- }
- Method::TabClose(target) => {
- let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_not_found".into(),
- message: format!("tab {} not found", target.tab_id),
- },
- })
- .unwrap();
- };
- let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_not_found".into(),
- message: format!("tab {} not found", target.tab_id),
- },
- })
- .unwrap();
- };
- if ws.tabs.len() <= 1 {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_close_failed".into(),
- message: "cannot close the last tab in a workspace".into(),
- },
- })
- .unwrap();
- }
- if !ws.close_tab(tab_idx) {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "tab_close_failed".into(),
- message: format!("tab {} could not be closed", target.tab_id),
- },
- })
- .unwrap();
- }
- self.schedule_session_save();
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::TabClosed,
- data: crate::api::schema::EventData::TabClosed {
- tab_id: target.tab_id,
- workspace_id: self.public_workspace_id(ws_idx),
- },
- });
- 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 {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.target_pane_id),
- },
- })
- .unwrap();
- };
- let (rows, cols) = self.state.estimate_pane_size();
- let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.target_pane_id),
- },
- })
- .unwrap();
- };
- ws.layout.focus_pane(target_pane_id);
- let direction = match params.direction {
- crate::api::schema::SplitDirection::Right => {
- ratatui::layout::Direction::Horizontal
- }
- crate::api::schema::SplitDirection::Down => {
- ratatui::layout::Direction::Vertical
- }
- };
- let new_pane_id = match ws.split_focused(
- direction,
- rows,
- cols,
- params.cwd.map(std::path::PathBuf::from),
- self.state.pane_scrollback_limit_bytes,
- self.state.host_terminal_theme,
- ) {
- Ok(new_pane_id) => new_pane_id,
- Err(err) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_split_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- };
- if !params.focus {
- ws.layout.focus_pane(target_pane_id);
- }
- self.schedule_session_save();
- let pane = self.pane_info(ws_idx, new_pane_id).unwrap();
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::PaneCreated,
- data: crate::api::schema::EventData::PaneCreated { pane: pane.clone() },
- });
- SuccessResponse {
- id: request.id,
- result: ResponseResult::PaneInfo { pane },
- }
- }
- Method::PaneList(PaneListParams { workspace_id }) => {
- match self.collect_panes_for_workspace(workspace_id.as_deref()) {
- Ok(panes) => SuccessResponse {
- id: request.id,
- result: ResponseResult::PaneList { panes },
- },
- Err((code, message)) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody { code, message },
- })
- .unwrap();
- }
- }
- }
- Method::PaneGet(target) => {
- let Some((ws_idx, pane_id)) = self.parse_pane_id(&target.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", target.pane_id),
- },
- })
- .unwrap();
- };
- let Some(pane) = self.pane_info(ws_idx, pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", target.pane_id),
- },
- })
- .unwrap();
- };
- SuccessResponse {
- id: request.id,
- result: ResponseResult::PaneInfo { pane },
- }
- }
- Method::PaneRead(params) => {
- let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let Some((pane, workspace_id)) = self.lookup_runtime(ws_idx, pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let Some(tab_idx) = self
- .state
- .workspaces
- .get(ws_idx)
- .and_then(|ws| ws.find_tab_index_for_pane(pane_id))
- else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let requested_lines = params.lines.unwrap_or(80).min(1000) as usize;
- let text = match params.source {
- ReadSource::Visible => pane.visible_text(),
- ReadSource::Recent => pane.recent_text(requested_lines),
- ReadSource::RecentUnwrapped => pane.recent_unwrapped_text(requested_lines),
- };
- SuccessResponse {
- id: request.id,
- result: ResponseResult::PaneRead {
- read: PaneReadResult {
- pane_id: params.pane_id,
- workspace_id,
- tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(),
- source: params.source,
- text,
- revision: 0,
- truncated: false,
- },
- },
- }
- }
- Method::PaneReportAgent(params) => {
- let Some((_ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let Some(agent) = parse_agent_name(&params.agent) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "invalid_agent".into(),
- message: format!("unsupported agent {}", params.agent),
- },
- })
- .unwrap();
- };
- self.handle_internal_event(crate::events::AppEvent::HookStateReported {
- pane_id,
- source: params.source,
- agent,
- state: detect_state_from_api(params.state),
- message: params.message,
- });
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
- }
- Method::PaneClearAgentAuthority(params) => {
- let Some((_ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- self.handle_internal_event(crate::events::AppEvent::HookAuthorityCleared {
- pane_id,
- source: params.source,
- });
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
- }
- Method::PaneReleaseAgent(params) => {
- let Some((_ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let Some(agent) = parse_agent_name(&params.agent) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "invalid_agent".into(),
- message: format!("unsupported agent {}", params.agent),
- },
- })
- .unwrap();
- };
- self.handle_internal_event(crate::events::AppEvent::HookAgentReleased {
- pane_id,
- source: params.source,
- agent,
- });
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
- }
- Method::PaneSendText(params) => {
- let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let Some(runtime) = self.lookup_runtime_sender(ws_idx, pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .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: "pane_send_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
- }
- Method::PaneSendInput(params) => {
- let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let Some(runtime) = self.lookup_runtime_sender(ws_idx, pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let encoded_keys = match encode_api_keys(runtime, &params.keys) {
- Ok(encoded_keys) => encoded_keys,
- Err(key) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "invalid_key".into(),
- message: format!("unsupported key {key}"),
- },
- })
- .unwrap();
- }
- };
- if !params.text.is_empty() {
- let text_bytes = encode_api_text(runtime, &params.text);
- if let Err(err) = runtime.try_send_bytes(Bytes::from(text_bytes)) {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_send_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- }
- for bytes in encoded_keys {
- if let Err(err) = runtime.try_send_bytes(Bytes::from(bytes)) {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_send_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- }
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
- }
- Method::PaneClose(target) => {
- let Some((ws_idx, pane_id)) = self.parse_pane_id(&target.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", target.pane_id),
- },
- })
- .unwrap();
- };
- let workspace_id = self.state.workspaces[ws_idx].id.clone();
- let should_close_workspace = {
- let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", target.pane_id),
- },
- })
- .unwrap();
- };
- ws.close_pane(pane_id)
- };
- if should_close_workspace {
- self.state.selected = ws_idx;
- self.state.close_selected_workspace();
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::PaneClosed,
- data: crate::api::schema::EventData::PaneClosed {
- pane_id: target.pane_id.clone(),
- workspace_id: workspace_id.clone(),
- },
- });
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::WorkspaceClosed,
- data: crate::api::schema::EventData::WorkspaceClosed { workspace_id },
- });
- } else {
- self.schedule_session_save();
- self.emit_event(crate::api::schema::EventEnvelope {
- event: crate::api::schema::EventKind::PaneClosed,
- data: crate::api::schema::EventData::PaneClosed {
- pane_id: target.pane_id,
- workspace_id,
- },
- });
- }
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
- }
- Method::PaneSendKeys(params) => {
- let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let Some(runtime) = self.lookup_runtime_sender(ws_idx, pane_id) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_not_found".into(),
- message: format!("pane {} not found", params.pane_id),
- },
- })
- .unwrap();
- };
- let encoded_keys = match encode_api_keys(runtime, &params.keys) {
- Ok(encoded_keys) => encoded_keys,
- Err(key) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "invalid_key".into(),
- message: format!("unsupported key {key}"),
- },
- })
- .unwrap();
- }
- };
- for bytes in encoded_keys {
- if let Err(err) = runtime.try_send_bytes(Bytes::from(bytes)) {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_send_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- }
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
- }
- _ => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "not_implemented".into(),
- message: "method not implemented yet".into(),
- },
- })
- .unwrap();
- }
- };
-
- serde_json::to_string(&response).unwrap()
- }
-
pub(crate) fn dismiss_release_notes(&mut self) {
let preview = self
.state
@@ -2264,161 +1188,6 @@ impl App {
self.schedule_session_save();
Ok(idx)
}
-
- 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())
- }
- }
-
- 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),
- })
- }
-
- 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)?,
- })
- }
-
- 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)?,
- })
- }
-
- 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)
- }
-
- 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.detected_agent.map(agent_name),
- agent_status: pane_agent_status(pane.state, pane.seen),
- revision: 0,
- })
- }
-
- 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)))
- }
-
- 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)
- }
-
- 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),
- }
- }
}
fn tab_attention_priority(state: crate::detect::AgentState, seen: bool) -> u8 {