Skip to content

[Feature]: Read DOCX, PPTX and XLSX attachments as text #491

Open

Description

@wudilyy999

Problem / pain point

  • Claude Design accepts DOCX, PPTX and XLSX files as input next to text and images. In Open CoDesign, a .docx, .pptx or .xlsx attached in the composer reaches the model only as a note: "Binary or unsupported format (.docx). Use the filename as a hint, not quoted content." (readAttachment in apps/desktop/src/main/prompt-context.ts, where non-image binary files "stay filename-only for now"). Someone who attaches a brief, a deck to restyle, or a spreadsheet of content gets a design that never saw it.
  • The app can already read these formats. apps/desktop/src/main/document-preview.ts parses DOCX, PPTX and XLSX with jszip into text sections for the Files tab preview, but that text never reaches the model.

Proposed solution

Give the model the text of the three formats, using what is already in the tree:

  1. Move the OOXML text parsers (parseDocx, parsePptx, parseXlsx and their helpers) out of document-preview.ts into a module both callers use. The preview passes its own caps (16 sections, 24 lines each) and its behavior does not change; its tests guard that.
  2. In readAttachment, extract the text of a .docx, .pptx or .xlsx and return it as the attachment's excerpt, the way text attachments are returned: paragraphs for DOCX, one block per slide for PPTX, one block per sheet with a row per line for XLSX, cut at MAX_ATTACHMENT_CHARS with the existing truncation note. The module loads with await import() on first use.
  3. Reading is bounded by named constants with tests: the existing 10 MB attachment cap, a cap on the uncompressed bytes read from the archive, and caps on slides, sheets, rows and cells per row, applied before MAX_ATTACHMENT_CHARS cuts the joined text. They are separate from the preview's own caps, and anything a cap cuts is marked in the text so the model can tell the content is partial.
  4. A file that is not a valid archive, or lacks the expected parts, fails with ATTACHMENT_READ_FAILED like any other unreadable attachment, instead of silently falling back to the filename note. The error names the attachment and the format it expected.

