aboutsummaryrefslogtreecommitdiff
path: root/src/server/headless
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/headless')
-rw-r--r--src/server/headless/render.rs49
-rw-r--r--src/server/headless/tests/mod.rs98
2 files changed, 141 insertions, 6 deletions
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");