Skip to content

feat(web): accept PDF and text files in composer drag and drop - #8217

Closed
yash296 wants to merge 1 commit into
pingdotgg:mainfrom
yash296:feat/drop-pdf-and-text-files
Closed

yash296 wants to merge 1 commit into
pingdotgg:mainfrom
yash296:feat/drop-pdf-and-text-files

feat(web): accept PDF and text files in composer drag and drop

d90f78b
Select commit
Loading
Failed to load commit list.
MacroscopeApp / Macroscope - Correctness Check completed Aug 25, 2026 in 12m 53s

14 issues identified (89 code objects reviewed).

• Merge Base: 06de9e9
• Head: d90f78b

Details

✅ File Path Comments Posted Reason
✅ apps/server/src/attachmentMime.ts 0
❌ apps/web/src/components/chat/composerDroppedFiles.ts 2
❌ apps/web/src/types.ts 5
➖ apps/web/src/components/chat/composerDroppedFiles.test.ts Excluded by default ignore patterns
✅ apps/web/src/composerPlaceholder.ts 0
✅ apps/web/src/components/Sidebar.tsx 0
➖ apps/server/src/attachmentStore.test.ts Excluded by default ignore patterns
✅ apps/server/src/provider/Layers/GrokAdapter.ts 0
➖ apps/server/src/provider/Layers/CodexAdapter.test.ts Excluded by default ignore patterns
✅ apps/server/src/provider/Layers/CodexAdapter.ts 0
➖ apps/server/src/provider/Layers/ClaudeAdapter.test.ts Excluded by default ignore patterns
❌ apps/server/src/provider/Layers/CursorAdapter.ts 1
✅ apps/server/src/attachmentStore.ts 0
✅ apps/web/src/lib/composerDraftUploads.ts 0
✅ apps/web/src/historyBootstrap.ts 0
✅ apps/web/src/hooks/useHandleNewThread.ts 0
✅ packages/contracts/src/assets.ts 0
✅ apps/server/src/orchestration/Layers/ProjectionPipeline.ts 0
✅ apps/web/src/components/ChatView.logic.ts 0
✅ apps/server/src/assets/AttachmentUpload.ts 0
✅ apps/web/src/components/chat/MessagesTimeline.tsx 0
➖ apps/web/src/lib/attachmentUploadQueue.test.ts Excluded by default ignore patterns
✅ apps/web/src/components/preview/PreviewPanel.tsx 0
✅ packages/contracts/src/orchestration.ts 0
➖ apps/server/src/orchestration/Normalizer.attachments.test.ts Excluded by default ignore patterns
✅ apps/web/src/components/chat/ExpandedImagePreview.tsx 0
✅ apps/web/src/components/chat/ComposerPreviewAnnotationCards.tsx 0
✅ apps/web/src/promptStashStore.ts 0
❌ apps/server/src/provider/Layers/ClaudeAdapter.ts 1
➖ apps/web/src/components/preview/PreviewView.test.tsx Excluded by default ignore patterns
✅ apps/web/src/components/preview/PreviewView.tsx 0
❌ apps/server/src/orchestration/Normalizer.ts 1
✅ apps/web/src/lib/attachmentUploadQueue.ts 0
❌ apps/web/src/components/ChatView.tsx 2
➖ apps/web/src/composerDraftStore.test.ts Excluded by default ignore patterns
✅ apps/web/src/composerDraftStore.ts 0
❌ apps/web/src/components/chat/ChatComposer.tsx 2

Filtered Issues Details

