From 207be3c771d281baae6e5fa0fb74be9a056e97a2 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Tue, 1 Sep 2026 15:48:22 +0300 Subject: refactor: render the shell in the client (#3487) * feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations --- src/api/schema/commands.rs | 16 +++++++++ src/api/schema/common.rs | 19 +++++----- src/api/schema/integrations.rs | 17 +++++++++ src/api/schema/panes.rs | 80 ++++++++++++++++++++++++++++++++++++++++++ src/api/schema/response.rs | 29 ++++++++++++++- src/api/schema/tests.rs | 75 +++++++++++++++++++++++++++++++++++++++ src/api/schema/workspaces.rs | 3 ++ 7 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 src/api/schema/commands.rs (limited to 'src/api/schema') diff --git a/src/api/schema/commands.rs b/src/api/schema/commands.rs new file mode 100644 index 00000000..c9ccd4bc --- /dev/null +++ b/src/api/schema/commands.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct CommandInvokeParams { + /// Opaque endpoint-issued command identifier from the client-shell projection. + pub command_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tab_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pane_id: Option, + /// Client-owned selection coordinates, validated against the pane's content revision. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub selection: Option, +} diff --git a/src/api/schema/common.rs b/src/api/schema/common.rs index 7313002c..15f56e6c 100644 --- a/src/api/schema/common.rs +++ b/src/api/schema/common.rs @@ -35,6 +35,17 @@ pub struct PaneTarget { pub pane_id: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct ProductAnnouncementDismissParams { + pub version: String, + pub id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct ReleaseNotesDismissParams { + pub version: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct TabTarget { pub tab_id: String, @@ -109,14 +120,6 @@ impl NotificationShowSound { pub fn is_none(&self) -> bool { matches!(self, Self::None) } - - pub fn to_sound(self) -> Option { - match self { - Self::None => None, - Self::Done => Some(crate::sound::Sound::Done), - Self::Request => Some(crate::sound::Sound::Request), - } - } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] diff --git a/src/api/schema/integrations.rs b/src/api/schema/integrations.rs index 6151dedb..0fce2913 100644 --- a/src/api/schema/integrations.rs +++ b/src/api/schema/integrations.rs @@ -1,5 +1,22 @@ use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IntegrationState { + NotInstalled, + Current, + Outdated, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct IntegrationInfo { + pub target: IntegrationTarget, + pub label: String, + pub command: String, + pub available: bool, + pub state: IntegrationState, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct IntegrationInstallParams { pub target: IntegrationTarget, diff --git a/src/api/schema/panes.rs b/src/api/schema/panes.rs index 6435a422..517870f7 100644 --- a/src/api/schema/panes.rs +++ b/src/api/schema/panes.rs @@ -48,6 +48,17 @@ pub struct PaneInputSetParams { pub right_click: PaneRightClickTarget, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaneLinkActivateParams { + pub pane_id: String, + pub viewport_row: u16, + pub col: u16, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content_revision: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub offset_from_bottom: Option, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "snake_case")] pub enum PaneDirection { @@ -231,6 +242,75 @@ pub struct PaneResizeParams { pub amount: Option, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaneScrollParams { + pub pane_id: String, + pub offset_from_bottom: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaneTextPoint { + pub row: u32, + pub col: u16, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaneSelectionReadParams { + pub pane_id: String, + pub anchor: PaneTextPoint, + pub cursor: PaneTextPoint, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content_revision: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum PaneCopyMotion { + LineEnd, + FirstNonBlank, + NextWordStart, + PreviousWordStart, + NextWordEnd, + NextBigWordStart, + PreviousBigWordStart, + NextBigWordEnd, + PreviousParagraph, + NextParagraph, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaneCopyMotionParams { + pub pane_id: String, + pub cursor: PaneTextPoint, + pub motion: PaneCopyMotion, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content_revision: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum PaneCopySearchDirection { + Forward, + Backward, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaneTextRange { + pub start: PaneTextPoint, + pub end: PaneTextPoint, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaneCopySearchParams { + pub pane_id: String, + pub query: String, + pub direction: PaneCopySearchDirection, + pub cursor: PaneTextPoint, + pub content_revision: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub previous: Option, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, Default)] pub struct PaneListParams { #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/src/api/schema/response.rs b/src/api/schema/response.rs index da2813a6..180d6645 100644 --- a/src/api/schema/response.rs +++ b/src/api/schema/response.rs @@ -9,7 +9,7 @@ use super::integrations::{ use super::panes::{ LayoutDescription, PaneEdgesResult, PaneFocusDirectionResult, PaneInfo, PaneLayoutSnapshot, PaneMoveResult, PaneNeighborResult, PaneProcessInfo, PaneReadResult, PaneResizeResult, - PaneSwapResult, PaneZoomResult, + PaneSwapResult, PaneTextPoint, PaneTextRange, PaneZoomResult, }; use super::plugins::{ InstalledPluginInfo, PluginActionInfo, PluginCommandLogInfo, PluginInvocationContext, @@ -162,6 +162,25 @@ pub enum ResponseResult { PaneRead { read: PaneReadResult, }, + PaneSelection { + pane_id: String, + text: String, + }, + PaneCopyMotion { + pane_id: String, + cursor: PaneTextPoint, + content_revision: u64, + }, + PaneCopySearch { + pane_id: String, + content_revision: u64, + matches: Vec, + total: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + current: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + current_global: Option, + }, PaneGraphicsFrameAck { sequence: u64, revision: u64, @@ -210,6 +229,9 @@ pub enum ResponseResult { changed: bool, reason: ClientWindowTitleReason, }, + IntegrationList { + integrations: Vec, + }, IntegrationInstall { target: IntegrationTarget, details: IntegrationInstallResult, @@ -252,6 +274,11 @@ pub enum ResponseResult { context: PluginInvocationContext, log: PluginCommandLogInfo, }, + PaneLinkActivated { + #[serde(default, skip_serializing_if = "Option::is_none")] + url: Option, + handled: bool, + }, PluginLogList { logs: Vec, }, diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs index 663f3fc4..e4a7f2a3 100644 --- a/src/api/schema/tests.rs +++ b/src/api/schema/tests.rs @@ -50,6 +50,7 @@ fn request_uses_dot_method_names() { let request = Request { id: "req_1".into(), method: Method::WorkspaceCreate(WorkspaceCreateParams { + source_workspace_id: None, cwd: Some("/tmp".into()), focus: true, label: Some("api".into()), @@ -272,6 +273,54 @@ fn request_round_trips_for_agent_explain() { assert_eq!(restored, request); } +#[test] +fn integration_list_request_and_response_round_trip() { + let request = Request { + id: "req_integrations".into(), + method: Method::IntegrationList(EmptyParams::default()), + }; + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "integration.list"); + assert_eq!(serde_json::from_value::(json).unwrap(), request); + + let response = SuccessResponse { + id: "req_integrations".into(), + result: ResponseResult::IntegrationList { + integrations: vec![IntegrationInfo { + target: IntegrationTarget::Codex, + label: "codex".into(), + command: "codex".into(), + available: true, + state: IntegrationState::Outdated, + }], + }, + }; + let json = serde_json::to_value(&response).unwrap(); + assert_eq!(json["result"]["type"], "integration_list"); + assert_eq!(json["result"]["integrations"][0]["state"], "outdated"); + assert_eq!( + serde_json::from_value::(json).unwrap(), + response + ); +} + +#[test] +fn command_invoke_request_round_trips_without_command_text() { + let request = Request { + id: "req_command".into(), + method: Method::CommandInvoke(CommandInvokeParams { + command_id: "cmd_0123456789abcdef0123456789abcdef".into(), + workspace_id: Some("w1".into()), + tab_id: Some("w1:t1".into()), + pane_id: Some("w1:p1".into()), + selection: None, + }), + }; + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "command.invoke"); + assert_eq!(serde_json::from_value::(json).unwrap(), request); +} + #[test] fn notification_show_request_parses() { let json = r#"{"id":"req_1","method":"notification.show","params":{"title":"build failed","body":"api workspace","position":"top-left","sound":"request"}}"#; @@ -1263,6 +1312,32 @@ fn event_wait_parses_typed_match() { ); } +#[test] +fn pane_link_activate_round_trips() { + let request = Request { + id: "req_pane_link".into(), + method: Method::PaneLinkActivate(PaneLinkActivateParams { + pane_id: "w1:p1".into(), + viewport_row: 3, + col: 7, + content_revision: Some(42), + offset_from_bottom: Some(5), + }), + }; + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "pane.link.activate"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); + + let response = ResponseResult::PaneLinkActivated { + url: Some("https://example.test".into()), + handled: false, + }; + let json = serde_json::to_string(&response).unwrap(); + let restored: ResponseResult = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); +} + #[test] fn plugin_action_list_and_invoke_round_trips() { let list = Request { diff --git a/src/api/schema/workspaces.rs b/src/api/schema/workspaces.rs index bfc63252..1b7132c4 100644 --- a/src/api/schema/workspaces.rs +++ b/src/api/schema/workspaces.rs @@ -6,6 +6,9 @@ use super::common::AgentStatus; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct WorkspaceCreateParams { + /// Workspace whose focused pane supplies the `follow` cwd policy. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_workspace_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option, #[serde(default)] -- cgit v1.2.3