From 9e9bc8a144667a6b7debfd4f0c7a31ff0e11493a Mon Sep 17 00:00:00 2001 From: Can Celik Date: Sun, 6 Sep 2026 02:27:06 +0300 Subject: feat: manage multiple ssh machines from one client (#3670) --- src/api/schema.rs | 2 + src/api/schema/common.rs | 6 + src/api/schema/response.rs | 6 + src/api/schema/server.rs | 6 + src/api/schema/tests.rs | 2 + src/api/server.rs | 13 + src/cli.rs | 2 + src/cli/machine.rs | 295 +++ src/cli/spec.rs | 19 +- src/cli/spec/completion.rs | 14 + src/cli/spec/machine.rs | 49 + src/cli/status.rs | 12 + src/client/attach.rs | 3 +- src/client/catalog_reload.rs | 276 +++ src/client/clipboard_forwarding.rs | 16 + src/client/clipboard_images.rs | 22 +- src/client/config_reload.rs | 129 ++ src/client/endpoint.rs | 120 ++ src/client/endpoint/activation.rs | 1152 ++++++++++ src/client/endpoint/activation/model.rs | 174 ++ src/client/endpoint/activation/protocol.rs | 219 ++ src/client/endpoint/activation_tests.rs | 1305 +++++++++++ src/client/endpoint/catalog.rs | 578 +++++ src/client/endpoint/control.rs | 63 + src/client/endpoint/health.rs | 99 + src/client/endpoint/message_policy.rs | 124 ++ src/client/endpoint/registry.rs | 629 ++++++ src/client/endpoint/supervisor.rs | 492 +++++ src/client/endpoint/writer.rs | 400 ++++ src/client/endpoint_commands.rs | 484 ++++- src/client/events.rs | 34 + src/client/handshake.rs | 34 +- src/client/loop_config.rs | 16 + src/client/mod.rs | 2262 ++++++++++---------- src/client/shell.rs | 12 + src/client/shell/actions.rs | 75 +- src/client/shell/agent_sidebar.rs | 106 +- src/client/shell/aggregate_navigation.rs | 262 +++ src/client/shell/composition.rs | 148 +- src/client/shell/endpoint_agent_state.rs | 263 +++ src/client/shell/endpoint_agents.rs | 129 ++ src/client/shell/endpoint_navigation.rs | 290 +++ src/client/shell/endpoint_notices.rs | 80 + src/client/shell/endpoint_sidebar.rs | 452 ++++ src/client/shell/endpoints.rs | 506 +++++ src/client/shell/graphics.rs | 5 +- src/client/shell/input.rs | 69 +- src/client/shell/input_source.rs | 41 + src/client/shell/mobile.rs | 399 ++-- src/client/shell/mouse.rs | 81 +- src/client/shell/notification_policy.rs | 319 +++ src/client/shell/notifications.rs | 417 ++-- src/client/shell/overlay_input.rs | 183 +- src/client/shell/overlays.rs | 223 +- src/client/shell/render.rs | 51 +- src/client/shell/settings.rs | 6 +- src/client/shell/sidebar.rs | 33 +- src/client/shell/state.rs | 328 +-- .../shell/tests/agents_worktrees_notifications.rs | 8 +- src/client/shell/tests/copy.rs | 10 +- src/client/shell/tests/endpoint_requests.rs | 298 +++ src/client/shell/tests/endpoints.rs | 1205 +++++++++++ src/client/shell/tests/keybindings_settings.rs | 5 +- src/client/shell/tests/mobile.rs | 95 +- src/client/shell/tests/mod.rs | 4 +- src/client/shell/tests/popup_focus_projection.rs | 2 +- src/client/shell/tests/startup_overlays.rs | 1 + src/client/shell/worktrees.rs | 61 +- src/client/shell_runtime.rs | 739 +++++++ src/client/startup.rs | 24 + src/client/state.rs | 231 ++ src/client/terminal_sessions.rs | 13 +- src/client/tests/mod.rs | 25 + src/client/transport.rs | 146 ++ src/config/sidebar.rs | 5 + src/ghostty/mod.rs | 12 + src/ipc.rs | 7 - src/kitty_graphics/surface.rs | 4 + src/main.rs | 15 +- src/pane.rs | 4 + src/pane/terminal.rs | 12 + src/platform/client_state.rs | 32 + src/platform/linux.rs | 2 +- src/platform/macos.rs | 2 +- src/platform/mod.rs | 3 + src/platform/unix_common.rs | 19 + src/platform/windows.rs | 42 + src/protocol/endpoint.rs | 44 + src/remote.rs | 6 + src/remote/args.rs | 126 ++ src/remote/attach.rs | 949 ++++---- src/remote/process.rs | 87 + src/remote/restart_policy.rs | 161 ++ src/remote/saved.rs | 136 ++ src/server/autodetect.rs | 46 +- src/server/client_commands.rs | 30 + src/server/client_endpoint_control.rs | 24 + src/server/client_shell_graphics.rs | 42 + src/server/client_transport.rs | 123 +- src/server/clients.rs | 13 +- src/server/headless.rs | 217 +- src/server/headless/client_views.rs | 36 +- src/server/headless/endpoint_requests.rs | 130 ++ src/server/headless/render.rs | 30 +- src/server/headless/retained_surface.rs | 113 +- src/server/headless/surface_interest.rs | 86 + src/server/headless/tests/mod.rs | 68 +- src/server/headless/tests/pane_graphics.rs | 247 ++- src/server/headless/tests/surface_interest.rs | 725 +++++++ src/server/mod.rs | 1 + src/terminal/runtime.rs | 4 + src/ui/sidebar.rs | 4 +- src/ui/sidebar/tokens.rs | 35 + src/update.rs | 6 + 114 files changed, 17374 insertions(+), 2676 deletions(-) create mode 100644 src/cli/machine.rs create mode 100644 src/cli/spec/completion.rs create mode 100644 src/cli/spec/machine.rs create mode 100644 src/client/catalog_reload.rs create mode 100644 src/client/clipboard_forwarding.rs create mode 100644 src/client/config_reload.rs create mode 100644 src/client/endpoint.rs create mode 100644 src/client/endpoint/activation.rs create mode 100644 src/client/endpoint/activation/model.rs create mode 100644 src/client/endpoint/activation/protocol.rs create mode 100644 src/client/endpoint/activation_tests.rs create mode 100644 src/client/endpoint/catalog.rs create mode 100644 src/client/endpoint/control.rs create mode 100644 src/client/endpoint/health.rs create mode 100644 src/client/endpoint/message_policy.rs create mode 100644 src/client/endpoint/registry.rs create mode 100644 src/client/endpoint/supervisor.rs create mode 100644 src/client/endpoint/writer.rs create mode 100644 src/client/events.rs create mode 100644 src/client/loop_config.rs create mode 100644 src/client/shell/aggregate_navigation.rs create mode 100644 src/client/shell/endpoint_agent_state.rs create mode 100644 src/client/shell/endpoint_agents.rs create mode 100644 src/client/shell/endpoint_navigation.rs create mode 100644 src/client/shell/endpoint_notices.rs create mode 100644 src/client/shell/endpoint_sidebar.rs create mode 100644 src/client/shell/endpoints.rs create mode 100644 src/client/shell/input_source.rs create mode 100644 src/client/shell/notification_policy.rs create mode 100644 src/client/shell/tests/endpoint_requests.rs create mode 100644 src/client/shell/tests/endpoints.rs create mode 100644 src/client/shell_runtime.rs create mode 100644 src/client/startup.rs create mode 100644 src/client/state.rs create mode 100644 src/client/transport.rs create mode 100644 src/platform/client_state.rs create mode 100644 src/remote/args.rs create mode 100644 src/remote/process.rs create mode 100644 src/remote/restart_policy.rs create mode 100644 src/remote/saved.rs create mode 100644 src/server/client_endpoint_control.rs create mode 100644 src/server/headless/endpoint_requests.rs create mode 100644 src/server/headless/surface_interest.rs create mode 100644 src/server/headless/tests/surface_interest.rs (limited to 'src') diff --git a/src/api/schema.rs b/src/api/schema.rs index 074cd4ed..ba9f0c3a 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -69,6 +69,8 @@ pub enum Method { ClientWindowTitleSet(ClientWindowTitleSetParams), #[serde(rename = "client.window_title.clear")] ClientWindowTitleClear(EmptyParams), + #[serde(rename = "client_shell.surface.set")] + ClientShellSurfaceSet(ClientShellSurfaceSetParams), #[serde(rename = "session.snapshot")] SessionSnapshot(EmptyParams), #[serde(rename = "workspace.create")] diff --git a/src/api/schema/common.rs b/src/api/schema/common.rs index 15f56e6c..bf0d7caf 100644 --- a/src/api/schema/common.rs +++ b/src/api/schema/common.rs @@ -61,6 +61,12 @@ pub struct ClientWindowTitleSetParams { pub title: String, } +/// Updates whether the requesting client shell receives and controls pane presentation. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct ClientShellSurfaceSetParams { + pub active: bool, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "snake_case")] pub enum SplitDirection { diff --git a/src/api/schema/response.rs b/src/api/schema/response.rs index 180d6645..e6d4de4d 100644 --- a/src/api/schema/response.rs +++ b/src/api/schema/response.rs @@ -295,6 +295,12 @@ pub enum ResponseResult { status: crate::config::ConfigReloadStatus, diagnostics: Vec, }, + /// Acknowledgement for the client-shell surface interest lease. This method is new on the + /// endpoint protocol, so its revision-bearing result can establish an activation floor. + ClientShellSurfaceSet { + active: bool, + projection_revision: u64, + }, Ok {}, } diff --git a/src/api/schema/server.rs b/src/api/schema/server.rs index 96e51564..b274bc95 100644 --- a/src/api/schema/server.rs +++ b/src/api/schema/server.rs @@ -21,4 +21,10 @@ pub struct ServerCapabilities { /// Stable client-owned endpoint generation supported by this server. #[serde(default, skip_serializing_if = "Option::is_none")] pub endpoint_protocol_generation: Option, + /// Whether this server supports explicit client-shell surface interest. + #[serde(default)] + pub surface_interest: bool, + /// Whether this server supports endpoint health probes. + #[serde(default)] + pub health_check: bool, } diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs index 693f0e85..b566e8f6 100644 --- a/src/api/schema/tests.rs +++ b/src/api/schema/tests.rs @@ -725,6 +725,8 @@ fn success_response_round_trips() { live_handoff: true, detached_server_daemon: true, endpoint_protocol_generation: Some(1), + surface_interest: true, + health_check: true, }), }, }; diff --git a/src/api/server.rs b/src/api/server.rs index 655d89c5..b8ef7c38 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -69,6 +69,8 @@ fn default_capabilities() -> Option { live_handoff: crate::platform::capabilities().live_handoff, detached_server_daemon: crate::platform::current_process_is_detached_server_daemon(), endpoint_protocol_generation: Some(crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION), + surface_interest: true, + health_check: true, }) } @@ -352,6 +354,14 @@ fn handle_request( }); } + if matches!(&request.method, Method::ClientShellSurfaceSet(_)) { + return error_response_json( + request.id, + "connection_local_only", + "client_shell.surface.set is only available through a client shell endpoint".into(), + ); + } + if matches!(&request.method, Method::ServerStop(_)) { if let Some(server_stop) = server_stop { server_stop.store(true, Ordering::Release); @@ -386,6 +396,7 @@ pub(crate) fn api_method_name(method: &Method) -> &'static str { Method::CommandInvoke(_) => "command.invoke", Method::ClientWindowTitleSet(_) => "client.window_title.set", Method::ClientWindowTitleClear(_) => "client.window_title.clear", + Method::ClientShellSurfaceSet(_) => "client_shell.surface.set", Method::SessionSnapshot(_) => "session.snapshot", Method::WorkspaceCreate(_) => "workspace.create", Method::WorkspaceList(_) => "workspace.list", @@ -1136,6 +1147,8 @@ mod tests { endpoint_protocol_generation: Some( crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION, ), + surface_interest: true, + health_check: true, }), None, None, diff --git a/src/cli.rs b/src/cli.rs index b432528d..ea643f6a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -26,6 +26,7 @@ mod agent; mod api; mod completion; mod integration; +mod machine; mod notification; mod pane; mod plugin; @@ -113,6 +114,7 @@ pub fn maybe_run(args: &[String]) -> std::io::Result { "completion" | "completions" => completion::run_completion_command(&args[2..])?, "config" => run_config_command(&args[2..])?, "channel" => run_channel_command(&args[2..])?, + "machine" => machine::run_machine_command(&args[2..])?, "workspace" => workspace::run_workspace_command(&args[2..])?, "worktree" => worktree::run_worktree_command(&args[2..])?, "tab" => tab::run_tab_command(&args[2..])?, diff --git a/src/cli/machine.rs b/src/cli/machine.rs new file mode 100644 index 00000000..a55e4611 --- /dev/null +++ b/src/cli/machine.rs @@ -0,0 +1,295 @@ +use serde::Serialize; + +use crate::client::endpoint::{EndpointCatalog, ProfileId}; + +const HELP: &str = "Usage: + herdr machine list [--json] + herdr machine add --label