diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/clients.rs | 3 | ||||
| -rw-r--r-- | src/server/headless.rs | 19 | ||||
| -rw-r--r-- | src/server/headless/render.rs | 49 | ||||
| -rw-r--r-- | src/server/headless/tests/mod.rs | 98 |
4 files changed, 163 insertions, 6 deletions
diff --git a/src/server/clients.rs b/src/server/clients.rs index 972342ac..c2b419f4 100644 --- a/src/server/clients.rs +++ b/src/server/clients.rs @@ -177,6 +177,8 @@ pub(crate) struct ClientConnection { pub(crate) shell_location: Option<ClientShellLocation>, /// Last coherent shell replacement sent to this client. pub(crate) shell_snapshot: Option<crate::protocol::ClientShellSnapshot>, + /// View policy paired with the last coherent shell replacement. + pub(crate) shell_agent_view: Option<crate::api::schema::AgentViewSetParams>, /// Monotonic shell replacement revision for this connection. pub(crate) shell_projection_revision: u64, /// Whether this shell is waiting for one ordered endpoint command response. @@ -244,6 +246,7 @@ impl ClientConnection { staged_clipboard_files: Vec::new(), shell_location: None, shell_snapshot: None, + shell_agent_view: None, shell_projection_revision: 0, shell_endpoint_command_in_flight: false, shell_endpoint_command_surface_revision: None, diff --git a/src/server/headless.rs b/src/server/headless.rs index 2d652378..beda84b5 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -2006,6 +2006,21 @@ impl HeadlessServer { ); let location = crate::server::clients::ClientShellLocation::from_snapshot(&seed_snapshot); + let agent_view = self.app.state.agent_view_override.clone(); + let projection_message = match agent_view.as_ref() { + Some(view) => match crate::protocol::endpoint::agent_view_projection_message( + &seed_snapshot.boot_id, + seed_snapshot.revision, + Some(view), + ) { + Ok(message) => Some(message), + Err(err) => { + warn!(client_id, err = %err, "failed to encode endpoint agent view"); + return false; + } + }, + None => None, + }; let snapshot_message = match crate::protocol::endpoint::snapshot_message(&seed_snapshot) { Ok(message) => message, @@ -2016,10 +2031,14 @@ impl HeadlessServer { }; connection.shell_location = Some(location); connection.shell_snapshot = Some(seed_snapshot); + connection.shell_agent_view = agent_view; self.clients.insert(client_id, connection); if self.app.state.popup_pane.is_some() && self.popup_owner_tab_id.is_none() { self.popup_owner_tab_id = self.shell_tab_id_for_client(client_id); } + if let Some(message) = projection_message { + self.send_to_client(client_id, message); + } self.send_to_client(client_id, snapshot_message); if surface_active { self.foreground_client_id = Some(client_id); diff --git a/src/server/headless/render.rs b/src/server/headless/render.rs index 87c96964..dd2cce38 100644 --- a/src/server/headless/render.rs +++ b/src/server/headless/render.rs @@ -479,6 +479,7 @@ impl HeadlessServer { .clients .get(&client_id) .and_then(|client| client.shell_location.clone()); + let agent_view = self.app.state.agent_view_override.clone(); let Some(client) = self.clients.get_mut(&client_id) else { continue; }; @@ -495,19 +496,52 @@ impl HeadlessServer { self.server_config_diagnostic_without_keybindings.clone() }; candidate.revision = client.shell_projection_revision; - if client.shell_snapshot.as_ref() != Some(&candidate) { + if client.shell_snapshot.as_ref() != Some(&candidate) + || client.shell_agent_view != agent_view + { client.shell_projection_revision = client.shell_projection_revision.saturating_add(1); candidate.revision = client.shell_projection_revision; - let message = match crate::protocol::endpoint::snapshot_message(&candidate) { - Ok(message) => message, + let projection_message = if agent_view.is_some() + || client.shell_agent_view.is_some() + { + match crate::protocol::endpoint::agent_view_projection_message( + &candidate.boot_id, + candidate.revision, + agent_view.as_ref(), + ) { + Ok(message) => Some(message), + Err(err) => { + warn!(client_id, err = %err, "failed to encode endpoint agent view"); + broken_clients.push(client_id); + continue; + } + } + } else { + None + }; + let snapshot_message = + match crate::protocol::endpoint::snapshot_message(&candidate) { + Ok(message) => message, + Err(err) => { + warn!(client_id, err = %err, "failed to encode endpoint snapshot"); + broken_clients.push(client_id); + continue; + } + }; + let projection_framed = match projection_message + .as_ref() + .map(Self::frame_server_message) + .transpose() + { + Ok(framed) => framed, Err(err) => { - warn!(client_id, err = %err, "failed to encode endpoint snapshot"); + warn!(client_id, err = %err, "failed to frame endpoint agent view"); broken_clients.push(client_id); continue; } }; - let framed = match Self::frame_server_message(&message) { + let snapshot_framed = match Self::frame_server_message(&snapshot_message) { Ok(framed) => framed, Err(err) => { warn!(client_id, err = %err, "failed to frame endpoint snapshot"); @@ -519,11 +553,14 @@ impl HeadlessServer { broken_clients.push(client_id); continue; }; - if writer.control.send(framed).is_err() { + if projection_framed.is_some_and(|framed| writer.control.send(framed).is_err()) + || writer.control.send(snapshot_framed).is_err() + { broken_clients.push(client_id); continue; } client.shell_snapshot = Some(candidate); + client.shell_agent_view = agent_view; } shell_projection_revision = client.shell_projection_revision; if !client.shell_surface_active { diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs index bb196be5..0ad5f8a3 100644 --- a/src/server/headless/tests/mod.rs +++ b/src/server/headless/tests/mod.rs @@ -13,6 +13,16 @@ fn client_shell_snapshot(message: ServerMessage) -> Box<crate::protocol::ClientS Box::new(serde_json::from_str(&data).expect("decode client shell snapshot")) } +fn client_agent_view_projection( + message: ServerMessage, +) -> crate::protocol::endpoint::EndpointAgentViewProjection { + let ServerMessage::EndpointControl { kind, data } = message else { + panic!("expected client agent view projection"); + }; + assert_eq!(kind, crate::protocol::endpoint::AGENT_VIEW_PROJECTION_KIND); + serde_json::from_str(&data).expect("decode client agent view projection") +} + fn test_headless_server() -> HeadlessServer { test_headless_server_with_event_hub(api::EventHub::default()) } @@ -737,6 +747,94 @@ fn terminal_client_endpoint_request_error_removes_client() { } #[tokio::test] +async fn client_shell_pairs_agent_view_set_replacement_and_clear_with_snapshots() { + use crate::api::schema::{ + AgentViewBuiltinField, AgentViewField, AgentViewFilter, AgentViewSetParams, AgentViewValue, + }; + + let mut server = test_headless_server(); + let (writer, control_rx, _render_rx) = test_client_writer(); + assert!( + server.handle_server_event(ServerEvent::ClientShellConnected { + client_id: 77, + surface_cols: 80, + surface_rows: 23, + cell_width_px: 0, + cell_height_px: 0, + pixel_mouse: false, + direct_graphics: false, + endpoint_keybindings: false, + mouse_capture: false, + surface_active: false, + writer, + }) + ); + let initial = client_shell_snapshot(read_server_message( + control_rx.recv().expect("initial snapshot"), + )); + + let mut view = AgentViewSetParams { + source: "example.views".into(), + label: Some("focus".into()), + filter: Some(AgentViewFilter::Eq { + field: AgentViewField::Builtin(AgentViewBuiltinField::Status), + value: AgentViewValue::String("working".into()), + }), + sort: Vec::new(), + }; + server.app.state.agent_view_override = Some(view.clone()); + server.render_and_stream(); + let set = client_agent_view_projection(read_server_message( + control_rx.recv().expect("set projection"), + )); + let set_snapshot = client_shell_snapshot(read_server_message( + control_rx.recv().expect("set snapshot"), + )); + assert!(set.revision > initial.revision); + assert_eq!(set.revision, set_snapshot.revision); + assert_eq!( + set.view.map(serde_json::from_value).transpose().unwrap(), + Some(view.clone()) + ); + + view.filter = Some(AgentViewFilter::Eq { + field: AgentViewField::Builtin(AgentViewBuiltinField::Status), + value: AgentViewValue::String("blocked".into()), + }); + server.app.state.agent_view_override = Some(view.clone()); + server.render_and_stream(); + let replacement = client_agent_view_projection(read_server_message( + control_rx.recv().expect("replacement projection"), + )); + let replacement_snapshot = client_shell_snapshot(read_server_message( + control_rx.recv().expect("replacement snapshot"), + )); + assert!(replacement.revision > set.revision); + assert_eq!(replacement.revision, replacement_snapshot.revision); + assert_eq!( + replacement + .view + .map(serde_json::from_value) + .transpose() + .unwrap(), + Some(view) + ); + + server.app.state.agent_view_override = None; + server.render_and_stream(); + let cleared = client_agent_view_projection(read_server_message( + control_rx.recv().expect("clear projection"), + )); + let cleared_snapshot = client_shell_snapshot(read_server_message( + control_rx.recv().expect("clear snapshot"), + )); + assert!(cleared.revision > replacement.revision); + assert_eq!(cleared.revision, cleared_snapshot.revision); + assert!(cleared.view.is_none()); + assert!(cleared_snapshot.agent_view_label.is_none()); +} + +#[tokio::test] async fn client_shell_receives_metadata_then_shell_free_pane_surface() { let mut server = test_headless_server(); let mut workspace = crate::workspace::Workspace::test_new("shell-only-label"); |
