diff --git a/crates/weft-appd/src/ipc.rs b/crates/weft-appd/src/ipc.rs index 215a7dd..b892841 100644 --- a/crates/weft-appd/src/ipc.rs +++ b/crates/weft-appd/src/ipc.rs @@ -10,6 +10,12 @@ pub enum Request { QueryAppState { session_id: u64 }, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionInfo { + pub session_id: u64, + pub app_id: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] pub enum Response { @@ -20,7 +26,7 @@ pub enum Response { session_id: u64, }, RunningApps { - session_ids: Vec, + sessions: Vec, }, AppState { session_id: u64, @@ -101,11 +107,21 @@ mod tests { } } + #[test] + fn session_info_roundtrip() { + let info = super::SessionInfo { + session_id: 3, + app_id: "com.example.app".into(), + }; + let bytes = rmp_serde::to_vec(&info).unwrap(); + let decoded: super::SessionInfo = rmp_serde::from_slice(&bytes).unwrap(); + assert_eq!(decoded.session_id, 3); + assert_eq!(decoded.app_id, "com.example.app"); + } + #[tokio::test] async fn frame_write_read_roundtrip() { - let resp = Response::RunningApps { - session_ids: vec![1, 2, 3], - }; + let resp = Response::RunningApps { sessions: vec![] }; let mut buf: Vec = Vec::new(); write_frame(&mut buf, &resp).await.unwrap(); diff --git a/crates/weft-appd/src/main.rs b/crates/weft-appd/src/main.rs index 3928f95..eb0ed4f 100644 --- a/crates/weft-appd/src/main.rs +++ b/crates/weft-appd/src/main.rs @@ -9,13 +9,18 @@ mod ipc; mod runtime; mod ws; -use ipc::{AppStateKind, Request, Response}; +use ipc::{AppStateKind, Request, Response, SessionInfo}; pub(crate) type Registry = Arc>; +struct SessionEntry { + app_id: String, + state: AppStateKind, +} + struct SessionRegistry { next_id: u64, - sessions: std::collections::HashMap, + sessions: std::collections::HashMap, broadcast: tokio::sync::broadcast::Sender, abort_senders: std::collections::HashMap>, } @@ -33,10 +38,16 @@ impl Default for SessionRegistry { } impl SessionRegistry { - fn launch(&mut self, _app_id: &str) -> u64 { + fn launch(&mut self, app_id: &str) -> u64 { self.next_id += 1; let id = self.next_id; - self.sessions.insert(id, AppStateKind::Starting); + self.sessions.insert( + id, + SessionEntry { + app_id: app_id.to_owned(), + state: AppStateKind::Starting, + }, + ); id } @@ -52,20 +63,26 @@ impl SessionRegistry { rx } - fn running_ids(&self) -> Vec { - self.sessions.keys().copied().collect() + fn running_sessions(&self) -> Vec { + self.sessions + .iter() + .map(|(&session_id, e)| SessionInfo { + session_id, + app_id: e.app_id.clone(), + }) + .collect() } fn state(&self, session_id: u64) -> AppStateKind { self.sessions .get(&session_id) - .cloned() + .map(|e| e.state.clone()) .unwrap_or(AppStateKind::NotFound) } pub(crate) fn set_state(&mut self, session_id: u64, state: AppStateKind) { if let Some(entry) = self.sessions.get_mut(&session_id) { - *entry = state; + entry.state = state; } } @@ -216,8 +233,8 @@ pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response { } } Request::QueryRunning => { - let session_ids = registry.lock().await.running_ids(); - Response::RunningApps { session_ids } + let sessions = registry.lock().await.running_sessions(); + Response::RunningApps { sessions } } Request::QueryAppState { session_id } => { let state = registry.lock().await.state(session_id); @@ -315,7 +332,12 @@ mod tests { .await; let resp = dispatch(Request::QueryRunning, ®).await; match resp { - Response::RunningApps { session_ids } => assert_eq!(session_ids.len(), 2), + Response::RunningApps { sessions } => { + assert_eq!(sessions.len(), 2); + let mut ids: Vec<&str> = sessions.iter().map(|s| s.app_id.as_str()).collect(); + ids.sort(); + assert_eq!(ids, vec!["a", "b"]); + } _ => panic!("expected RunningApps"), } } @@ -368,15 +390,20 @@ mod tests { } #[test] - fn registry_running_ids_reflects_live_sessions() { + fn registry_running_sessions_reflects_live_sessions() { let mut reg = SessionRegistry::default(); - let id1 = reg.launch("a"); - let id2 = reg.launch("b"); - let mut ids = reg.running_ids(); - ids.sort(); - assert_eq!(ids, vec![id1, id2]); + let id1 = reg.launch("com.example.a"); + let id2 = reg.launch("com.example.b"); + let mut sessions = reg.running_sessions(); + sessions.sort_by_key(|s| s.session_id); + assert_eq!(sessions.len(), 2); + assert_eq!(sessions[0].session_id, id1); + assert_eq!(sessions[0].app_id, "com.example.a"); + assert_eq!(sessions[1].session_id, id2); + assert_eq!(sessions[1].app_id, "com.example.b"); reg.terminate(id1); - assert_eq!(reg.running_ids(), vec![id2]); + assert_eq!(reg.running_sessions().len(), 1); + assert_eq!(reg.running_sessions()[0].session_id, id2); } #[test] diff --git a/infra/shell/system-ui.html b/infra/shell/system-ui.html index 322bc49..4093ef0 100644 --- a/infra/shell/system-ui.html +++ b/infra/shell/system-ui.html @@ -208,23 +208,25 @@ removeTaskbarEntry(msg.session_id); } } else if (msg.type === 'RUNNING_APPS') { - msg.session_ids.forEach(function (id) { - ensureTaskbarEntry(id); + msg.sessions.forEach(function (s) { + ensureTaskbarEntry(s.session_id, s.app_id); }); } else if (msg.type === 'LAUNCH_ACK') { - ensureTaskbarEntry(msg.session_id); + ensureTaskbarEntry(msg.session_id, null); } } - function ensureTaskbarEntry(sessionId) { + function ensureTaskbarEntry(sessionId, appId) { var id = 'task-' + sessionId; if (document.getElementById(id)) { return; } + var label = appId ? appId.split('.').pop() : String(sessionId); var el = document.createElement('weft-taskbar-app'); el.id = id; el.dataset.sessionId = sessionId; - el.textContent = '● ' + sessionId; + if (appId) { el.dataset.appId = appId; } + el.textContent = '● ' + label; el.style.cssText = 'font-size:12px;color:var(--text-secondary);padding:0 6px;cursor:pointer;'; - el.title = 'Session ' + sessionId; + el.title = appId ? appId + ' (session ' + sessionId + ')' : 'Session ' + sessionId; var taskbar = document.querySelector('weft-taskbar'); var clock = document.getElementById('clock'); taskbar.insertBefore(el, clock);