Skip to content

fix(site/src/pages/AgentsPage): inline text-file attachments as text#26556

Draft
johnstcn wants to merge 4 commits into
mainfrom
fix/inline-text-chat-attachments
Draft

fix(site/src/pages/AgentsPage): inline text-file attachments as text#26556
johnstcn wants to merge 4 commits into
mainfrom
fix/inline-text-chat-attachments

Conversation

@johnstcn

@johnstcn johnstcn commented Jun 19, 2026

Copy link
Copy Markdown
Member

Problem

Attaching a non-image/non-PDF file (for example a .json file) to a Coder Agents chat uploads and stores the file correctly, but the attachment is silently dropped before the model ever sees it.

Root cause: the message is sent as a file part referencing the uploaded file. Each provider's conversion layer in the fantasy SDK only accepts a small set of file media types (OpenAI Responses: image/*, application/pdf; Anthropic: image/*, application/pdf, text/*). Anything else, including application/json, is discarded with a CallWarning that is recorded in the run's normalized_response.warnings but never surfaced to the user. Switching models does not help.

Fix

Read text-family uploads (text/plain, text/markdown, text/csv, application/json) in the browser and send their contents to the model as an inline text part instead of a droppable provider file part. Text parts are accepted by every provider, so the content always reaches the model. Images and PDFs are unchanged and still sent as file parts.

This is intentionally frontend-only. The server remains authoritative on stored media types (it byte-sniffs and enforces an allowlist), so no backend behavior changes.

Changes (all under site/src/pages/AgentsPage/)

  • utils/chatAttachments.ts: new pure helpers isInlinableTextAttachment (text family derived from the generated ChatAttachmentMediaTypes, with a filename-extension fallback that reuses chatAttachmentExtraExtensions), formatInlinedAttachmentText, and attachmentToContentPart; the PendingAttachment type now lives here.
  • utils/resolvePendingAttachments.ts: new shared resolver used by both send paths. It turns uploaded attachments into the send payload, inlining text-family files as text parts, reading on demand when needed, and falling back to a file part (with a console.warn) if the read fails.
  • hooks/useFileAttachments.ts: broadened the existing content read from text/plain only to the full inlinable text family, using the shared readAgentAttachmentText.
  • components/ChatPageContent.tsx: chat send handler uses resolvePendingAttachments; re-exports PendingAttachment.
  • components/AgentCreateForm.tsx and AgentCreatePage.tsx: the new-chat create path now uses the same resolver and attachmentToContentPart, so text-family attachments are inlined on chat creation too (previously only the existing-chat path was fixed).
  • AgentChatPage.tsx: send assembly uses attachmentToContentPart; the optimistic media-type map excludes inlined attachments so they do not render as file chips on edit.

Testing

  • pnpm exec vitest run for chatAttachments.test.ts (33 tests) and resolvePendingAttachments.test.ts (6 tests): 39 total, covering classification, extension fallback, formatting, content-part mapping, on-demand read, read-failure fallback, restored zero-byte attachments, and upload-error skipping.
  • pnpm lint (biome + tsc + circular deps + knip + react-compiler) clean.
  • make pre-commit passed.
  • Manual: attach a .json file on both an OpenAI and an Anthropic model from both the create form and an existing chat; the content reaches the model and no file part media type application/json not supported warning is produced.

Known limitations

  • Programmatic / non-web API clients that upload text-like files still hit the server-side provider drop. Addressing that would require a backend change and is out of scope here.
  • Model-level vision capability (for example, an image sent to a non-vision model) is unchanged; no per-model capability is currently surfaced.
  • On edit, an inlined attachment reappears as editable text concatenated with the typed message. This mirrors the server's string_agg(part->>'text', '') aggregation and is lossless; distinguishing inlined content from typed text would require a content-format change and is out of scope.
Implementation notes and decision log

This change came out of an investigation into why a .json attachment failed silently. Key findings that shaped the approach:

  • Both OpenAI Responses and Anthropic drop unsupported file media types in the fantasy SDK; the warning is captured but never shown. The behavior is provider-agnostic, so switching models does not help.
  • The server is authoritative on media type: uploads are byte-sniffed, allowlist-enforced, and stored with the detected type; the send path requires file_id and re-derives the type from the stored row. A client cannot relabel a file, so "fudge the content-type" approaches do not work and a binary cannot be smuggled in as text.
  • The storable text family that providers may drop is exactly text/plain, text/markdown, text/csv, application/json, all safe to inline as text.

Scope decisions:

  • An over-engineering review cut an earlier, larger plan that added a generated provider capability matrix and an attach-time "this file will be dropped" warning. After text inlining, the only remaining storable file-part types are images and PDFs, both accepted by current providers, so that warning had no case that could fire. Surfacing the existing server-side CallWarning is tracked as a follow-up to ship alongside the first reachable drop.
  • The upload is intentionally kept for inlinable text files (rather than skipped) because the chip, text-preview dialog, manual-inline escape hatch, and persist-restore all depend on an uploaded fileId. The file is simply sent as text rather than referenced as a file part.