apps/web/src/components/ChatView.tsx
  • line 1629: The composer persistence effect serializes each ComposerAttachment without its discriminant: the object written to stagedAttachmentById includes id, name, mimeType, sizeBytes, and dataUrl, but omits type. Since normalizePersistedAttachment defaults missing types to "image" for backward compatibility, a persisted PDF is rehydrated as an image after reload and is then sent/rendered with the wrong kind. Persist type: image.type for new attachments. [ Cross-file consolidated ]
  • line 2759: Dropped text is read asynchronously, but after await file.text() the code inserts via the live composer helpers without checking that the composer/thread is still the one that received the drop. If the user navigates to another thread while a text file is being read, the completed contents can be inserted into the new prompt (or the old draft can be updated using the new prompt snapshot), mixing content between drafts. Capture and validate the drop target before insertion, or write directly to the original draft target. [ Skipped comment generation ]
  • line 2769: After successfully reading text blocks, any insertion refusal is reported as a read failure: insertComposerTextAtEnd returns false while connecting, during approval/pending input, or when project selection is required, but the code appends all text filenames to unreadableNames and reports Could not read .... The file was readable; the user gets misleading recovery guidance and the content is discarded. Handle a busy/blocked composer separately from file.text() failures. [ Out of scope (post-validation triage) ]
  • line 5631: A document-only send uses IMAGE_ONLY_BOOTSTRAP_PROMPT, whose text explicitly says the user attached images and asks the provider to use image(s). With a PDF-only drop and no prompt, Claude receives a contradictory bootstrap message even though the attachment is a document, which can cause it to misinterpret or ignore the PDF. Choose the bootstrap text based on whether the attachments are images or documents. [ Cross-file consolidated ]
  • line 5645: The upload preflight error remains image-specific: when a PDF upload fails, onSend reports Retry or remove failed image uploads before sending. even though the failed item is a document. This gives the user incorrect recovery guidance for the newly supported file type; use attachment/file wording. [ Out of scope (post-validation triage) ]
  • line 5692: When attachment uploads are unavailable, the turnAttachmentsPromise fallback serializes every staged attachment as { type: "image" }. A newly supported PDF therefore reaches the turn as an image attachment instead of a document, so the no-upload path cannot send PDFs correctly and may trigger the wrong provider validation/adapter behavior. Preserve the attachment's type when constructing this fallback payload. [ Cross-file consolidated ]
  • line 5700: The optimistic user message created for a send labels every attachment as type: "image", including newly supported documents. Until the server message replaces it, a PDF is rendered through the image preview/lightbox path (and its blob URL is treated as an image), producing a broken preview and incorrect attachment semantics. Derive the optimistic attachment type from image.type. [ Cross-file consolidated ]
  • line 5767: For an attachment-only first message, the thread title fallback is hard-coded to Image: .... A PDF-only drop therefore creates a misleading thread title such as Image: spec.pdf, even though the attachment is a document. Use a generic file/attachment label or branch on firstComposerImage.type. [ Out of scope (post-validation triage) ]
apps/web/src/components/chat/ChatComposer.tsx
  • line 765: Once PDFs are included in composerAttachments, this call feeds them into attachmentUploadBlockReason, whose user-facing messages still say Image still uploading / failed image. A PDF upload failure or pending upload therefore gives the user the wrong object and makes the new file attachment UI misleading. [ Out of scope (post-validation triage) ]
  • line 1629: The persistence effect serializes every staged attachment without copying its type field. A dropped PDF is therefore saved as an attachment with no type, and hydrateAttachmentsFromPersisted defaults missing types to "image"; after a reload the PDF is rendered and dispatched as an image instead of a document, breaking the new PDF flow and potentially causing provider rejection or corrupted attachment handling. [ Cross-file consolidated ]
  • line 2392: The intentional document exclusion from the stash records the PDF name in oversizedImageNames. Restoring that stash consequently tells the user the PDF “exceeded the stash size limit,” even when it is tiny; the actual reason is that documents are unsupported by the image re-encoding stash. This gives a false explanation for a dropped attachment. [ Already posted ]
  • line 2663: The new document attachment is added to the same list that ChatView uses for its image-only title fallback. Sending a PDF without text therefore auto-titles the thread Image: <file>.pdf, which is incorrect for a document attachment and makes the new document support misleading in thread history. [ Out of scope (post-validation triage) ]
  • line 2663: The new document branch creates a ComposerAttachment with type: "document", but the existing send path in ChatView converts every attachment in the no-upload-capability fallback to type: "image" before calling startThreadTurn. Thus PDFs dropped in an environment where supportsAttachmentUploads is false are sent as image blocks (and the Codex/Cursor/etc. image path), so Claude/OpenCode document support cannot work for that common fallback. [ Cross-file consolidated ]
  • line 2769: The async text-drop handler captures activeThreadId only for error reporting, then resumes by calling the render's insertComposerTextAtEnd after each file.text() await. If the user switches threads while the file is being read, that stale callback reads the shared promptRef for the new thread but writes through the old composerDraftTarget, mixing the new thread's prompt and the dropped file into the wrong draft (and leaving the visible thread without the file). [ Cross-file consolidated ]
  • line 2773: When insertComposerTextAtEnd rejects insertion, the handler appends every textFiles name to unreadableNames, including files that were successfully read and already produced blocks (and even oversized files). The resulting Could not read ... error is false and hides the actual reason insertion was rejected, while the successfully read contents are discarded. [ Out of scope (post-validation triage) ]
  • line 2776: The text-drop failure expression reports oversizedNames instead of unreadableNames whenever both lists are non-empty. Dropping one oversized file and one unreadable file therefore hides the read failure entirely, so the user is not told that the second file was also not inserted. [ Out of scope (post-validation triage) ]
  • line 2992: getSendContext now returns document attachments through the renamed attachments ref, but the existing optimistic-message construction in ChatView hardcodes every returned attachment to type: "image". After sending a PDF on a server that uploads it correctly, the immediate timeline renders the PDF blob as an image (a broken preview) until the server message replaces it, so document attachments are not displayed correctly during the send handoff. [ Cross-file consolidated ]
