agentmux_srv/backend/
agent_line.rs1use std::collections::BTreeMap;
18
19use crate::backend::obj::{self, MetaMapType};
20use crate::backend::storage::store::Store;
21use crate::backend::wconfig::SettingsType;
22
23pub const META_OSC_TITLE: &str = "term:osc_title";
25
26pub const MAX_LINE_CHARS: usize = 200;
28
29pub fn sharing_on(settings: &SettingsType) -> bool {
32 settings.extra.get("swarm:sharesummaries").and_then(|v| v.as_bool()).unwrap_or(true)
33}
34
35pub fn clean_line(text: &str) -> Option<String> {
38 let folded = text
39 .split(|c: char| c.is_whitespace() || c.is_control())
40 .filter(|w| !w.is_empty())
41 .collect::<Vec<_>>()
42 .join(" ");
43 let line: String = folded.chars().take(MAX_LINE_CHARS).collect();
44 (!line.is_empty()).then_some(line)
45}
46
47pub fn line_from_meta(meta: &MetaMapType) -> Option<String> {
49 let title = obj::meta_get_string(meta, crate::ambient::title::META_TITLE, "");
50 if crate::ambient::validate::is_usable_title(&title) {
51 if let Some(line) = clean_line(&title) {
52 return Some(line);
53 }
54 }
55 clean_line(&obj::meta_get_string(meta, META_OSC_TITLE, ""))
56}
57
58pub fn agent_line_of_block(mstore: &Store, block_id: &str) -> Option<String> {
60 let block = mstore.get::<obj::Block>(block_id).ok()??;
61 line_from_meta(&block.meta)
62}
63
64pub fn lines_by_name<'a>(
68 mstore: &Store,
69 settings: &SettingsType,
70 agents: impl IntoIterator<Item = (&'a str, &'a str)>,
71) -> Option<BTreeMap<&'a str, String>> {
72 sharing_on(settings).then(|| {
73 agents
74 .into_iter()
75 .filter_map(|(name, block_id)| Some((name, agent_line_of_block(mstore, block_id)?)))
76 .collect()
77 })
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 fn meta(pairs: &[(&str, &str)]) -> MetaMapType {
85 pairs.iter().map(|(k, v)| (k.to_string(), serde_json::json!(v))).collect()
86 }
87
88 #[test]
89 fn the_generated_title_wins_over_the_osc_title() {
90 let m = meta(&[
91 (crate::ambient::title::META_TITLE, "Fixing the login race"),
92 (META_OSC_TITLE, "claude"),
93 ]);
94 assert_eq!(line_from_meta(&m).as_deref(), Some("Fixing the login race"));
95 }
96
97 #[test]
98 fn an_unusable_title_falls_back_to_the_osc_title_and_then_to_none() {
99 let m = meta(&[(crate::ambient::title::META_TITLE, "(none yet)"), (META_OSC_TITLE, " Build ")]);
100 assert_eq!(line_from_meta(&m).as_deref(), Some("Build"));
101 assert_eq!(line_from_meta(&meta(&[(crate::ambient::title::META_TITLE, "none")])), None);
102 assert_eq!(line_from_meta(&MetaMapType::default()), None);
103 }
104
105 #[test]
106 fn a_line_is_one_bounded_line() {
107 assert_eq!(clean_line(" two\nlines\t\u{7}here ").as_deref(), Some("two lines here"));
108 assert_eq!(clean_line(" \n\t "), None);
109 let long = "é".repeat(MAX_LINE_CHARS + 50);
110 assert_eq!(clean_line(&long).unwrap().chars().count(), MAX_LINE_CHARS);
111 }
112
113 #[test]
114 fn sharing_is_on_unless_turned_off() {
115 let read = |v: serde_json::Value| sharing_on(&serde_json::from_value(v).unwrap());
116 assert!(read(serde_json::json!({})));
117 assert!(!read(serde_json::json!({ "swarm:sharesummaries": false })));
118 assert!(read(serde_json::json!({ "swarm:sharesummaries": "no" })), "not a boolean: the default");
119 }
120}