List a session's background tasks above its subagents

The count beside the status said how much work was going and never what,
so "3 bg tasks" was a number with no way to find out what it was about.

Drivers now report the tasks themselves rather than a size:
`Driver::background_tasks` returns `Vec<BackgroundTask>` -- id, the
provider's own description, and a kind -- served by
`GET /sessions/{id}/background`. It is runtime state, never persisted,
and `null` is "nobody has said", which is what a session with no process
answers and what the panel says in words rather than drawing as an empty
list. `description` is optional because Codex names a background terminal
by a process id, and a number drawn as a name is worse than admitting
there is none.

Claude's `background_tasks_changed` entries turn out to be objects
carrying `task_id`, `task_type` and `description`, so each is read rather
than counted -- and an `ambient` one is now dropped from the list and the
count alike, on the CLI's own instruction: a live-update watcher is not
activity, and counting one left a session reading `waiting` with nothing
to wait for.

The phone draws them in the right-hand panel above the subagents,
collapsed to "2 bg tasks running" and pushing the subagents down when
opened. Both lists are items of one lazy column, so neither can run off
the panel, and the section is refetched whenever the live count moves --
a card for work that has finished is exactly the stale measurement the
count exists not to be.

Verified against the real Claude CLI (2.1.261): a backgrounded `sleep 120`
came back as `{"id":"br16327wr","description":"Sleep for 120 seconds",
"kind":"command"}`, and on the emulator against the echo rig the section
appeared, expanded, and dropped a card as its task finished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-19 23:29:40 -04:00
1 parent c8bfc958ad
commit 942edd6b31
17 files changed
+620 -115

No files matched your search

+39 -8
View File
@@ -12,7 +12,7 @@
//! side uses. Only ids matching [`is_subagent_id`] are ever turned into a
//! path.
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
@@ -68,6 +68,9 @@ pub struct SubagentInfo {
/// session's but with no driver behind it.
pub struct Subagent {
dir: PathBuf,
/// Its own name, from `meta.json`, so an open subagent can be listed
/// without reading every subagent's directory back off disk.
title: String,
transcript: Mutex<Transcript>,
events: broadcast::Sender<SeqEvent>,
/// Mirrors the transcript's last `Status` event, kept live rather than
@@ -81,6 +84,10 @@ pub struct Subagent {
}
impl Subagent {
fn title(&self) -> &str {
&self.title
}
pub fn transcript_path(&self) -> PathBuf {
self.dir.join("transcript.jsonl")
}
@@ -129,9 +136,11 @@ pub struct Subagents {
/// The session's own directory; subagents live under `<dir>/subagents`.
dir: PathBuf,
live: Mutex<HashMap<String, Arc<Subagent>>>,
/// Open ids, seeded from disk so an adopted session starts with the measured count rather than
/// waiting to see lifecycle edges which are already behind its stdout cursor.
open: Mutex<HashSet<String>>,
/// Open subagents by id, with the title each is known by, seeded from disk so an adopted
/// session starts with the measured set rather than waiting to see lifecycle edges which are
/// already behind its stdout cursor. The title is held here so that listing what is open
/// costs no directory read -- `GET /sessions/{id}/background` asks often.
open: Mutex<HashMap<String, String>>,
}
impl Subagents {
@@ -139,14 +148,14 @@ impl Subagents {
let subagents = Self {
dir: session_dir,
live: Mutex::new(HashMap::new()),
open: Mutex::new(HashSet::new()),
open: Mutex::new(HashMap::new()),
};
subagents.open.lock().unwrap().extend(
subagents
.list(true)
.into_iter()
.filter(|info| info.status == SessionStatus::Running)
.map(|info| info.id),
.map(|info| (info.id, info.title)),
);
subagents
}
@@ -217,6 +226,7 @@ impl Subagents {
let (events, _) = broadcast::channel(EVENT_BUFFER);
Ok(Arc::new(Subagent {
dir,
title: meta.title,
transcript: Mutex::new(transcript),
events,
status: Mutex::new(status),
@@ -239,7 +249,10 @@ impl Subagents {
match self.open_or_create(id, title, prompt) {
Ok(subagent) => {
if subagent.is_open() {
self.open.lock().unwrap().insert(id.to_string());
self.open
.lock()
.unwrap()
.insert(id.to_string(), subagent.title().to_string());
}
live.insert(id.to_string(), subagent);
}
@@ -302,6 +315,21 @@ impl Subagents {
self.open.lock().unwrap().len()
}
/// The live subagents, each with the title it is known by. Ordered by
/// id, which says nothing about when they started but does mean two
/// readings agree; read from memory, so a caller may ask often.
pub fn open_list(&self) -> Vec<(String, String)> {
let mut open: Vec<(String, String)> = self
.open
.lock()
.unwrap()
.iter()
.map(|(id, title)| (id.clone(), title.clone()))
.collect();
open.sort();
open
}
/// Appends one event to a subagent's own transcript. A no-op, with a
/// debug log, for an id nothing was started under -- a child line for a
/// subagent this registry never opened is dropped rather than guessed
@@ -344,7 +372,10 @@ impl Subagents {
state: SessionStatus::Running,
})
{
self.open.lock().unwrap().insert(id.to_string());
self.open
.lock()
.unwrap()
.insert(id.to_string(), subagent.title().to_string());
}
}