apps/web/src/components/chat/MessagesTimeline.tsx
  • line 1021: The new document branch only runs for regularImages, but UserTimelineRow partitions attachments into previewImages by the reserved preview-annotation- filename prefix before reaching this branch. A normal PDF can legally have that name prefix; if its message has no parsed preview annotation, it is removed from regularImages and never rendered at all (and with an annotation it is forced through the image-only annotation card). This makes such document attachments disappear from the timeline. [ Cross-file consolidated ]
apps/web/src/composerDraftStore.ts
  • line 147: PersistedComposerThreadDraftState.attachments is widened to persist documents, but the normal composer serialization path writes staged attachments without a type field. On reload, normalizePersistedAttachment/hydrateAttachmentsFromPersisted treats an absent type as "image", so a staged PDF is restored as an image; its upload path then rejects application/pdf as an unsupported image and the PDF cannot be sent after a refresh/restart. [ Already posted ]
apps/web/src/lib/attachmentUploadQueue.ts
  • line 106: The new document branch routes PDFs through the existing upload-status path, but attachmentUploadBlockReason still reports "Image still uploading" and "Retry or remove the failed image" for every attachment. While a PDF is uploading or fails, the composer therefore shows misleading image-only guidance instead of describing the document, which is especially confusing for the newly supported document flow. [ Out of scope (post-validation triage) ]
  • line 258: By widening startAttachmentUpload to accept ComposerAttachment, PDF drops can reach the no-upload fallback used by ChatView. That fallback serializes every local attachment as { type: "image", ... } before reading its data URL, so when supportsAttachmentUploads is false a PDF is sent as an image and the server rejects it as an invalid image payload. The UI must either preserve image.type in that fallback or reject document drops when the capability is unavailable. [ Cross-file consolidated ]
apps/web/src/types.ts
  • line 39: PDF uploads are now included in the shared attachment upload wait, but the failure path still reports Retry or remove failed image uploads before sending. for any failed attachment. A failed PDF consequently gets an inaccurate error message and users are not told that the document upload is the problem. The message should refer to failed attachment/file uploads. [ Out of scope (post-validation triage) ]
  • line 39: Because documents are now valid ChatAttachment values but are deliberately routed into droppedImageNames when stashing, the stash UI counts an omitted PDF as an image and displays messages such as Some images were not restored / 1 image dropped. This misleads users about what happened to their PDF; the stash metadata and copy should use generic file/attachment wording for document omissions. [ Out of scope (post-validation triage) ]
  • line 39: The new document member also flows through PDF-only thread creation, but ChatView still uses the attachment-independent fallback titleSeed = \Image: ${firstComposerImageName}`whenever there is no text. As a result, dropping and sending onlyspec.pdfcreates a thread titledImage: spec.pdf`, which is incorrect for the newly supported document kind; the fallback must distinguish documents from images. [ Out of scope (post-validation triage) ]