Per-file testing follows the frontend guidelines: the new logic is pure functions covered by vitest. No Storybook story was added because the change introduces no new rendered UI and the send pipeline is not exercised by an existing story harness.


This PR was generated by Coder Agents on behalf of @johnstcn.

Text-family uploads (JSON, CSV, Markdown, plain text) were sent as
provider file parts and silently dropped by providers that only accept
images and PDFs. Read their contents in the browser and send them as a
text part so every provider sees the content.

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-19 21:03 UTC by @johnstcn
Spend: $60.21 / $100.00

Review history
  • R1 (2026-06-19), 1 P2, COMMENT. Review
  • R2 (2026-06-19): 14 reviewers, 3 Nit, 1 Note, 2 P2, 5 P3, COMMENT. Review
  • R3 (2026-06-19): 6 reviewers, 4 Nit, 1 Note, 2 P2, 6 P3, COMMENT. Review
  • R4 (2026-06-19): 4 reviewers, 4 Nit, 1 Note, 2 P2, 6 P3, APPROVE. Review

deep-review v0.8.0 | Round 4 | 4aa2482..bf9da5f

Last posted: Round 4, 13 findings (2 P2, 6 P3, 4 Nit, 1 Note), APPROVE. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Author fixed (0e92730) ChatPageContent.tsx:448 Unguarded await readTextFileContent in send handler silently drops the message on file-read failure R1 Netero Yes
CRF-2 P2 Author fixed (003289e) AgentCreateForm.tsx:401 Create form send path not updated; text-family attachments silently dropped on new chat creation R2 Mafu-san Yes
CRF-3 P3 Author fixed (003289e) chatAttachments.ts:150 readTextFileContent duplicates readAgentAttachmentText in same feature area R2 Robin P2 Yes
CRF-4 P3 Author contested; panel closed R3 (3/5 accept) messageParsing.ts:329 Edit path joins inlined text parts unseparated with user message R2 Hisoka Yes
CRF-5 P3 Author fixed (003289e) ChatPageContent.tsx:450 On-demand read failure swallowed with no diagnostic signal R2 Chopper Yes
CRF-6 P3 Author fixed (003289e) chatAttachments.ts:110 Comment claims auto-sync that doesn't hold for future application/* types R2 Leorio Yes
CRF-7 P3 Author fixed (003289e) chatAttachments.ts:121 Extension list duplication without compile-time coupling R2 Meruem P3, Hisoka, Gon, Robin, Leorio Yes
CRF-8 Nit Author fixed (003289e) chatAttachments.ts:105 Block comment restates what name and filter predicate show R2 Gon Yes
CRF-9 Nit Author fixed (003289e) ChatPageContent.tsx:157 textContent field comment enumerates types already documented at constant definition R2 Gon Yes
CRF-10 Nit Author fixed (003289e) ChatPageContent.tsx:451 Catch comment cross-references unrelated code path R2 Gon Yes
CRF-11 Note Dropped by orchestrator (trivial 1-liner tested transitively) chatAttachments.ts:150 readTextFileContent has no direct test R2 Bisky No
CRF-12 Note Dropped by orchestrator (pre-existing code outside diff, scope creep) AttachmentBlocks.tsx:40 TEXT_ATTACHMENT_MEDIA_TYPES hardcoded set could use derived approach R2 Robin No
CRF-13 Note Author fixed (003289e) ChatPageContent.tsx:448 CRF-1 fix error path has no automated test coverage R2 Bisky Yes
CRF-14 P3 Author fixed (bf9da5f) resolvePendingAttachments.test.ts:107 No test for restored-attachment branch (zero-size text-family file) R3 Bisky Yes
CRF-15 Nit Author fixed (bf9da5f) ChatPageContent.tsx:153 PR description Changes section lists 4 files; diff has 9 R3 Mafu-san P3 Yes

Contested and acknowledged

CRF-4 (P3, messageParsing.ts:329) - Edit path joins inlined text parts unseparated with user message

  • Finding: getEditableUserMessagePayload joins all type === "text" parts with .join(""). After this PR, inlined text attachments are text parts, so on edit the user's typed message and the attached file content concatenate without separator and no file chip is shown.
  • Author defense: The .join("") mirrors the server's string_agg(part->>'text', '') in GetChatUserPromptsByChatID. Adding a separator would diverge from server-side prompt aggregation. Inlined text parts carry no marker to distinguish them from typed text, so they cannot be re-rendered as chips on edit without a content-format change. The behavior is lossless.
  • Panel closure (R3, 3/5): Mafuuu traced the contract and confirmed the behavior is lossless and consistent with the server-side aggregation. Meruem verified the schema has no field to distinguish user-typed text from inlined content, confirming a fix requires a schema change genuinely out of scope. Pariston verified the server-side string_agg claim and confirmed the frontend fix is at the right level for its stated scope.

Round log

Round 1

Netero-only. 1 P2. Reviewed against 4aa2482..1ba5ae8.

Round 2

