processFileAttachment reads a file from disk, classifies it, and either appends its text content to textBuilder or adds a binary part to binaryParts. Returns true when the path resolved to a real, regular file that we attempted to surface to the model — even if the content itself was rejected (too l
(ctx context.Context, att messages.Attachment, textBuilder *strings.Builder, binaryParts *[]chat.MessagePart)
| 534 | // missing files, but we do want them to cover "the agent has bigger tools |
| 535 | // than us" cases. |
| 536 | func (a *App) processFileAttachment(ctx context.Context, att messages.Attachment, textBuilder *strings.Builder, binaryParts *[]chat.MessagePart) bool { |
| 537 | absPath := att.FilePath |
| 538 | |
| 539 | fi, err := os.Stat(absPath) |
| 540 | if err != nil { |
| 541 | var reason string |
| 542 | switch { |
| 543 | case os.IsNotExist(err): |
| 544 | reason = "file does not exist" |
| 545 | case os.IsPermission(err): |
| 546 | reason = "permission denied" |
| 547 | default: |
| 548 | reason = fmt.Sprintf("cannot access file: %v", err) |
| 549 | } |
| 550 | slog.WarnContext(ctx, "skipping attachment", "path", absPath, "reason", reason) |
| 551 | a.sendEvent(ctx, runtime.Warning(fmt.Sprintf("Skipped attachment %s: %s", att.Name, reason), "")) |
| 552 | return false |
| 553 | } |
| 554 | |
| 555 | if !fi.Mode().IsRegular() { |
| 556 | slog.WarnContext(ctx, "skipping attachment: not a regular file", "path", absPath, "mode", fi.Mode().String()) |
| 557 | a.sendEvent(ctx, runtime.Warning(fmt.Sprintf("Skipped attachment %s: not a regular file", att.Name), "")) |
| 558 | return false |
| 559 | } |
| 560 | |
| 561 | const maxAttachmentSize = 100 * 1024 * 1024 // 100MB |
| 562 | if fi.Size() > maxAttachmentSize { |
| 563 | slog.WarnContext(ctx, "skipping attachment: file too large", "path", absPath, "size", fi.Size(), "max", maxAttachmentSize) |
| 564 | a.sendEvent(ctx, runtime.Warning(fmt.Sprintf("Skipped attachment %s: file too large (max 100MB)", att.Name), "")) |
| 565 | return true |
| 566 | } |
| 567 | |
| 568 | mimeType := chat.DetectMimeType(absPath) |
| 569 | |
| 570 | switch { |
| 571 | case chat.IsTextFile(absPath): |
| 572 | if fi.Size() > chat.MaxInlineFileSize { |
| 573 | slog.WarnContext(ctx, "skipping attachment: text file too large to inline", "path", absPath, "size", fi.Size(), "max", chat.MaxInlineFileSize) |
| 574 | a.sendEvent(ctx, runtime.Warning(fmt.Sprintf("Skipped attachment %s: text file too large to inline (max 5MB)", att.Name), "")) |
| 575 | return true |
| 576 | } |
| 577 | content, err := chat.ReadFileForInline(absPath) |
| 578 | if err != nil { |
| 579 | slog.WarnContext(ctx, "skipping attachment: failed to read file", "path", absPath, "error", err) |
| 580 | a.sendEvent(ctx, runtime.Warning(fmt.Sprintf("Skipped attachment %s: failed to read file", att.Name), "")) |
| 581 | return true |
| 582 | } |
| 583 | textBuilder.WriteString("\n\n") |
| 584 | textBuilder.WriteString(content) |
| 585 | |
| 586 | case chat.IsSupportedMimeType(mimeType): |
| 587 | // Route through ProcessAttachmentWithMetadata for normalised Document output. |
| 588 | // For images this also returns resize metadata used to emit a dimension note. |
| 589 | doc, resizeMeta, procErr := chat.ProcessAttachmentWithMetadata(chat.MessagePart{ |
| 590 | Type: chat.MessagePartTypeFile, |
| 591 | File: &chat.MessageFile{Path: absPath, MimeType: mimeType}, |
| 592 | }) |
| 593 | if procErr != nil { |
no test coverage detected