Files
learnharness/app
amosblomqvist e22f0bddb4 The activity dock: every background process, every view, bottom-right
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.
2026-09-11 03:12:32 +03:00
..