aboutsummaryrefslogtreecommitdiff
path: root/src/app/api
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-26 17:10:28 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-26 17:10:28 +0300
commit3750e0bae21c54da3947d337584f180a21d0bc67 (patch)
tree4542d8af76b4fee0c428ca5e9378a06946ef62ca /src/app/api
parent1b980eaa9d4cbc050b6b81525e4b6b72ae853be5 (diff)
feat: add pane metadata reporting
refs #36
Diffstat (limited to 'src/app/api')
-rw-r--r--src/app/api/panes.rs134
1 files changed, 132 insertions, 2 deletions
diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs
index 9a31ec34..a81a6080 100644
--- a/src/app/api/panes.rs
+++ b/src/app/api/panes.rs
@@ -3,8 +3,8 @@ use bytes::Bytes;
use crate::api::schema::{
EventData, EventEnvelope, EventKind, PaneClearAgentAuthorityParams, PaneListParams,
PaneReadParams, PaneReadResult, PaneReleaseAgentParams, PaneRenameParams,
- PaneReportAgentParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams,
- PaneSplitParams, PaneTarget, ReadFormat, ReadSource, ResponseResult,
+ PaneReportAgentParams, PaneReportMetadataParams, PaneSendInputParams, PaneSendKeysParams,
+ PaneSendTextParams, PaneSplitParams, PaneTarget, ReadFormat, ReadSource, ResponseResult,
};
use crate::app::{App, Mode};
@@ -198,6 +198,102 @@ impl App {
encode_success(id, ResponseResult::Ok {})
}
+ pub(super) fn handle_pane_report_metadata(
+ &mut self,
+ id: String,
+ params: PaneReportMetadataParams,
+ ) -> String {
+ let Some((_ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let agent_label = match params.agent.as_deref() {
+ Some(agent) => match normalize_reported_agent_label(agent) {
+ Some(agent_label) => Some(agent_label),
+ None => return invalid_agent(id),
+ },
+ None => None,
+ };
+ let Some(source) = normalize_optional_text(Some(params.source)) else {
+ return encode_error(id, "invalid_metadata_request", "missing metadata source");
+ };
+ let raw_title_set = params.title.is_some();
+ let raw_display_agent_set = params.display_agent.is_some();
+ let raw_custom_status_set = params.custom_status.is_some();
+ let raw_state_labels_set = !params.state_labels.is_empty();
+ let ttl = params.ttl_ms.map(std::time::Duration::from_millis);
+ let title = normalize_presentation_text(params.title);
+ let display_agent = normalize_presentation_text(params.display_agent);
+ let custom_status = normalize_custom_status(params.custom_status);
+ let applies_to_source = match params.applies_to_source {
+ Some(applies_to_source) => {
+ let Some(applies_to_source) = normalize_optional_text(Some(applies_to_source))
+ else {
+ return encode_error(
+ id,
+ "invalid_metadata_request",
+ "missing metadata authority source",
+ );
+ };
+ Some(applies_to_source)
+ }
+ None => None,
+ };
+ let state_labels = match normalize_state_labels(params.state_labels) {
+ Ok(labels) => labels,
+ Err(status) => {
+ return encode_error(
+ id,
+ "invalid_state_label",
+ format!("unknown state label: {status}"),
+ );
+ }
+ };
+ if raw_title_set && params.clear_title
+ || raw_display_agent_set && params.clear_display_agent
+ || raw_custom_status_set && params.clear_custom_status
+ || raw_state_labels_set && params.clear_state_labels
+ {
+ return encode_error(
+ id,
+ "invalid_metadata_request",
+ "cannot set and clear the same metadata field",
+ );
+ }
+ if title.is_none()
+ && display_agent.is_none()
+ && custom_status.is_none()
+ && state_labels.is_empty()
+ && !params.clear_title
+ && !params.clear_display_agent
+ && !params.clear_custom_status
+ && !params.clear_state_labels
+ {
+ return encode_error(
+ id,
+ "invalid_metadata_request",
+ "missing metadata field to set or clear",
+ );
+ }
+ self.handle_internal_event(crate::events::AppEvent::HookMetadataReported {
+ pane_id,
+ source,
+ agent_label,
+ applies_to_source,
+ title,
+ display_agent,
+ custom_status,
+ state_labels,
+ clear_title: params.clear_title,
+ clear_display_agent: params.clear_display_agent,
+ clear_custom_status: params.clear_custom_status,
+ clear_state_labels: params.clear_state_labels,
+ seq: params.seq,
+ ttl,
+ });
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+
pub(super) fn handle_pane_clear_agent_authority(
&mut self,
id: String,
@@ -353,6 +449,40 @@ impl App {
}
}
+fn normalize_optional_text(value: Option<String>) -> Option<String> {
+ let value = value?.trim().to_string();
+ (!value.is_empty()).then_some(value)
+}
+
+fn normalize_presentation_text(value: Option<String>) -> Option<String> {
+ let trimmed = value?.trim().to_string();
+ let normalized: String = trimmed
+ .chars()
+ .filter(|ch| !ch.is_control())
+ .take(80)
+ .collect();
+ (!normalized.trim().is_empty()).then(|| normalized.trim().to_string())
+}
+
+fn normalize_state_labels(
+ labels: std::collections::HashMap<String, String>,
+) -> Result<std::collections::HashMap<String, String>, String> {
+ labels
+ .into_iter()
+ .map(|(status, label)| {
+ let status = status.trim().to_ascii_lowercase();
+ if !matches!(
+ status.as_str(),
+ "idle" | "working" | "blocked" | "done" | "unknown"
+ ) {
+ return Err(status);
+ }
+ Ok(normalize_presentation_text(Some(label)).map(|label| (status, label)))
+ })
+ .filter_map(Result::transpose)
+ .collect()
+}
+
fn pane_not_found(id: String, pane_id: &str) -> String {
encode_error(id, "pane_not_found", format!("pane {pane_id} not found"))
}