agentmux_srv/server/service/
introspect.rs1use serde_json::json;
9
10use crate::backend::obj::*;
11use crate::backend::storage::store::Store;
12
13#[derive(Debug, serde::Serialize)]
16pub(crate) struct AgentContext {
17 pub block_id: String,
18 pub block_title: String,
19 pub tab_id: String,
20 pub tab_name: String,
21 pub window_id: Option<String>,
22 pub window_name: String,
23 pub workspace_id: Option<String>,
24 pub workspace_name: String,
25}
26
27pub(crate) fn workspace_id_for_tab(store: &Store, tab_id: &str) -> Option<String> {
30 store
31 .get_all::<Workspace>()
32 .unwrap_or_default()
33 .into_iter()
34 .find(|w| {
35 w.tabids.iter().any(|t| t == tab_id) || w.pinnedtabids.iter().any(|t| t == tab_id)
36 })
37 .map(|w| w.oid)
38}
39
40pub(crate) fn agent_layout(store: &Store) -> serde_json::Value {
44 let windows = store.get_all::<Window>().unwrap_or_default();
45 let workspaces = store.get_all::<Workspace>().unwrap_or_default();
46 let tabs = store.get_all::<Tab>().unwrap_or_default();
47 let blocks = store.get_all::<Block>().unwrap_or_default();
48
49 let panes_of = |tab: &Tab| -> Vec<serde_json::Value> {
50 tab.blockids
51 .iter()
52 .filter_map(|bid| blocks.iter().find(|b| &b.oid == bid))
53 .map(|b| {
54 json!({
55 "block_id": b.oid,
56 "view": meta_get_string(&b.meta, "view", ""),
57 "title": meta_get_string(&b.meta, "frame:title", ""),
58 })
59 })
60 .collect()
61 };
62
63 let windows_json: Vec<serde_json::Value> = windows
64 .iter()
65 .map(|w| {
66 let ws = workspaces.iter().find(|x| x.oid == w.workspaceid);
67 let tabs_json: Vec<serde_json::Value> = ws
68 .map(|ws| {
69 ws.pinnedtabids
71 .iter()
72 .chain(ws.tabids.iter())
73 .filter_map(|tid| tabs.iter().find(|t| &t.oid == tid))
74 .map(|t| {
75 json!({
76 "tab_id": t.oid,
77 "name": t.name,
78 "active": ws.activetabid == t.oid,
79 "panes": panes_of(t),
80 })
81 })
82 .collect()
83 })
84 .unwrap_or_default();
85 json!({
86 "window_id": w.oid,
87 "name": meta_get_string(&w.meta, "window:displayname", ""),
88 "workspace_id": w.workspaceid,
89 "workspace_name": ws.map(|x| x.name.clone()).unwrap_or_default(),
90 "tabs": tabs_json,
91 })
92 })
93 .collect();
94
95 json!({ "windows": windows_json })
96}
97
98pub(crate) fn agent_windows(store: &Store) -> serde_json::Value {
100 let workspaces = store.get_all::<Workspace>().unwrap_or_default();
101 let windows: Vec<serde_json::Value> = store
102 .get_all::<Window>()
103 .unwrap_or_default()
104 .iter()
105 .map(|w| {
106 let ws_name = workspaces
107 .iter()
108 .find(|x| x.oid == w.workspaceid)
109 .map(|x| x.name.clone())
110 .unwrap_or_default();
111 json!({
112 "window_id": w.oid,
113 "name": meta_get_string(&w.meta, "window:displayname", ""),
114 "workspace_id": w.workspaceid,
115 "workspace_name": ws_name,
116 })
117 })
118 .collect();
119 json!({ "windows": windows })
120}
121
122pub(crate) fn agent_workspaces(store: &Store) -> serde_json::Value {
124 let workspaces: Vec<serde_json::Value> = store
125 .get_all::<Workspace>()
126 .unwrap_or_default()
127 .iter()
128 .map(|w| {
129 json!({
130 "workspace_id": w.oid,
131 "name": w.name,
132 "tab_count": w.tabids.len() + w.pinnedtabids.len(),
133 "active_tab_id": w.activetabid,
134 })
135 })
136 .collect();
137 json!({ "workspaces": workspaces })
138}
139
140pub(crate) fn agent_tabs(store: &Store, workspace_id: Option<&str>) -> serde_json::Value {
143 let workspaces = store.get_all::<Workspace>().unwrap_or_default();
144 let scope: Option<Vec<String>> = workspace_id.map(|wid| {
145 workspaces
146 .iter()
147 .find(|w| w.oid == wid)
148 .map(|w| w.pinnedtabids.iter().chain(w.tabids.iter()).cloned().collect())
149 .unwrap_or_default()
150 });
151 let tabs: Vec<serde_json::Value> = store
152 .get_all::<Tab>()
153 .unwrap_or_default()
154 .iter()
155 .filter(|t| scope.as_ref().map(|ids| ids.contains(&t.oid)).unwrap_or(true))
156 .map(|t| {
157 json!({
158 "tab_id": t.oid,
159 "name": t.name,
160 "pane_count": t.blockids.len(),
161 })
162 })
163 .collect();
164 json!({ "tabs": tabs })
165}
166
167pub(crate) fn resolve_agent_context(store: &Store, block_id: &str) -> Result<AgentContext, String> {
175 let block = store
176 .get::<Block>(block_id)
177 .map_err(|e| e.to_string())?
178 .ok_or_else(|| format!("block not found: {block_id}"))?;
179 let block_title = meta_get_string(&block.meta, "frame:title", "");
180 let tab_id = block.parentoref.strip_prefix("tab:").unwrap_or("").to_string();
181 let tab = store
182 .get::<Tab>(&tab_id)
183 .map_err(|e| e.to_string())?
184 .ok_or_else(|| format!("tab not found for block {block_id}"))?;
185
186 let mut workspace_id = None;
187 let mut workspace_name = String::new();
188 let mut window_id = None;
189 let mut window_name = String::new();
190
191 if let Ok(workspaces) = store.get_all::<Workspace>() {
192 if let Some(ws) = workspaces
193 .into_iter()
194 .find(|w| w.tabids.contains(&tab_id) || w.pinnedtabids.contains(&tab_id))
195 {
196 workspace_name = ws.name.clone();
197 workspace_id = Some(ws.oid.clone());
198 if let Ok(windows) = store.get_all::<Window>() {
199 if let Some(win) = windows.into_iter().find(|w| w.workspaceid == ws.oid) {
200 window_name = meta_get_string(&win.meta, "window:displayname", "");
201 window_id = Some(win.oid);
202 }
203 }
204 }
205 }
206
207 Ok(AgentContext {
208 block_id: block_id.to_string(),
209 block_title,
210 tab_id,
211 tab_name: tab.name,
212 window_id,
213 window_name,
214 workspace_id,
215 workspace_name,
216 })
217}
218
219#[cfg(test)]
220mod agent_context_tests {
221 use super::{resolve_agent_context, workspace_id_for_tab};
222 use crate::backend::obj::Tab;
223 use crate::backend::storage::store::Store;
224 use crate::backend::wcore;
225
226 #[test]
227 fn workspace_id_for_tab_finds_owner_and_misses_cleanly() {
228 let store = Store::open_in_memory().unwrap();
229 wcore::ensure_initial_data(&store).unwrap();
230 let tab = store
231 .get_all::<Tab>()
232 .unwrap()
233 .into_iter()
234 .next()
235 .expect("seeded tab");
236 assert!(workspace_id_for_tab(&store, &tab.oid).is_some(), "owner found");
237 assert!(workspace_id_for_tab(&store, "nope").is_none(), "miss is None");
238 }
239
240 #[test]
244 fn resolves_block_to_tab_workspace_and_window() {
245 let store = Store::open_in_memory().unwrap();
246 wcore::ensure_initial_data(&store).unwrap();
247
248 let tab = store
249 .get_all::<Tab>()
250 .unwrap()
251 .into_iter()
252 .next()
253 .expect("seeded tab");
254 let block_id = tab.blockids.first().expect("seeded agent block").clone();
255
256 let ctx = resolve_agent_context(&store, &block_id).expect("resolves context");
257 assert_eq!(ctx.tab_id, tab.oid);
258 assert_eq!(ctx.workspace_name, "Starter workspace");
259 assert!(ctx.workspace_id.is_some(), "workspace should resolve");
260 assert!(ctx.window_id.is_some(), "window should resolve via reverse lookup");
261 }
262
263 #[test]
264 fn unknown_block_errors() {
265 let store = Store::open_in_memory().unwrap();
266 wcore::ensure_initial_data(&store).unwrap();
267 let err = resolve_agent_context(&store, "does-not-exist").unwrap_err();
268 assert!(err.contains("block not found"), "unexpected error: {err}");
269 }
270}