Skip to content

Add native sync queue for Duck.ai chat updates#8640

Merged
GerardPaligot merged 13 commits into
developfrom
feature/gerard/chat-history-sync
May 22, 2026
Merged

Add native sync queue for Duck.ai chat updates#8640
GerardPaligot merged 13 commits into
developfrom
feature/gerard/chat-history-sync

Conversation

@GerardPaligot
Copy link
Copy Markdown
Contributor

@GerardPaligot GerardPaligot commented May 20, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/project/481882893211075/task/1214962880887900?focus=true

Description

Wires Duck.ai chat pin/unpin into the existing native sync pipeline so the pinned state propagates to the user's other signed-in devices, and introduces a headless ChatHistoryReader so the chat-history screen picks up changes made elsewhere.

The rename UI is gated behind a new remote feature flag (renameChat, default OFF) — when enabled it persists locally, but cross-device propagation is intentionally deferred until JWE encryption lands on native (server requires titles to be JWE-encrypted, which native cannot produce today).

Steps to test this PR

Note

Prerequisites:

  • Install Internal Debug.
  • Set up Sync between two devices (e.g. this Android + Duck.ai web, or two Android devices on the same account).
  • Create at least 2 chats in Duck.ai so there's something to rename / pin.
  • On device A: 3-dot on a Recent row → Pin → on device B or web app, refresh the chat list → confirm the chat now appears in the Pinned section.
  • On device A: 3-dot on a Pinned row → Unpin → on device B or web app → confirm the chat moves back to Recent.
  • On device B: pin a chat → on device A, open the chat-history screen → confirm the chat appears in the Pinned section on device A.

UI changes

Screen.Recording.2026-05-21.at.15.12.46.mov

Note

Medium Risk
Medium risk because it changes the sync patch payload/queueing logic and adds a headless WebView-based refresher that could affect performance and sync correctness.

Overview
Adds a new native sync queue for Duck.ai chat updates (separate from deletions) and extends sync patch generation to send pinned-state changes, prioritizing deletions when a chatId is queued for both.

Pins/unpins now enqueue an update and trigger sync, bulk chat clearing also clears both deletion and update queues, and patch success removes acknowledged ids from both queues. The chat history screen additionally refreshes via a new headless ChatHistoryReader, and the per-row Rename action is now gated behind a new remote flag renameChat (default OFF).

Reviewed by Cursor Bugbot for commit d6eef35. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@GerardPaligot GerardPaligot marked this pull request as ready for review May 21, 2026 16:00
@GerardPaligot GerardPaligot force-pushed the feature/gerard/chat-history-sync branch from 5568bdf to 6112df7 Compare May 21, 2026 16:34
Server-side ai_chats expects title encrypted via JWE (per-purpose
protected key), not SyncCrypto. Native lacks that infra today, so
PATCHing a SyncCrypto-encoded title would corrupt server state for
the FE. Drop title from outgoing update entries and stop triggering
sync on rename. Pin/unpin still propagate (plain wire format).

Add ChatHistoryReader: loads Duck.ai in a hidden WebView on chat
history screen onResume so the FE can fetch /sync/ai_chats, decrypt
JWE titles, and push state into native Room via the
duckAiNativeStorage.putChat bridge.
The subscriptions bridge wiring and the post-load settle delay were
inherited from the visible Duck.ai fragment but neither serves the
receive-side chat-sync flow — drop both. The settle delay also wasn't
load-bearing once the fragment owns the WebView lifetime: the SPA's
putChat fan-out continues after refresh() returns and the Room flow
emits as writes land.

Wire tearDown() in ChatHistoryFragment.onDestroyView so the WebView's
destroy() is called exactly once when the screen exits.
Add DuckChatFeature.renameChat() (default false) and use it to
show/hide the Rename row in the chat-history overflow popup. The
toggle is read off the main thread each time the popup is opened so
remote-config changes take effect on the next overflow tap without
restarting the fragment.
The renameChat path no longer triggers sync since the title can't be
sent without JWE. The two tests asserting the old sync interaction
were left behind from the prior commit — remove them. The success
and failure paths are still covered by the two tests just above.
The encryption hook was injected to wrap the chat title for outgoing
PATCHes, but the title was dropped from the wire payload when rename
sync was deferred to the JWE work. SyncCrypto wraps with the sync
master key — the wrong algorithm for ai_chats titles anyway — so the
follow-up rename work will bring its own primitives. Remove the
constructor param, the test mock, the unused stub, and the orphaned
imports.
@GerardPaligot GerardPaligot force-pushed the feature/gerard/chat-history-sync branch from 6112df7 to 810c009 Compare May 22, 2026 13:45
Match the established codebase convention for headless WebViews used
for FE-to-native communication: PirDetachedWebViewProvider,
RealDuckChatDeleter, and RealChatSuggestionsReader all hold a
WebView(context) in a private field with no view-hierarchy attachment.
The earlier attached-INVISIBLE shape was an unverified hypothesis
that the SPA needed a real laid-out window to trigger chat-list
hydration — never confirmed by a putChat observation.

Drop the Activity cast, the findViewById(android.R.id.content)
lookup, addView/visibility setup, and the removeView line in
tearDown. Tightens getOrCreateWebView's return type since the only
nullable paths are gone.
DuckChatDataClearingPlugin.deleteAllChats() was clearing the
pendingChatDeletions queue but not the new pendingChatUpdates queue,
so stale pin/unpin update entries would survive a fire-all
indefinitely. Add the symmetric clearPendingChatUpdates() call inside
the same successful-deletion branch, and lock the behaviour in with
matching positive and negative test assertions.
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e732534. Configure here.

runCatching catches every Throwable including CancellationException,
which resets the coroutine's cancellation state and breaks structured
concurrency — a child coroutine that was supposed to be cancelled
silently keeps running. Switch to the same try / rethrow-cancellation
/ catch-exception cascade RenameChatViewModel uses in this module.
@GerardPaligot GerardPaligot merged commit 9d3b856 into develop May 22, 2026
13 checks passed
@GerardPaligot GerardPaligot deleted the feature/gerard/chat-history-sync branch May 22, 2026 18:47
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.

2 participants