Skip to content

Add fire actions to native chat history screen#8579

Open
GerardPaligot wants to merge 20 commits into
developfrom
feature/gerard/chat-history-native-fire
Open

Add fire actions to native chat history screen#8579
GerardPaligot wants to merge 20 commits into
developfrom
feature/gerard/chat-history-native-fire

Conversation

@GerardPaligot
Copy link
Copy Markdown
Contributor

@GerardPaligot GerardPaligot commented May 15, 2026

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

Description

Wires the data-clearing actions on the Duck.ai chat history screen so they perform real deletions.

  • Per-row Delete: the 3-dot menu's "Delete" entry on each row now deletes that conversation and closes any Duck.ai tab pointing at it.
  • Select mode: the toolbar overflow → "Select" enters a multi-select mode (long-press a row also enters it). The toolbar title shows the selection count, a Select-all header toggles every visible row, and the fire icon opens a "Delete N chats?" confirmation.
  • Fire-all: tapping the fire icon outside select mode also opens the confirmation. Pinned chats are spared — only Recent chats are cleared. Single-recent uses the existing single-tab path; N ≥ 2 routes through the new Selected variant.

Steps to test this PR

Note

Prerequisites:

  • Install Internal Debug.
  • In Settings → Developer Settings → Feature Flags, confirm duckAiChatHistory (self, historyScreen) is ON and duckChat → useNativeStorageChatData is ON.
  • Create at least 4 chats in Duck.ai; pin 1 or 2 of them.

Per-row Delete

  • Open the Chats screen → tap the 3-dot on any row → tap Delete → confirm the row disappears and the row count decreases.
  • If a Duck.ai tab was open on that chat (open it first in a browser tab), confirm the tab is closed after the delete.

Select mode

  • Tap the toolbar overflow → Select. Confirm the toolbar switches to a count title with a back/exit arrow.
  • Long-press a row from default mode → confirm it enters select mode with that row pre-selected.
  • Tap the Select all header → confirm every visible row is checked and the title updates.
  • With ≥2 selected, tap the fire icon → confirm the dialog title reads "Delete N chats?" → confirm → confirm the selected chats disappear and any matching open Duck.ai tabs are closed.
  • With 1 selected, tap the fire icon → confirm the dialog isn't shown and the single-chat delete flow happens immediately.
  • Tap the back arrow / system back → confirm select mode exits with nothing deleted.

Fire-all

  • In default mode, tap the fire icon → confirm dialog title reads "Delete N chats?" where N is the Recent count (pinned excluded).
  • Confirm → check Recent is empty and Pinned chats are untouched.
  • Repeat with exactly one Recent chat → confirm the chat is fired without showing the dialog.

Empty / edge cases

  • Pin all chats → confirm the fire icon is hidden (Recent is empty, nothing to fire-all).
  • Delete the last chat → confirm the empty state takes over.

UI changes

image image image image image

Note

Medium Risk
Adds new Duck.ai chat deletion paths (single, selected subset, and all) that trigger tab closures and adjust Fire dialog behavior, which could inadvertently close the wrong tabs or delete the wrong chats if URL/chatID matching is incorrect. Changes touch data-clearing orchestration and UI state/selection logic, increasing the chance of edge-case regressions.

Overview
Enables the native Duck.ai chat history screen to perform real deletions: per-row delete now clears that chat, and a new multi-select mode (long-press or overflow → Select) supports select-all, selection count UI, and a Fire confirmation dialog titled via a new plural resource.

Adds a new FireDialogOrigin.ChatHistory flow and a ManualDataClearing.clearSelectedDuckAiChats API so the Fire dialog can clear only a provided set of chat URLs without restarting the process.

Introduces DuckAiTabsCleanupPlugin, a data-clearing plugin that closes open Duck.ai tabs after chat clears (all or selected subset), matching selected clears by chatID query param to handle URL drift; clearSingleTabData also reorders operations to swap the tab URL before dispatching chat clear to avoid the tab being closed by this cleanup.

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

Copy link
Copy Markdown
Contributor Author

