aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs125
-rw-r--r--src/app/agents.rs4
-rw-r--r--src/app/api.rs17
-rw-r--r--src/app/api/panes.rs134
-rw-r--r--src/app/creation.rs6
-rw-r--r--src/app/mod.rs2
-rw-r--r--src/app/runtime.rs16
7 files changed, 287 insertions, 17 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index da9e538a..ab2539bf 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -93,10 +93,11 @@ pub struct PaneStateUpdate {
pub previous_agent_label: Option<String>,
pub previous_known_agent: Option<Agent>,
pub previous_state: AgentState,
+ pub previous_presentation: crate::terminal::EffectivePresentation,
pub agent_label: Option<String>,
pub known_agent: Option<Agent>,
pub state: AgentState,
- pub custom_status: Option<String>,
+ pub presentation: crate::terminal::EffectivePresentation,
}
// ---------------------------------------------------------------------------
@@ -333,23 +334,37 @@ impl AppState {
let terminal = self.terminals.get(&pane.attached_terminal_id);
let pane_number = ws.public_pane_number(pane_id).unwrap_or(0);
let label = terminal
- .and_then(|terminal| terminal.manual_label.as_deref())
- .or_else(|| terminal.and_then(|terminal| terminal.agent_name.as_deref()))
- .or_else(|| terminal.and_then(|terminal| terminal.effective_agent_label()))
- .map(str::to_string)
+ .and_then(|terminal| terminal.effective_title())
+ .or_else(|| {
+ terminal
+ .and_then(|terminal| terminal.manual_label.as_deref().map(str::to_string))
+ })
+ .or_else(|| {
+ terminal.and_then(|terminal| terminal.agent_name.as_deref().map(str::to_string))
+ })
+ .or_else(|| {
+ terminal
+ .and_then(|terminal| terminal.effective_agent_label().map(str::to_string))
+ })
.or_else(|| {
launch_label(terminal.and_then(|terminal| terminal.launch_argv.as_ref()))
})
.unwrap_or_else(|| format!("pane {pane_number}"));
- let agent_label = terminal
- .and_then(|terminal| terminal.agent_name.as_deref())
- .or_else(|| terminal.and_then(|terminal| terminal.effective_agent_label()));
+ let display_agent = terminal.and_then(|terminal| terminal.effective_display_agent());
+ let agent_label = display_agent.as_deref().or_else(|| {
+ terminal
+ .and_then(|terminal| terminal.agent_name.as_deref())
+ .or_else(|| terminal.and_then(|terminal| terminal.effective_agent_label()))
+ });
let custom_status = terminal.and_then(|terminal| terminal.effective_custom_status());
let state = terminal
.map(|terminal| terminal.state)
.unwrap_or(AgentState::Unknown);
+ let status_label = terminal
+ .map(|terminal| terminal.effective_presentation().state_labels)
+ .and_then(|labels| labels.get(state_label_text(state, pane.seen)).cloned());
let status = custom_status
- .map(str::to_string)
+ .or(status_label)
.or_else(|| agent_label.map(|_| state_label_text(state, pane.seen).to_string()));
let meta = match (agent_label, status.as_deref()) {
(Some(agent_label), Some(status)) => format!("{agent_label} ยท {status}"),
@@ -656,6 +671,60 @@ fn activity_summary_for_panes<'a>(
// ---------------------------------------------------------------------------
impl AppState {
+ pub(crate) fn next_agent_metadata_expiry(&self) -> Option<std::time::Instant> {
+ self.terminals
+ .values()
+ .filter_map(|terminal| terminal.next_agent_metadata_expiry())
+ .min()
+ }
+
+ pub(crate) fn expire_agent_metadata_at(
+ &mut self,
+ scheduled_deadline: std::time::Instant,
+ now: std::time::Instant,
+ ) -> Vec<PaneStateUpdate> {
+ let pane_terminals: Vec<_> = self
+ .workspaces
+ .iter()
+ .enumerate()
+ .flat_map(|(ws_idx, ws)| {
+ ws.tabs.iter().flat_map(move |tab| {
+ tab.layout
+ .pane_ids()
+ .into_iter()
+ .filter_map(move |pane_id| {
+ ws.pane_state(pane_id)
+ .map(|pane| (ws_idx, pane_id, pane.attached_terminal_id.clone()))
+ })
+ })
+ })
+ .collect();
+ pane_terminals
+ .into_iter()
+ .filter_map(|(ws_idx, pane_id, terminal_id)| {
+ let mutation = self
+ .terminals
+ .get_mut(&terminal_id)?
+ .expire_agent_metadata_at(scheduled_deadline, now)?;
+ let change = mutation.effective_state_change?;
+ let update = PaneStateUpdate {
+ pane_id,
+ ws_idx,
+ previous_agent_label: change.previous_agent_label.clone(),
+ previous_known_agent: change.previous_known_agent,
+ previous_state: change.previous_state,
+ previous_presentation: change.previous_presentation.clone(),
+ agent_label: change.agent_label.clone(),
+ known_agent: change.known_agent,
+ state: change.state,
+ presentation: change.presentation.clone(),
+ };
+ self.apply_pane_state_change(ws_idx, pane_id, &change);
+ Some(update)
+ })
+ .collect()
+ }
+
pub(crate) fn pane_is_in_active_tab(&self, ws_idx: usize, pane_id: PaneId) -> bool {
let Some(active_ws_idx) = self.active else {
return false;
@@ -1844,6 +1913,41 @@ impl AppState {
})
.into_iter()
.collect(),
+ AppEvent::HookMetadataReported {
+ pane_id,
+ source,
+ agent_label,
+ applies_to_source,
+ title,
+ display_agent,
+ custom_status,
+ state_labels,
+ clear_title,
+ clear_display_agent,
+ clear_custom_status,
+ clear_state_labels,
+ seq,
+ ttl,
+ } => self
+ .update_terminal_state(pane_id, |terminal| {
+ terminal.set_agent_metadata(crate::terminal::AgentMetadataReport {
+ source,
+ agent_label,
+ applies_to_source,
+ title,
+ display_agent,
+ custom_status,
+ state_labels,
+ clear_title,
+ clear_display_agent,
+ clear_custom_status,
+ clear_state_labels,
+ ttl,
+ seq,
+ })
+ })
+ .into_iter()
+ .collect(),
AppEvent::HookAuthorityCleared {
pane_id,
source,
@@ -1904,10 +2008,11 @@ impl AppState {
previous_agent_label: change.previous_agent_label.clone(),
previous_known_agent: change.previous_known_agent,
previous_state: change.previous_state,
+ previous_presentation: change.previous_presentation.clone(),
agent_label: change.agent_label.clone(),
known_agent: change.known_agent,
state: change.state,
- custom_status: change.custom_status.clone(),
+ presentation: change.presentation.clone(),
};
self.apply_pane_state_change(ws_idx, pane_id, &change);
Some(update)
diff --git a/src/app/agents.rs b/src/app/agents.rs
index b1276a06..b9534ce2 100644
--- a/src/app/agents.rs
+++ b/src/app/agents.rs
@@ -408,7 +408,11 @@ impl App {
terminal_id: pane.terminal_id,
name: terminal.agent_name.clone(),
agent: pane.agent,
+ title: pane.title,
+ display_agent: pane.display_agent,
agent_status: pane.agent_status,
+ custom_status: pane.custom_status,
+ state_labels: pane.state_labels,
workspace_id: pane.workspace_id,
tab_id: pane.tab_id,
pane_id: pane.pane_id,
diff --git a/src/app/api.rs b/src/app/api.rs
index 2488ce40..6319bdb6 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -88,6 +88,7 @@ impl App {
for update in &pane_updates {
self.emit_pane_state_update(update);
}
+ self.sync_agent_metadata_deadline();
if let Some((pane_id, agent)) = released_agent {
if pane_updates.iter().any(|update| update.pane_id == pane_id) {
if let Some((ws_idx, _)) = self.find_pane(pane_id) {
@@ -209,7 +210,7 @@ impl App {
}
}
- fn emit_pane_state_update(&self, update: &crate::app::actions::PaneStateUpdate) {
+ pub(crate) fn emit_pane_state_update(&self, update: &crate::app::actions::PaneStateUpdate) {
let Some(pane_id) = self.public_pane_id(update.ws_idx, update.pane_id) else {
return;
};
@@ -226,7 +227,9 @@ impl App {
});
}
- if update.previous_state != update.state {
+ if update.previous_state != update.state
+ || update.previous_presentation != update.presentation
+ {
let agent_status = self
.state
.workspaces
@@ -234,14 +237,17 @@ impl App {
.and_then(|ws| ws.pane_state(update.pane_id))
.map(|pane| pane_agent_status(update.state, pane.seen))
.unwrap_or_else(|| pane_agent_status(update.state, true));
- let custom_status = update.custom_status.clone();
+ let presentation = update.presentation.clone();
self.emit_event(crate::api::schema::EventEnvelope {
event: crate::api::schema::EventKind::PaneAgentStatusChanged,
data: crate::api::schema::EventData::PaneAgentStatusChanged {
pane_id,
workspace_id,
agent_status,
- custom_status,
+ title: presentation.title,
+ display_agent: presentation.display_agent,
+ custom_status: presentation.custom_status,
+ state_labels: presentation.state_labels,
},
});
}
@@ -413,6 +419,9 @@ impl App {
Method::PaneReportAgent(params) => {
return self.handle_pane_report_agent(request.id, params);
}
+ Method::PaneReportMetadata(params) => {
+ return self.handle_pane_report_metadata(request.id, params);
+ }
Method::PaneClearAgentAuthority(params) => {
return self.handle_pane_clear_agent_authority(request.id, params);
}
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"))
}
diff --git a/src/app/creation.rs b/src/app/creation.rs
index 5bb14953..a034fdc4 100644
--- a/src/app/creation.rs
+++ b/src/app/creation.rs
@@ -274,6 +274,7 @@ impl App {
&& ws
.focused_pane_id()
.is_some_and(|focused| focused == pane_id);
+ let presentation = terminal.effective_presentation();
Some(crate::api::schema::PaneInfo {
pane_id: self.public_pane_id(ws_idx, pane_id)?,
terminal_id: terminal.id.to_string(),
@@ -285,8 +286,11 @@ impl App {
.map(|cwd| cwd.display().to_string()),
label: terminal.manual_label.clone(),
agent: terminal.effective_agent_label().map(str::to_string),
+ title: presentation.title,
+ display_agent: presentation.display_agent,
agent_status: pane_agent_status(terminal.state, pane.seen),
- custom_status: terminal.effective_custom_status().map(str::to_string),
+ custom_status: presentation.custom_status,
+ state_labels: presentation.state_labels,
revision: terminal.revision,
})
}
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 1c221be8..cad09a71 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -102,6 +102,7 @@ pub struct App {
pub(crate) next_resize_poll: Instant,
pub(crate) next_animation_tick: Option<Instant>,
pub(crate) next_auto_update_check: Option<Instant>,
+ pub(crate) agent_metadata_deadline: Option<Instant>,
pub(crate) selection_autoscroll_deadline: Option<Instant>,
pub(crate) selection_highlight_clear_deadline: Option<Instant>,
pub(crate) session_save_deadline: Option<Instant>,
@@ -563,6 +564,7 @@ impl App {
next_animation_tick: None,
next_auto_update_check: auto_updates_enabled(no_session)
.then_some(Instant::now() + AUTO_UPDATE_CHECK_INTERVAL),
+ agent_metadata_deadline: None,
session_save_deadline: None,
selection_autoscroll_deadline: None,
selection_highlight_clear_deadline: None,
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
index 2e1f4d81..64232397 100644
--- a/src/app/runtime.rs
+++ b/src/app/runtime.rs
@@ -206,6 +206,17 @@ impl App {
self.save_session_now();
}
+ if let Some(deadline) = self
+ .agent_metadata_deadline
+ .filter(|deadline| now >= *deadline)
+ {
+ for update in self.state.expire_agent_metadata_at(deadline, now) {
+ self.emit_pane_state_update(&update);
+ }
+ self.sync_agent_metadata_deadline();
+ changed = true;
+ }
+
self.sync_animation_timer(now);
changed
}
@@ -232,6 +243,10 @@ impl App {
false
}
+ pub(crate) fn sync_agent_metadata_deadline(&mut self) {
+ self.agent_metadata_deadline = self.state.next_agent_metadata_expiry();
+ }
+
pub(crate) fn sync_animation_timer(&mut self, now: Instant) {
self.sync_animation_timer_with_interval(now, ANIMATION_INTERVAL);
}
@@ -444,6 +459,7 @@ impl App {
self.next_animation_tick,
self.git_refresh_deadline(),
self.next_auto_update_check,
+ self.agent_metadata_deadline,
self.session_save_deadline,
self.selection_autoscroll_deadline,
self.selection_highlight_clear_deadline,