Skip to content

feat(agents) use a TanStack DB union query for the timeline#4347

Open
samwillis wants to merge 16 commits into
mainfrom
entity-timeline-query
Open

feat(agents) use a TanStack DB union query for the timeline#4347
samwillis wants to merge 16 commits into
mainfrom
entity-timeline-query

Conversation

@samwillis
Copy link
Copy Markdown
Contributor

Summary

  • Adds createEntityTimelineQuery, a multi-source TanStack DB timeline query for agent streams.
  • Migrates the agents UI timeline to consume the query directly, with live child collections for run items so streamed text updates through DB's fine-grained reactivity instead of rematerializing the whole chat timeline.
  • Adds stable timeline ordering for streamed and optimistic rows, improves pending queued-message placement, and keeps the chat pinned to the bottom during streaming.
  • Adds a patch changeset for @electric-ax/agents-runtime and @electric-ax/agents-server-ui.

Dependencies

This PR depends on TanStack DB support from:

Test plan

  • pnpm --filter @electric-ax/agents-runtime typecheck
  • pnpm --filter @electric-ax/agents-runtime build
  • pnpm --filter @electric-ax/agents-server-ui typecheck
  • Manual browser smoke testing of sending messages and streamed agent responses in the agents UI

Made with Cursor

@netlify
Copy link
Copy Markdown

netlify Bot commented May 18, 2026

Deploy Preview for electric-next ready!

Name Link
🔨 Latest commit f75fdb5
🔍 Latest deploy log https://app.netlify.com/projects/electric-next/deploys/6a0dd1d2894d4c0008f9eab4
😎 Deploy Preview https://deploy-preview-4347--electric-next.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@samwillis samwillis force-pushed the entity-timeline-query branch from 78078c2 to 065f4fc Compare May 18, 2026 14:34
@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

❌ Patch coverage is 11.89591% with 474 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.33%. Comparing base (e6a0bff) to head (32dc629).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/agents-runtime/src/entity-timeline.ts 5.40% 175 Missing ⚠️
...agents-server-ui/src/components/EntityTimeline.tsx 0.00% 122 Missing ⚠️
.../agents-server-ui/src/components/AgentResponse.tsx 0.00% 103 Missing ⚠️
...es/agents-server-ui/src/hooks/useEntityTimeline.ts 0.00% 31 Missing ⚠️
packages/agents-runtime/src/entity-stream-db.ts 61.70% 18 Missing ⚠️
...agents-server-ui/src/components/views/ChatView.tsx 0.00% 14 Missing ⚠️
packages/agents-server-ui/src/lib/sendMessage.ts 0.00% 11 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #4347       +/-   ##
===========================================
+ Coverage   36.13%   59.33%   +23.20%     
===========================================
  Files         173      292      +119     
  Lines       12759    29246    +16487     
  Branches     4240     7891     +3651     
