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.
This commit is contained in:
amosblomqvist
2026-09-11 03:12:32 +03:00
parent a023231edd
commit e22f0bddb4
4 changed files with 632 additions and 3 deletions
+7 -1
View File
@@ -3,6 +3,7 @@ import "katex/dist/katex.min.css";
import "./globals.css";
import { AppShell, SidebarProvider } from "@/components/sidebar";
import { TreePanelModeProvider } from "@/components/tree-panel";
import { ActivityDock, ActivityProvider } from "@/components/activity-dock";
export const metadata: Metadata = {
title: "Learning Harness",
@@ -15,7 +16,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<body className="min-h-dvh antialiased">
<SidebarProvider>
<TreePanelModeProvider>
<AppShell>{children}</AppShell>
<ActivityProvider>
<AppShell>{children}</AppShell>
{/* The reserved bottom-right corner of every view — renders
nothing while no background process runs. */}
<ActivityDock />
</ActivityProvider>
</TreePanelModeProvider>
</SidebarProvider>
</body>