Skip to content

Files in this chat — persistent per-chat artifact registry #3024

Description

@oxoxDev

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:

  1. ChatFilesChip (new component) — header chip with paperclip icon + count badge. Hidden when artifact count = 0. Owns popover open/close + click-outside + Esc.
  2. 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).
  3. 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).
  4. Delete flow — frontend deleteArtifact() service + removeArtifactForThread reducer; optimistic FE removal with rollback on RPC error. RPC openhuman.ai_delete_artifact already exists.
  5. 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

  • ChatFilesChip in header — paperclip + count badge appears in the chat header when at least one artifact exists for the active thread; hidden when count = 0; aria-label chat.files.chip.aria ("{count} files in this chat").
  • ChatFilesPanel popover — opens on chip click; closes on Esc + click-outside; each row shows kind icon + title + human-readable size; Download / Reveal / Delete buttons per row.
  • Delete with confirm — Delete row → confirm modal → optimistic remove from slice + RPC openhuman.ai_delete_artifact → on RPC error, re-insert + show toast chat.files.delete.failed.
  • Cross-restart persistence — generated .pptx survives app quit + relaunch; only status === 'ready' entries are persisted (in_progress / failed are session-only via createTransform outbound filter).
  • Above-composer card delegates ready to panel — the existing ArtifactCard render block keeps in_progress / failed cards visible there but hides ready cards (they live in the panel only). No double-rendering.
  • Empty state — panel shows 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).
  • i18n parity — 12 new keys × 14 locales; pnpm i18n:check (parity) and pnpm i18n:english:check (no English leaks) both green for the new keys.
  • Diff coverage ≥ 80% — the implementing PR meets the changed-lines coverage gate per .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

Metadata

Metadata

Assignees

Labels

featureNet-new user-facing capability or product behavior.react-uiReact app work in app/src: pages, components, providers, store, and UX.subtaskSubtask of a larger tracked effort.
No fields configured for Feature.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions