Browser acceptance of the activity panel surfaced the product problem: a
background bash registers in both planes, so the header grew two near-
identical popovers — "background jobs" with lifecycle and duration,
"activities" with live output — listing the same work twice.
The service planes stay orthogonal (jobs: model-visible control with a
consuming read cursor; activities: transient non-consuming observation).
The join happens per row in the UI projection instead:
- dsh-client-ui-activity renders the single merged "tasks" list. Each
jobsBySession row joins the activity carrying its correlation.jobId —
the job supplies identity, lifecycle (stopping exists only there),
duration, and the model-visible detail; the activity supplies the
expandable live-output panel. Standalone activities (workflow runs)
keep their own rows; without the activity registry, job rows still
render and the control degrades to the old job list exactly.
- dsh-client-ui-jobs is deleted (pre-release, no shim); the merged entry
takes its slot order. The job locale namespace folds into activity.
- User-facing copy says "task" (任务): N tasks running / N 个任务进行中.
- Hierarchy: live rows first as two lines (label over kind badge and
status, ticking duration), a Finished divider when both sections
exist, settled rows compacted to one de-emphasized line, and the
output panel hangs behind a spine under its row.
Deliberately not joined: subagent rows stay bare (the subagent panel is
their surface) and foreground commands stay in tool cards.
Both web e2e scenarios now assert one Tasks list; the background-job
scenario's row gained a live-output panel, proving the join end to end.
Agent Note: 2026-08-25-unified-task-list.