Summary
A persistent "Files in this chat" surface inside the chat view that lists every artifact the agent has generated in the current thread, with per-row Download / Reveal-in-folder / Delete affordances. Survives app restarts (for files created in-session) so the user can return to a chat and still grab a deck they made earlier.
Problem
The inline ArtifactCard shipped in #2779 (PR #3017) sits above the composer as a single ephemeral card per artifact:
- Scrolling, sending the next turn, or restarting the app loses the card.
- The
artifactsByThread Redux slice is in-memory only (app/src/store/index.ts:158 registers chatRuntimeReducer directly, no persistReducer), so files vanish across app restarts even though the underlying payload is still on disk under <workspace>/artifacts/<uuid>/.
- There is no place to enlist all files involved in a chat.
User feedback (live dev:app test, 2026-05-30): "there should be a place to enlist all the files involved in a chat" — flagged after a generated climate-change-an-overview.pptx was visible once, then the user couldn't find it again after a new turn was sent.
Solution
Frontend-only addition layered on top of the artifact pipeline already shipped in #2779:
ChatFilesChip (new component) — header chip with paperclip icon + count badge. Hidden when artifact count = 0. Owns popover open/close + click-outside + Esc.
ChatFilesPanel (new component) — popover panel listing per-thread artifacts (title + kind icon + size + Download + Reveal + Delete with confirm modal). Uses useT() + Tailwind + inline SVGs (mirrors ArtifactCard.tsx style — no new icon dep).
- Persistence — wrap
chatRuntimeReducer with persistReducer + a createTransform that keeps only status === 'ready' entries on storage write (in_progress / failed stay session-scoped — should not survive a restart).
- Delete flow — frontend
deleteArtifact() service + removeArtifactForThread reducer; optimistic FE removal with rollback on RPC error. RPC openhuman.ai_delete_artifact already exists.
- Above-composer card on
ready — filter status === 'ready' OUT of the existing above-composer block (delegated to the panel). in_progress / failed keep showing there for live feedback.
Known backend gap (deferred to a follow-up)
ArtifactMeta (src/openhuman/artifacts/types.rs:80-102) has no thread_id field, and openhuman.ai_list_artifacts is workspace-scoped (only offset / limit). So a true cold-load-from-RPC for a single thread is not possible today. This issue ships persisted-slice + RPC fetch for the artifacts the slice knows about; a follow-up should persist thread_id into ArtifactMeta + add a thread_id filter param on ai_list_artifacts for full cold-load support.
Files (estimated)
| File |
New/Modified |
Est. LOC |
app/src/components/chat/ChatFilesPanel.tsx |
NEW |
200 |
app/src/components/chat/ChatFilesChip.tsx |
NEW |
90 |
app/src/components/chat/__tests__/ChatFilesPanel.test.tsx |
NEW |
220 |
app/src/components/chat/__tests__/ChatFilesChip.test.tsx |
NEW |
90 |
app/src/store/__tests__/chatRuntimeSlice.test.ts |
MODIFIED |
+80 |
app/src/services/__tests__/artifactDownloadService.test.ts |
NEW |
120 |
app/src/pages/Conversations.tsx |
MODIFIED |
+30 / -20 |
app/src/store/chatRuntimeSlice.ts |
MODIFIED |
+35 |
app/src/store/index.ts |
MODIFIED |
+40 |
app/src/services/artifactDownloadService.ts |
MODIFIED |
+35 |
app/src/lib/i18n/en.ts |
MODIFIED |
+12 |
app/src/lib/i18n/{ar,bn,de,es,fr,hi,id,it,ko,pl,pt,ru,zh-CN}.ts × 13 |
MODIFIED |
+156 |
Total estimated: 1100–1700 LOC (FE-only; 0 Rust, 0 Tauri).
Acceptance criteria
Related
Summary
A persistent "Files in this chat" surface inside the chat view that lists every artifact the agent has generated in the current thread, with per-row Download / Reveal-in-folder / Delete affordances. Survives app restarts (for files created in-session) so the user can return to a chat and still grab a deck they made earlier.
Problem
The inline
ArtifactCardshipped in #2779 (PR #3017) sits above the composer as a single ephemeral card per artifact:artifactsByThreadRedux slice is in-memory only (app/src/store/index.ts:158registerschatRuntimeReducerdirectly, nopersistReducer), so files vanish across app restarts even though the underlying payload is still on disk under<workspace>/artifacts/<uuid>/.User feedback (live dev:app test, 2026-05-30): "there should be a place to enlist all the files involved in a chat" — flagged after a generated
climate-change-an-overview.pptxwas visible once, then the user couldn't find it again after a new turn was sent.Solution
Frontend-only addition layered on top of the artifact pipeline already shipped in #2779:
ChatFilesChip(new component) — header chip with paperclip icon + count badge. Hidden when artifact count = 0. Owns popover open/close + click-outside + Esc.ChatFilesPanel(new component) — popover panel listing per-thread artifacts (title + kind icon + size + Download + Reveal + Delete with confirm modal). UsesuseT()+ Tailwind + inline SVGs (mirrorsArtifactCard.tsxstyle — no new icon dep).chatRuntimeReducerwithpersistReducer+ acreateTransformthat keeps onlystatus === 'ready'entries on storage write (in_progress/failedstay session-scoped — should not survive a restart).deleteArtifact()service +removeArtifactForThreadreducer; optimistic FE removal with rollback on RPC error. RPCopenhuman.ai_delete_artifactalready exists.ready— filterstatus === 'ready'OUT of the existing above-composer block (delegated to the panel).in_progress/failedkeep showing there for live feedback.Known backend gap (deferred to a follow-up)
ArtifactMeta(src/openhuman/artifacts/types.rs:80-102) has nothread_idfield, andopenhuman.ai_list_artifactsis workspace-scoped (onlyoffset/limit). So a true cold-load-from-RPC for a single thread is not possible today. This issue ships persisted-slice + RPC fetch for the artifacts the slice knows about; a follow-up should persistthread_idintoArtifactMeta+ add athread_idfilter param onai_list_artifactsfor full cold-load support.Files (estimated)
app/src/components/chat/ChatFilesPanel.tsxapp/src/components/chat/ChatFilesChip.tsxapp/src/components/chat/__tests__/ChatFilesPanel.test.tsxapp/src/components/chat/__tests__/ChatFilesChip.test.tsxapp/src/store/__tests__/chatRuntimeSlice.test.tsapp/src/services/__tests__/artifactDownloadService.test.tsapp/src/pages/Conversations.tsxapp/src/store/chatRuntimeSlice.tsapp/src/store/index.tsapp/src/services/artifactDownloadService.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/{ar,bn,de,es,fr,hi,id,it,ko,pl,pt,ru,zh-CN}.ts× 13Total estimated: 1100–1700 LOC (FE-only; 0 Rust, 0 Tauri).
Acceptance criteria
chat.files.chip.aria("{count} files in this chat").openhuman.ai_delete_artifact→ on RPC error, re-insert + show toastchat.files.delete.failed..pptxsurvives app quit + relaunch; onlystatus === 'ready'entries are persisted (in_progress / failed are session-only viacreateTransformoutbound filter).readyto panel — the existingArtifactCardrender block keeps in_progress / failed cards visible there but hidesreadycards (they live in the panel only). No double-rendering.chat.files.panel.empty("No files yet. Ask the agent to generate one.") when count = 0 (chip is hidden, so this is only seen if the panel is open while the last artifact is deleted).pnpm i18n:check(parity) andpnpm i18n:english:check(no English leaks) both green for the new keys..github/workflows/pr-ci.yml. New Vitest suites cover: panel populated render, optimistic delete + RPC rollback, download/reveal delegation, confirm-modal cancel path, chip hidden-when-zero, chip count badge, slice persist-transform filter.Related
artifactsdomain)thread_idintoArtifactMeta+ addthread_idfilter param onopenhuman.ai_list_artifactsso artifacts from old sessions (pre-persistence) become listable.