aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/clients.rs3
-rw-r--r--src/server/headless.rs19
-rw-r--r--src/server/headless/render.rs49
-rw-r--r--src/server/headless/tests/mod.rs98
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");