diff --git a/src/features/helpers/controller.py b/src/features/helpers/controller.py
index df50d86b..f62a1208 100644
--- a/src/features/helpers/controller.py
+++ b/src/features/helpers/controller.py
@@ -87,6 +87,8 @@ class Helpers(Controller):
     @lasting
     def stop(self, n: int):
         row = self._unfinished(n, "finished")
+        if self.actor == USER:
+            row = self.update(n, stopped_by_user=True)
         places = Environments(self.record, actor=SYSTEM)
         place = places._titled(row.environment)
         if not place:
diff --git a/src/features/helpers/resource.py b/src/features/helpers/resource.py
index 506dc2c2..56b303b5 100644
--- a/src/features/helpers/resource.py
+++ b/src/features/helpers/resource.py
@@ -18,6 +18,7 @@ class Helper(Shape, Resource):
         Field(default="", name="environment"),
         Field(default="", name="worktree"),
         Field(default="", name="report"),
+        Field(default=False, name="stopped_by_user"),
     ]
     type = HELPER
     icon = "bot"
diff --git a/src/features/phone/controller.py b/src/features/phone/controller.py
index bf1868c2..3156a231 100644
--- a/src/features/phone/controller.py
+++ b/src/features/phone/controller.py
@@ -8,6 +8,7 @@ import secrets
 import tempfile
 import time
 from pathlib import Path
+from enum import StrEnum
 from typing import TypedDict
 
 import controllers.types as types_module
@@ -16,11 +17,11 @@ from controllers.base import CONTROLLERS, Controller
 from controllers.marks import internal
 from controllers.messages import Messages
 from agents.control import pause, resume
-from controllers.types import Agents, Environments, Nudges
+from controllers.types import Agents, Environments, Nudges, Todos
 from engine.sessions import Sessions
 from controllers.notices import Notices
 from engine.record import Record
-from features.format import VIEWER
+from features.format import VIEWER, formatted
 from features.message_buttons.shaping import Button, spent
 from engine.project_files import matching
 from features.phone.export import Export, export
@@ -35,6 +36,7 @@ from resources.shapes import level_named
 from resources.base import AGENT, PROJECT, SYSTEM, USER, Refused, titled
 from features.work_modes.modes import mode_of, pick
 from features.helpers.controller import Helpers
+from surfaces.summary import lately_summarized
 
 CODE_SECONDS = 600
 HELPERS_SHOWN = 10
@@ -58,6 +60,74 @@ HIDDEN = ("phone", "share", "plugin")
 WAITING = ("question", "plan", "report", "doc")
 
 
+class WorkState(StrEnum):
+    NEEDS = "needs"
+    WORKING = "working"
+    IDLE = "idle"
+    REPORTED = "reported"
+    FINISHED = "finished"
+    STOPPED = "stopped"
+    ENDED = "ended"
+
+
+@dataclass(frozen=True)
+class HelperAgent:
+    status: str = ""
+    at: float = 0.0
+    started: float = 0.0
+    tool: str = ""
+    file: str = ""
+
+    @classmethod
+    def from_payload(cls, payload: dict) -> "HelperAgent":
+        return cls(status=payload.get("status", ""), at=float(payload.get("at", 0.0)),
+                   started=float(payload.get("started", 0.0)), tool=payload.get("tool", ""), file=payload.get("file", ""))
+
+
+@dataclass(frozen=True)
+class HelperSnapshot:
+    name: str = ""
+    owner: str = ""
+    attention_kind: str = ""
+    attention_text: str = ""
+    silent: bool = False
+    agent: HelperAgent = HelperAgent()
+    todo: int = 0
+
+    @classmethod
+    def from_payload(cls, payload: dict) -> "HelperSnapshot":
+        attention = payload.get("attention") or {}
+        agent = payload.get("agent") or {}
+        work = payload.get("work") or payload.get("last") or {}
+        return cls(name=payload.get("name", ""), owner=payload.get("owner", ""),
+                   attention_kind=attention.get("kind", ""), attention_text=attention.get("text", ""),
+                   silent=bool(payload.get("silent")), agent=HelperAgent.from_payload(agent), todo=int(work.get("todo", 0)))
+
+
+def helper_state(row, environment: HelperSnapshot, now: float) -> WorkState:
+    if row.completed:
+        return WorkState.FINISHED
+    if row.report:
+        return WorkState.REPORTED
+    if environment.attention_kind or environment.silent:
+        return WorkState.NEEDS
+    if not environment.agent.status or environment.agent.status == "stopped":
+        return WorkState.STOPPED if row.stopped_by_user else WorkState.ENDED
+    if now - environment.agent.at <= 300:
+        return WorkState.WORKING
+    return WorkState.IDLE
+
+
+def helper_reason(environment: HelperSnapshot, now: float) -> str:
+    if environment.attention_kind == "question":
+        return f"Asks a question: {environment.attention_text}"
+    if environment.attention_kind == "permission":
+        return f"Wants a permission: {environment.attention_text}"
+    if environment.silent:
+        return f"Silent for {max(5, int((now - environment.agent.at) / 60))} min"
+    return ""
+
+
 def readable(kind: str) -> bool:
     return kind in CONTROLLERS and kind not in HIDDEN
 
