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 },
}
#[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<u64>,
sessions: Vec<SessionInfo>,
},
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<u8> = Vec::new();
write_frame(&mut buf, &resp).await.unwrap();

View file

@ -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<Mutex<SessionRegistry>>;
struct SessionEntry {
app_id: String,
state: AppStateKind,
}
struct SessionRegistry {
next_id: u64,
sessions: std::collections::HashMap<u64, AppStateKind>,
sessions: std::collections::HashMap<u64, SessionEntry>,
broadcast: tokio::sync::broadcast::Sender<Response>,
abort_senders: std::collections::HashMap<u64, tokio::sync::oneshot::Sender<()>>,
}
@ -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<u64> {
self.sessions.keys().copied().collect()
fn running_sessions(&self) -> Vec<SessionInfo> {
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, &reg).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]

View file

@ -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);