The old activity dock (fixed bottom-right, every view) was a cross-session
widget parked inside the session view — the same boundary confusion that
got the plan window moved. The sidebar is the app's cross-session surface,
so the agents section lives at its bottom strip now (user-locked design):
- ALWAYS present: empty means empty — the header ('Background agents',
matching the Sessions label language) sits alone; nothing hides and
nothing decorates (the total-cost span only renders when populated).
- Populates and grows when processes run: same blobs (sub-agents +
extraction runs, one poll, inferred cost, elapsed heartbeat), capped by
max-h-[40dvh] + internal scroll so a pile never crowds the session list.
- Per-blob END button (unchanged abort contract); the detail popover
becomes an INLINE expansion — the section scrolls, a floating popover
would clip at its bounds.
- Sidebar-native skin: border-t separator matching the sidebar's edge,
session-row hover language, no floating-card chrome.
- Sidebar closed = the learner opted out; no ambient signal for now
(user decision — invisibility until a clearer observability story).
- Narrow-mode count badge/drawer machinery retired with the dock.
app/layout.tsx: ActivityDock mount removed, ActivityProvider stays (the
sidebar consumes the context). Live-verified: populated rows with inline
detail + session link, abort makes the row vanish, idle renders the
header alone, closed sidebar hides the section with nothing floating.
Gates: typecheck, lint, 423/423 tests.
--text-xxs (10px) joins the ladder; every component-level text-[9|10|11px]
literal in owned components now pulls from the ladder (graph-view.tsx and
home.tsx untouched — graph stream). Cosmetic pins updated where touched.
POST /api/activity/abort landed on main (fde46c2). 404 not_found /
409 not_running mean the process reached the desired end state on its
own — refresh and clear the abort state, no error surfaced. Genuine
failures (network/400) show 'Abort failed — try again' on the blob.
One quiet row beneath the composer/question shell (not flush to the
screen bottom; static height, outside the morphing shell):
- LEFT: context window + session cost, clickable → per-turn detail
popover (context, total, unpriced count, one row per turn with
model/steps/cost). All folds from core/prices.ts — sessionCost,
contextWindow, turnCost — no cost math in components.
- RIGHT: model + "Thinking —" — a placeholder slot for the coming
model/thinking-level CONTROL (later work with its contract changes);
display-only today, model read from the last turn's usage,
pretty-printed (Glm 5.3 Flash).
- Data: the record's turns log, seeded on mount, refreshed when the
local stream settles, kept fresh by the existing record re-syncs.
No turns yet → "—" everywhere; unpriceable turns never fold into a
fake $0 (the doctrine).
- formatUsd/formatTokens moved to components/usage-format.ts (shared
with the activity dock); prettyModel joins them, pure + tested.
One reserved corner of the shell, one blob per running BACKGROUND
process — session sub-agents (research/demo/visual) and global
extraction runs. Teaching turns are deliberately NOT here: the dock
exists to surface what the learner can't otherwise see; a running
teaching turn is already fully visible where it happens (user decision).
- Data: ONE poll of GET /api/activity (jobs across ALL chats,
title-joined, oldest-first + extraction runs), folded client-side
with core/prices.ts (jobCost). Adaptive interval — 2.5s while
anything runs, 10s idle, 4× slower on hidden tabs; a failed fetch
keeps the last feed (no flicker to empty on a blip).
- Per blob: kind icon, elapsed (ticking), cost, activity line, and an
explicit END on the blob's face. Click → detail popover (context
line, model, steps, open-session link). The context line is the LAST
model call's tokens vs the model's limit (JobUsage.lastStep — pending
agents-stream contract; "—" until it populates), NEVER a cumulative
sum. Absent data renders "—", never zero (the doctrine).
- Header: total running cost (sum of priced blobs, "—" when nothing
is priced). Narrow viewports collapse to a count badge → drawer.
- END posts to /api/activity/abort {kind, id} — the backend is being
composed by master over the streams' mechanisms; until it exists a
click locks the button and the popover says "Abort not available
yet" (never a dead control).
- Idle renders NOTHING. Mounted once in the root layout; every view
gets it for free.
toBlobs / formatElapsed / formatUsd / formatTokens are pure + tested;
SSR render tests pin blobs, the "—" doctrine, the face END, and the
priced total.