GerardPaligot commented May 15, 2026

@GerardPaligot GerardPaligot changed the base branch from develop to graphite-base/8579 May 15, 2026 16:02
@GerardPaligot GerardPaligot force-pushed the feature/gerard/chat-history-native-fire branch from 0b7066d to 50cd37b Compare May 15, 2026 16:02
@GerardPaligot GerardPaligot changed the base branch from graphite-base/8579 to feature/gerard/data-clearing-selected-url May 15, 2026 16:03
Base automatically changed from feature/gerard/data-clearing-selected-url to develop May 15, 2026 17:49
@GerardPaligot GerardPaligot force-pushed the feature/gerard/chat-history-native-fire branch from ecf63f0 to 0c07887 Compare May 15, 2026 20:35
@GerardPaligot GerardPaligot marked this pull request as ready for review May 18, 2026 08:25
Wires the toolbar fire icon to a confirmation dialog reusing
SingleTabFireDialog via a new ChatHistory origin. Confirming clears all
Duck.ai chats and closes any open Duck.ai tabs without touching browser
tabs or stored fire preferences.
Entered from the toolbar overflow Select entry or a row long-press, with a
Select-all header below the toolbar and a brand-blue check swap on each
selected row. The toolbar fire icon doubles as Delete-selected when the
selection is non-empty. Chat-resume, Duck.ai open, and fire-icon routing
move to the ViewModel so the Fragment no longer holds a DuckChatInternal
reference. Multi-chat deletes still flow through the existing repository
path with a TODO for the upcoming `ClearableData.DuckChats.Selected`
pipeline.
Adds a `ClearableData.DuckChats.Selected(chatUrls)` variant and a dedicated
`ManualDataClearing.clearSelectedDuckAiChats` entry point so chat-history
Delete-selected (and the N=1 Fire-all fast-path) clear only the targeted
chats and their open tabs, with sync recording batched per user action.
The chat-history origin carries the URL subset on
`FireDialogOrigin.ChatHistory` so the dialog picks the surgical path on
its own. Tab cleanup that used to live inline in `performGranularClear`
moves to a new `DuckAiTabsCleanupPlugin` that handles both All and
Selected dispatches.
The N≥2 Fire-all path used to wipe every Duck.ai chat (Pinned included)
because it dispatched `DuckChats.All`. Capture the Recent chat IDs into
`PendingConfirmation.FireAll(chatIds)` (mirroring `DeleteSelected`) and
let the dialog clear only those URLs via `ClearableData.DuckChats.Selected`.
Drops the now-unused `fireOptionsOverride` field and the `options`
parameter on `clearDataUsingManualFireOptions` — both were only there to
support the buggy options-driven path.
Tab URLs drift over a session (server redirects, accumulated params,
fragments after replies), so exact-string set membership only matched
the freshly-opened tab and missed older tabs of the same chat. Extract
chatID from both sides of the comparison so every tab pointing at a
cleared chat gets closed, regardless of URL drift.
The 3-dot overflow on each chat row now fires the chat-history-screen
single-delete path (via the existing `dispatchSelectedClear` helper)
instead of showing a Coming-soon snackbar. Pin / Rename / Download
keep the placeholder snackbar.
Make `selectedChatUrls` non-nullable and derive `count` from its size so
the two fields can't drift. An empty set is a valid no-op shape — the
dialog still renders but the destructive dispatch becomes a no-op via
the `Selected(emptySet())` plugin contract.

