diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/mod.rs | 1 | ||||
| -rw-r--r-- | src/api/schema.rs | 93 | ||||
| -rw-r--r-- | src/api/server.rs | 1 | ||||
| -rw-r--r-- | src/api/subscriptions.rs | 32 |
4 files changed, 126 insertions, 1 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index 1f2ab2bb..67964e83 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -39,6 +39,7 @@ pub(crate) fn request_changes_ui(request: &Request) -> bool { | Method::PaneSplit(_) | Method::PaneRename(_) | Method::PaneReportAgent(_) + | Method::PaneReportMetadata(_) | Method::PaneClearAgentAuthority(_) | Method::PaneReleaseAgent(_) | Method::PaneClose(_) diff --git a/src/api/schema.rs b/src/api/schema.rs index 4929d4a7..675d0cbc 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -82,6 +84,8 @@ pub enum Method { PaneRead(PaneReadParams), #[serde(rename = "pane.report_agent")] PaneReportAgent(PaneReportAgentParams), + #[serde(rename = "pane.report_metadata")] + PaneReportMetadata(PaneReportMetadataParams), #[serde(rename = "pane.clear_agent_authority")] PaneClearAgentAuthority(PaneClearAgentAuthorityParams), #[serde(rename = "pane.release_agent")] @@ -350,6 +354,36 @@ pub struct PaneReportAgentParams { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaneReportMetadataParams { + pub pane_id: String, + pub source: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub applies_to_source: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_agent: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub custom_status: Option<String>, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub state_labels: HashMap<String, String>, + #[serde(default)] + pub clear_title: bool, + #[serde(default)] + pub clear_display_agent: bool, + #[serde(default)] + pub clear_custom_status: bool, + #[serde(default)] + pub clear_state_labels: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub seq: Option<u64>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ttl_ms: Option<u64>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PaneClearAgentAuthorityParams { pub pane_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -759,7 +793,15 @@ pub struct AgentInfo { pub name: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub agent: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_agent: Option<String>, pub agent_status: AgentStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub custom_status: Option<String>, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub state_labels: HashMap<String, String>, pub workspace_id: String, pub tab_id: String, pub pane_id: String, @@ -782,9 +824,15 @@ pub struct PaneInfo { pub label: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub agent: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_agent: Option<String>, pub agent_status: AgentStatus, #[serde(default, skip_serializing_if = "Option::is_none")] pub custom_status: Option<String>, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub state_labels: HashMap<String, String>, pub revision: u64, } @@ -853,6 +901,12 @@ pub struct PaneAgentStatusChangedEvent { pub agent: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub custom_status: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_agent: Option<String>, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub state_labels: HashMap<String, String>, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -921,7 +975,13 @@ pub enum EventData { workspace_id: String, agent_status: AgentStatus, #[serde(default, skip_serializing_if = "Option::is_none")] + title: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + display_agent: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] custom_status: Option<String>, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + state_labels: HashMap<String, String>, }, } @@ -993,6 +1053,33 @@ mod tests { } #[test] + fn request_round_trips_for_pane_report_metadata() { + let request = Request { + id: "req_metadata".into(), + method: Method::PaneReportMetadata(PaneReportMetadataParams { + pane_id: "1-1".into(), + source: "user:claude-title".into(), + agent: Some("claude".into()), + applies_to_source: Some("herdr:claude".into()), + title: Some("Refactor auth".into()), + display_agent: Some("Claude auth".into()), + custom_status: Some("refactor auth".into()), + state_labels: HashMap::from([("working".into(), "deep in the mines".into())]), + clear_title: false, + clear_display_agent: false, + clear_custom_status: false, + clear_state_labels: false, + seq: Some(42), + ttl_ms: Some(3_600_000), + }), + }; + + let json = serde_json::to_string(&request).unwrap(); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); + } + + #[test] fn request_round_trips_for_pane_clear_agent_authority() { let request = Request { id: "req_clear".into(), @@ -1307,8 +1394,11 @@ mod tests { cwd: Some("/worktrees/herdr/worktree-api".into()), label: None, agent: None, + title: None, + display_agent: None, agent_status: AgentStatus::Unknown, custom_status: None, + state_labels: HashMap::new(), revision: 0, }, worktree: WorktreeInfo { @@ -1353,8 +1443,11 @@ mod tests { cwd: Some("/tmp/review".into()), label: None, agent: None, + title: None, + display_agent: None, agent_status: AgentStatus::Unknown, custom_status: None, + state_labels: HashMap::new(), revision: 0, }, }, diff --git a/src/api/server.rs b/src/api/server.rs index 696e1b65..cd130e47 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -303,6 +303,7 @@ fn api_method_name(method: &Method) -> &'static str { Method::PaneSendInput(_) => "pane.send_input", Method::PaneRead(_) => "pane.read", Method::PaneReportAgent(_) => "pane.report_agent", + Method::PaneReportMetadata(_) => "pane.report_metadata", Method::PaneClearAgentAuthority(_) => "pane.clear_agent_authority", Method::PaneReleaseAgent(_) => "pane.release_agent", Method::PaneClose(_) => "pane.close", diff --git a/src/api/subscriptions.rs b/src/api/subscriptions.rs index a6d8ff97..aaa7e5ac 100644 --- a/src/api/subscriptions.rs +++ b/src/api/subscriptions.rs @@ -50,9 +50,29 @@ pub(super) struct ActiveAgentStatusChangedSubscription { status_filter: Option<crate::api::schema::AgentStatus>, last_status: Option<crate::api::schema::AgentStatus>, emit_initial_match: bool, + last_presentation: Option<PanePresentationSnapshot>, request_prefix: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct PanePresentationSnapshot { + title: Option<String>, + display_agent: Option<String>, + custom_status: Option<String>, + state_labels: std::collections::HashMap<String, String>, +} + +impl PanePresentationSnapshot { + fn from(pane: &crate::api::schema::PaneInfo) -> Self { + Self { + title: pane.title.clone(), + display_agent: pane.display_agent.clone(), + custom_status: pane.custom_status.clone(), + state_labels: pane.state_labels.clone(), + } + } +} + pub(super) struct ActiveEventSubscription { event_kind: crate::api::schema::EventKind, last_sequence: u64, @@ -187,6 +207,7 @@ impl ActiveSubscription { status_filter: agent_status, last_status: Some(probe.agent_status), emit_initial_match, + last_presentation: Some(PanePresentationSnapshot::from(&probe)), request_prefix: format!("{request_id}:sub:{index}"), }, )) @@ -268,13 +289,19 @@ impl ActiveAgentStatusChangedSubscription { ) .ok()?; let current_status = pane.agent_status; + let current_presentation = PanePresentationSnapshot::from(&pane); let previous_status = self.last_status.replace(current_status); + let previous_presentation = self.last_presentation.replace(current_presentation.clone()); + let presentation_changed = previous_presentation + .as_ref() + .is_some_and(|previous| previous != ¤t_presentation); + let status_changed = previous_status.is_some_and(|previous| previous != current_status); let should_emit = if self.emit_initial_match { self.emit_initial_match = false; self.status_filter .is_some_and(|wanted| wanted == current_status) } else { - previous_status.is_some_and(|previous| previous != current_status) + (status_changed || presentation_changed) && !self .status_filter .is_some_and(|wanted| wanted != current_status) @@ -290,7 +317,10 @@ impl ActiveAgentStatusChangedSubscription { workspace_id: pane.workspace_id, agent_status: current_status, agent: pane.agent, + title: pane.title, + display_agent: pane.display_agent, custom_status: pane.custom_status, + state_labels: pane.state_labels, }), }) } |
