qcontrol-live-view
Live qflow visualization of qcontrol agent events — agents pulsing to event-type nodes, fed by qdash SSE, with a growing timeline scrubber
New in this cycle: Live qcontrol events — agents pulsing to typed event nodes, growing timeline
Why
qcontrol now streams every agent event (file/exec/network/http/llm/agent/mcp) through qdash, whose live view runs at http://127.0.0.1:7878/live as tables. qflow's spatial, animated idioms should make this stream immediately legible: which agents are active, what kinds of events they emit, at what rate, and where the errors are.
Objective
A working ExpLiveView.r1.vue experiment that:
- Subscribes to qdash SSE (
/api/events/stream) through a translation layer and renders agents (sources) → event-type nodes (destinations) with one pulse per event - Seeds history via
/api/eventsbackfill and shows a YouTube-style play bar whose track grows with live events; scrubbing rebuilds state at time t; "jump to live" resumes tailing - Filters: everything/curated mode toggle + agents/families/types selection matching qdash /live semantics, initializable from URL params
- Survives the full firehose (pulse budgets, marker binning, history cap, stale-agent eviction)
Scope
dev/experiments/c12.qcontrol-live-view/— App.vue nav shell, ExpLiveView.r1.vue, PlayBar.vue,lib/(QdashFeed, backfill, adapter, filterModel, classifier, history, mockFeed), helpers copied from c11 (LiveFeed, NodeRegistry, TrailPulse, trail-texture, shapes/)dev/vite.config.js—/api/eventsproxy →http://127.0.0.1:7878(qdash has no CORS + a local-host check; EventSource can't set headers)dev/App.vue— experiment list entry
Key protocol/reference files (qcontrol repo): crates/qdash/ui/app/lib/live/sse-protocol.ts,
backfill.ts, event-table-projections.ts (filter semantics — ported, not imported),
schemas/ts/qcontrol-run.ts (RunRecord types). qmap c28 notableEventClassifier.js is the
severity/category basis for scrubber markers.
Design decisions:
sourceAddress = run.id(always present; one node per run/session); display labelagent_id · pid; filtering by canonicalagent_idto match qdash's agents axisdestinationAddress = event.type; family =type.split('.')[0]; per-family palette, severity overrides pulse tint- qdash /live filter is page-local (not URL-encoded) → c12 defines the URL contract
(
?agents=&families=&types=); a qdash-side "Open in qflow" link is deferred to r2
Plan: /Users/mark/.claude/plans/create-a-plan-for-curried-clarke.md
Key Changes
- r1 shipped:
dev/experiments/c12.qcontrol-live-view/— nav shellApp.vue,ExpLiveView.r1.vue(c10 two-pass pipeline: agents → event-type nodes, trail pulses, growth-throttled relayout, fit-to-content),PlayBar.vue - Translation layer in
lib/:adapter.js(RunRecord → dual-shape flow record, family palette, severity pulse tint),classifier.js(qmap c28 classifier adapted to RunRecord fields + envelope-severity catch-all),mockFeed.js(synthetic wrapper frames through the real adapter for offline dev) - Live feed:
lib/QdashFeed.js(EventSource client for qdash data-only frames, 200ms micro-batching, status machine) +lib/backfill.js(JS port of qdash's splice client: first-connect newest-N, reconnect gap-fill, id-dedupe). Feed instance retained across pause/resume so reconnects gap-fill instead of re-backfilling (avoids duplicate history) - Filters:
lib/filterModel.jsports qdash /live semantics (agents axis → types override families), everything/curated modes, agent/family chips with pre-filter counts, URL params (?agents=&families=&types=) read on mount + written via replaceState; filter changes rebuild retroactively from history - Timeline:
lib/history.js(50k-cap record log, notables subset) +PlayBar.vue(growing timestamp axis, severity-binned marker diamonds ≤150 bins with aggregated hover labels, LIVE button). Scrub → throttled (~120ms) scene rebuild at time t with pulses only in the trailing 2s window; jump-to-live rebuilds and resumes tailing; live batches keep recording to history (scene-gated) while scrubbed back - Perf: pulse budget (≤12/batch prioritized notable→curated→rest, ≤40 in-flight),
pulse skip when
document.hidden, stale-agent eviction viagetStaleNodes(5min)every 30s (following only, fade + destroy + relayout) - Harness:
/api/eventsVite proxy (changeOrigin, SSE verified through it), dev/App.vue entry, CYCLES.md entries - Agent pawn sprites: agents render as the q-nuxt-layer pixel-pawn SVG (vendored as
agent-pawn.svg— the published package doesn't export assets) rasterized nearest-neighbor vialib/agentTexture.js(Image + canvas + Texture.from; Graphics.svg() can't parse its class-based CSS). Blink = white halo circle behind the sprite; falls back to circles if the texture fails. New defaults: pulses isometric, agents circular-packed, event types arc - Pawn blink = white-silhouette overlay sprite (second rasterization with source-in composite — the sprite analog of draw-white-then-tint, since tint can't brighten)
- Replay playback: PlayBar play/pause + cycling speed chip (1–16×). Incremental cursor over history applies crossed records through handleBatch (no per-tick rebuilds); reaching the live edge jumps back to tailing. Play while following replays from the beginning
- Bug fix: scrub rebuilds destroyed players mid-ease; pixi-ease then broke on the dead
targets and stopped ALL later animations, leaving rebuilt players frozen at alpha 0
(pulses unaffected — TrailPulse animates via ticker). Fix:
ease.removeAll()+ clear pending blink timeouts in clearScene, plus destroyed-guard in blinkPlayer - Agent spacing:
drawnRadiusaccounts for the pawn's wider footprint (1.2×), and a new Agent Padding slider threads into grid/packed-grid/circular layouts - Label controls: Position (top/bottom/left/right, default right) + Offset slider. Left/right placement is edge-aligned — the wrapper shifts by half the text width so the label's near edge sits flush to the node edge regardless of text length
- Pulse layering: dedicated
pulseHolderas the bottom child of the root holder so trails render behind nodes. Each TrailPulse is its own render group (plain sub-container + per-frame Graphics redraws = the c2 render-group cache gotcha otherwise)
Outcome
In progress (as of 2026-07-29) — r1 functional end-to-end. Working: live qdash SSE feed through the Vite proxy with backfill seeding, mock mode, agents-as-pawns → event-type nodes, filters (modes + agents/families chips + URL params), timeline with scrub/replay/jump-to-live, scene-reviewer pass clean.
Pick up here
- Visual tuning by eye: pulse density under full firehose (budgets in the Pulses panel), arc angles for the event ring, agent padding default, and whether pulses-behind-nodes needs a brighter destination flash (arrivals are partially hidden under busy pawns now)
- r2 candidates (from plan, deferred): per-vendor agent logos — q-nuxt-layer has
components/agent/registry.js(alias matching) +assets/svgs/ai-agent-logos/; the sprite plumbing inlib/agentTexture.jsmakes this a texture-map swap keyed onagent_id. qdash-side "Open in qflow" link (serialize /live filter into c12's URL params — qcontrol repo change). Foldedfile.*frame count-weighting (payload fold count unverified). Play-from-t already shipped; incremental rebuild snapshots only if 50k replays feel slow. - Known rough edges: replay cursor re-anchors on history trim (guard in
replayTick); backfill limit change restarts the feed (full reset);agent-pawn.svgis vendored — sync manually if the q-nuxt-layer original changes, or add an assets export to the package
Visual record
10 captures — heroes, page slices, and full-page renders. Click to view.