Out of scope: PDF (no text extractor in the tree, so it needs a new dependency), the legacy .doc, .ppt, .xls (binary formats), images inside documents, and documents that sit in the workspace rather than being attached (the editor's view reads text files and image attachments only). Those keep today's filename note, which will name the format.

Tests build small DOCX, PPTX and XLSX archives in memory with jszip, so there are no binary fixtures: text extraction for each format, truncation, an invalid archive, and each cap.

I expect about 250 lines with tests, plus the moved parsers, which git shows as a move. One PR.

Alternatives considered

  • Ask the model to read the file with its tools: the editor's view does not read binary files, and a tool that returned the same text would cost a call per file on every run.
  • Convert with a system tool (textutil, LibreOffice): macOS only or a large optional install, and it runs a program on a user file. The parsers already in the tree read the archive as text and run nothing.
  • A PDF reader at the same time: needs a dependency, so it deserves its own proposal.

Scope

Generation capability

Hard-constraints check

  • I have read the hard constraints in CLAUDE.md and this proposal does not conflict with any of them. No new dependencies (jszip is already used by the desktop app), no network or model call, files are read from disk as text and never executed, and the parser loads on first use.

Activity

  1. github-actions commented on Oct 14, 2026

    @github-actions
    Contributor

    Thanks for the detailed proposal — this is a clear, well-scoped feature request, not a bug, and the write-up makes the gap easy to confirm.

    Understanding

    Composer attachments today split into images (passed through) and everything else. For a .docx, .pptx, or .xlsx, the attachment reaches the model only as a filename hint rather than its text — so a brief, a deck to restyle, or a spreadsheet of content is effectively invisible to the model. Meanwhile the desktop app already parses those three OOXML formats into text for the Files tab preview, so there are two parallel paths and only one gets the content to the model.

    Assessment

    The proposal lines up with the project's hard constraints:

    • Lazy-load is required, not optional. Hard constraint 5 ("Lazy-load heavy features") means the parser must not run at app start. Your await import() on first use is the right shape for this.
    • No new dependency. jszip is already a desktop dependency (pnpm-lock.yaml, apps/desktop importer: jszip ^3.10.1 → 3.10.1), so this stays inside the permissive-dependency rule (hard constraint 4) without a dependency review.
    • No new model/network calls. Reading the archive as text and never executing it matches the stated constraints.

    On the design itself, the two points I'd want kept explicit if this becomes a PR:

    1. The preview path must not change behavior. Moving parseDocx / parsePptx / parseXlsx and their helpers out of the preview module is only safe if the preview keeps its own caps (16 sections / 24 lines) and its existing tests continue to guard that. Treat the preview caps and the new attachment caps as separate constants rather than defaulting one from the other.
    2. Failure semantics. Falling back to the filename note on an unreadable archive is arguably worse than failing loudly, because the model silently plans around a file it never read. Routing corrupt archives / missing parts through ATTACHMENT_READ_FAILED (as you propose) is the more honest behavior; just make sure the error names the file so the user can tell which attachment failed.

    Scope boundaries

    Agreeing with your out-of-scope choices — PDF (needs a new text-extraction dependency), the legacy binary .doc / .ppt / .xls, embedded images, and workspace files that aren't composer attachments all keep today's filename note. Keeping PDF out of this PR is the right call; it deserves its own proposal given the dependency question.

    Questions / next steps

    • Do you want to open the PR yourself, or should this be left for a maintainer to pick up?
    • For XLSX, what do you intend for a sheet with very wide rows — truncate at MAX_ATTACHMENT_CHARS on the joined text (your stated approach), or cap cells-per-row first? A one-line note in the proposal would make the truncation test unambiguous.
    • Confirm the parsers are moved (not copied), so the preview and the attachment path share one implementation. A duplicated parser would drift.

    No blocking concerns from my side — the shape, the constraint check, and the test plan all look sound.


    Open-CoDesign Bot

  2. wudilyy999 commented on Oct 14, 2026

    @wudilyy999
    ContributorAuthor

    Thanks. Both design points are accepted as written, and the answers to the three questions:

    • The PR. I will open it myself.
    • Wide XLSX rows. The caps apply in order: cells per row, then rows per sheet, then sheets, then MAX_ATTACHMENT_CHARS on the joined text. The preview parser already stops at 8 cells per row and 24 rows per sheet; the attachment path gets its own named constants for the cell, row and sheet caps and does not read the preview's, and the PR will state the numbers and why. Anything a cap cuts is marked in the text (a row cut at the cell cap ends with …, and a sheet cut at the row cap ends with a line saying how many rows were left out), so the model can tell the content is partial and the truncation test has one answer.
    • Moved, not copied. parseDocx, parsePptx, parseXlsx and their helpers move to one module. document-preview.ts imports it and passes its own caps, so there is one implementation, and the preview's existing tests are the guard.

    On failure: the ATTACHMENT_READ_FAILED message will name the attachment and the format it expected, for example Could not read "brief.docx" as a DOCX file, with the cause attached.

  3. github-actions commented on Oct 14, 2026

    @github-actions
    Contributor

    Thanks — that closes all three open questions, and both design points are now unambiguous.

    Confirmed as written

    • XLSX caps apply in order: cells per row → rows per sheet → sheets → MAX_ATTACHMENT_CHARS on the joined text, with attachment-path constants that don't inherit from the preview's.
    • parseDocx / parsePptx / parseXlsx and their helpers move (not copy) into one module; document-preview.ts imports it and passes its own caps, so the preview's existing tests stay the guard.
    • A corrupt archive or missing part goes through ATTACHMENT_READ_FAILED naming the attachment and the expected format, cause attached, instead of a silent filename fallback.
    • The PR is yours to open.

    Worth putting in the PR description

    1. Caps as an explicit parameter. The shared module will serve two callers with different budgets — the preview's section/line caps and the per-format cell/row caps (the preview's XLSX limits you noted, 8 cells / 24 rows). Keeping those numbers at the preview's call site rather than as shared defaults is what makes a later change to the attachment caps unable to silently shift preview output; the preview's tests can only catch that drift if the numbers are stated at that call site.
    2. Marker strings defined once. Since the cell-cap … and the "N rows left out" line are the model's only signal that content is partial, defining them in one place and asserting them as literals in tests keeps a future wording tweak from quietly dropping that signal.
    3. Keep the two failure modes distinct. ATTACHMENT_READ_FAILED for a corrupt DOCX/PPTX/XLSX should read differently from the filename note kept for out-of-scope formats (PDF, legacy .doc/.ppt/.xls), so a user with a legacy format isn't told their file is broken. It would also be worth confirming in the description that the failure is per-attachment — one bad DOCX shouldn't abort a run whose other attachments read fine.

    PR housekeeping: the description will want the four PRINCIPLES §5b checks marked green (per the hard constraints in CLAUDE.md), and the repo's Changesets flow expects an entry alongside the code — CHANGELOG.md shouldn't be hand-edited.

    When the PR is up, the description is the right place for the numbers and the reasoning behind them, as you said.


    Open-CoDesign Bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions