Wire per-row Download in chat history overflow menu#8610
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
13f65fe to
7814890
Compare
c9c0069 to
3ade15f
Compare
7814890 to
8ccddb3
Compare
c1b6b95 to
d95ddfc
Compare
0bdc467 to
f77064b
Compare
44f76c2 to
c1a8944
Compare
f77064b to
6530893
Compare
6530893 to
65337eb
Compare
c1a8944 to
160544c
Compare
160544c to
50b6ef6
Compare
65337eb to
c62b930
Compare
38ec1f3 to
19959f6
Compare
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 19959f6. Configure here.
malmstein
left a comment
There was a problem hiding this comment.
smoke-tested on device. the per-row Download from the chat history overflow works as expected, and the new exporter + writer split looks clean and well-covered by tests. one observation worth flagging though: the touch feedback on the overflow trigger (the three-dot icon on each chat history row) is way too big — a large rounded ripple that extends well beyond the icon's touch target. screenshot to follow as a separate PR conversation comment. might be pre-existing rather than introduced here, but worth tightening while we're in this area.
Surfaces "Download complete for <filename>" with a Show action that opens the Downloads screen, matching the design. The ViewModel already emits the filename via ShowDownloadComplete — only the Fragment copy and string format needed updating. Also removes a duplicate setPinned override accidentally added to RecordingRenameRepository, which was breaking compileDebugUnitTestKotlin.
- spotlessApply on three :app files to keep DownloadsScreenNoParams alphabetically after AppScope / DI imports. - Add an instruction attribute to duck_ai_chat_history_download_complete so the MissingInstruction lint rule passes for the %1$s filename arg. - Drop the now-stale setPinned override in RecordingRenameRepository; setPinned is no longer on ChatHistoryRepository after the rebase.
Brings Android's chat-history export in line with the macOS/Windows reference for every chat type. ChatExporter branches by ChatType and returns a sealed ExportResult so the writer can produce either a `.txt` (discussion/voice) or a `.zip` (image-generation, bundling chat.txt with a UTF-8 BOM and the resolved image bytes). Filename pattern switches from the sanitized chat title to the cross-platform `duck.ai_yyyy-MM-dd_HH-mm-ss.<txt|zip>` shape. Turns are now separated with the reference's `--------------------` divider. Voice turns with no model response omit the assistant block entirely. DuckAiChatStore gains openFileRef(uuid) so the impl module can stream image bytes for the zip without leaking the chat-files dir; the same path-traversal guard used by deleteChat applies. Positional fileRef ↔ turn association is a documented known assumption pending confirmation on the FE message schema for image-generation chats.
The on-disk file at <chat-files-dir>/<uuid> is a JSON envelope written by the FE bridge via writeText(params.toString()), with the image bytes base64-encoded inside a "data" field — not raw image bytes. Reading the file as a stream gave back JSON, which is why the image-N.jpeg entries exported for image-generation chats were unreadable. Replace openFileRef(uuid): InputStream? with readFileRef(uuid): FileRefContent? which parses the envelope, decodes the base64 data, and exposes the FE-provided fileName + mimeType. Tests cover plain base64, the data:<mime>;base64,<payload> URL prefix path, missing data field, malformed JSON, and the path-traversal guard. Use java.util.Base64 instead of android.util.Base64 — it's available from Java 8 / API 26, which matches the project's minSdk, and works in pure-JVM unit tests without a Robolectric runner.
Fire FileDownloadCallbackPlugin from the chat-export writer so the browser-menu badge (DownloadBadgePlugin) and any other registered plugins react the same way they do for browser-driven downloads. Without this, a freshly exported chat sat in the Downloads list with no indicator that a new file had arrived. Also drops the speculative MediaScannerConnection.scanFile call: no other code in the project uses it (browser downloads delegate to the system DownloadManager, which notifies MediaScanner internally), and the public-Downloads folder is browseable by file managers without it. Removing it lets us drop the unused Context dependency on the writer and the unused mimeType field on ExportPayload.Text.
Replace the list-scan lookup with the DAO's direct getById path —
one Room query instead of fetching and parsing every chat just to
pick one out by id.
getChatContent stays in place. DuckAiChat is a deliberately reduced
view of the FE-owned JSON ("The raw message tree is not exposed —
consumers get precomputed classification flags instead"), so the
exporter still needs the raw-blob escape hatch to walk messages and
render turn-by-turn output.
Removes the ~30-line model-id → display-string map that ChatExporter
used to source provider attributions from. The exporter now takes a
ModelDisplay? built by the caller; null falls back to rendering the
raw model id ("using the gpt-5.2 Model") so unknown models still
produce a valid export instead of a broken header.
- ChatExporter.export() gains a modelDisplay parameter.
- ChatHistoryRepository.exportChat() threads the resolved display
through to the exporter and now exposes chat.model on
ChatHistoryItem so the ViewModel can do the lookup.
- ModelDisplay moves to the models package next to AIChatModel and
ModelProvider. New helpers — ModelProvider.possessive and
AIChatModel.toModelDisplay() — bridge the live /duckchat/v1/models
shape into the export-header strings.
The wire-up to DuckAiModelManager comes in the next commit; this one
passes null from the ViewModel as a placeholder.
Decision: https://app.asana.com/1/137249556945/task/1215062212938438
…odel
onDownloadRequested now snapshot-reads the /duckchat/v1/models cache
via DuckAiModelManager.modelState and threads the resulting
ModelDisplay through to ChatHistoryRepository.exportChat. The repo
stays a plain data-layer method — no model-manager dependency, no
hidden network side-effect inside the export path.
The lookup chain is:
ChatHistoryItem.model (from this row's DuckAiChat)
-> modelState.models.firstOrNull { it.id == model }
-> aiChatModel.toModelDisplay()
-> "using OpenAI's GPT-5 mini Model" in the export header.
Cache miss (cold start, fetch error, or model id unknown to the API)
falls back to ModelDisplay null, which the exporter renders as
"using the <raw-id> Model". The header stays valid; only the provider
attribution is dropped.
New tests cover the happy path (manager has the model -> repo
receives the full display) and the cache-miss path (repo receives
null and the export still completes).
DuckAiModelManager only fetches /duckchat/v1/models when entitlements change, which can leave the cache stale for users who haven't toggled their plan recently. Without a refresh, onDownloadRequested may read an empty model list and fall back to the raw-id header even though a fresh fetch would have populated the labels. Kick off fetchModels() from the ViewModel's init block on viewModelScope so it starts the moment the screen opens. The call is fire-and-forget — the manager already swallows network failures, and onDownloadRequested still degrades gracefully if the user taps Download before the fetch completes. Test asserts fetchModels is invoked exactly once on construction.
Pass over comments added across this branch: - Drop references to specs/ files (research.md R-16, export-example.txt, chat-example.json); the specs directory is gitignored so the pointers would dangle once merged. - Condense verbose KDoc on ModelDisplay, ChatExporter, ChatExportWriter, ChatHistoryRepository.exportChat, FileRefContent, readFileRef — same contract, fewer words. - Drop test-block narration that restated the assertions it preceded; the assertion messages already carry that text. No behavior change; tests, spotless, and lint stay green.
Last caller was the download row, which now invokes viewModel.onDownloadRequested(...) directly. The matching string duck_ai_chat_history_coming_soon is left intact — menu_chat_history_default.xml still references it as the title of two placeholder menu items.
2a63418 to
d02b9ed
Compare


Task/Issue URL: https://app.asana.com/1/137249556945/task/1214820120386826?focus=true
Description
Wires the Download action on the Duck.ai chat history screen so a chat is exported as a plain-text
.txtfile saved to the device's Downloads folder and registered with the in-app Downloads screen. A snackbar confirms the save with the resulting filename and offers a Show action that opens the Downloads screen.Steps to test this PR
Note
Prerequisites:
duckAiChatHistory(self,historyScreen) is ON andduckChat → useNativeStorageChatDatais ON.Happy path
.txtfile listed.Guards
<title>-1.txt, not an overwrite.UI changes
Note
Medium Risk
Writes user chat content and images to external storage and decodes native file refs; mistakes could affect privacy or corrupt exports, but scope is localized to Duck.ai history/download flows.
Overview
Implements Download on Duck.ai chat history rows: chats export to the public Downloads folder as cross-platform plain text (or a zip with
chat.txtand images for image-generation chats), with timestampedduck.ai_*filenames and-1,-2collision suffixes. Exports register in the in-app Downloads list and trigger the same “new download” callbacks as browser downloads.The history UI shows a completion snackbar with Show to open Downloads; failures show an error snackbar.
DownloadsScreensmoves from the app module intodownloads-api(with navigation-api), and call sites update imports accordingly.DuckAiChatStoregainsgetChatContentandreadFileRef(base64 / data-URL decoding, path traversal checks) so exports can read stored JSON and image blobs. A small layout tweak adjusts the row overflow menu hit target.Reviewed by Cursor Bugbot for commit d02b9ed. Bugbot is set up for automated code reviews on this repo. Configure here.