iOS: Wire chat-history sheet — row taps, new chat, search#5204
Open
SabrinaTardio wants to merge 3 commits into
Open
iOS: Wire chat-history sheet — row taps, new chat, search#5204SabrinaTardio wants to merge 3 commits into
SabrinaTardio wants to merge 3 commits into
Conversation
- AIChatHistoryViewModel: rename `openDuckAiTapped` → `newChatTapped` (used by both the empty-state CTA and the toolbar compose button — they produce the same outcome, opening Duck.ai on a fresh chat). Add `chatTapped(at:)` for row taps, which resolves the chat at the given index path and emits the chatId to the delegate. - Delegate is renamed accordingly: `viewModelDidRequestOpenNewChat` replaces the old `viewModelDidRequestOpenDuckAi`, and a new `viewModelDidRequestOpenChat(chatId:)` carries the row-tap intent. - AIChatHistoryViewController: forward `didSelectRowAt` to `chatTapped(at:)`. Wire the compose `UIBarButtonItem` to `newChatTapped`. - AIChatHistoryEmptyStateView: button calls `newChatTapped`. - MainViewController: implement both delegate methods. New chat dismisses + `openAIChat()`. Row tap builds the URL via `aiChatSettings.aiChatURL.withChatID(chatId)` then routes through the existing `onChatHistorySelected(url:)` path — same behaviour as the autocomplete chat-history tap. - Add view-model tests for both new intents (valid + invalid index paths) and update the mock delegate. Pixels deferred to the end of the feature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filter the chat list by title as the user types in the search bar at
the top of the sheet. Implementation lives in the view model — the
view controller just forwards `UISearchBarDelegate` text changes.
- AIChatHistoryViewModel: add `@Published query: String` + an
`updateQuery(_:)` intent. Restructure the subscription to
`Publishers.CombineLatest(chats, $query.throttle(latest: true))` and
apply a case-insensitive substring filter on title before splitting
into pinned / recent. `throttle(latest: true)` emits the first value
immediately (no initial wait) and forwards the latest value at most
once per 150ms while the user types.
- The chats publisher is materialised into `Result<[DuckAiChat], Error>`
so a storage failure becomes a sentinel value the combined publisher
can react to (sets `loadFailed`, clears the lists) without tearing
down the stream.
- AIChatHistoryViewController conforms to `UISearchBarDelegate` and
routes `textDidChange` to `viewModel.updateQuery(_:)`.
- Behaviour during search mirrors Android: pinned / recent split is
preserved (each side is filtered against the query); when no results
match, the existing empty state shows. Reusing the empty-state copy
("No recent chats") during search is intentional parity — design
follow-up to refine the copy.
- Live updates compose for free: storage writes re-fire the chats
publisher, CombineLatest fires with the latest chats + the current
query, and the filtered list updates automatically. Verified with the
reactive pipeline established in #5170.
- Tests cover filter-by-title (case-insensitive), empty query restores
full list, whitespace-only query restores full list, and the
no-matches → empty state path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 fd54d45. Configure here.
Only show the illustrated empty state when the user has no chats AND isn't searching. When a search returns no matches, leave the table view visible — its search-bar header stays put so the user can backspace to clear the query without dismissing the sheet. Behaviour: - No chats, no query → illustrated empty state (welcome). - No chats, query active → empty table with search bar header. - Chats present, no matches → empty table with search bar header. - Chats present, with matches → filtered list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bunn
reviewed
Jun 1, 2026
| // `throttle(latest: true)` emits the first value immediately (no initial wait on subscribe) | ||
| // and then forwards the latest value at most once per interval while the user types. | ||
| let queryStream = $query | ||
| .throttle(for: .milliseconds(150), scheduler: DispatchQueue.main, latest: true) |
Contributor
There was a problem hiding this comment.
Maybe a debounce would be better here?
Bunn
approved these changes
Jun 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Task/Issue URL: https://app.asana.com/1/137249556945/project/414235014887631/task/1215232204008019?focus=true
Tech Design URL:
CC:
Description
Implements Row tap, new tab button and search
chatID=…). Routes through the existingonChatHistorySelected(url:)path used by autocomplete history taps, so navigation behaviour (boundary handling, NTP transform, etc.) is identical.viewModelDidRequestOpenNewChat).ChatHistoryViewModel.reduce).Testing Steps
aiChatNativeChatHistoryON.chatID=…).Impact and Risks
Low. All changes are inside the iOS app target — no shared-package changes. Behaviour is gated behind
aiChatNativeChatHistory(defaultinternalOnly). The new code is purely additive on top of the publisher pipeline landed in #5170.What could go wrong?
testChatTapped_validIndexPath_notifiesDelegateWithChatIdand the matching invalid-index test.CombineLatestis misbehaving with the throttled query stream. Verified manually (live update while filtered, step 9) and covered by tests for empty / whitespace / no-match queries..storageUnavailable; the error alert from iOS: Wire Duck.ai chat-history view model to local data source #5170 fires regardless of whether search is active.Quality Considerations
CombineLatestemission; sub-millisecond at expected chat counts. Throttle (150ms,latest: true) caps the reload rate during fast typing.UserTextfrom iOS: Wire Duck.ai chat-history view model to local data source #5170 (stillNotLocalizedString; final copy follow-up before public rollout).Notes to Reviewer
Internal references:
Definition of Done | Engineering Expectations | Tech Design Template
Note
Low Risk
iOS-only, feature-flagged (
aiChatNativeChatHistory), additive wiring on an existing publisher; navigation reuses established Duck.ai URL paths.Overview
Wires the native Chats history sheet so users can open a specific chat, start a new one, and filter the list by title.
Row selection dismisses the sheet and navigates to Duck.ai with a
chatIDURL via the existingonChatHistorySelectedpath (same as autocomplete history). Compose and the empty-state Open Duck.ai button both request a fresh chat through a renamed delegate (viewModelDidRequestOpenNewChat).Search drives a throttled (150ms) case-insensitive title filter while keeping pinned/recent sections; chat updates from storage still merge via
CombineLatest, with load failures caught so the stream does not terminate. The illustrated empty state appears only when there are no chats and the search field is empty—zero search matches keep the table and search bar visible.Reviewed by Cursor Bugbot for commit f6c9e10. Bugbot is set up for automated code reviews on this repo. Configure here.