feat(appd): include app_id in RunningApps response; update system UI

ipc.rs:
- Add SessionInfo { session_id: u64, app_id: String } struct.
- Change RunningApps { session_ids: Vec<u64> } to
  RunningApps { sessions: Vec<SessionInfo> } so callers can display
  meaningful app names without a follow-up QueryAppState round-trip.
- Add session_info_roundtrip test.

main.rs:
- Add SessionEntry { app_id: String, state: AppStateKind } to store
  app_id alongside state in SessionRegistry.
- launch() stores app_id in the entry.
- running_sessions() replaces running_ids(); returns Vec<SessionInfo>.
- state() reads from SessionEntry.state.
- set_state() writes to SessionEntry.state.
- QueryRunning dispatch uses running_sessions().
- Test registry_running_ids_reflects_live_sessions renamed to
  registry_running_sessions_reflects_live_sessions and updated to
  assert both session_id and app_id fields.
- dispatch_query_running test asserts app_id values are present.

system-ui.html:
- RUNNING_APPS handler uses msg.sessions[].{session_id,app_id}.
- ensureTaskbarEntry(sessionId, appId): shows the last component of the
  reverse-domain app ID as the taskbar label; sets data-app-id attribute;
  tooltip shows full app ID and session number.
- LAUNCH_ACK handler passes null for appId (session ID only available
  at launch time; app_id arrives in RUNNING_APPS on reconnect).
This commit is contained in:
Marco Allegretti 2026-03-11 10:42:40 +01:00
parent d6ede23183
commit fdeb440766
3 changed files with 73 additions and 28 deletions

View file

@ -10,6 +10,12 @@ pub enum Request {
QueryAppState { session_id: u64 }, 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] #[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Response { pub enum Response {
@ -20,7 +26,7 @@ pub enum Response {
session_id: u64, session_id: u64,
}, },
RunningApps { RunningApps {
session_ids: Vec<u64>, sessions: Vec<SessionInfo>,
}, },
AppState { AppState {
session_id: u64, 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] #[tokio::test]
async fn frame_write_read_roundtrip() { async fn frame_write_read_roundtrip() {
let resp = Response::RunningApps { let resp = Response::RunningApps { sessions: vec![] };
session_ids: vec![1, 2, 3],
};
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
write_frame(&mut buf, &resp).await.unwrap(); write_frame(&mut buf, &resp).await.unwrap();

View file

@ -9,13 +9,18 @@ mod ipc;
mod runtime; mod runtime;
mod ws; mod ws;
use ipc::{AppStateKind, Request, Response}; use ipc::{AppStateKind, Request, Response, SessionInfo};
pub(crate) type Registry = Arc<Mutex<SessionRegistry>>; pub(crate) type Registry = Arc<Mutex<SessionRegistry>>;
struct SessionEntry {
app_id: String,
state: AppStateKind,
}
struct SessionRegistry { struct SessionRegistry {
next_id: u64, next_id: u64,
sessions: std::collections::HashMap<u64, AppStateKind>, sessions: std::collections::HashMap<u64, SessionEntry>,
broadcast: tokio::sync::broadcast::Sender<Response>, broadcast: tokio::sync::broadcast::Sender<Response>,
abort_senders: std::collections::HashMap<u64, tokio::sync::oneshot::Sender<()>>, abort_senders: std::collections::HashMap<u64, tokio::sync::oneshot::Sender<()>>,
} }
@ -33,10 +38,16 @@ impl Default for SessionRegistry {
} }
impl SessionRegistry { impl SessionRegistry {
fn launch(&mut self, _app_id: &str) -> u64 { fn launch(&mut self, app_id: &str) -> u64 {
self.next_id += 1; self.next_id += 1;
let id = self.next_id; 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 id
} }
@ -52,20 +63,26 @@ impl SessionRegistry {
rx rx
} }
fn running_ids(&self) -> Vec<u64> { fn running_sessions(&self) -> Vec<SessionInfo> {
self.sessions.keys().copied().collect() self.sessions
.iter()
.map(|(&session_id, e)| SessionInfo {
session_id,
app_id: e.app_id.clone(),
})
.collect()
} }
fn state(&self, session_id: u64) -> AppStateKind { fn state(&self, session_id: u64) -> AppStateKind {
self.sessions self.sessions
.get(&session_id) .get(&session_id)
.cloned() .map(|e| e.state.clone())
.unwrap_or(AppStateKind::NotFound) .unwrap_or(AppStateKind::NotFound)
} }
pub(crate) fn set_state(&mut self, session_id: u64, state: AppStateKind) { pub(crate) fn set_state(&mut self, session_id: u64, state: AppStateKind) {
if let Some(entry) = self.sessions.get_mut(&session_id) { 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 => { Request::QueryRunning => {
let session_ids = registry.lock().await.running_ids(); let sessions = registry.lock().await.running_sessions();
Response::RunningApps { session_ids } Response::RunningApps { sessions }
} }
Request::QueryAppState { session_id } => { Request::QueryAppState { session_id } => {
let state = registry.lock().await.state(session_id); let state = registry.lock().await.state(session_id);
@ -315,7 +332,12 @@ mod tests {
.await; .await;
let resp = dispatch(Request::QueryRunning, &reg).await; let resp = dispatch(Request::QueryRunning, &reg).await;
match resp { 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"), _ => panic!("expected RunningApps"),
} }
} }
@ -368,15 +390,20 @@ mod tests {
} }
#[test] #[test]
fn registry_running_ids_reflects_live_sessions() { fn registry_running_sessions_reflects_live_sessions() {
let mut reg = SessionRegistry::default(); let mut reg = SessionRegistry::default();
let id1 = reg.launch("a"); let id1 = reg.launch("com.example.a");
let id2 = reg.launch("b"); let id2 = reg.launch("com.example.b");
let mut ids = reg.running_ids(); let mut sessions = reg.running_sessions();
ids.sort(); sessions.sort_by_key(|s| s.session_id);
assert_eq!(ids, vec![id1, id2]); 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); 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] #[test]

View file

@ -208,23 +208,25 @@
removeTaskbarEntry(msg.session_id); removeTaskbarEntry(msg.session_id);
} }
} else if (msg.type === 'RUNNING_APPS') { } else if (msg.type === 'RUNNING_APPS') {
msg.session_ids.forEach(function (id) { msg.sessions.forEach(function (s) {
ensureTaskbarEntry(id); ensureTaskbarEntry(s.session_id, s.app_id);
}); });
} else if (msg.type === 'LAUNCH_ACK') { } 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; var id = 'task-' + sessionId;
if (document.getElementById(id)) { return; } if (document.getElementById(id)) { return; }
var label = appId ? appId.split('.').pop() : String(sessionId);
var el = document.createElement('weft-taskbar-app'); var el = document.createElement('weft-taskbar-app');
el.id = id; el.id = id;
el.dataset.sessionId = sessionId; 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.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 taskbar = document.querySelector('weft-taskbar');
var clock = document.getElementById('clock'); var clock = document.getElementById('clock');
taskbar.insertBefore(el, clock); taskbar.insertBefore(el, clock);