1use serde::Serialize;
15
16use crate::backend::blockcontroller::META_KEY_SHELL_SUBBLOCK_ID;
17use crate::backend::obj::{self, Block, Tab, Window, Workspace};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "lowercase")]
22pub enum PaneKind {
23 Agent,
25 Terminal,
27 Other,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
34pub struct PaneEntry {
35 pub block_id: String,
36 pub view: String,
38 pub kind: PaneKind,
39 pub label: String,
40 pub window_id: String,
41 pub window: String,
42 pub tab_id: String,
43 pub tab: String,
44 pub tab_active: bool,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub agent: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub running: Option<bool>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub parent_block_id: Option<String>,
54}
55
56pub struct PaneFacts<'a> {
58 pub label: &'a dyn Fn(&Block) -> (String, bool),
62 pub agent_name: &'a dyn Fn(&str) -> Option<String>,
64 pub running: &'a dyn Fn(&str) -> Option<bool>,
66 pub hidden: &'a dyn Fn(&str) -> bool,
68}
69
70pub fn list(windows: &[Window], workspaces: &[Workspace], tabs: &[Tab], blocks: &[Block], facts: &PaneFacts) -> Vec<PaneEntry> {
73 let block = |id: &str| blocks.iter().find(|b| b.oid == id);
74 let mut out = Vec::new();
75 for window in windows {
76 let Some(ws) = workspaces.iter().find(|w| w.oid == window.workspaceid) else { continue };
77 let window_name = obj::meta_get_string(&window.meta, "window:displayname", "");
78 for tab in ws.pinnedtabids.iter().chain(ws.tabids.iter()).filter_map(|id| tabs.iter().find(|t| &t.oid == id)) {
79 let entry = |b: &Block, kind: PaneKind, label: String| PaneEntry {
80 block_id: b.oid.clone(),
81 view: obj::meta_get_string(&b.meta, "view", ""),
82 kind,
83 label,
84 window_id: window.oid.clone(),
85 window: window_name.clone(),
86 tab_id: tab.oid.clone(),
87 tab: tab.name.clone(),
88 tab_active: ws.activetabid == tab.oid,
89 agent: None,
90 running: None,
91 parent_block_id: None,
92 };
93 for b in tab.blockids.iter().filter_map(|id| block(id)) {
94 let (label, agent) = (facts.label)(b);
95 let view = obj::meta_get_string(&b.meta, "view", "");
96 if agent || view == "agent" {
97 if (facts.hidden)(&b.oid) {
98 continue;
99 }
100 let label = if agent { label } else { obj::meta_get_string(&b.meta, "frame:title", "") };
104 out.push(PaneEntry { agent: (facts.agent_name)(&b.oid), ..entry(b, PaneKind::Agent, label.clone()) });
105 let drawer = obj::meta_get_string(&b.meta, META_KEY_SHELL_SUBBLOCK_ID, "");
106 if let Some(shell) = block(&drawer).filter(|_| !drawer.is_empty()) {
107 let shell_label = if label.is_empty() { "Agent shell".to_string() } else { format!("{label} shell") };
108 out.push(PaneEntry {
109 running: (facts.running)(&shell.oid),
110 parent_block_id: Some(b.oid.clone()),
111 ..entry(shell, PaneKind::Terminal, shell_label)
112 });
113 }
114 } else if view == "term" {
115 out.push(PaneEntry { running: (facts.running)(&b.oid), ..entry(b, PaneKind::Terminal, label) });
116 } else {
117 out.push(entry(b, PaneKind::Other, obj::meta_get_string(&b.meta, "frame:title", "")));
120 }
121 }
122 }
123 }
124 out
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn block(id: &str, view: &str, extra: &[(&str, &str)]) -> Block {
132 let mut b = Block { oid: id.into(), ..Default::default() };
133 b.meta.insert("view".into(), serde_json::json!(view));
134 for (k, v) in extra {
135 b.meta.insert((*k).into(), serde_json::json!(v));
136 }
137 b
138 }
139
140 fn tab(id: &str, name: &str, blocks: &[&str]) -> Tab {
141 Tab { oid: id.into(), name: name.into(), blockids: blocks.iter().map(|s| s.to_string()).collect(), ..Default::default() }
142 }
143
144 fn label(b: &Block) -> (String, bool) {
145 let title = obj::meta_get_string(&b.meta, "frame:title", "");
146 let agent = obj::meta_get_string(&b.meta, "agentName", "");
147 if !agent.is_empty() {
148 (agent, true)
149 } else {
150 (if title.is_empty() { "Terminal".into() } else { title }, false)
151 }
152 }
153
154 fn tree() -> (Vec<Window>, Vec<Workspace>, Vec<Tab>, Vec<Block>) {
155 let mut window = Window { oid: "w1".into(), workspaceid: "ws1".into(), ..Default::default() };
156 window.meta.insert("window:displayname".into(), serde_json::json!("Main"));
157 let ws = Workspace {
158 oid: "ws1".into(),
159 tabids: vec!["t2".into()],
160 pinnedtabids: vec!["t1".into()],
161 activetabid: "t2".into(),
162 ..Default::default()
163 };
164 let tabs = vec![tab("t1", "Pinned", &["ed", "wid"]), tab("t2", "Work", &["ag", "hid", "sh", "gone"])];
165 let blocks = vec![
166 block("ed", "editor", &[("frame:title", "notes.md")]),
167 block("wid", "ext:acme.board", &[]),
168 block("ag", "agent", &[("agentName", "Ada"), (META_KEY_SHELL_SUBBLOCK_ID, "drawer")]),
169 block("drawer", "term", &[]),
170 block("hid", "agent", &[("agentName", "Secret"), (META_KEY_SHELL_SUBBLOCK_ID, "hid-drawer")]),
171 block("hid-drawer", "term", &[]),
172 block("sh", "term", &[("frame:title", "build")]),
173 ];
174 (vec![window], vec![ws], tabs, blocks)
175 }
176
177 #[test]
178 fn every_pane_in_order_with_drawers_after_their_agents_and_hidden_agents_left_out() {
179 let (windows, workspaces, tabs, blocks) = tree();
180 let facts = PaneFacts {
181 label: &label,
182 agent_name: &|id| (id == "ag").then(|| "ada".to_string()),
183 running: &|id| match id {
184 "drawer" => Some(true),
185 "sh" => Some(false),
186 _ => None,
187 },
188 hidden: &|id| id == "hid",
189 };
190 let panes = list(&windows, &workspaces, &tabs, &blocks, &facts);
191 let ids: Vec<&str> = panes.iter().map(|p| p.block_id.as_str()).collect();
192 assert_eq!(ids, ["ed", "wid", "ag", "drawer", "sh"], "pinned tab first; a missing block is skipped");
193
194 assert_eq!(panes[0].kind, PaneKind::Other);
195 assert_eq!((panes[0].label.as_str(), panes[0].tab.as_str(), panes[0].tab_active), ("notes.md", "Pinned", false));
196 assert_eq!(panes[0].window, "Main");
197
198 assert_eq!((panes[1].kind, panes[1].label.as_str()), (PaneKind::Other, ""), "an untitled widget isn't called Terminal");
199
200 assert_eq!(panes[2].kind, PaneKind::Agent);
201 assert_eq!(panes[2].agent.as_deref(), Some("ada"));
202 assert!(panes[2].tab_active);
203
204 assert_eq!(panes[3].kind, PaneKind::Terminal);
205 assert_eq!(panes[3].label, "Ada shell");
206 assert_eq!(panes[3].parent_block_id.as_deref(), Some("ag"));
207 assert_eq!(panes[3].running, Some(true));
208 assert_eq!(panes[3].tab_id, "t2", "the drawer is shown where its agent is");
209
210 assert_eq!((panes[4].kind, panes[4].running), (PaneKind::Terminal, Some(false)));
211 assert!(!ids.contains(&"hid-drawer"), "a hidden agent's shell is hidden with it");
212 }
213
214 #[test]
215 fn the_wire_omits_what_a_pane_does_not_have() {
216 let (windows, workspaces, tabs, blocks) = tree();
217 let facts = PaneFacts { label: &label, agent_name: &|_| None, running: &|_| None, hidden: &|_| false };
218 let panes = list(&windows, &workspaces, &tabs, &blocks, &facts);
219 let editor = serde_json::to_value(&panes[0]).unwrap();
220 assert_eq!(editor["kind"], "other");
221 assert_eq!(editor["view"], "editor");
222 for absent in ["agent", "running", "parent_block_id"] {
223 assert!(editor.get(absent).is_none(), "{absent}");
224 }
225 let agent = serde_json::to_value(panes.iter().find(|p| p.block_id == "ag").unwrap()).unwrap();
226 assert_eq!(agent["kind"], "agent");
227 assert!(agent.get("agent").is_none(), "an agent that is not registered has no feed name");
228 }
229
230 #[test]
231 fn an_agent_pane_with_no_agent_yet_is_not_called_terminal() {
232 let mut window = Window { oid: "w".into(), workspaceid: "ws".into(), ..Default::default() };
233 window.meta.insert("window:displayname".into(), serde_json::json!(""));
234 let ws = Workspace { oid: "ws".into(), tabids: vec!["t".into()], activetabid: "t".into(), ..Default::default() };
235 let tabs = vec![tab("t", "Tab 1", &["empty"])];
236 let blocks = vec![block("empty", "agent", &[(META_KEY_SHELL_SUBBLOCK_ID, "sh")]), block("sh", "term", &[])];
239 let facts = PaneFacts {
240 label: &|_| ("Terminal".to_string(), false),
241 agent_name: &|_| None,
242 running: &|_| None,
243 hidden: &|_| false,
244 };
245 let panes = list(&[window], &[ws], &tabs, &blocks, &facts);
246 assert_eq!((panes[0].kind, panes[0].label.as_str()), (PaneKind::Agent, ""), "the device names it Agent");
247 assert_eq!(panes[1].label, "Agent shell");
248 }
249}