Bundle plumbing drops `ARG_CHAT_HISTORY_COUNT` and deserialization no
longer downgrades a malformed ChatHistory bundle to the Browser origin
(which would have surfaced the wrong destructive UI). DuckAiTabsCleanup
drops the lint-suppressed `:duckchat-impl` import in favour of the
stable `"chatID"` literal.
Single(url) is just Selected(setOf(url)) on the data side; collapse the two variants
so callers describe the shape in one place. The in-chat fire flow preserves the old
"don't touch this tab" behaviour by replacing the tab URL before dispatching the
clear, so the tabs-cleanup plugin's chatID match no longer finds this tab. Duplicate
tabs at the same chatID now also get closed on the in-chat fire path — same multi-tab
fix we already shipped for the chat-history path.
stateIn(WhileSubscribed) doesn't guarantee subscribers observe the Loading initial
value, so manually awaiting Loading then initial Loaded races against the StateFlow
replay. Route every test in the file through the existing awaitInitialLoaded helper
that already tolerates both orderings.
searchState, confirmationState and modeState were three independent MutableStateFlows
fed into a 4-arg combine. Any action that needed to mutate more than one of them
(only onDeleteSelectedConfirmed today) produced two upstream signals to combine,
which could surface as one or two downstream frames depending on scheduler timing —
the test for that action was flaky in suite runs as a result.

Consolidate into a single UiControls(search, confirmation, mode) behind one
MutableStateFlow. Every action is now one .update { it.copy(...) } → exactly one
combine frame. Restores the deterministic awaitItem contract the test relies on.
Switches the 18 tests we authored on this branch (7 in DataClearingTest, 11 in the
new DuckAiTabsCleanupPluginTest) from the camelCase whenX_thenY style to the
backtick natural-English style already used elsewhere in duckchat-impl tests.
The pre-existing 68 _then tests in DataClearingTest are left alone — only the
tests this branch introduces are renamed.
onDeleteAllClicked routed ChatHistory clears to clearSelectedDuckAiChats but left
shouldRestartAfterClearing at its default true, so after a bounded chat-subset
clear the dialog still called killAndRestartProcess(). Set the flag to false on
that branch, mirroring onDeleteThisTabClicked.
The callback's isEnabled flag was only assigned inside the Loaded branch of
render(); a transition out of Loaded (e.g. last chat deleted externally while
in select mode) left it enabled but with no matching case in handleOnBackPressed,
silently consuming every back press and the toolbar nav arrow.

Re-derive isEnabled from the rendered state at the end of render() via a pure
helper, so every transition recomputes it and any future state defaults to off.
…after suspend

applyDefaultToolbar now clears navigationContentDescription so the back arrow
doesn't keep announcing "Exit selection mode" to screen readers after leaving
select mode — that CD is only set by applySelectModeToolbar on the same toolbar
instance.

renderConfirmation re-checks the fragment tag after createFireDialog returns.
The outer tag check is synchronous, but createFireDialog is suspending, so two
near-simultaneous emissions could both pass the outer guard and race to show
the dialog under the same tag. The post-suspend check serialises on show().
… too

ChatHistory always takes the without-restart path now (shouldRestartAfterClearing
is set to false on that branch), so the dialog emits EVENT_CLEAR_WITHOUT_RESTART_STARTED
instead of EVENT_ON_CLEAR_STARTED. The fragment listener only handled the latter,
leaving controls.confirmation non-null after the user confirmed — which let any
subsequent render re-open the dialog.

Handle both events under the same arm: from the fragment's perspective they
mean the same thing (the dialog is now driving the clear; drop the pending
confirmation state).
The unchecked drawable was r=9 inside a 24x24 viewport (18dp visible)
while the checked drawable is a filled r=12 disc (24dp visible),
matching the Figma spec of a 24px outlined circle. Resize the unchecked
vector to a 2dp ring at full 24dp and drive the swap via a state-list
drawable + duplicateParentState, replacing the imperative
setImageResource toggle in the view holder.
Animate a 300ms Y-axis flip on the row's icon container when the user
toggles a row in or out of selection, swapping the chat-type drawable
and accent background at the apex. The flip runs only on selection
changes detected via DiffUtil.getChangePayload and routed through the
3-arg onBindViewHolder, so scrolling and full rebinds stay snappy.
@GerardPaligot GerardPaligot force-pushed the feature/gerard/chat-history-native-fire branch from fc2d91c to 23c4ac2 Compare May 19, 2026 08:48
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 23c4ac2. Configure here.

Comment thread app/src/main/java/com/duckduckgo/app/global/view/SingleTabFireDialogViewModel.kt Outdated
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.

1 participant