aboutsummaryrefslogtreecommitdiff
path: root/src/terminal
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-13 03:26:03 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-13 03:30:13 +0300
commit5cfe5e5e730246948b0c9428ad876b6938590aad (patch)
tree53d4bbe1aff6887f63f279eed1b5e4e11e7331b7 /src/terminal
parent7f3364a468f7dcfd8c9c84e5737fe5699f0e3a93 (diff)
feat: add configurable sidebar metadata tokens
Diffstat (limited to 'src/terminal')
-rw-r--r--src/terminal/metadata.rs439
-rw-r--r--src/terminal/mod.rs2
-rw-r--r--src/terminal/runtime.rs4
-rw-r--r--src/terminal/state.rs229
-rw-r--r--src/terminal/title.rs64
5 files changed, 247 insertions, 491 deletions
diff --git a/src/terminal/metadata.rs b/src/terminal/metadata.rs
index 2666b32f..225d609a 100644
--- a/src/terminal/metadata.rs
+++ b/src/terminal/metadata.rs
@@ -12,12 +12,10 @@ pub struct AgentMetadata {
pub applies_to_source: Option<String>,
pub title: Option<String>,
pub display_agent: Option<String>,
- pub custom_status: Option<String>,
pub state_labels: HashMap<String, String>,
pub reported_at: Instant,
title_reported_at: Option<Instant>,
display_agent_reported_at: Option<Instant>,
- custom_status_reported_at: Option<Instant>,
state_label_reported_at: HashMap<String, Instant>,
pub ttl: Option<Duration>,
expiry_event_pending: bool,
@@ -30,11 +28,9 @@ pub struct AgentMetadataReport {
pub applies_to_source: Option<String>,
pub title: Option<String>,
pub display_agent: Option<String>,
- pub custom_status: Option<String>,
pub state_labels: HashMap<String, String>,
pub clear_title: bool,
pub clear_display_agent: bool,
- pub clear_custom_status: bool,
pub clear_state_labels: bool,
pub ttl: Option<Duration>,
pub seq: Option<u64>,
@@ -44,7 +40,6 @@ pub struct AgentMetadataReport {
pub struct EffectivePresentation {
pub title: Option<String>,
pub display_agent: Option<String>,
- pub custom_status: Option<String>,
pub state_labels: HashMap<String, String>,
}
@@ -53,36 +48,67 @@ impl EffectivePresentation {
Self {
title: None,
display_agent: None,
- custom_status: None,
state_labels: HashMap::new(),
}
}
}
impl TerminalState {
- fn accept_metadata_report(&mut self, source: &str, seq: Option<u64>) -> bool {
+ pub(crate) fn metadata_report_sequence_is_fresh(&self, source: &str, seq: Option<u64>) -> bool {
+ crate::metadata_tokens::sequence_is_fresh(&self.metadata_report_sequences, source, seq)
+ }
+
+ pub(crate) fn accept_metadata_report(
+ &mut self,
+ source: &str,
+ seq: Option<u64>,
+ includes_tokens: bool,
+ ) -> Result<bool, ()> {
let Some(seq) = seq else {
- return true;
+ return Ok(true);
};
-
- if self
- .metadata_report_sequences
- .get(source)
- .is_some_and(|last_seq| seq <= *last_seq)
+ if !self.metadata_report_sequence_is_fresh(source, Some(seq)) {
+ return Ok(false);
+ }
+ if includes_tokens
+ && !self.metadata_token_sequence_sources.contains(source)
+ && self.metadata_token_sequence_sources.len()
+ >= crate::metadata_tokens::MAX_SEQUENCE_SOURCES
{
- return false;
+ return Err(());
}
-
self.metadata_report_sequences
.insert(source.to_string(), seq);
- true
+ if includes_tokens {
+ self.metadata_token_sequence_sources
+ .insert(source.to_string());
+ }
+ Ok(true)
+ }
+
+ fn metadata_guards_match(
+ &self,
+ agent_label: Option<&str>,
+ applies_to_source: Option<&str>,
+ ) -> bool {
+ if agent_label.is_some_and(|agent| self.effective_agent_label() != Some(agent)) {
+ return false;
+ }
+ !applies_to_source.is_some_and(|source| {
+ self.hook_authority
+ .as_ref()
+ .is_none_or(|authority| authority.source != source)
+ })
}
pub fn set_agent_metadata(
&mut self,
report: AgentMetadataReport,
) -> Option<TerminalStateMutation> {
- if !self.accept_metadata_report(&report.source, report.seq) {
+ if !matches!(
+ self.accept_metadata_report(&report.source, report.seq, false),
+ Ok(true)
+ ) {
return None;
}
@@ -100,17 +126,12 @@ impl TerminalState {
let previous_presentation = self.effective_presentation_for_state_at(previous_state, now);
let has_set_fields = report.title.is_some()
|| report.display_agent.is_some()
- || report.custom_status.is_some()
|| !report.state_labels.is_empty();
let report_source = report.source.clone();
let report_has_ttl = report.ttl.is_some();
- if report.clear_title
- || report.clear_display_agent
- || report.clear_custom_status
- || report.clear_state_labels
- {
+ if report.clear_title || report.clear_display_agent || report.clear_state_labels {
let metadata = self
.agent_metadata
.entry(report.source.clone())
@@ -120,12 +141,10 @@ impl TerminalState {
applies_to_source: report.applies_to_source.clone(),
title: None,
display_agent: None,
- custom_status: None,
state_labels: HashMap::new(),
reported_at: now,
title_reported_at: None,
display_agent_reported_at: None,
- custom_status_reported_at: None,
state_label_reported_at: HashMap::new(),
ttl: report.ttl,
expiry_event_pending: false,
@@ -138,10 +157,6 @@ impl TerminalState {
metadata.display_agent = None;
metadata.display_agent_reported_at = None;
}
- if report.clear_custom_status {
- metadata.custom_status = None;
- metadata.custom_status_reported_at = None;
- }
if report.clear_state_labels {
metadata.state_labels.clear();
metadata.state_label_reported_at.clear();
@@ -160,10 +175,6 @@ impl TerminalState {
metadata.display_agent = Some(display_agent);
metadata.display_agent_reported_at = Some(now);
}
- if let Some(custom_status) = report.custom_status {
- metadata.custom_status = Some(custom_status);
- metadata.custom_status_reported_at = Some(now);
- }
for (state, label) in report.state_labels {
metadata.state_labels.insert(state.clone(), label);
metadata.state_label_reported_at.insert(state, now);
@@ -176,7 +187,6 @@ impl TerminalState {
} else {
let title_reported_at = report.title.as_ref().map(|_| now);
let display_agent_reported_at = report.display_agent.as_ref().map(|_| now);
- let custom_status_reported_at = report.custom_status.as_ref().map(|_| now);
let state_label_reported_at = report
.state_labels
.keys()
@@ -190,12 +200,10 @@ impl TerminalState {
applies_to_source: report.applies_to_source,
title: report.title,
display_agent: report.display_agent,
- custom_status: report.custom_status,
state_labels: report.state_labels,
reported_at: now,
title_reported_at,
display_agent_reported_at,
- custom_status_reported_at,
state_label_reported_at,
ttl: report.ttl,
expiry_event_pending: false,
@@ -231,11 +239,6 @@ impl TerminalState {
session_ref_changed: false,
})
}
- pub fn effective_custom_status(&self) -> Option<String> {
- self.effective_presentation_for_state_at(self.state, Instant::now())
- .custom_status
- }
-
pub fn effective_title(&self) -> Option<String> {
self.effective_presentation_for_state_at(self.state, Instant::now())
.title
@@ -337,7 +340,7 @@ impl TerminalState {
fn effective_presentation_for_state_at_with_ttl(
&self,
- state: AgentState,
+ _state: AgentState,
now: Instant,
enforce_ttl: bool,
) -> EffectivePresentation {
@@ -345,30 +348,9 @@ impl TerminalState {
presentation.title = self.newest_metadata_title(now, enforce_ttl);
presentation.display_agent = self.newest_metadata_display_agent(now, enforce_ttl);
presentation.state_labels = self.effective_metadata_state_labels(now, enforce_ttl);
- presentation.custom_status =
- self.effective_custom_status_for_state_at_with_ttl(state, now, enforce_ttl);
presentation
}
- fn effective_custom_status_for_state_at_with_ttl(
- &self,
- _state: AgentState,
- now: Instant,
- enforce_ttl: bool,
- ) -> Option<String> {
- if let Some(custom_status) = self.newest_metadata_custom_status(now, enforce_ttl) {
- return Some(custom_status);
- }
-
- if self.visible_blocker_overrides_hook() {
- return None;
- }
-
- self.hook_authority
- .as_ref()
- .and_then(|authority| authority.custom_status.clone())
- }
-
fn valid_agent_metadata(
&self,
now: Instant,
@@ -393,13 +375,6 @@ impl TerminalState {
.and_then(|metadata| metadata.display_agent.clone())
}
- fn newest_metadata_custom_status(&self, now: Instant, enforce_ttl: bool) -> Option<String> {
- self.valid_agent_metadata(now, enforce_ttl)
- .filter(|metadata| metadata.custom_status.is_some())
- .max_by_key(|metadata| metadata.custom_status_reported_at)
- .and_then(|metadata| metadata.custom_status.clone())
- }
-
fn effective_metadata_state_labels(
&self,
now: Instant,
@@ -432,7 +407,6 @@ impl TerminalState {
) -> bool {
if metadata.title.is_none()
&& metadata.display_agent.is_none()
- && metadata.custom_status.is_none()
&& metadata.state_labels.is_empty()
{
return false;
@@ -446,7 +420,6 @@ impl TerminalState {
fn agent_metadata_is_visible_ignoring_ttl(&self, metadata: &AgentMetadata) -> bool {
(metadata.title.is_some()
|| metadata.display_agent.is_some()
- || metadata.custom_status.is_some()
|| !metadata.state_labels.is_empty())
&& self.agent_metadata_matches_guards(metadata)
}
@@ -483,21 +456,10 @@ impl TerminalState {
}
fn agent_metadata_matches_guards(&self, metadata: &AgentMetadata) -> bool {
- if metadata
- .agent_label
- .as_deref()
- .is_some_and(|agent| self.effective_agent_label() != Some(agent))
- {
- return false;
- }
- if metadata.applies_to_source.as_deref().is_some_and(|source| {
- self.hook_authority
- .as_ref()
- .is_none_or(|authority| authority.source != source)
- }) {
- return false;
- }
- true
+ self.metadata_guards_match(
+ metadata.agent_label.as_deref(),
+ metadata.applies_to_source.as_deref(),
+ )
}
}
@@ -513,129 +475,39 @@ mod tests {
TerminalState::new(TerminalId::alloc(), "/tmp".into())
}
- fn set_metadata_custom_status(
- terminal: &mut TerminalState,
- source: &str,
- agent_label: Option<&str>,
- applies_to_source: Option<&str>,
- custom_status: Option<&str>,
- clear_custom_status: bool,
- ) -> Option<TerminalStateMutation> {
- terminal.set_agent_metadata(AgentMetadataReport {
- source: source.into(),
- agent_label: agent_label.map(str::to_string),
- applies_to_source: applies_to_source.map(str::to_string),
- title: None,
- display_agent: None,
- custom_status: custom_status.map(str::to_string),
- state_labels: HashMap::new(),
- clear_title: false,
- clear_display_agent: false,
- clear_custom_status,
- clear_state_labels: false,
- ttl: None,
- seq: None,
- })
- }
-
- #[test]
- fn user_agent_metadata_overrides_hook_custom_status_without_changing_state() {
- let mut terminal = test_terminal();
- terminal.set_hook_authority_with_custom_status(
- "herdr:claude".into(),
- "claude".into(),
- AgentState::Working,
- None,
- Some("thinking".into()),
- None,
- );
-
- let mutation = set_metadata_custom_status(
- &mut terminal,
- "user:claude-title",
- Some("claude"),
- Some("herdr:claude"),
- Some("refactor auth"),
- false,
- );
-
- assert_eq!(terminal.state, AgentState::Working);
- assert_eq!(
- terminal.effective_custom_status().as_deref(),
- Some("refactor auth")
- );
- let change = mutation.unwrap().effective_state_change.unwrap();
- assert_eq!(change.previous_state, AgentState::Working);
- assert_eq!(change.state, AgentState::Working);
- assert_eq!(
- change.previous_presentation.custom_status.as_deref(),
- Some("thinking")
- );
- assert_eq!(
- change.presentation.custom_status.as_deref(),
- Some("refactor auth")
- );
- }
-
- #[test]
- fn user_agent_metadata_requires_matching_lifecycle_source() {
- let mut terminal = test_terminal();
- terminal.set_hook_authority(
- "herdr:codex".into(),
- "codex".into(),
- AgentState::Working,
- None,
- None,
- );
- set_metadata_custom_status(
- &mut terminal,
- "user:claude-title",
- Some("claude"),
- Some("herdr:claude"),
- Some("refactor auth"),
- false,
- );
-
- assert_eq!(terminal.effective_custom_status(), None);
- }
-
#[test]
- fn clearing_user_agent_metadata_restores_hook_custom_status() {
+ fn presentation_sequences_remain_unbounded_while_token_sequences_are_bounded() {
let mut terminal = test_terminal();
- terminal.set_hook_authority_with_custom_status(
- "herdr:claude".into(),
- "claude".into(),
- AgentState::Working,
- None,
- Some("thinking".into()),
- None,
- );
- set_metadata_custom_status(
- &mut terminal,
- "user:claude-title",
- Some("claude"),
- Some("herdr:claude"),
- Some("refactor auth"),
- false,
- );
-
- set_metadata_custom_status(&mut terminal, "user:claude-title", None, None, None, true);
-
+ for index in 0..=crate::metadata_tokens::MAX_SEQUENCE_SOURCES {
+ assert_eq!(
+ terminal.accept_metadata_report(&format!("source-{index}"), Some(1), false),
+ Ok(true)
+ );
+ }
+ for index in 0..crate::metadata_tokens::MAX_SEQUENCE_SOURCES {
+ assert_eq!(
+ terminal.accept_metadata_report(&format!("source-{index}"), Some(2), true),
+ Ok(true)
+ );
+ }
assert_eq!(
- terminal.effective_custom_status().as_deref(),
- Some("thinking")
+ terminal.accept_metadata_report(
+ &format!("source-{}", crate::metadata_tokens::MAX_SEQUENCE_SOURCES),
+ Some(2),
+ true,
+ ),
+ Err(())
);
}
#[test]
fn user_agent_metadata_overrides_presentation_fields_only() {
let mut terminal = test_terminal();
- terminal.set_hook_authority_with_custom_status(
+ terminal.set_hook_authority(
"herdr:claude".into(),
"claude".into(),
AgentState::Working,
None,
- Some("thinking".into()),
None,
);
@@ -645,11 +517,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: Some("Refactor auth".into()),
display_agent: Some("Claude: auth".into()),
- custom_status: Some("middleware".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,
ttl: None,
seq: None,
@@ -660,7 +530,6 @@ mod tests {
let presentation = terminal.effective_presentation();
assert_eq!(presentation.title.as_deref(), Some("Refactor auth"));
assert_eq!(presentation.display_agent.as_deref(), Some("Claude: auth"));
- assert_eq!(presentation.custom_status.as_deref(), Some("middleware"));
assert_eq!(
presentation.state_labels.get("working").map(String::as_str),
Some("deep in the mines")
@@ -679,11 +548,9 @@ mod tests {
applies_to_source: None,
title: Some("Prompt title".into()),
display_agent: None,
- custom_status: None,
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: None,
seq: None,
@@ -707,27 +574,23 @@ mod tests {
None,
);
- set_metadata_custom_status(
- &mut terminal,
- "user:claude-title",
- Some("claude"),
- Some("herdr:claude"),
- Some("first"),
- false,
- );
- set_metadata_custom_status(
- &mut terminal,
- "user:claude-title",
- Some("claude"),
- Some("herdr:claude"),
- Some("second"),
- false,
- );
+ for title in ["first", "second"] {
+ terminal.set_agent_metadata(AgentMetadataReport {
+ source: "user:claude-title".into(),
+ agent_label: Some("claude".into()),
+ applies_to_source: Some("herdr:claude".into()),
+ title: Some(title.into()),
+ display_agent: None,
+ state_labels: HashMap::new(),
+ clear_title: false,
+ clear_display_agent: false,
+ clear_state_labels: false,
+ ttl: None,
+ seq: None,
+ });
+ }
- assert_eq!(
- terminal.effective_custom_status().as_deref(),
- Some("second")
- );
+ assert_eq!(terminal.effective_title().as_deref(), Some("second"));
}
#[test]
@@ -746,11 +609,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: Some("Prompt title".into()),
display_agent: None,
- custom_status: None,
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: None,
seq: Some(1),
@@ -760,12 +621,10 @@ mod tests {
agent_label: Some("claude".into()),
applies_to_source: Some("herdr:claude".into()),
title: None,
- display_agent: None,
- custom_status: Some("activity".into()),
+ display_agent: Some("Claude activity".into()),
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: None,
seq: Some(1),
@@ -773,7 +632,10 @@ mod tests {
let presentation = terminal.effective_presentation();
assert_eq!(presentation.title.as_deref(), Some("Prompt title"));
- assert_eq!(presentation.custom_status.as_deref(), Some("activity"));
+ assert_eq!(
+ presentation.display_agent.as_deref(),
+ Some("Claude activity")
+ );
}
#[test]
@@ -792,26 +654,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: None,
display_agent: Some("First display".into()),
- custom_status: Some("old".into()),
- state_labels: HashMap::new(),
- clear_title: false,
- clear_display_agent: false,
- clear_custom_status: false,
- clear_state_labels: false,
- ttl: None,
- seq: Some(1),
- });
- terminal.set_agent_metadata(AgentMetadataReport {
- source: "user:second".into(),
- agent_label: Some("claude".into()),
- applies_to_source: Some("herdr:claude".into()),
- title: None,
- display_agent: None,
- custom_status: Some("new".into()),
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: None,
seq: Some(1),
@@ -822,11 +667,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: Some("Fresh title".into()),
display_agent: None,
- custom_status: None,
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: true,
- clear_custom_status: false,
clear_state_labels: false,
ttl: None,
seq: Some(2),
@@ -835,40 +678,6 @@ mod tests {
let presentation = terminal.effective_presentation();
assert_eq!(presentation.title.as_deref(), Some("Fresh title"));
assert_eq!(presentation.display_agent, None);
- assert_eq!(presentation.custom_status.as_deref(), Some("new"));
- }
-
- #[test]
- fn metadata_can_set_other_fields_while_clearing_missing_source() {
- let mut terminal = test_terminal();
- terminal.set_hook_authority(
- "herdr:claude".into(),
- "claude".into(),
- AgentState::Working,
- None,
- None,
- );
-
- terminal.set_agent_metadata(AgentMetadataReport {
- source: "user:status".into(),
- agent_label: Some("claude".into()),
- applies_to_source: Some("herdr:claude".into()),
- title: None,
- display_agent: None,
- custom_status: Some("activity".into()),
- state_labels: HashMap::new(),
- clear_title: true,
- clear_display_agent: false,
- clear_custom_status: false,
- clear_state_labels: false,
- ttl: None,
- seq: None,
- });
-
- assert_eq!(
- terminal.effective_custom_status().as_deref(),
- Some("activity")
- );
}
#[test]
@@ -887,11 +696,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: Some("Old title".into()),
display_agent: None,
- custom_status: Some("old".into()),
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::from_millis(1)),
seq: None,
@@ -902,27 +709,25 @@ mod tests {
source: "user:status".into(),
agent_label: Some("claude".into()),
applies_to_source: Some("herdr:claude".into()),
- title: None,
+ title: Some("Fresh title".into()),
display_agent: None,
- custom_status: Some("fresh".into()),
state_labels: HashMap::new(),
- clear_title: true,
+ clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: None,
seq: None,
});
assert_eq!(terminal.next_agent_metadata_expiry(), None);
- assert_eq!(terminal.effective_custom_status().as_deref(), Some("fresh"));
+ assert_eq!(terminal.effective_title().as_deref(), Some("Fresh title"));
assert!(terminal
.expire_agent_metadata_at(
old_deadline + Duration::from_millis(1),
old_deadline + Duration::from_millis(1)
)
.is_none());
- assert_eq!(terminal.effective_custom_status().as_deref(), Some("fresh"));
+ assert_eq!(terminal.effective_title().as_deref(), Some("Fresh title"));
}
#[test]
@@ -940,12 +745,10 @@ mod tests {
agent_label: Some("claude".into()),
applies_to_source: Some("herdr:claude".into()),
title: Some("Prompt title".into()),
- display_agent: None,
- custom_status: Some("old".into()),
+ display_agent: Some("Old display".into()),
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::from_millis(1)),
seq: None,
@@ -958,11 +761,9 @@ mod tests {
applies_to_source: None,
title: None,
display_agent: None,
- custom_status: None,
state_labels: HashMap::new(),
clear_title: false,
- clear_display_agent: false,
- clear_custom_status: true,
+ clear_display_agent: true,
clear_state_labels: false,
ttl: None,
seq: None,
@@ -973,7 +774,7 @@ mod tests {
terminal.effective_presentation().title.as_deref(),
Some("Prompt title")
);
- assert_eq!(terminal.effective_custom_status(), None);
+ assert_eq!(terminal.effective_presentation().display_agent, None);
let mutation = terminal
.expire_agent_metadata_at(old_deadline, old_deadline)
@@ -996,13 +797,11 @@ mod tests {
source: "user:status".into(),
agent_label: Some("claude".into()),
applies_to_source: Some("herdr:claude".into()),
- title: None,
+ title: Some("Activity".into()),
display_agent: None,
- custom_status: Some("activity".into()),
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::from_millis(1)),
seq: None,
@@ -1017,11 +816,11 @@ mod tests {
assert_eq!(change.previous_state, AgentState::Working);
assert_eq!(change.state, AgentState::Working);
assert_eq!(
- change.previous_presentation.custom_status.as_deref(),
- Some("activity")
+ change.previous_presentation.title.as_deref(),
+ Some("Activity")
);
- assert_eq!(change.presentation.custom_status, None);
- assert_eq!(terminal.effective_custom_status(), None);
+ assert_eq!(change.presentation.title, None);
+ assert_eq!(terminal.effective_title(), None);
}
#[test]
@@ -1038,13 +837,11 @@ mod tests {
source: "user:status".into(),
agent_label: Some("claude".into()),
applies_to_source: Some("herdr:claude".into()),
- title: None,
+ title: Some("Stale".into()),
display_agent: None,
- custom_status: Some("stale".into()),
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::from_millis(1)),
seq: None,
@@ -1055,7 +852,7 @@ mod tests {
.and_then(|metadata| terminal.agent_metadata_expiry(metadata))
.unwrap();
assert_eq!(terminal.next_agent_metadata_expiry(), None);
- assert_eq!(terminal.effective_custom_status(), None);
+ assert_eq!(terminal.effective_title(), None);
terminal.set_hook_authority(
"herdr:claude".into(),
@@ -1071,7 +868,7 @@ mod tests {
assert!(mutation.is_none());
assert!(!terminal.agent_metadata.contains_key("user:status"));
- assert_eq!(terminal.effective_custom_status(), None);
+ assert_eq!(terminal.effective_title(), None);
}
#[test]
@@ -1090,11 +887,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: Some("First".into()),
display_agent: None,
- custom_status: None,
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::from_millis(1)),
seq: None,
@@ -1106,11 +901,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: None,
display_agent: Some("Second".into()),
- custom_status: None,
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::from_millis(2)),
seq: None,
@@ -1149,13 +942,11 @@ mod tests {
source: "user:status".into(),
agent_label: Some("claude".into()),
applies_to_source: Some("herdr:claude".into()),
- title: None,
+ title: Some("Instant".into()),
display_agent: None,
- custom_status: Some("instant".into()),
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::ZERO),
seq: None,
@@ -1170,11 +961,11 @@ mod tests {
let change = mutation.effective_state_change.unwrap();
assert_eq!(
- change.previous_presentation.custom_status.as_deref(),
- Some("instant")
+ change.previous_presentation.title.as_deref(),
+ Some("Instant")
);
- assert_eq!(change.presentation.custom_status, None);
- assert_eq!(terminal.effective_custom_status(), None);
+ assert_eq!(change.presentation.title, None);
+ assert_eq!(terminal.effective_title(), None);
}
#[test]
@@ -1191,13 +982,11 @@ mod tests {
source: "user:status".into(),
agent_label: Some("claude".into()),
applies_to_source: Some("herdr:claude".into()),
- title: None,
+ title: Some("Instant".into()),
display_agent: None,
- custom_status: Some("instant".into()),
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::ZERO),
seq: None,
@@ -1220,7 +1009,7 @@ mod tests {
);
assert_eq!(terminal.next_agent_metadata_expiry(), None);
- assert_eq!(terminal.effective_custom_status(), None);
+ assert_eq!(terminal.effective_title(), None);
}
#[test]
@@ -1239,11 +1028,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: Some("Expired title".into()),
display_agent: None,
- custom_status: None,
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: false,
clear_state_labels: false,
ttl: Some(Duration::ZERO),
seq: None,
@@ -1256,11 +1043,9 @@ mod tests {
applies_to_source: Some("herdr:claude".into()),
title: None,
display_agent: Some("Fresh display".into()),
- custom_status: None,
state_labels: HashMap::new(),
clear_title: false,
clear_display_agent: false,
- clear_custom_status: true,
clear_state_labels: false,
ttl: None,
seq: None,
diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs
index 03432a56..a67b7bae 100644
--- a/src/terminal/mod.rs
+++ b/src/terminal/mod.rs
@@ -2,6 +2,7 @@ mod id;
mod runtime;
mod runtime_registry;
pub mod state;
+mod title;
pub use id::TerminalId;
pub use runtime::TerminalRuntime;
@@ -10,3 +11,4 @@ pub use state::{
AgentMetadataReport, EffectivePresentation, EffectiveStateChange, TerminalState,
TerminalStateMutation,
};
+pub(crate) use title::stripped_terminal_title;
diff --git a/src/terminal/runtime.rs b/src/terminal/runtime.rs
index 1ff799b8..29d19140 100644
--- a/src/terminal/runtime.rs
+++ b/src/terminal/runtime.rs
@@ -307,6 +307,10 @@ impl TerminalRuntime {
self.0.detection_text()
}
+ pub fn terminal_title(&self) -> Option<String> {
+ self.0.terminal_title()
+ }
+
pub fn agent_osc_title(&self) -> String {
self.0.agent_osc_title()
}
diff --git a/src/terminal/state.rs b/src/terminal/state.rs
index 2b17404a..632558fd 100644
--- a/src/terminal/state.rs
+++ b/src/terminal/state.rs
@@ -22,7 +22,6 @@ pub struct HookAuthority {
pub agent_label: String,
pub state: AgentState,
pub message: Option<String>,
- pub custom_status: Option<String>,
pub reported_at: Instant,
pub session_ref: Option<crate::agent_resume::AgentSessionRef>,
}
@@ -59,6 +58,12 @@ pub struct EffectiveStateChange {
pub presentation: EffectivePresentation,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub(crate) struct TerminalTitleChange {
+ pub(crate) raw_changed: bool,
+ pub(crate) stripped_changed: bool,
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TerminalStateMutation {
pub effective_state_change: Option<EffectiveStateChange>,
@@ -79,13 +84,16 @@ pub struct TerminalState {
fallback_observed_at: Option<Instant>,
pub hook_authority: Option<HookAuthority>,
pub agent_metadata: HashMap<String, AgentMetadata>,
+ pub metadata_tokens: crate::metadata_tokens::MetadataTokens,
pub persisted_agent_session: Option<crate::agent_resume::PersistedAgentSession>,
+ pub terminal_title: Option<String>,
pub manual_label: Option<String>,
pub agent_name: Option<String>,
hook_report_sequences: HashMap<String, u64>,
suppressed_full_lifecycle_hook_reports: HashMap<String, SuppressedFullLifecycleHookReport>,
stale_full_lifecycle_hook_sessions: HashMap<String, Vec<StaleFullLifecycleHookSession>>,
metadata_report_sequences: HashMap<String, u64>,
+ metadata_token_sequence_sources: std::collections::HashSet<String>,
pub state: AgentState,
pub last_agent_state_change_seq: Option<u64>,
pub revision: u64,
@@ -106,13 +114,16 @@ impl TerminalState {
fallback_observed_at: None,
hook_authority: None,
agent_metadata: HashMap::new(),
+ metadata_tokens: crate::metadata_tokens::MetadataTokens::default(),
persisted_agent_session: None,
+ terminal_title: None,
manual_label: None,
agent_name: None,
hook_report_sequences: HashMap::new(),
suppressed_full_lifecycle_hook_reports: HashMap::new(),
stale_full_lifecycle_hook_sessions: HashMap::new(),
metadata_report_sequences: HashMap::new(),
+ metadata_token_sequence_sources: std::collections::HashSet::new(),
state: AgentState::Unknown,
last_agent_state_change_seq: None,
revision: 0,
@@ -123,6 +134,28 @@ impl TerminalState {
}
}
+ pub(crate) fn terminal_title_stripped(&self) -> Option<String> {
+ self.terminal_title
+ .as_deref()
+ .and_then(super::stripped_terminal_title)
+ }
+
+ pub(crate) fn set_terminal_title(&mut self, title: Option<String>) -> TerminalTitleChange {
+ if self.terminal_title == title {
+ return TerminalTitleChange::default();
+ }
+ let previous_stripped = self.terminal_title_stripped();
+ self.terminal_title = title;
+ let stripped_changed = previous_stripped != self.terminal_title_stripped();
+ if stripped_changed {
+ self.revision = self.revision.wrapping_add(1);
+ }
+ TerminalTitleChange {
+ raw_changed: true,
+ stripped_changed,
+ }
+ }
+
pub fn with_launch_argv(mut self, argv: Vec<String>) -> Self {
self.launch_argv = Some(argv);
self
@@ -331,25 +364,11 @@ impl TerminalState {
message: Option<String>,
seq: Option<u64>,
) -> Option<EffectiveStateChange> {
- self.set_hook_authority_with_custom_status(source, agent_label, state, message, None, seq)
- }
-
- #[cfg(test)]
- pub fn set_hook_authority_with_custom_status(
- &mut self,
- source: String,
- agent_label: String,
- state: AgentState,
- message: Option<String>,
- custom_status: Option<String>,
- seq: Option<u64>,
- ) -> Option<EffectiveStateChange> {
- self.set_hook_authority_with_custom_status_at(
+ self.set_hook_authority_at(
source,
agent_label,
state,
message,
- custom_status,
None,
seq,
Instant::now(),
@@ -363,29 +382,26 @@ impl TerminalState {
agent_label: String,
state: AgentState,
message: Option<String>,
- custom_status: Option<String>,
session_ref: Option<crate::agent_resume::AgentSessionRef>,
seq: Option<u64>,
) -> Option<TerminalStateMutation> {
- self.set_hook_authority_with_custom_status_at(
+ self.set_hook_authority_at(
source,
agent_label,
state,
message,
- custom_status,
session_ref,
seq,
Instant::now(),
)
}
- pub fn set_hook_authority_with_custom_status_at(
+ pub fn set_hook_authority_at(
&mut self,
source: String,
agent_label: String,
state: AgentState,
message: Option<String>,
- custom_status: Option<String>,
session_ref: Option<crate::agent_resume::AgentSessionRef>,
seq: Option<u64>,
now: Instant,
@@ -476,7 +492,6 @@ impl TerminalState {
agent_label,
state,
message,
- custom_status,
reported_at: now,
session_ref,
});
@@ -1524,7 +1539,6 @@ mod tests {
"pi".into(),
AgentState::Idle,
None,
- None,
crate::agent_resume::AgentSessionRef::path(old_session),
Some(10),
);
@@ -1548,7 +1562,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(new_session.clone()),
Some(12),
);
@@ -1576,7 +1589,6 @@ mod tests {
"pi".into(),
AgentState::Idle,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_a.clone()),
Some(10),
);
@@ -1593,7 +1605,6 @@ mod tests {
"pi".into(),
AgentState::Idle,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_b.clone()),
Some(12),
);
@@ -1610,7 +1621,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_a.clone()),
Some(14),
);
@@ -1628,7 +1638,6 @@ mod tests {
"pi".into(),
AgentState::Idle,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_b),
Some(15),
);
@@ -1681,7 +1690,6 @@ mod tests {
"pi".into(),
AgentState::Idle,
None,
- None,
crate::agent_resume::AgentSessionRef::path(old_session.clone()),
Some(10),
);
@@ -1698,7 +1706,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(new_session),
Some(12),
);
@@ -1725,7 +1732,6 @@ mod tests {
"omp".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(old_session.clone()),
Some(10),
);
@@ -1754,7 +1760,6 @@ mod tests {
"omp".into(),
AgentState::Blocked,
Some("waiting".into()),
- None,
crate::agent_resume::AgentSessionRef::path(new_session.clone()),
Some(12),
);
@@ -1771,7 +1776,6 @@ mod tests {
"omp".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(old_session),
Some(13),
);
@@ -1785,13 +1789,12 @@ mod tests {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
None,
- None,
Some(10),
now,
);
@@ -1813,13 +1816,12 @@ mod tests {
AgentState::Working
);
- let stale = terminal.set_hook_authority_with_custom_status_at(
+ let stale = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
None,
- None,
Some(9),
now + Duration::from_millis(2),
);
@@ -1833,13 +1835,12 @@ mod tests {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
None,
- None,
Some(20),
now,
);
@@ -1853,12 +1854,11 @@ mod tests {
true,
now + Duration::from_millis(1),
);
- let late = terminal.set_hook_authority_with_custom_status_at(
+ let late = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- Some("late".into()),
None,
Some(21),
now + Duration::from_millis(2),
@@ -1881,7 +1881,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_path.clone()),
Some(20),
);
@@ -1900,7 +1899,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- Some("late".into()),
crate::agent_resume::AgentSessionRef::path(session_path),
Some(21),
);
@@ -1946,7 +1944,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_path.clone()),
Some(20),
);
@@ -1957,7 +1954,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_path),
Some(22),
);
@@ -1976,7 +1972,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(test_session_path("old.jsonl")),
Some(20),
);
@@ -1987,7 +1982,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(test_session_path("new.jsonl")),
Some(22),
);
@@ -2006,7 +2000,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(test_session_path("old.jsonl")),
Some(1000),
);
@@ -2017,7 +2010,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(test_session_path("new.jsonl")),
Some(1500),
);
@@ -2039,7 +2031,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_a.clone()),
Some(1000),
);
@@ -2050,7 +2041,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_b),
Some(1500),
);
@@ -2062,7 +2052,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- Some("late".into()),
crate::agent_resume::AgentSessionRef::path(session_a),
Some(2500),
);
@@ -2071,7 +2060,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_c),
Some(2500),
);
@@ -2091,7 +2079,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(test_session_path("one.jsonl")),
Some(20),
);
@@ -2101,7 +2088,6 @@ mod tests {
"pi".into(),
AgentState::Idle,
None,
- None,
crate::agent_resume::AgentSessionRef::path(test_session_path("two.jsonl")),
Some(21),
);
@@ -2219,7 +2205,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(old_session.clone()),
Some(1000),
);
@@ -2259,7 +2244,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- Some("late".into()),
crate::agent_resume::AgentSessionRef::path(old_session),
Some(500),
);
@@ -2268,7 +2252,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(new_session),
Some(500),
);
@@ -2286,12 +2269,11 @@ mod tests {
let new_session = test_session_path("new-after-process-exit.jsonl");
let now = Instant::now();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(old_session),
Some(1000),
now,
@@ -2306,12 +2288,11 @@ mod tests {
now + Duration::from_millis(1),
);
- let early_new = terminal.set_hook_authority_with_custom_status_at(
+ let early_new = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(new_session.clone()),
Some(500),
now + Duration::from_millis(2),
@@ -2338,12 +2319,11 @@ mod tests {
false,
now + Duration::from_millis(4),
);
- let fresh_new = terminal.set_hook_authority_with_custom_status_at(
+ let fresh_new = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(new_session),
Some(500),
now + Duration::from_millis(5),
@@ -2360,12 +2340,11 @@ mod tests {
let old_session = test_session_path("old-before-nosession-process-exit.jsonl");
let now = Instant::now();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(old_session),
Some(1000),
now,
@@ -2380,13 +2359,12 @@ mod tests {
now + Duration::from_millis(1),
);
- let early_without_session = terminal.set_hook_authority_with_custom_status_at(
+ let early_without_session = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
None,
- None,
Some(500),
now + Duration::from_millis(2),
);
@@ -2412,13 +2390,12 @@ mod tests {
false,
now + Duration::from_millis(4),
);
- let fresh_without_session = terminal.set_hook_authority_with_custom_status_at(
+ let fresh_without_session = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
None,
- None,
Some(500),
now + Duration::from_millis(5),
);
@@ -2436,12 +2413,11 @@ mod tests {
let session_c = test_session_path("generation-c.jsonl");
let now = Instant::now();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_a.clone()),
Some(1000),
now,
@@ -2474,12 +2450,11 @@ mod tests {
false,
now + Duration::from_millis(3),
);
- let generation_b = terminal.set_hook_authority_with_custom_status_at(
+ let generation_b = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_b),
Some(500),
now + Duration::from_millis(4),
@@ -2514,22 +2489,20 @@ mod tests {
now + Duration::from_millis(7),
);
- let late_generation_a = terminal.set_hook_authority_with_custom_status_at(
+ let late_generation_a = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- Some("late".into()),
crate::agent_resume::AgentSessionRef::path(session_a),
Some(250),
now + Duration::from_millis(8),
);
- let generation_c = terminal.set_hook_authority_with_custom_status_at(
+ let generation_c = terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_c),
Some(250),
now + Duration::from_millis(9),
@@ -2596,7 +2569,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("fresh-session"),
Some(22),
);
@@ -2638,7 +2610,6 @@ mod tests {
"mastracode".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("mastracode-new"),
Some(20),
)
@@ -2669,7 +2640,6 @@ mod tests {
agent_label.into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id(&old_session),
Some(20),
);
@@ -2682,7 +2652,6 @@ mod tests {
agent_label.into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id(&old_session),
Some(22),
);
@@ -2695,7 +2664,6 @@ mod tests {
agent_label.into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id(&new_session),
Some(23),
);
@@ -2709,12 +2677,11 @@ mod tests {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Omp), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:omp".into(),
"omp".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("omp-old"),
Some(1000),
now,
@@ -2734,7 +2701,6 @@ mod tests {
"omp".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("omp-old"),
Some(500),
);
@@ -2764,7 +2730,6 @@ mod tests {
"omp".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("omp-new"),
Some(500),
);
@@ -2898,41 +2863,15 @@ mod tests {
}
#[test]
- fn visible_blocker_suppresses_stale_hook_custom_status() {
- let mut terminal = test_terminal();
- terminal.set_detected_state(Some(Agent::Codex), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status(
- "herdr:codex".into(),
- "codex".into(),
- AgentState::Working,
- None,
- Some("planning".into()),
- None,
- );
-
- terminal.set_detected_state_with_visible_blocker(
- Some(Agent::Codex),
- AgentState::Blocked,
- true,
- false,
- false,
- );
-
- assert_eq!(terminal.state, AgentState::Blocked);
- assert_eq!(terminal.effective_custom_status(), None);
- }
-
- #[test]
fn fallback_idle_does_not_override_hook_working() {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Claude), AgentState::Working);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:claude".into(),
"claude".into(),
AgentState::Working,
None,
- Some("thinking".into()),
None,
None,
now,
@@ -2950,10 +2889,6 @@ mod tests {
assert_eq!(terminal.fallback_state, AgentState::Idle);
assert_eq!(terminal.state, AgentState::Working);
- assert_eq!(
- terminal.effective_custom_status().as_deref(),
- Some("thinking")
- );
}
#[test]
@@ -2961,12 +2896,11 @@ mod tests {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::OpenCode), AgentState::Working);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:opencode".into(),
"opencode".into(),
AgentState::Working,
None,
- Some("thinking".into()),
None,
None,
now,
@@ -2983,10 +2917,6 @@ mod tests {
assert_eq!(terminal.fallback_state, AgentState::Working);
assert_eq!(terminal.state, AgentState::Working);
- assert_eq!(
- terminal.effective_custom_status().as_deref(),
- Some("thinking")
- );
}
#[test]
@@ -2994,14 +2924,13 @@ mod tests {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Claude), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:claude".into(),
"claude".into(),
AgentState::Idle,
None,
None,
None,
- None,
now,
);
@@ -3025,14 +2954,13 @@ mod tests {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:hermes".into(),
"hermes".into(),
AgentState::Idle,
None,
None,
None,
- None,
now,
);
@@ -3056,14 +2984,13 @@ mod tests {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Kilo), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:kilo".into(),
"kilo".into(),
AgentState::Idle,
None,
None,
None,
- None,
now,
);
@@ -3096,14 +3023,13 @@ mod tests {
now,
);
- let change = terminal.set_hook_authority_with_custom_status_at(
+ let change = terminal.set_hook_authority_at(
"herdr:claude".into(),
"claude".into(),
AgentState::Idle,
None,
None,
None,
- None,
now + Duration::from_millis(100),
);
@@ -3131,12 +3057,11 @@ mod tests {
false,
now,
);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:codex".into(),
"codex".into(),
AgentState::Blocked,
None,
- Some("permission".into()),
None,
None,
now + Duration::from_millis(1201),
@@ -3156,10 +3081,6 @@ mod tests {
assert_eq!(terminal.fallback_state, AgentState::Working);
assert_eq!(terminal.state, AgentState::Blocked);
- assert_eq!(
- terminal.effective_custom_status().as_deref(),
- Some("permission")
- );
assert!(change.effective_state_change.is_none());
}
@@ -3271,14 +3192,13 @@ mod tests {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:pi".into(),
"pi".into(),
AgentState::Working,
None,
None,
None,
- None,
now,
);
@@ -3379,13 +3299,12 @@ mod tests {
false,
observed,
);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:claude".into(),
"claude".into(),
AgentState::Working,
None,
None,
- None,
Some(1),
observed + Duration::from_secs(1),
);
@@ -3416,22 +3335,20 @@ mod tests {
false,
observed,
);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:codex".into(),
"codex".into(),
AgentState::Working,
None,
None,
- None,
Some(1),
observed,
);
- terminal.set_hook_authority_with_custom_status_at(
+ terminal.set_hook_authority_at(
"herdr:codex".into(),
"codex".into(),
AgentState::Working,
None,
- Some("new turn".into()),
None,
Some(2),
observed + Duration::from_secs(1),
@@ -3448,7 +3365,7 @@ mod tests {
);
let authority = terminal.hook_authority.as_ref().expect("hook authority");
- assert_eq!(authority.custom_status.as_deref(), Some("new turn"));
+ assert_eq!(authority.reported_at, observed + Duration::from_secs(1));
assert_eq!(terminal.state, AgentState::Working);
assert_eq!(terminal.effective_agent_label(), Some("codex"));
}
@@ -3530,7 +3447,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_path.clone()),
Some(20),
)
@@ -3560,7 +3476,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_path.clone()),
Some(20),
);
@@ -3570,7 +3485,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(new_session_path),
Some(19),
);
@@ -3595,7 +3509,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_path),
Some(20),
);
@@ -3607,7 +3520,6 @@ mod tests {
AgentState::Working,
None,
None,
- None,
Some(21),
)
.expect("accepted report");
@@ -3638,7 +3550,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(test_session_path("new.jsonl")),
Some(20),
)
@@ -4019,12 +3930,11 @@ mod tests {
let mut terminal = test_terminal();
let now = std::time::Instant::now();
terminal
- .set_hook_authority_with_custom_status_at(
+ .set_hook_authority_at(
"herdr:opencode".into(),
"opencode".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("opencode-session"),
Some(20),
now + Duration::from_millis(1),
@@ -4066,7 +3976,6 @@ mod tests {
"opencode".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("opencode-session"),
Some(22),
);
@@ -4078,7 +3987,6 @@ mod tests {
"opencode".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("opencode-new-session"),
Some(23),
);
@@ -4102,7 +4010,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path("/tmp/pi-session.jsonl"),
Some(21),
);
@@ -4152,7 +4059,6 @@ mod tests {
"opencode".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("opencode-session"),
Some(20),
)
@@ -4164,7 +4070,6 @@ mod tests {
"opencode".into(),
AgentState::Blocked,
Some("needs approval".into()),
- None,
crate::agent_resume::AgentSessionRef::id("nested-session"),
Some(21),
)
@@ -4224,7 +4129,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_path),
Some(20),
);
@@ -4246,7 +4150,6 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::path(session_path),
Some(20),
);
@@ -4419,7 +4322,6 @@ mod tests {
"claude".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("claude-session"),
Some(20),
);
@@ -4448,7 +4350,6 @@ mod tests {
"hermes".into(),
AgentState::Working,
None,
- None,
crate::agent_resume::AgentSessionRef::id("hermes-session"),
Some(20),
);
diff --git a/src/terminal/title.rs b/src/terminal/title.rs
new file mode 100644
index 00000000..32ba19c4
--- /dev/null
+++ b/src/terminal/title.rs
@@ -0,0 +1,64 @@
+const CLAUDE_ACTIVITY_GLYPHS: &str = "·✢✳✶✻✽";
+
+pub(crate) fn stripped_terminal_title(title: &str) -> Option<String> {
+ let title = title.trim();
+ if title.is_empty() {
+ return None;
+ }
+
+ let mut chars = title.char_indices();
+ let (_, first) = chars.next()?;
+ let after_first = &title[first.len_utf8()..];
+ let recognized =
+ matches!(first, '\u{2800}'..='\u{28ff}') || CLAUDE_ACTIVITY_GLYPHS.contains(first);
+ let stripped = if recognized
+ && (after_first.is_empty() || after_first.chars().next().is_some_and(char::is_whitespace))
+ {
+ after_first.trim()
+ } else {
+ title
+ };
+
+ (!stripped.is_empty()).then(|| stripped.to_string())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::stripped_terminal_title;
+
+ #[test]
+ fn strips_one_recognized_leading_activity_glyph() {
+ for title in ["⠋ task", "✳ task", " ⠙ task ", "✢ task", "✻ task"] {
+ assert_eq!(stripped_terminal_title(title).as_deref(), Some("task"));
+ }
+ assert_eq!(
+ stripped_terminal_title("⠋ ⠙ task").as_deref(),
+ Some("⠙ task")
+ );
+ }
+
+ #[test]
+ fn preserves_unrecognized_or_unbounded_symbols() {
+ for (title, expected) in [
+ ("★task", "★task"),
+ ("★ production", "★ production"),
+ ("✨ task", "✨ task"),
+ ("☼ status", "☼ status"),
+ ("@ task", "@ task"),
+ ("task ⠋ detail", "task ⠋ detail"),
+ ("[prod] task", "[prod] task"),
+ ] {
+ assert_eq!(stripped_terminal_title(title).as_deref(), Some(expected));
+ }
+ }
+
+ #[test]
+ fn preserves_unicode_text_and_elides_empty_results() {
+ assert_eq!(
+ stripped_terminal_title(" ⠋ 修复🙂标题 ").as_deref(),
+ Some("修复🙂标题")
+ );
+ assert_eq!(stripped_terminal_title(" "), None);
+ assert_eq!(stripped_terminal_title("⠋ "), None);
+ }
+}