# Chat Attachments How a user's attached files reach the model on a chat turn. **One rule, per attachment:** if the provider+model can take it as a native input, send the **native file**; otherwise send its **extracted text**, inlined and capped. The `read_file` tool only exists to page the overflow of large text — it is never the *only* way the model sees content. This is deliberate: visibility must not depend on the model choosing to call a tool. A weak (or non-tool-calling) model still sees every attachment, and a provider that handles a modality natively keeps doing so — no capability regression. ## Routing matrix Decided per file part in `prepareChatMessages` (`src/main/ai/messages/attachmentRouting.ts`): | Attachment | Native when | What the model receives | |---|---|---| | image | model is vision | native image part (inline) | | image | non-vision | OCR text, inline (capped) | | pdf | provider+model native PDF | native PDF part (inline) | | pdf | otherwise | extracted text, inline (capped) | | office (`docx/xlsx/pptx/odf`) | — | extracted text, inline (capped) | | text / code | — | decoded text, inline (capped) | | audio | model is audio-capable | native audio part (inline) | | audio | otherwise | short note ("can't process audio") | | video | model is video-capable | native video part (inline) | | video | otherwise | short note ("can't process video") | | other (binary: zip/exe/…) | — | short note ("unsupported file type") | - **Native** → the file part is left in place and materialized as a `data:` URL by `materializeNativeFilePart` (`src/main/ai/messages/fileProcessor.ts`), which also normalizes the `mediaType` to the on-disk MIME. The provider gets the real file as a user-message part. (The function is named for the boundary: provider File-API upload for large files would slot in behind the same signature.) - Binary / unsupported types are **not** auto-decoded — they'd inline as mojibake — so they get a short note instead. - Any per-file failure (missing entry, parse error, unconfigured OCR, failed materialization) degrades to a `[could not read this file].` note rather than dropping the file or failing the request. - **Non-native** → the file part is replaced by its extracted text (see the cap below). The internal `fileEntryId` is never written into the prompt. Only `fileEntryId`-backed (first-party chat) attachments are routed. Gateway / external file parts (no `fileEntryId`) are left untouched, so the OpenAI- compatible passthrough is unaffected. ## The cap (the only context guard) Extracted text is bounded so multi-turn context stays in control: - text ≤ cap → inlined in full. - text > cap → inline the first `cap` chars + a trailer: - tool-capable model: `[truncated N/total — call read_file("name", offset=N) for more]` - otherwise: `[truncated N/total]` Default cap ≈ 8k chars/file (tunable). ## `read_file` — text-only overflow tool `src/main/ai/tools/fileLookup.ts` + `tools/adapters/aiSdk/builtin/ReadFileTool.ts`. - Input `{ filename, offset?, limit? }`. The `filename` is the model-facing **handle** (unique, normalized — see `collectFileAttachments`), resolved to an entry id against a per-request allow-list. The model never sees or guesses entry ids, and can only read files attached to the current conversation. - Returns **text only** (extracted / OCR), paginated. Errors are sanitized to filename-level messages; details are logged, not returned. - Exposed to tool-capable models whenever the request carries first-party file attachments (`applies: scope.hasFileAttachments`). It pages over-cap text; when everything inlines within the cap the model simply never needs to call it. - Because native media is kept inline (never routed through the tool), `read_file` carries no media result — no `toModelOutput` base64 re-read, no resend re-materialization. ## Extraction & OCR | Concern | Owner | |---|---| | office/pdf/text → text | `extractDocumentText` (`src/main/ai/messages/attachmentTextExtraction.ts`) | | image → text (non-vision) | `FileProcessingService.ocrImage` (`src/main/features/fileProcessing/`) | `ai/` reaches OCR through the `FileProcessingService` rather than deep-importing the feature, keeping processor/handler internals in that domain. Both `extractDocumentText` and the OCR path are path-free and cache their result by content version (30 min), so the eager every-turn pass over history doesn't re-extract or re-OCR the same file. `extractDocumentText` reads bytes through `FileManager.read` (PDF via `pdf-parse`, office via `officeparser`/`word-extractor`, text via `decodeTextWithAutoEncoding`) and dispatches on the `FileEntry` canonical `ext`. ## Capability resolution `resolveNativeFileSupport` (`src/main/ai/runtime/aiSdk/params/nativeFileSupport.ts`) derives the "native" column from `(provider, model)`: image/audio/video ride on the model capability alone (`isVision` / `isAudio` / `isVideo`, `@shared/utils/model`), while PDF additionally requires a first-party provider (`supportsNativePdf`). There is no `pdf-compatibility` middleware — native PDFs pass through inline, non-native PDFs go through extraction. ## Invariants - Content visibility never depends on a tool call. - `fileEntryId` never reaches the model (filename in, filename out). - Native modalities keep provider-native handling. - Per-turn context is bounded by the cap.