What it enables
- Browse every visible (non-hidden) workflow in reverse-chronological order without loading them all at once.
- See new workflows appear within 2 seconds while anything is in flight, and within 6 seconds otherwise — even when the browser tab is in the background.
- Recognize who launched each workflow from a per-user avatar
(
/team/{author}.png) or a colored-initials fallback.
What changed
Backend — paginated list endpoint
GET /v1/workflows(handled bylist_workflowsinmother-ai/src/routes.rs) now accepts:limit— page size. Clamped to[1, 100]; default20.cursor— opaque base64 (URL_SAFE_NO_PAD) of<updated_at_rfc3339>|<workflow_id>returned by the prior page’snext_cursor.
- Response shape extended (
WorkflowListResponseinmother-ai/src/models.rs) — addsnext_cursor: Option<String>. Null when fewer thanlimitrows were returned (i.e. end of list). - Postgres path uses keyset ordering — the
whereclause addsand (wr.updated_at, wr.workflow_id) < ($cursor_updated_at, $cursor_workflow_id), andorder by wr.updated_at desc, wr.workflow_id descprovides the tie-break required for the cursor comparison to be stable across rows sharing an updated_at second. - Redis fallback honors
limitand emits a synthetic cursor for parity, but is dev-only; real deployments run the Postgres path.
Frontend — infinite scroll + dynamic polling
- The panel uses
useInfiniteQuerywith anIntersectionObserversentinel at the bottom of the list. When the sentinel intersects (120 px before the actual end), the next page is fetched. - Polling cadence is computed from the loaded pages: if any workflow is in
a non-terminal state (
running/processing/queued/pending), the query refetches every 2 s; otherwise every 6 s. refetchIntervalInBackground: trueandrefetchOnWindowFocus: true— the panel keeps polling even when the tab is blurred (this was the primary “the list never updates” symptom: the previous fixedrefetchIntervaldefaulted to pausing on blur).- End-of-list marker — once
hasNextPageis false, a muted “No more workflows” line is rendered at the bottom. - Row entry animation — each
WorkflowRowis wrapped in a Framer Motionmotion.divwithinitial={{ opacity: 0, y: 12 }}→animate={{ opacity: 1, y: 0 }}and a per-indexdelay(Math.min(i * 0.03, 0.3)). The cap keeps a freshly-loaded second page from stalling visibly. Rows also carrylayoutso a workflow that bumps to the top on update glides instead of snapping.
Avatar identity normalization
- All frontend write sites that previously emitted
created_by: ${selectedUser}-uinow emitselectedUserdirectly. Five files touched:components/layout/footer/task-output-modal.tsx,components/graph/new-system-dialog.tsx,components/workflow/editor/workflow-creation-screen.tsx,components/workflow/editor/prompt-editor.tsx,components/workflow/panels/workflow-prompt-composer.tsx. - Defensive read normalization: when deriving the avatar slug, the panel
strips a trailing
-uiso legacy DB rows resolve to the same/team/{author}.pngas fresh ones. TheonError→ initials fallback remains for unknown authors. - Known team images live at
apps/frontend/public/team/(currentlyazat.png,terry.png). Adding a new teammate means dropping<handle>.pngin there and using the matchingcreated_by.
Impact scope
- Wire-compatible: existing callers that hit
GET /v1/workflowswithout query params still get a valid response —limitdefaults to 20 (down from the prior hard cap of 200) andnext_cursoris additive. - Auth follow-up: once Cerebrum auth lands,
actor.user_idwill populatecreated_byserver-side and the frontend-side defaults become a safety net only. The Rustunwrap_or_else(|| "user".to_string())atroutes.rs:283will then be removed.