Skip to content

feat: attach files to chat messages as downloadable cards - #404

Open
wuxiangru915 wants to merge 2 commits into
agegr:mainfrom
wuxiangru915:feat/file-attachments
Open

feat: attach files to chat messages as downloadable cards#404
wuxiangru915 wants to merge 2 commits into
agegr:mainfrom
wuxiangru915:feat/file-attachments

Conversation

@wuxiangru915

Copy link
Copy Markdown

Summary

The chat input currently supports image attachments only. This PR adds a file attachment flow (paperclip button next to the image button, plus unified drag-and-drop), so you can attach any file to a message and the agent can read it.

Replaces #344 / #398 (closed). This design does not depend on text/uri-list at all — the concern that made those PRs unreliable in some environments no longer applies.

Behavior

  • 📎 button next to the image button opens a file picker (any type, multiple). Each file is uploaded immediately and shown as a card: file icon + original name + size + remove button — mirroring the image thumbnail row.
  • Drag & drop: dropping a non-image file now attaches it as a card too (uniform with the button; image drops keep the existing image-attach behavior).
  • On send, each attached file is appended to the message text as an [附件] <path> line. The locally-running agent reads the file via that absolute path. In the UI, those lines render as clickable download cards — the raw path is never shown (it's only in the hover title).
  • Download: clicking a card downloads the file via GET /api/attachments.

Why this works everywhere

No browser-path magic: the file content is uploaded through the app (the browser always provides the bytes), so it works identically in Chrome, Firefox, Safari, any OS. Paths come from the server, never from text/uri-list.

Implementation

  • app/api/drop-files/route.ts: multipart upload → persistent ~/.local/share/pi-web/attachments/<uuid>/<original-name> (one uuid subdir per file, so original names are always preserved — uploading a.pdf twice yields two a.pdf files, and a file literally named a-1.pdf never collides). Name sanitized (no traversal), 25MB/file, 100MB total.
  • app/api/attachments/route.ts: serves downloads, path restricted to the attachments directory (?path= absolute, must be inside; ?name= kept for legacy root files).
  • lib/file-attachments.ts: [附件] <path> line parsing/serialization + size formatting. Unit-tested.
  • lib/upload-dropped-files.ts: client upload helper. Unit-tested (stubbed fetch).
  • components/ChatInput.tsx: attachedFiles state (draft-integrated like images/text), upload-on-select, card UI, send plumbing.
  • hooks/useAgentSession.ts: handleSend(message, images, files) appends attachment paths to the message sent to the agent.
  • components/MessageView.tsx: renders [附件] lines as download cards (coexists with the v0.8.7 slash-command collapsing).

Verification

  • 28 unit tests pass (file-attachments, upload-dropped-files, drop-files route, attachments route, MessageView SSR incl. coexisting with slash-collapse tests); tsc --noEmit, eslint, production next build clean.
  • End-to-end in a real browser on top of v0.8.7: button upload ×2 same-name files → two cards both named a.pdf; drag txt → card; message rendering shows cards with hidden raw paths; download works incl. CJK file names; upload persists across server restarts.

@wuxiangru915

wuxiangru915 commented Aug 7, 2026

Copy link
Copy Markdown
Author

Here's a screenshot of the file attachment cards in action (📎 button + drag & drop attach files as cards with original names, sizes and remove buttons; sent messages render them as clickable download cards):

file attachment cards

@wuxiangru915
wuxiangru915 force-pushed the feat/file-attachments branch from 9944faf to 880431c Compare August 7, 2026 02:56
Adds a file attachment flow alongside the existing image attachments:

- New paperclip button next to the image button opens a file picker; any
  non-image file is uploaded to a persistent local directory
  (~/.local/share/pi-web/attachments/<uuid>/<original-name>) and shown as a
  card with the original file name and size.
- Dragging a file onto the chat window now attaches it as a card too
  (instead of inserting a path), so the behavior is uniform.
- On send, each attached file is appended to the message as an
  "[附件] <path>" line; the agent (running locally) reads the file via that
  path. User messages render these lines as clickable download cards, never
  showing the raw path.
- New POST /api/drop-files writes uploads (25MB/file, 100MB total, name
  sanitized, one uuid subdirectory per file so original names are always
  preserved — no -1 suffix renaming). New GET /api/attachments serves
  downloads, restricted to the attachments directory.
- File attachments are saved in the same in-memory draft as images/text and
  restored when switching sessions.
@wuxiangru915
wuxiangru915 force-pushed the feat/file-attachments branch from 880431c to ba1b27b Compare August 8, 2026 02:40
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