@@ -81,6 +151,12 @@ class Mark(Marked, total=False):
     command: str
 
 
+class HelperTodo(TypedDict, total=False):
+    n: int
+    title: str
+    completed: float
+
+
 @dataclass(frozen=True)
 class Session:
     n: int
@@ -153,6 +229,7 @@ class Running(TypedDict):
     auto: bool
     mode: str
     helpers: list[dict]
+    subagents: list[dict]
 
 
 class Feed(TypedDict):
@@ -443,7 +520,11 @@ class Phones(Controller):
         return USER not in row.seen or any(not spent(button, buttons, pressed) for button in buttons)
 
     def _reaches(self, phone: Phone, row) -> bool:
-        return row.data.get("environment") in (phone.environment, None, "")
+        environment = row.data.get("environment")
+        if environment in (phone.environment, None, ""):
+            return True
+        place = Environments(self._home(phone), actor=SYSTEM)._titled(environment)
+        return bool(place and place.helping and place.launched_from == phone.environment)
 
     def _file(self, phone: Phone, ref: str, name: str) -> Path:
         kind, _, n = ref.partition(":")
@@ -460,15 +541,72 @@ class Phones(Controller):
     def _running(self, home: Record, environment: str) -> Running:
         holder = Sessions(home.root).holder(environment)
         row = Agents(home, actor=SYSTEM)._titled(holder) if holder else None
-        shared = dict(state=agent_state(home, environment), auto=bool(home.setting("features", {}).get(AUTO)), mode=mode_of(home), helpers=self._helpers_of(home))
+        summary = lately_summarized(home.root)
+        current = next((entry for entry in summary["environments"] if entry["name"] == environment), {"subagents": []})
+        shared = dict(state=agent_state(home, environment), auto=bool(home.setting("features", {}).get(AUTO)), mode=mode_of(home),
+                      helpers=self._helpers_of(home, summary), subagents=self._subagents_of(home, current["subagents"]))
         if row is None:
             return Running(paused=False, context=0, usage=[], **shared)
         return Running(paused=bool(row.paused), context=int(row.context), usage=list(row.usage.get("windows", [])), **shared)
 
-    def _helpers_of(self, home: Record) -> list[dict]:
-        return [{"n": row.n, "title": row.title, "completed": row.completed,
-                 "data": {"name": row.name, "provider": row.provider, "model": row.model, "report": row.report}}
-                for row in Helpers(home, actor=SYSTEM).all(completed=True, last=HELPERS_SHOWN)]
+    def _helpers_of(self, home: Record, summary: dict) -> list[dict]:
+        environments = list(map(HelperSnapshot.from_payload, summary["helpers"]))
+        now = time.time()
+        found = []
+        for row in Helpers(home, actor=SYSTEM).all(completed=True, last=HELPERS_SHOWN):
+            environment = next((entry for entry in environments if entry.owner == row.ref), HelperSnapshot())
+            agent = environment.agent
+            todo = self._helper_todo(home, environment)
+            state = helper_state(row, environment, now)
+            view = shaped(row, home, VIEWER)
+            file = Path(agent.file).name if agent.file else ""
+            doing = " ".join(part for part in (agent.tool, file) if part)
+            found.append({"n": row.n, "title": view["title"], "name": row.name, "provider": row.provider, "model": row.model,
+                          "state": state.value, "reason": formatted(helper_reason(environment, now), home, VIEWER),
+                          "now": doing,
+                          "started": agent.started or row.created, "at": agent.at or row.updated, "todo": todo,
+                          "completed_at": row.completed, "stopped_by_user": row.stopped_by_user,
+                          "report": formatted(row.report, home, VIEWER) if row.report else ""})
+        return found
+
+    def _helper_todo(self, home: Record, environment: HelperSnapshot) -> HelperTodo:
+        if not environment.todo:
+            return {}
+        record = Record(home.root, environment.name)
+        row = Todos(record, actor=SYSTEM).load(environment.todo)
+        view = shaped(row, record, VIEWER)
+        return {"n": row.n, "title": view["title"], "completed": row.completed}
+
+    def _subagents_of(self, home: Record, rows: list[dict]) -> list[dict]:
+        found = []
+        for row in rows:
+            match row:
+                case {"running": True}:
+                    state = "working"
+                case {"refusal": refusal} if refusal:
+                    state = "refused"
+                case {"status": "stopped"}:
+                    state = "stopped"
+                case _:
+                    state = "finished"
+            found.append({**row, "state": state, "task": formatted(row.get("task", ""), home, VIEWER),
+                          "outcome": formatted(row.get("outcome", ""), home, VIEWER),
+                          "refusal": formatted(row.get("refusal", ""), home, VIEWER)})
+        return found
+
+    def _helper(self, phone: Phone, n: int) -> dict:
+        home = self._home(phone)
+        row = Helpers(home, actor=SYSTEM).load(n)
+        if not self._reaches(phone, row):
+            raise Refused(f"helper {n} is not in this phone's environment")
+        summary = lately_summarized(home.root)
+        payload = next((entry for entry in summary["helpers"] if entry["owner"] == row.ref), {})
+        environment = HelperSnapshot.from_payload(payload)
+        helper = next(entry for entry in self._helpers_of(home, summary) if entry["n"] == n)
+        record = Record(home.root, row.environment)
+        agent = Agents(record, actor=SYSTEM).primary()
+        activity = [] if agent is None else Session.from_view(shaped(agent, record, VIEWER)).marks()[-5:][::-1]
+        return {**helper, "activity": activity, "running": bool(environment.agent.status and environment.agent.status != "stopped")}
 
     def _holder(self, phone: Phone) -> str:
         holder = Sessions(self._home(phone).root).holder(phone.environment)
diff --git a/src/features/phone/routes.py b/src/features/phone/routes.py
index 67ddcdda..9442d05c 100644
--- a/src/features/phone/routes.py
+++ b/src/features/phone/routes.py
@@ -261,6 +261,14 @@ class PhoneRoutes:
                 return self.json(handler, 200, {**self.phones(handler)._feed(phone, float(before)), "build": built()})
             except ValueError:
                 return handler.answer(400, "before is a time in seconds")
+        if rest == ["helper"]:
+            asked = parse_qs(urlsplit(handler.path).query).get("n", [""])[0]
+            if not asked.isdigit():
+                return handler.answer(400, "a helper number is required")
+            try:
+                return self.json(handler, 200, self.phones(handler)._helper(phone, int(asked)))
+            except Refused as refused:
+                return handler.answer(404, str(refused))
         if rest == ["bar"]:
             return self.json(handler, 200, self.phones(handler)._bar(phone))
         if len(rest) == 3:
diff --git a/src/features/phone/test.py b/src/features/phone/test.py
index 6ca47e85..1a10db12 100644
--- a/src/features/phone/test.py
+++ b/src/features/phone/test.py
@@ -16,6 +16,7 @@ from controllers.base import Controller
 from controllers.types import CONTROLLERS, Comments, Docs, Messages, Notices, Questions, Todos
 from engine.record import Record
 from features.phone.controller import SAID, Phones
+from features.helpers.controller import Helpers
 from features.sharing.controller import Shares
 from features.sharing.server import ShareHandler
 from features.sharing.services import wanted
@@ -281,17 +282,43 @@ def test_a_question_is_answered_once_and_a_changed_plan_is_not_approved(served,
     said = [m for m in Messages(record, actor=SYSTEM).summaries() if m["title"] == "I accept this proposal"]
     assert said and call(base, "/p/press", {"ref": f"doc:{proposal.n}", "label": "Change it"}, key).status == 409, \
         "a button pressed on the phone says its words, and the other button of the same choice is gone"
+    now = time.time()
+    CONTROLLERS["agent"](record, actor=SYSTEM).create(
+        "codex-1",
+        status="working",
+        at=now,
+        thoughts=[{"at": now, "text": "Weighing it"}],
+        cards=[{"at": now, "label": "Agent committed abc1234", "icon": "commit"}],
+        subagent_rows=[{"id": "sub-1", "session": "session-1", "task": "Check the phone feed", "type": "Explore", "model": "haiku",
+                        "at": now, "running": True, "tool": "Read", "file": "src/features/phone/controller.py"}],
+    )
+    helper = Helpers(record, actor=SYSTEM).create("Split the phone view", name="Rhea", provider="codex", model="gpt-5-codex",
+                                                  environment=f"{record.env}-rhea")
+    from controllers.types import Environments
+    Environments(record, actor=SYSTEM).create(helper.environment, owner=helper.ref, launched_from=record.env)
+    helper_record = Record(record.root, helper.environment)
+    todo = Todos(helper_record, actor=SYSTEM).create(helper.title)
+    CONTROLLERS["work"](helper_record, actor=SYSTEM).create(helper.title, todo=todo.n)
+    CONTROLLERS["agent"](helper_record, actor=SYSTEM).create("codex-rhea", status="working", at=now, started=now - 60,
+                                                             tool="Edit", file="src/web/src/phone/PhoneHome.vue")
+    from surfaces.summary import summarize
+    monkeypatch.setattr(controller, "lately_summarized", summarize)
     fed = call(base, "/p/feed", key=key).body
     assert fed["agent"] == "offline", "the phone sees no agent running"
     assert fed["build"].startswith("phone-") and fed["build"].endswith(".js"), "the phone learns which build of its app is installed"
-    now = time.time()
-    CONTROLLERS["agent"](record, actor=SYSTEM).create("codex-1", thoughts=[{"at": now, "text": "Weighing it"}],
-                                                       cards=[{"at": now, "label": "Agent committed abc1234", "icon": "commit"}])
-    shown = {item["type"]: item.get("label") for item in call(base, "/p/feed", key=key).body["items"] if item["type"] in ("thought", "card")}
-    assert shown.get("thought") == "Weighing it" and shown.get("card", "").startswith("Agent committed"), "the agent's thoughts and chat marks reach the phone"
+    shown = [(item["type"], item.get("label", "")) for item in fed["items"]]
+    assert ("thought", "Weighing it") in shown and any(kind == "card" and label.startswith("Agent committed") for kind, label in shown), \
+        "the agent's thoughts and chat marks reach the phone"
     assert call(base, "/p/stop", {}, key).status == 422 and call(base, "/p/pause", {}, key).status == 422, "with no agent running there is nothing to stop or pause"
     running = call(base, "/p/feed", key=key).body["running"]
     assert (running["state"], running["paused"], running["usage"]) == ("offline", False, []), "the phone sees the agent's state, pause, context and usage"
+    assert running["helpers"][0]["state"] == "working" and running["helpers"][0]["todo"]["n"] == todo.n, \
+        "the feed gives the phone a helper's state, current work and to-do"
+    assert running["subagents"][0]["id"] == "sub-1" and running["subagents"][0]["state"] == "working", \
+        "the feed gives the phone the current environment's subagents"
+    detail = call(base, f"/p/helper?n={helper.n}", key=key)
+    assert detail.status == 200 and detail.body["todo"]["title"] == helper.title and detail.body["running"], \
+        "the phone can open a helper from the environment that launched it"
     from engine.sessions import Sessions
     from surfaces.agent_state import agent_state
     Sessions(record.root).write("claude-4242", environment=record.env, pid=os.getpid(), since=time.time() - 60)
@@ -300,9 +327,7 @@ def test_a_question_is_answered_once_and_a_changed_plan_is_not_approved(served,
     Sessions(record.root).write("d2c1c997-real", environment=record.env, pid=os.getpid(), since=time.time() - 30, seen=time.time())
     assert agent_state(record, record.env) == "working", "an older launch record of the same agent never hides the session that reports"
     Sessions(record.root).write("d2c1c997-real", environment="")
-    from controllers.types import Environments
     from features.phone.places import shown
-    Environments(record, actor=SYSTEM).create(f"{record.env}-rhea", owner="helper:1", launched_from=record.env)
     assert f"{record.env}-rhea" not in shown(record.root), "a helper's own environment stays out of the phone's places"
     Sessions(record.root).write("claude-4242", environment="")
     assert call(base, "/p/auto", {"on": True}, key).status == 201 and call(base, "/p/feed", key=key).body["running"]["auto"] is True, \
diff --git a/src/providers/claude.py b/src/providers/claude.py
index fec57a96..61b66c60 100644
--- a/src/providers/claude.py
+++ b/src/providers/claude.py
@@ -79,6 +79,22 @@ class Crew:
     errors: dict = field(default_factory=dict)
     moved_to_background: set = field(default_factory=set)
 
+
+@dataclass(frozen=True)
+class SubagentFacts:
+    tool: str
+    file: str
+    outcome: str
+
+
+def subagent_facts(rows: list[Row]) -> SubagentFacts:
+    calls = [(block.name, block.input) for row in rows if row.type == "assistant" for block in row.of_type("tool_use")]
+    tool, given = calls[-1] if calls else ("", {})
+    file = next((given.get(key) for key in ("file_path", "notebook_path", "path") if given.get(key)), "")
+    answers = [block.text for row in rows if row.type == "assistant" for block in row.of_type("text") if block.text.strip()]
+    return SubagentFacts(tool, file, answers[-1][:2000] if answers else "")
+
+
 SPEAKERS = {SUMMARY: SUMMARY, HUMAN: "user", AGENT: "agent"}
 ORIGINS = {"peer": PEER, "task-notification": TASK}
 
@@ -480,10 +496,11 @@ class Claude(Provider):
             written = session.stat().st_mtime if session is not None and session.is_file() else 0.0
             writing = now - written <= QUIET_SUBAGENT
             running = not status or writing and (status == "returned" or written > done)
+            facts = asdict(subagent_facts(self.recent(session))) if session else {}
             subagents.append({"id": use.id, "task_id": ids.get(use.id, ""), "task": use.description if use.description else "subagent", "type": use.subagent_type,
                               "model": use.model, "running": running, "at": use.at, "ended": 0.0 if running else done,
                               "status": "" if running else status, "refusal": held.errors.get(use.id),
-                              "session": session.stem.removeprefix("agent-") if session else "", "skills": self.skills(session)})
+                              "session": session.stem.removeprefix("agent-") if session else "", "skills": self.skills(session), **facts})
         shells = []
         for use in (u for u in uses if u.name == "Bash" and (u.background or u.id in held.moved_to_background)):
             status, done = ended.get(use.id, ("", 0.0))
diff --git a/src/surfaces/summary.py b/src/surfaces/summary.py
index fababde7..7e6b1c32 100644
--- a/src/surfaces/summary.py
+++ b/src/surfaces/summary.py
@@ -16,7 +16,7 @@ from typing import TypedDict
 
 SHOWN = ("building", "ready", "active", "waiting", "done")
 RECENT = 600.0
-SUBAGENT_FIELDS = ("session", "task", "type", "model", "at", "ended", "status", "running")
+SUBAGENT_FIELDS = ("id", "session", "task", "type", "model", "at", "ended", "status", "running", "refusal", "tool", "file", "outcome")
 
 
 def last_written(agent, session: str) -> float:
@@ -37,7 +37,8 @@ def subagents(agent) -> list[dict]:
         active = max(float(sub.get("at", 0.0)), last_written(agent, sub["session"]))
         running = bool(sub.get("running")) and live and now - active < RECENT
         if running or now - float(sub.get("ended", 0.0)) < RECENT:
-            shown.append({**{key: sub.get(key) for key in SUBAGENT_FIELDS}, "running": running, "active": active, "parent": agent.n})
+            shown.append({**{key: sub.get(key) for key in SUBAGENT_FIELDS}, "running": running, "active": active, "parent": agent.n,
+                          "report": agent.subagent_reports.get(sub.get("id"), 0)})
     return shown
 
 KEPT_FOR = 1.0
