Skip to content

feat(web): attach PDFs, ZIPs, and other files to a turn

MacroscopeApp / Macroscope - UI Consistency failed Aug 25, 2026 in 4m 53s

UI Consistency: 2 issues found

Two interaction findings on changed lines in the web attachment UI:

  • apps/web/src/composerDraftStore.ts β€” addFiles dedupes incoming files by mimeType/sizeBytes/name, which is exactly the metadata a hydrated needs-reattach marker keeps. Re-picking the file, as the composer row's "Attach again" label and docs/user/composer.md instruct, is silently skipped, leaving the row unresolved and send blocked with no feedback.
  • apps/web/src/components/chat/ChatComposer.tsx β€” the new paste guard returns early when there are pending user inputs or no free attachment slots, dropping the two error messages the previous path produced (plan-question rejection and the per-message attachment limit). An image-only clipboard then pastes nothing with no toast or composer error.

Shared-primitive usage, Tailwind ownership, and environment scoping in the new UI looked consistent: composer file rows and the paperclip trigger use Button/Tooltip primitives, the timeline file rows follow the existing semantic-row pattern used for image attachments, capability gating (attachmentEnvironmentConfig) and the download RPC both read the thread's environmentId, and onFileDownload defaults to a no-op rather than an active-environment fallback.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Files examined: apps/web/src/components/chat/ChatComposer.tsx, apps/web/src/components/chat/MessagesTimeline.tsx, apps/web/src/components/chat/ComposerStashMenu.tsx, apps/web/src/components/chat/composerAttachmentFiles.ts, apps/web/src/components/ChatView.tsx, apps/web/src/components/Sidebar.tsx, apps/web/src/composerDraftStore.ts, apps/web/src/hooks/useHandleNewThread.ts, plus apps/web/src/components/ui/* for primitive contracts (Button sizes icon-xs/icon-sm, Tooltip, toast warning type) used as context.

Checks performed:

  • Compared the new composer file rows and timeline file rows against the existing image chip/tile markup for primitive usage, icon sizing, and remove/retry affordances; both reuse Button variant="ghost" size="icon-xs" and Tooltip, matching the image path.
  • Confirmed the paperclip trigger lives in the footer that is already suppressed for collapsed-mobile, approval, and pending-input states, so the new control cannot appear in states where attaching is rejected.
  • Traced the download action: downloadFileAttachment resolves readPreparedConnection(environmentId) from ChatViewContent's thread-scoped environmentId, and maxFileAttachmentBytes derives from environmentById.get(environmentId), so capability gating and RPC target the same environment.
  • Verified the needs-reattach dedupe path by reading addFiles at head (lines 3155-3194) against the hydration shape produced by toHydratedThreadDraft and the persisted marker written by partializeComposerDraftStoreState.
  • No global CSS, theme variant, or scroll/mask changes are present in this diff, so those rule sections did not apply.

πŸ” Agent activity β€” 18 tool calls

πŸ’­ I'm checking that the new UI elements use consistent components -- ChatComposer's buttons rely on the Button primitive properly, but MessagesTimeline's new file rows use raw anchor, div, and button elements instead, which could be inconsistent with the design system even though it's functioning as a semantic download link. I want to verify how environmentId gets resolved for downloadFileAttachment in ChatView, and whether it correctly reflects the thread's environment rather than some other default. I'll dig into the actual files to confirm these patterns.

1. git_tools β€” onFileDownload|NOOP_DOWNLOAD_ATTACHMENT

2. git_tools β€” MessagesTimeline

πŸ’­ Since MessagesTimeline is only used in ChatView, I'm confirming that the environmentId passed for downloadFileAttachment reflects the thread's own environment rather than whatever's currently active, which seems correct. Now I want to dig into the user file rows in MessagesTimeline to see how existing image attachments render and what primitives they reuse.

3. browse_code β€” apps/web/src/components/chat/MessagesTimeline.tsx

πŸ’­ The existing image button using raw button with cursor-zoom-in confirms this pattern is consistent with the codebase style, so it's fine to use here. Now I want to check the composer file rows in ChatComposer around lines 3640-3720 to compare them against the existing image chip rendering for consistency in sizing and variant patterns.

4. browse_code β€” apps/web/src/components/chat/ChatComposer.tsx

πŸ’­ The composer file rows use primitives properly, that's good. Now I want to check the paperclip button against the surrounding toolbar buttons for consistency in size, variant, and tooltip usage, so I'm looking around lines 3850-3930.

5. browse_code β€” apps/web/src/components/chat/ChatComposer.tsx

πŸ’­ I'm placing the paperclip button in the right-side action area next to send, using existing primitives. I want to check whether the hidden file input should use "hidden" or a "sr-only" class, so I should look at how existing file inputs elsewhere in the web app handle this for consistency.

6. git_tools β€” type="file"

πŸ’­ The hidden vs sr-only difference on that input doesn't seem worth flagging since programmatic clicks still function fine either way. More relevant is that the paperclip button's visibility is correctly gated on whether file attachment capability exists on the server, and I'm now checking the mobile collapsed composer state for how it handles file rows. I'm also weighing whether file rows should show a "non-persisted" indicator like image rows do, and checking hit-target sizing consistency on removal buttons β€” both seem acceptable since they mirror image row behavior. The bigger question I'm digging into is whether using text-secondary-label on the FileIcon inside a message bubble is correct, since the bubble has its own surface with text-message-foreground, and I want to verify this follows the surface-ownership color guidelines properly.

7. git_tools β€” const environmentId

8. git_tools β€” environmentId,?$

πŸ’­ Since environmentId is thread-scoped from props, I want to verify whether attachmentEnvironmentConfig, which sets maxFileAttachmentBytes, is derived from that same environmentId β€” checking around line 2130 in ChatView.

9. git_tools β€” attachmentEnvironmentConfig

πŸ’­ I'm noticing an inconsistency: the paperclip button is gated only on the fileAttachments capability rather than on supportsAttachmentUploads, yet images can already be attached via drag/drop or paste with a non-upload dataUrl fallback regardless of that gating. So on older servers without file attachments, users lose the file-dialog affordance entirely, though they can still drag/drop or paste images β€” this feels more like a product gap than a clear rule violation. I'm now checking whether the footer containing the paperclip button gets hidden on the collapsed mobile composer, tracing the conditional wrapping around it. I'm also noticing the attach button sits in the right actions group alongside send/stop, rather than with the other secondary controls on the left β€” a bit unusual placement-wise, but not clearly wrong given badges already live there too. And on compact mobile layouts, an extra icon could crowd the right side, though that's speculative rather than a confirmed issue. Accessibility-wise the button and tooltip setup look fine. The more concrete thing to check is whether the attach button stays visible during approval/plan-question states while image attachment chips are hidden β€” that inconsistency would need verifying in the footer rendering logic.

10. browse_code β€” apps/web/src/components/chat/ChatComposer.tsx

πŸ’­ I'm confirming the footer visibility logic checks out, then checking whether the mobile app is in scope and noticing the icon size inconsistency between size-4 and size-3.5 across the component. Now I'm scanning MessagesTimeline for structural issues around the file rows block and previewUrl handling, but nothing looks broken there. The more interesting question is whether the composer draft row and timeline row duplicate an existing shared attachment-row primitive β€” they're similar but not identical enough to clearly warrant consolidation, so I want to check if a FilePill or AttachmentRow component already exists in the ui library before flagging it.

11. browse_code β€” apps/web/src/components/ui/*.tsx

πŸ’­ No file-row primitive exists. Now I'm checking whether the toast call using a "warning" type in useHandleNewThread is actually supported by the toast component's type definitions.

12. git_tools β€” "warning"|'warning'|type?:|type:

13. git_tools β€” warning

πŸ’­ The warning toast type looks fine. I'm noting the pluralization logic in the file-stayed toast message is a bit awkward but not a real consistency issue, and the composer's text-foreground styling looks correctly applied. I'm also glancing at the ComposerStashMenu's use of a non-null assertion on the files array. More importantly, I'm spotting a real interaction inconsistency: the paperclip attach button stays enabled even when the prompt editor itself is disabled (during connecting states, approval states, or when project selection is required), meaning users could attach files to a thread that isn't actually ready to receive them. That connection concern feels marginal, so I want something more concrete. I'm checking whether servers without file attachment support correctly hide the attach button entirely (relying on drag/paste instead), which seems like acceptable "hide rather than guess" behavior, and confirming the file input's lack of an accept attribute and the multi-select slot-limit error toast both behave reasonably. Now I'm looking at the mobile hit target size for remove/retry icons in the file row list to see if it's adequate for touch devices. Let me check whether other similar hover:underline rows elsewhere in the repo add a focus-visible ring, to see if this is an established pattern worth flagging.

14. git_tools β€” hover:underline

πŸ’­ The focus-visible ring pattern is inconsistent across raw buttons, so that's not a strong finding on its own. Let me instead check the Sidebar.tsx change where it adds composer.files.length to a count, making sure it doesn't double count or misalign with the label.

15. browse_code β€” apps/web/src/components/Sidebar.tsx

πŸ’­ I'm checking the unknown attachment row keying and the previewUrl anchor case in MessagesTimeline, noting the download icon logic and that previewUrl takes precedence over downloadable being false. Cursor styling looks fine β€” anchors with href default to pointer while buttons get explicit cursor-pointer. But I spot a potential functional bug: needs-reattach files only show plain grey "Attach again" text with no way to actually reattach from the row itself, and worse, the dedupe key based on mimeType/size/name in addFiles means re-picking the same file after a reload would just get silently skipped as a duplicate β€” that seems like a real issue worth flagging. I want to verify this dedupe logic actually works the way I think, tracing through how existing file IDs and the known-files set get built to confirm the same file would indeed be silently dropped rather than reattached, leaving the user with no feedback despite the documented recovery instructions telling them to "attach the file again or remove it." Though it's borderline against the enumerated rules, I think this is still worth flagging: the "Attach again" row promises a fix but re-picking the same file gets silently deduped by name/size/mime, so the user's action produces no actual result β€” a real interaction-semantics defect introduced by the changed behavior. I also notice the "attach again" state uses plain muted text rather than the amber warning styling used elsewhere for non-persisted images or failed uploads with retry β€” a minor inconsistency worth flagging alongside the main comment rather than separately. Then checking the paste handler, I want to verify pasting an image when attachment slots are full still surfaces an error toast rather than silently failing.

16. browse_code β€” apps/web/src/components/chat/ChatComposer.tsx

πŸ’­ I'm noticing a regression: pasting an image used to trigger a toast telling users to attach images after answering plan questions, but now with pending user inputs present, the paste silently returns early with no feedback at all, effectively swallowing the error message users used to see. With pending inputs there's also silent dropping β€” no toast, no error state. It's marginal though, so I'll focus my report on the primary reattach dead-end finding, mentioning the missing paste feedback only briefly as secondary, and double-check that nothing else clears the needs-reattach marker before finalizing. The cleanest fix: in addFiles, when an incoming file matches a needs-reattach entry, treat it as a replacement rather than skipping it. I'll write an inline comment on the composerDraftStore.ts dedupe block, so now I need to locate the exact line numbers in the head file's diff to anchor the comment correctly.

17. browse_code β€” apps/web/src/composerDraftStore.ts

πŸ’­ Confirming the file at HEAD matches the diff's post-image, I'll comment on the dedupe skip around line 3171. I'll also check whether the "Attach again" row in ChatComposer needs its own comment, but decide one reference is sufficient. For the second finding, the paste handler silently drops feedback when at capacity or with pending inputs. I'll add a brief inline comment there suggesting to only skip the toast for the synthetic text-paste case, letting addComposerAttachments still report capacity or pending-input errors as it already does elsewhere. Now I'm confident this is an unintended regression worth flagging as a lower-severity note. I'll write up both comments together, with the first covering the addFiles dedupe issue in composerDraftStore.ts around line 3171. For a fix, I'm sketching how to change the dedup logic so that instead of tracking known files purely by identity, it maps the mime/size/name key to file IDs specifically for entries needing reattachment, so an incoming file with actual bytes can replace that marker rather than being skipped as a duplicate. Now I'm working out where to place my two inline review comments β€” one describing this composerDraftStore fix in prose since the diff got too complex to show cleanly, and another on the ChatComposer paste-handling block, making sure I pick line numbers that actually exist on the added side of the diff so the comments anchor correctly.

18. modify_pr β€” Reviewed the web composer/timeline attachment UI. Two interaction findings on changed lines, both about the new needs-reattach / capacity paths giving the user no working path or feedback.


Agent Credits: 212 credits