diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 02:43:45 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 03:47:07 +0300 |
| commit | fbd20ad6e488fb3b9efc1612fdffee6e672976c1 (patch) | |
| tree | d21daf93241786f8aa6fde23b41cdb9e15182b79 /src/app/api.rs | |
| parent | d35c642ef36ee5ac2afe7c1b18ad5a717bc807d8 (diff) | |
feat: add plugin v1 system
Add manifest-driven plugin actions, event hooks, pane entrypoints, link handlers, and host APIs.
Support plugin link, GitHub install, build commands, installed plugin registry, pane movement, runtime context, and plugin docs.
Diffstat (limited to 'src/app/api.rs')
| -rw-r--r-- | src/app/api.rs | 166 |
1 files changed, 160 insertions, 6 deletions
diff --git a/src/app/api.rs b/src/app/api.rs index 3a7276c4..6dc48953 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -2,8 +2,11 @@ use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; mod agents; +mod env; mod integrations; +mod layouts; mod panes; +pub(crate) mod plugins; mod responses; mod tabs; mod workspaces; @@ -56,6 +59,37 @@ impl App { return; } + if let AppEvent::PluginCommandFinished { + log_id, + finished_unix_ms, + exit_code, + stdout, + stderr, + error, + } = ev + { + self.state.plugin_commands_in_flight = + self.state.plugin_commands_in_flight.saturating_sub(1); + if let Some(log) = self + .state + .plugin_command_logs + .iter_mut() + .find(|log| log.log_id == log_id) + { + log.finished_unix_ms = Some(finished_unix_ms); + log.exit_code = exit_code; + log.stdout = Some(stdout); + log.stderr = Some(stderr); + log.error = error; + log.status = if log.error.is_none() && log.exit_code == Some(0) { + crate::api::schema::PluginCommandStatus::Succeeded + } else { + crate::api::schema::PluginCommandStatus::Failed + }; + } + return; + } + if let AppEvent::WorktreeAddFinished(result) = ev { self.handle_worktree_add_finished(result); return; @@ -327,7 +361,6 @@ impl App { let Some((ws_idx, pane_state)) = self.find_pane(pane_id) else { return false; }; - let public_pane_id = self.public_pane_id(ws_idx, pane_id); let terminal_id = pane_state.attached_terminal_id.clone(); let Some(terminal) = self.state.terminals.get(&terminal_id) else { return false; @@ -339,6 +372,9 @@ impl App { .get(&terminal_id) .map(|runtime| runtime.current_size()) .unwrap_or_else(|| self.state.estimate_pane_size()); + let Some(launch_env) = self.pane_launch_env(ws_idx, pane_id, Vec::new()) else { + return false; + }; let runtime = match crate::terminal::TerminalRuntime::spawn( pane_id, rows, @@ -347,10 +383,10 @@ impl App { self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode), + &launch_env, self.event_tx.clone(), self.render_notify.clone(), self.render_dirty.clone(), - public_pane_id.as_deref(), ) { Ok(runtime) => runtime, Err(err) => { @@ -373,7 +409,7 @@ impl App { true } - pub(crate) fn emit_pane_state_update(&self, update: &crate::app::actions::PaneStateUpdate) { + pub(crate) fn emit_pane_state_update(&mut self, update: &crate::app::actions::PaneStateUpdate) { let Some(pane_id) = self.public_pane_id(update.ws_idx, update.pane_id) else { return; }; @@ -573,7 +609,8 @@ impl App { } } - pub(super) fn emit_event(&self, event: crate::api::schema::EventEnvelope) { + pub(super) fn emit_event(&mut self, event: crate::api::schema::EventEnvelope) { + self.run_plugin_event_hooks(&event); self.event_hub.push(event); } @@ -716,6 +753,15 @@ impl App { Method::NotificationShow(params) => { return self.handle_notification_show(request.id, params); } + Method::ClientWindowTitleSet(_) | Method::ClientWindowTitleClear(_) => { + return responses::encode_success( + request.id, + ResponseResult::ClientWindowTitle { + changed: false, + reason: crate::api::schema::ClientWindowTitleReason::NoForegroundClient, + }, + ); + } Method::WorkspaceList(_) => return self.handle_workspace_list(request.id), Method::WorkspaceGet(target) => return self.handle_workspace_get(request.id, target), Method::WorkspaceCreate(params) => { @@ -757,6 +803,11 @@ impl App { Method::PaneMove(params) => return self.handle_pane_move(request.id, params), Method::PaneZoom(params) => return self.handle_pane_zoom(request.id, params), Method::PaneLayout(params) => return self.handle_pane_layout(request.id, params), + Method::PaneProcessInfo(params) => { + return self.handle_pane_process_info(request.id, params); + } + Method::LayoutExport(params) => return self.handle_layout_export(request.id, params), + Method::LayoutApply(params) => return self.handle_layout_apply(request.id, params), Method::PaneNeighbor(params) => return self.handle_pane_neighbor(request.id, params), Method::PaneEdges(params) => return self.handle_pane_edges(request.id, params), Method::PaneFocusDirection(params) => { @@ -764,6 +815,7 @@ impl App { } Method::PaneResize(params) => return self.handle_pane_resize(request.id, params), Method::PaneList(params) => return self.handle_pane_list(request.id, params), + Method::PaneCurrent(params) => return self.handle_pane_current(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), @@ -794,6 +846,39 @@ impl App { Method::IntegrationUninstall(params) => { return self.handle_integration_uninstall(request.id, params); } + Method::PluginLink(params) => { + return self.handle_plugin_link(request.id, params); + } + Method::PluginList(params) => { + return self.handle_plugin_list(request.id, params); + } + Method::PluginUnlink(params) => { + return self.handle_plugin_unlink(request.id, params); + } + Method::PluginEnable(params) => { + return self.handle_plugin_enable(request.id, params); + } + Method::PluginDisable(params) => { + return self.handle_plugin_disable(request.id, params); + } + Method::PluginActionList(params) => { + return self.handle_plugin_action_list(request.id, params); + } + Method::PluginActionInvoke(params) => { + return self.handle_plugin_action_invoke(request.id, params); + } + Method::PluginLogList(params) => { + return self.handle_plugin_log_list(request.id, params); + } + Method::PluginPaneOpen(params) => { + return self.handle_plugin_pane_open(request.id, params); + } + Method::PluginPaneFocus(params) => { + return self.handle_plugin_pane_focus(request.id, params); + } + Method::PluginPaneClose(params) => { + return self.handle_plugin_pane_close(request.id, params); + } _ => { return responses::encode_error( request.id, @@ -1186,6 +1271,75 @@ mod tests { ); } + #[tokio::test] + async fn pane_process_info_returns_response_for_existing_pane() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &crate::config::Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = vec![crate::workspace::Workspace::test_new("process-info")]; + app.state.ensure_test_terminals(); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + let terminal_id = app.state.workspaces[0].tabs[0].panes[&pane_id] + .attached_terminal_id + .clone(); + let (runtime, _rx) = crate::terminal::TerminalRuntime::test_with_channel(80, 24); + app.terminal_runtimes.insert(terminal_id, runtime); + let target = app.public_pane_id(0, pane_id).unwrap(); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "process_info".into(), + method: crate::api::schema::Method::PaneProcessInfo( + crate::api::schema::PaneProcessInfoParams { + pane_id: Some(target.clone()), + }, + ), + }); + let response: serde_json::Value = serde_json::from_str(&response).unwrap(); + + assert_eq!(response["result"]["type"], "pane_process_info"); + assert_eq!(response["result"]["process_info"]["pane_id"], target); + } + + #[test] + fn client_window_title_api_reports_no_foreground_client_in_app_mode() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &crate::config::Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + + let set = app.handle_api_request(crate::api::schema::Request { + id: "title_set".into(), + method: crate::api::schema::Method::ClientWindowTitleSet( + crate::api::schema::ClientWindowTitleSetParams { + title: "plugin review".into(), + }, + ), + }); + let set: serde_json::Value = serde_json::from_str(&set).unwrap(); + assert_eq!(set["result"]["type"], "client_window_title"); + assert_eq!(set["result"]["changed"], false); + assert_eq!(set["result"]["reason"], "no_foreground_client"); + + let clear = app.handle_api_request(crate::api::schema::Request { + id: "title_clear".into(), + method: crate::api::schema::Method::ClientWindowTitleClear( + crate::api::schema::EmptyParams::default(), + ), + }); + let clear: serde_json::Value = serde_json::from_str(&clear).unwrap(); + assert_eq!(clear["result"]["type"], "client_window_title"); + assert_eq!(clear["result"]["reason"], "no_foreground_client"); + } + #[cfg(unix)] #[tokio::test] async fn herdr_toast_context_uses_live_root_runtime_cwd_label() { @@ -1236,10 +1390,10 @@ mod tests { 0, crate::terminal_theme::TerminalTheme::default(), crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), + &crate::pane::PaneLaunchEnv::default(), events, std::sync::Arc::new(tokio::sync::Notify::new()), std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), - None, ) .unwrap(); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); @@ -1328,10 +1482,10 @@ mod tests { 0, crate::terminal_theme::TerminalTheme::default(), crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), + &crate::pane::PaneLaunchEnv::default(), events, std::sync::Arc::new(tokio::sync::Notify::new()), std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), - None, ) .unwrap(); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); |