@@ -68,6 +69,14 @@ def plan(p, todos: dict) -> PlanSummary:
             "phases": len(p.phases), "rows": len(rows), "done": sum(bool(todos.get(n)) for n in rows)}
 
 
+def attention_of(questions: list, prompts: list) -> dict:
+    if prompts:
+        return {"kind": "permission", "text": prompts[-1].title}
+    if questions:
+        return {"kind": "question", "text": questions[-1].title}
+    return {}
+
+
 def environment(record: Record) -> dict:
     agent = Agents(record, actor=SYSTEM).primary()
     shelf = Works(record, actor=SYSTEM)
@@ -79,6 +88,9 @@ def environment(record: Record) -> dict:
     todos = {row["n"]: bool(row["completed"]) for row in Todos(record, actor=SYSTEM).summaries() if not row["deleted"]}
     work = lambda w: {"n": w.n, "title": w.title, "todo": w.todo, "parked": bool(w.parked), "awaiting": w.awaiting,
                       "completed": w.completed} if w else None
+    questions = Questions(record, actor=SYSTEM)._standing()
+    prompts = [n for n in Notices(record, actor=SYSTEM)._standing() if n.data.get("action") == "permission"]
+    attention = attention_of(questions, prompts)
     return {
         "name": record.env,
         "agent": {"status": agent.status or "stopped", "provider": agent.provider, "model": agent.model, "context": agent.context,
@@ -90,12 +102,13 @@ def environment(record: Record) -> dict:
         "subagents": subagents(agent),
         "auto": automatic(record),
         "silent": agent_state(record, record.env) == SILENT,
+        "attention": attention,
         "counts": {
             "messages": sum(USER not in row["seen"] and not row["completed"] and not row["deleted"] for row in Messages(record, actor=SYSTEM).summaries()),
-            "questions": len(Questions(record, actor=SYSTEM)._standing()),
+            "questions": len(questions),
             "todos": len([n for n, done in todos.items() if not done]),
             "suggestions": len(Suggestions(record, actor=SYSTEM)._standing()),
-            "prompts": sum(n.data.get("action") == "permission" for n in Notices(record, actor=SYSTEM)._standing()),
+            "prompts": len(prompts),
         },
     }
 
diff --git a/src/web/src/api/phone.js b/src/web/src/api/phone.js
index 3ebdde72..8214056f 100644
--- a/src/web/src/api/phone.js
+++ b/src/web/src/api/phone.js
@@ -70,6 +70,7 @@ export const phone = {
     stop: () => sent("./stop", {}),
     auto: (on) => sent("./auto", {on}),
     mode: (mode) => sent("./mode", {mode}),
+    helper: (n) => got(`./helper?n=${n}`),
     helperStop: (n) => sent("./helper/stop", {n}),
     exportUrl: (ref) => `./export/${ref.replace(":", "/")}`,
     exported: async (ref, fallback) => {
diff --git a/src/web/src/domain/helpers.js b/src/web/src/domain/helpers.js
index 1c05e91d..53db372c 100644
--- a/src/web/src/domain/helpers.js
+++ b/src/web/src/domain/helpers.js
@@ -1,8 +1,44 @@
-export const helperState = (row) => (row.completed ? "finished" : row.data?.report ? "reported" : "running");
+export const counted = (count, one, many) => `${count} ${count === 1 ? one : many}`;
 
-export const HELPER_WORDS = {running: "Running", reported: "Reported", finished: "Finished"};
+export const helperState = (row) => row.state || (row.completed ? "finished" : row.data?.report ? "reported" : "running");
 
-export const helperLine = (row) => [row.data?.provider, row.data?.model].filter(Boolean).join(" · ");
+export const HELPER_WORDS = {
+    needs: "Needs you",
+    working: "Working",
+    running: "Working",
+    idle: "Idle",
+    reported: "Reported",
+    finished: "Finished",
+    stopped: "Stopped",
+    ended: "Ended, no report",
+    refused: "Refused",
+};
+
+export const FINISHED_STATES = new Set(["finished", "stopped", "ended", "refused"]);
+export const stateRank = (state) => ({needs: 0, working: 1, running: 1, idle: 2, reported: 3}[state] ?? 4);
+export const stateAt = (row) => row.completed_at || row.ended || row.at || row.started || 0;
+
+export function agentsInOrder(rows) {
+    return [...rows].sort((a, b) => stateRank(a.state) - stateRank(b.state) || stateAt(b) - stateAt(a));
+}
+
+export function agentsShown(rows, older = false) {
+    const open = agentsInOrder(rows.filter((row) => !FINISHED_STATES.has(row.state)));
+    const finished = agentsInOrder(rows.filter((row) => FINISHED_STATES.has(row.state)));
+    return older ? [...open, ...finished] : [...open, ...finished.slice(0, 3)];
+}
+
+export function agentCounts(helpers, subagents, now = Date.now() / 1000) {
+    const rows = [...helpers, ...subagents];
+    return {
+        needs: rows.filter((row) => row.state === "needs").length,
+        working: rows.filter((row) => ["working", "running", "idle"].includes(row.state)).length,
+        reported: helpers.filter((row) => row.state === "reported").length,
+        finished: rows.filter((row) => FINISHED_STATES.has(row.state) && now - stateAt(row) <= 3600).length,
+    };
+}
+
+export const helperLine = (row) => [row.provider || row.data?.provider, row.model || row.data?.model].filter(Boolean).join(" · ");
 
 export function helpersInOrder(rows, keepFinished = 5) {
     const open = rows.filter((row) => helperState(row) !== "finished");
@@ -16,9 +52,9 @@ export function helpersInOrder(rows, keepFinished = 5) {
 export const helperView = (row) => ({
     row,
     n: row.n,
-    name: row.data?.name || `Helper ${row.n}`,
+    name: row.name || row.data?.name || `Helper ${row.n}`,
     title: row.title,
     line: helperLine(row),
     state: helperState(row),
-    report: row.data?.report || "",
+    report: row.report || row.data?.report || "",
 });
