aboutsummaryrefslogtreecommitdiff
path: root/src/app/api.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 20:19:54 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 20:22:57 +0300
commit866aaf740e10fcfce169aaf81380ea0257fbe272 (patch)
treef815392b7c6b24a7c0de14e655bd635656383813 /src/app/api.rs
parentca579c402d5668560ca13059f82c03178be18b2a (diff)
refactor: separate server client boundaries
Diffstat (limited to 'src/app/api.rs')
-rw-r--r--src/app/api.rs1339
1 files changed, 62 insertions, 1277 deletions
diff --git a/src/app/api.rs b/src/app/api.rs
index 6eb95f03..c8ae530a 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -1,13 +1,14 @@
use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};
-use super::{
- api_helpers::{
- detect_state_from_api, encode_api_keys, encode_api_text, normalize_custom_status,
- normalize_reported_agent_label, pane_agent_status,
- },
- App, Mode, OverlayPaneState, ToastKind,
-};
+mod agents;
+mod integrations;
+mod panes;
+mod responses;
+mod tabs;
+mod workspaces;
+
+use super::{api_helpers::pane_agent_status, App, Mode, OverlayPaneState, ToastKind};
use crate::events::AppEvent;
impl App {
@@ -20,7 +21,10 @@ impl App {
if let AppEvent::GitStatusRefreshed { results } = ev {
self.git_refresh_in_flight = false;
self.last_git_remote_status_refresh = Instant::now();
- if self.state.apply_workspace_git_statuses(results) {
+ if self
+ .state
+ .apply_workspace_git_statuses(&self.terminal_runtimes, results)
+ {
self.render_dirty.store(true, Ordering::Release);
self.render_notify.notify_one();
}
@@ -75,8 +79,11 @@ impl App {
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.runtime_for_pane_in_workspace(ws_idx, pane_id)
- {
+ if let Some(runtime) = self.state.runtime_for_pane_in_workspace(
+ &self.terminal_runtimes,
+ ws_idx,
+ pane_id,
+ ) {
runtime.begin_graceful_release(agent);
}
}
@@ -156,6 +163,7 @@ impl App {
}
self.sync_toast_deadline(previous_toast);
+ self.shutdown_detached_terminal_runtimes();
}
fn restore_overlay_after_exit(&mut self, overlay: OverlayPaneState) {
@@ -293,12 +301,10 @@ impl App {
pane_id: crate::layout::PaneId,
event: crate::ghostty::FocusEvent,
) {
- let Some(runtime) = self
- .state
- .workspaces
- .get(ws_idx)
- .and_then(|_| self.state.runtime_for_pane_in_workspace(ws_idx, pane_id))
- else {
+ let Some(runtime) = self.state.workspaces.get(ws_idx).and_then(|_| {
+ self.state
+ .runtime_for_pane_in_workspace(&self.terminal_runtimes, ws_idx, pane_id)
+ }) else {
return;
};
runtime.try_send_focus_event(event);
@@ -306,13 +312,7 @@ impl App {
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, IntegrationInstallResult, IntegrationUninstallResult, Method,
- PaneListParams, PaneReadResult, ReadFormat, ReadSource, ResponseResult,
- SuccessResponse, TabListParams,
- };
+ use crate::api::schema::{Method, ResponseResult, SuccessResponse};
let response = match request.method {
Method::ServerStop(_) => {
@@ -332,1280 +332,65 @@ impl App {
},
}
}
- 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::WorkspaceList(_) => return self.handle_workspace_list(request.id),
+ Method::WorkspaceGet(target) => return self.handle_workspace_get(request.id, target),
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);
- crate::logging::workspace_renamed(&workspace.id);
- }
- }
- 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();
- }
- }
+ return self.handle_workspace_create(request.id, params);
}
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),
- },
- }
+ return self.handle_workspace_focus(request.id, target)
}
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());
- crate::logging::workspace_renamed(&ws.id);
- 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),
- },
- }
+ return self.handle_workspace_rename(request.id, params);
}
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
- .focused_runtime_in_workspace(ws_idx)
- .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 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 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,
- scrollback_limit_bytes,
- host_terminal_theme,
- &default_shell,
- )
- });
- match result {
- Ok((tab_idx, terminal, runtime)) => {
- self.state
- .terminal_runtimes
- .insert(terminal.id.clone(), runtime);
- self.state.terminals.insert(terminal.id.clone(), terminal);
- if let Some(label) = label {
- let workspace_id = self.state.workspaces[ws_idx].id.clone();
- let tab_id = self
- .public_tab_id(ws_idx, tab_idx)
- .unwrap_or_else(|| format!("{}:{}", workspace_id, tab_idx + 1));
- 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);
- crate::logging::tab_renamed(&workspace_id, &tab_id);
- }
- }
- 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 workspace_id = self.state.workspaces[ws_idx].id.clone();
- let tab_id = self
- .public_tab_id(ws_idx, tab_idx)
- .unwrap_or_else(|| format!("{}:{}", workspace_id, tab_idx + 1));
- 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());
- crate::logging::tab_renamed(&workspace_id, &tab_id);
- 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 terminal_ids = self.state.terminal_ids_for_tab(ws_idx, tab_idx);
- 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.state.remove_unattached_terminal_ids(terminal_ids);
- 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::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::AgentStart(params) => {
- let (agent, argv) = match self.start_agent(params) {
- Ok(started) => started,
- Err(err) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: self.agent_start_error_body(err),
- })
- .unwrap();
- }
- };
- SuccessResponse {
- id: request.id,
- result: ResponseResult::AgentStarted { agent, argv },
- }
- }
- 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 {
- 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 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.state.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 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 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 serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "pane_split_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- None => {
- 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();
- }
- };
- if params.focus {
- self.state.switch_workspace(ws_idx);
- self.state.switch_tab(target_tab_idx);
- self.state.mode = Mode::Terminal;
- }
- self.state
- .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(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::PaneRename(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(terminal_id) = self
- .state
- .workspaces
- .get(ws_idx)
- .and_then(|ws| ws.terminal_id(pane_id))
- .cloned()
- 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(terminal) = self.state.terminals.get_mut(&terminal_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();
- };
- 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();
- 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.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: 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,
- },
- },
- }
- }
+ return self.handle_workspace_close(request.id, target)
+ }
+ Method::TabList(params) => return self.handle_tab_list(request.id, params),
+ Method::TabGet(target) => return self.handle_tab_get(request.id, target),
+ Method::TabCreate(params) => return self.handle_tab_create(request.id, params),
+ Method::TabFocus(target) => return self.handle_tab_focus(request.id, target),
+ Method::TabRename(params) => return self.handle_tab_rename(request.id, params),
+ Method::TabClose(target) => return self.handle_tab_close(request.id, target),
+ Method::AgentList(_) => return self.handle_agent_list(request.id),
+ Method::AgentGet(target) => return self.handle_agent_get(request.id, target),
+ Method::AgentFocus(target) => return self.handle_agent_focus(request.id, target),
+ Method::AgentRename(params) => return self.handle_agent_rename(request.id, params),
+ Method::AgentStart(params) => return self.handle_agent_start(request.id, params),
+ Method::AgentRead(params) => return self.handle_agent_read(request.id, params),
+ Method::AgentSend(params) => return self.handle_agent_send(request.id, params),
+ Method::PaneSplit(params) => return self.handle_pane_split(request.id, params),
+ Method::PaneList(params) => return self.handle_pane_list(request.id, params),
+ Method::PaneGet(target) => return self.handle_pane_get(request.id, target),
+ Method::PaneRename(params) => return self.handle_pane_rename(request.id, params),
+ Method::PaneRead(params) => return self.handle_pane_read(request.id, params),
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_label) = normalize_reported_agent_label(&params.agent) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "invalid_agent".into(),
- message: "agent label must not be empty".into(),
- },
- })
- .unwrap();
- };
- 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,
- });
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
+ return self.handle_pane_report_agent(request.id, params);
}
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,
- seq: params.seq,
- });
- SuccessResponse {
- id: request.id,
- result: ResponseResult::Ok {},
- }
+ return self.handle_pane_clear_agent_authority(request.id, params);
}
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_label) = normalize_reported_agent_label(&params.agent) else {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "invalid_agent".into(),
- message: "agent label must not be empty".into(),
- },
- })
- .unwrap();
- };
- 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,
- });
- 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 {},
- }
+ return self.handle_pane_release_agent(request.id, params);
}
+ Method::PaneSendText(params) => return self.handle_pane_send_text(request.id, params),
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 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 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.state.remove_unattached_terminal_ids(terminal_id);
- 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 self.handle_pane_send_input(request.id, params)
}
+ Method::PaneClose(target) => return self.handle_pane_close(request.id, target),
+ Method::PaneSendKeys(params) => return self.handle_pane_send_keys(request.id, params),
Method::IntegrationInstall(params) => {
- let target = params.target;
- let messages = match crate::integration::install_target(target) {
- Ok(messages) => messages,
- Err(err) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "integration_install_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- };
-
- SuccessResponse {
- id: request.id,
- result: ResponseResult::IntegrationInstall {
- target,
- details: IntegrationInstallResult { messages },
- },
- }
+ return self.handle_integration_install(request.id, params);
}
Method::IntegrationUninstall(params) => {
- let target = params.target;
- let messages = match crate::integration::uninstall_target(target) {
- Ok(messages) => messages,
- Err(err) => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "integration_uninstall_failed".into(),
- message: err.to_string(),
- },
- })
- .unwrap();
- }
- };
-
- SuccessResponse {
- id: request.id,
- result: ResponseResult::IntegrationUninstall {
- target,
- details: IntegrationUninstallResult { messages },
- },
- }
+ return self.handle_integration_uninstall(request.id, params);
}
_ => {
- return serde_json::to_string(&ErrorResponse {
- id: request.id,
- error: ErrorBody {
- code: "not_implemented".into(),
- message: "method not implemented yet".into(),
- },
- })
- .unwrap();
+ return responses::encode_error(
+ request.id,
+ "not_implemented",
+ "method not implemented yet",
+ );
}
};