aboutsummaryrefslogtreecommitdiff
path: root/src/app/api/panes.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/api/panes.rs')
-rw-r--r--src/app/api/panes.rs353
1 files changed, 353 insertions, 0 deletions
diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs
new file mode 100644
index 00000000..b09ecb2a
--- /dev/null
+++ b/src/app/api/panes.rs
@@ -0,0 +1,353 @@
+use bytes::Bytes;
+
+use crate::api::schema::{
+ EventData, EventEnvelope, EventKind, PaneClearAgentAuthorityParams, PaneListParams,
+ PaneReadParams, PaneReadResult, PaneReleaseAgentParams, PaneRenameParams,
+ PaneReportAgentParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams,
+ PaneSplitParams, PaneTarget, ReadFormat, ReadSource, ResponseResult,
+};
+use crate::app::{App, Mode};
+
+use super::super::api_helpers::{
+ detect_state_from_api, encode_api_keys, encode_api_text, normalize_custom_status,
+ normalize_reported_agent_label,
+};
+use super::responses::{encode_error, encode_success};
+
+impl App {
+ pub(super) fn handle_pane_split(&mut self, id: String, params: PaneSplitParams) -> String {
+ let Some((ws_idx, target_pane_id)) = self.parse_pane_id(&params.target_pane_id) else {
+ return pane_not_found(id, &params.target_pane_id);
+ };
+ let (rows, cols) = self.state.estimate_pane_size();
+ let split_cwd = params.cwd.map(std::path::PathBuf::from).or_else(|| {
+ self.state.workspaces.get(ws_idx).and_then(|ws| {
+ let tab_idx = ws.find_tab_index_for_pane(target_pane_id)?;
+ ws.tabs.get(tab_idx)?.cwd_for_pane(
+ target_pane_id,
+ &self.state.terminals,
+ &self.terminal_runtimes,
+ )
+ })
+ });
+ let default_shell = self.state.default_shell.clone();
+ let scrollback_limit_bytes = self.state.pane_scrollback_limit_bytes;
+ let host_terminal_theme = self.state.host_terminal_theme;
+ let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
+ return pane_not_found(id, &params.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 (target_tab_idx, new_pane) = match ws.split_pane(
+ target_pane_id,
+ direction,
+ rows,
+ cols,
+ split_cwd,
+ scrollback_limit_bytes,
+ host_terminal_theme,
+ &default_shell,
+ params.focus,
+ ) {
+ Some(Ok(result)) => result,
+ Some(Err(err)) => return encode_error(id, "pane_split_failed", err.to_string()),
+ None => return pane_not_found(id, &params.target_pane_id),
+ };
+ if params.focus {
+ self.state.switch_workspace(ws_idx);
+ self.state.switch_tab(target_tab_idx);
+ self.state.mode = Mode::Terminal;
+ }
+ self.terminal_runtimes
+ .insert(new_pane.terminal.id.clone(), new_pane.runtime);
+ self.state
+ .terminals
+ .insert(new_pane.terminal.id.clone(), new_pane.terminal);
+ self.schedule_session_save();
+ let pane = self.pane_info(ws_idx, new_pane.pane_id).unwrap();
+ self.emit_event(EventEnvelope {
+ event: EventKind::PaneCreated,
+ data: EventData::PaneCreated { pane: pane.clone() },
+ });
+
+ encode_success(id, ResponseResult::PaneInfo { pane })
+ }
+
+ pub(super) fn handle_pane_list(&mut self, id: String, params: PaneListParams) -> String {
+ match self.collect_panes_for_workspace(params.workspace_id.as_deref()) {
+ Ok(panes) => encode_success(id, ResponseResult::PaneList { panes }),
+ Err((code, message)) => encode_error(id, &code, message),
+ }
+ }
+
+ pub(super) fn handle_pane_get(&mut self, id: String, target: PaneTarget) -> String {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&target.pane_id) else {
+ return pane_not_found(id, &target.pane_id);
+ };
+ let Some(pane) = self.pane_info(ws_idx, pane_id) else {
+ return pane_not_found(id, &target.pane_id);
+ };
+
+ encode_success(id, ResponseResult::PaneInfo { pane })
+ }
+
+ pub(super) fn handle_pane_rename(&mut self, id: String, params: PaneRenameParams) -> String {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(terminal_id) = self
+ .state
+ .workspaces
+ .get(ws_idx)
+ .and_then(|ws| ws.terminal_id(pane_id))
+ .cloned()
+ else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(terminal) = self.state.terminals.get_mut(&terminal_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ match params.label.map(|label| label.trim().to_string()) {
+ Some(label) if !label.is_empty() => terminal.set_manual_label(label),
+ _ => terminal.clear_manual_label(),
+ }
+ self.state.mark_session_dirty();
+ let pane = self.pane_info(ws_idx, pane_id).unwrap();
+
+ encode_success(id, ResponseResult::PaneInfo { pane })
+ }
+
+ pub(super) fn handle_pane_read(&mut self, id: String, params: PaneReadParams) -> String {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some((pane, workspace_id)) = self.lookup_runtime(ws_idx, pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(tab_idx) = self
+ .state
+ .workspaces
+ .get(ws_idx)
+ .and_then(|ws| ws.find_tab_index_for_pane(pane_id))
+ else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ 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),
+ },
+ };
+
+ encode_success(
+ id,
+ 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,
+ format: params.format,
+ text,
+ revision: 0,
+ truncated: false,
+ },
+ },
+ )
+ }
+
+ pub(super) fn handle_pane_report_agent(
+ &mut self,
+ id: String,
+ params: PaneReportAgentParams,
+ ) -> String {
+ let Some((_ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(agent_label) = normalize_reported_agent_label(&params.agent) else {
+ return invalid_agent(id);
+ };
+ self.handle_internal_event(crate::events::AppEvent::HookStateReported {
+ pane_id,
+ source: params.source,
+ agent_label,
+ state: detect_state_from_api(params.state),
+ message: params.message,
+ custom_status: normalize_custom_status(params.custom_status),
+ seq: params.seq,
+ });
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+
+ pub(super) fn handle_pane_clear_agent_authority(
+ &mut self,
+ id: String,
+ params: PaneClearAgentAuthorityParams,
+ ) -> String {
+ let Some((_ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ self.handle_internal_event(crate::events::AppEvent::HookAuthorityCleared {
+ pane_id,
+ source: params.source,
+ seq: params.seq,
+ });
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+
+ pub(super) fn handle_pane_release_agent(
+ &mut self,
+ id: String,
+ params: PaneReleaseAgentParams,
+ ) -> String {
+ let Some((_ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(agent_label) = normalize_reported_agent_label(&params.agent) else {
+ return invalid_agent(id);
+ };
+ self.handle_internal_event(crate::events::AppEvent::HookAgentReleased {
+ pane_id,
+ source: params.source,
+ known_agent: crate::detect::parse_agent_label(&agent_label),
+ agent_label,
+ seq: params.seq,
+ });
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+
+ pub(super) fn handle_pane_send_text(
+ &mut self,
+ id: String,
+ params: PaneSendTextParams,
+ ) -> String {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(runtime) = self.lookup_runtime_sender(ws_idx, pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ if let Err(err) = runtime.try_send_bytes(Bytes::from(params.text)) {
+ return encode_error(id, "pane_send_failed", err.to_string());
+ }
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+
+ pub(super) fn handle_pane_send_input(
+ &mut self,
+ id: String,
+ params: PaneSendInputParams,
+ ) -> String {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(runtime) = self.lookup_runtime_sender(ws_idx, pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let encoded_keys = match encode_api_keys(runtime, &params.keys) {
+ Ok(encoded_keys) => encoded_keys,
+ Err(key) => return encode_error(id, "invalid_key", format!("unsupported key {key}")),
+ };
+ 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 encode_error(id, "pane_send_failed", err.to_string());
+ }
+ }
+ for bytes in encoded_keys {
+ if let Err(err) = runtime.try_send_bytes(Bytes::from(bytes)) {
+ return encode_error(id, "pane_send_failed", err.to_string());
+ }
+ }
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+
+ pub(super) fn handle_pane_close(&mut self, id: String, target: PaneTarget) -> String {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&target.pane_id) else {
+ return pane_not_found(id, &target.pane_id);
+ };
+ let workspace_id = self.state.workspaces[ws_idx].id.clone();
+ let terminal_id = self.state.terminal_id_for_pane(ws_idx, pane_id);
+ let should_close_workspace = {
+ let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
+ return pane_not_found(id, &target.pane_id);
+ };
+ ws.close_pane(pane_id)
+ };
+ if should_close_workspace {
+ self.state.selected = ws_idx;
+ self.state.close_selected_workspace();
+ self.shutdown_detached_terminal_runtimes();
+ self.emit_event(EventEnvelope {
+ event: EventKind::PaneClosed,
+ data: EventData::PaneClosed {
+ pane_id: target.pane_id.clone(),
+ workspace_id: workspace_id.clone(),
+ },
+ });
+ self.emit_event(EventEnvelope {
+ event: EventKind::WorkspaceClosed,
+ data: EventData::WorkspaceClosed { workspace_id },
+ });
+ } else {
+ self.state.remove_unattached_terminal_ids(terminal_id);
+ self.shutdown_detached_terminal_runtimes();
+ self.schedule_session_save();
+ self.emit_event(EventEnvelope {
+ event: EventKind::PaneClosed,
+ data: EventData::PaneClosed {
+ pane_id: target.pane_id,
+ workspace_id,
+ },
+ });
+ }
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+
+ pub(super) fn handle_pane_send_keys(
+ &mut self,
+ id: String,
+ params: PaneSendKeysParams,
+ ) -> String {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(runtime) = self.lookup_runtime_sender(ws_idx, pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let encoded_keys = match encode_api_keys(runtime, &params.keys) {
+ Ok(encoded_keys) => encoded_keys,
+ Err(key) => return encode_error(id, "invalid_key", format!("unsupported key {key}")),
+ };
+ for bytes in encoded_keys {
+ if let Err(err) = runtime.try_send_bytes(Bytes::from(bytes)) {
+ return encode_error(id, "pane_send_failed", err.to_string());
+ }
+ }
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+}
+
+fn pane_not_found(id: String, pane_id: &str) -> String {
+ encode_error(id, "pane_not_found", format!("pane {pane_id} not found"))
+}
+
+fn invalid_agent(id: String) -> String {
+ encode_error(id, "invalid_agent", "agent label must not be empty")
+}