Panel (14 reviewers). CRF-1 addressed. 1 P2, 5 P3, 3 Nit, 1 Note new. 2 dropped. Reviewed against 4aa2482..0e92730.

Round 3

Panel (6 reviewers). 9 addressed, CRF-4 contested and closed (3/5). 1 P3, 1 Nit new. Reviewed against 4aa2482..003289e.

Round 4

Netero-only. CRF-14, CRF-15 addressed. No new findings. All 15 findings resolved. APPROVE. Reviewed against 4aa2482..bf9da5f.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-pass review (Netero). 1 P2 finding.

The change solves a real problem cleanly: text-family uploads that providers silently drop are inlined as text parts so every provider sees the content. The helper extraction into chatAttachments.ts is well-factored, the set is derived from the generated allowlist, and the test coverage is solid (33 tests, extension fallback included).

One error-handling gap in the send path needs attention before the full review panel runs.

This is a first-pass review only. These are mechanical findings; the full review panel has not yet reviewed this PR. The panel will review after this finding is addressed.

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AgentsPage/components/ChatPageContent.tsx Outdated

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel review (Round 2). CRF-1 (unguarded await) verified fixed. 1 P2, 5 P3, 3 Nit, 1 Note.

The approach is sound: text-family uploads that providers silently drop are inlined as text parts so every provider sees the content. The helper extraction is clean, the set derivation from the generated allowlist keeps it in sync automatically, and the test coverage is thorough (33 tests). The CRF-1 fix is well-structured with correct fallback semantics.

The main gap is that the fix was applied to the chat page's send handler but not the create form's send handler, which constructs file parts the same way and hits the same silent drop.

"Both paths are in the same directory, use the same hook, and render the same AgentChatInput component. A grep for type: file, file_id would have surfaced this." (Mafu-san)


site/src/pages/AgentsPage/components/AgentCreateForm.tsx:391

P2 [CRF-2] Create form send path not updated; text-family attachments are silently dropped on new chat creation.

handleSendWithAttachments (line 391) collects only state.fileId strings and passes them as fileIDs to handleSend. In AgentCreatePage.tsx:89-92, each ID becomes { type: "file", file_id: fileID }. No check for isInlinableTextAttachment, no reading of textContents (destructured at line 380 but unused).

The hook change in this PR broadened text reading to the full inlinable text family for both consumers. The chat page's send handler was updated to use that text content. This send handler ignores it. A user creating a new chat with a .json attachment from the create form still hits the silent provider drop.

(Mafu-san)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AgentsPage/utils/chatAttachments.ts Outdated
Comment thread site/src/pages/AgentsPage/components/ChatPageContent.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatPageContent.tsx Outdated
Comment thread site/src/pages/AgentsPage/utils/chatAttachments.ts Outdated
Comment thread site/src/pages/AgentsPage/utils/chatAttachments.ts Outdated
Comment thread site/src/pages/AgentsPage/utils/chatAttachments.ts Outdated
Comment thread site/src/pages/AgentsPage/components/ChatPageContent.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatPageContent.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatPageContent.tsx Outdated

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel review (Round 3). All R2 findings addressed or closed. 1 P3, 1 Nit new.

The R3 fixup is a well-executed structural response to the R2 findings. Rather than patching each finding individually, the author extracted resolvePendingAttachments as shared logic, eliminating the create-form gap (CRF-2), the duplicate reader (CRF-3), and the silent catch (CRF-5) in one consolidation. Both send paths now call the same function. 38 tests pass across both test files.

CRF-4 (edit path joins text parts unseparated) was contested and closed by the panel (3/5 accept). The defense holds: .join("") mirrors the server's string_agg, the schema has no field to distinguish user-typed text from inlined content, and the behavior is lossless. The edit-path UX degradation (no file chip, attached content mixed into the editor) is a known trade-off. If this matters, it needs a human decision: file a ticket for a content-format marker or accept the gap.

"The resolvePendingAttachments extraction is a clean refactor that unifies both submit paths. Boring and correct." (Hisoka)

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AgentsPage/utils/resolvePendingAttachments.test.ts
Comment thread site/src/pages/AgentsPage/components/ChatPageContent.tsx

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All findings resolved. Approving.

Across 4 rounds this PR addressed 2 P2s (unguarded await in send handler, create-form send path gap), 5 P3s, 3 Nits, and 1 Note. One contested finding (edit-path text joining) was closed by the panel after the author demonstrated it mirrors the server-side aggregation contract.

The final shape is clean: resolvePendingAttachments unifies both send paths, text-family uploads are inlined as text parts so every provider sees the content, error fallback logs a console.warn and degrades to a file part, and 39 tests cover classification, formatting, on-demand read, read failure, restored zero-byte files, and upload-error skipping. Test density is 49%.

CRF-4 (edit-path UX degradation for inlined text attachments) is a known trade-off documented in the PR's Known Limitations section. A human decision is needed on whether to file a ticket for a content-format marker or accept the gap.

🤖 This review was automatically generated with Coder Agents.

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