===========================================
+ Hits         4610    17353    +12743     
- Misses       8136    11876     +3740     
- Partials       13       17        +4     
Flag Coverage Δ
packages/agents 67.52% <ø> (?)
packages/agents-mcp 77.54% <ø> (?)
packages/agents-runtime 79.83% <24.90%> (?)
packages/agents-server 73.89% <ø> (-0.04%) ⬇️
packages/agents-server-ui 6.55% <0.00%> (-0.12%) ⬇️
packages/electric-ax 42.61% <ø> (?)
packages/experimental 87.73% <ø> (?)
packages/react-hooks 86.48% <ø> (?)
packages/start 82.83% <ø> (?)
packages/typescript-client 94.39% <ø> (?)
packages/y-electric 56.05% <ø> (?)
typescript 59.33% <11.89%> (+23.20%) ⬆️
unit-tests 59.33% <11.89%> (+23.20%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@KyleAMathews
Copy link
Copy Markdown
Contributor

Overall I think this is the right approach. Moving the agents timeline from the aggregate createEntityIncludesQuery -> normalize -> buildTimelineEntries path to a row-oriented createEntityTimelineQuery is the right architecture for streaming. Modeling the timeline as a multi-source TanStack DB query also seems like the right use of the DB PRs: source alias as the discriminant, outer $key as timeline identity, and live child collections for run contents.

A few concrete things I noticed / would double-check:

1. Synthetic pending row $key may not match TanStack’s union key encoding

I saw code constructing a bridged pending timeline row with a key like:

$key: `inbox:${inlinePendingInbox.key}`

But the TanStack multi-source key encoding in the dependency PR appears to encode branch keys more carefully than plain ${alias}:${key} — e.g. distinguishing string/number keys.

If this synthetic row ever needs to reconcile with, de-dupe against, or be replaced by the real live-query row, the key may not match the real outer $key. That could cause a remount, duplicate bubble, or failed de-dupe.

I’d prefer not to manually reconstruct TanStack’s internal union $key format in Electric. Can this use the actual live-query row, or de-dupe using the source-local inbox key instead?

2. Tool-call run items may be keyed inconsistently

For nested run items, I think text rows use the union row key:

key={item.$key}

but tool calls may use the source-local tool-call key:

key={item.toolCall.key}

Since run.items is itself a multi-source union, I think rendered children should consistently use item.$key. Otherwise text/tool-call rows with overlapping source-local keys could collide or remount incorrectly.

This seems like a small concrete fix.

3. Ordering probably needs an explicit tie-breaker

The _timeline_order direction is good — especially because streaming updates should not move old timeline rows.

But the ordering looks like it often relies on a single order token with fallback "~":

coalesce(..., `~`)

If two rows have the same _timeline_order, or if multiple rows fall back to "~", then ordering may depend on underlying insertion/subscription behavior unless TanStack DB guarantees stable ordering for equal sort values.

This is probably worth making deterministic with a secondary stable key, especially at the top-level timeline and nested run-items level.

Not a design objection — just trying to avoid nondeterministic timeline order in edge cases.

4. Optimistic reconciliation may flicker if server keys differ

The optimistic inbox keys look like they’re generated client-side, e.g. something like:

optimistic-${Date.now()}-${index}

If the server-confirmed inbox row comes back with a different key, does TanStack DB reconcile it as the same row, or does the optimistic row disappear and the server row appear as a new row?

If it’s the latter, this could produce a transient duplicate/remount/flicker. Maybe the mutation layer already handles this, but I’d want to confirm because it’s the most likely place for visible weirdness in the optimistic path.

5. $synced === false may be broader than “optimistic pending insert”

Replacing custom _optimistic with TanStack’s $synced virtual prop seems right.

One question: does the timeline filter include any unsynced inbox row? If so, is that intentionally equivalent to “local optimistic pending message”? $synced === false could also describe local edits/cancels/other unsynced inbox mutations.

If only optimistic pending inserts should appear inline, the condition may need to be a bit narrower than just unsynced.

6. First pending-message bridge may affect edit/cancel affordances

The “bridge the first pending queued message into the timeline” behavior makes sense.

One edge I’d check in code: if that first pending message is removed from the pendingMessages list passed to the composer/drawer, can the user still edit/cancel it while it is displayed inline?

If not, this may be a small UX regression: the first queued message is visible inline, but no longer editable/cancellable like the rest of the queue.

7. Completion scroll looks like it may force-scroll even after user scrolled up

The scroll changes mostly look good, but this effect looked suspicious:

if (!previousStreamingAgentKey || lastStreamingAgentKey) return

isNearBottom.current = true
setShowJumpToBottom(false)
scrollToTimelineEnd()

If I’m reading this correctly, when a streaming run completes, it resets isNearBottom and scrolls to bottom unconditionally.

That means a user who scrolls up during streaming may get yanked back down when the run completes. Usually I’d expect completion to scroll only if the user was still pinned.

Similarly, if scrollToTimelineEnd() schedules a RAF and the user scrolls up before that RAF fires, it might still write scrollTop = scrollHeight. For non-forced auto-scroll paths, it may be safer to re-check “still pinned” inside the RAF.

8. caseWhen cast should go away before this is released

I saw this kind of cast:

const { caseWhen } = TanStackDB as typeof TanStackDB & {
  caseWhen: <T>(condition: unknown, value: T) => T
}

Totally fine while stacked on unreleased TanStack DB work, but I wouldn’t want that in the final version. Once the TanStack PRs land, Electric should consume the real public export/types.

Things I would not block on:

  • The general multi-source from approach. That looks like the right model to me.

samwillis and others added 11 commits May 20, 2026 16:04
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Use a fine-grained timeline query for the agents UI so streamed run items update through TanStack DB instead of rematerializing the whole chat timeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rely on TanStack DB's virtual $synced prop instead of carrying a custom _optimistic field through inbox rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use the timeline order token for pending local inbox rows so optimistic timeline ordering no longer depends on the legacy _seq field.

Co-authored-by: Cursor <cursoragent@cursor.com>
Bridge the first pending queued message into the timeline while there is no active run so it does not briefly appear in the pending drawer.

Co-authored-by: Cursor <cursoragent@cursor.com>
Document the multi-source row structure and live child collections returned by createEntityTimelineQuery.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pin the chat timeline on content resize while near the bottom and force a final bottom scroll when a streaming run completes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mark the agents runtime and server UI for a patch release because the timeline query now uses fine-grained TanStack DB reactivity.

Co-authored-by: Cursor <cursoragent@cursor.com>
Point the agents packages at the TanStack DB PR build and migrate the timeline query off the old multi-source from API.

Co-authored-by: Cursor <cursoragent@cursor.com>
@samwillis samwillis force-pushed the entity-timeline-query branch from 065f4fc to f75fdb5 Compare May 20, 2026 15:22
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

Electric Agents Desktop Builds

Build artifacts for commit 32dc629.

Platform Status Artifact
macOS Apple Silicon Passed DMG
macOS Intel Passed DMG
Windows x64 Passed Installer
Linux x64 Passed AppImage / deb

Workflow run

KyleAMathews and others added 2 commits May 20, 2026 10:41
Switch the agents packages from the temporary TanStack DB PR tarballs to the published npm releases now that unionAll has shipped.

Co-authored-by: Cursor <cursoragent@cursor.com>
@samwillis samwillis marked this pull request as ready for review May 21, 2026 09:21
samwillis and others added 3 commits May 21, 2026 10:23
Remove the planning document from the branch while keeping local copies out of version control.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use stable local keys for inline pending rows, deterministic timeline tie-breakers, public TanStack caseWhen types, and safer pinned-scroll behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@samwillis
Copy link
Copy Markdown
Contributor Author

Review from @KyleAMathews is addressed

@samwillis samwillis changed the title Add reactive agents timeline query feat(agents) use a TanStack DB union query for the timeline May 21, 2026
}))

return q
.unionAll({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants