Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -195,17 +195,26 @@ public final class ChiefComposer {
session.send(routed)
}

/// New Message's images as an agent prompt's attachments, bytes and order kept.
/// New Message's images as agent prompt attachments, normalized to formats agents accept.
static func attachments(_ images: [ConversationComposeImage]) -> [AgentAttachment] {
images.enumerated().map { index, image in
let ext = switch image.mimeType.lowercased() {
case "image/png": "png"
case "image/gif": "gif"
case "image/webp": "webp"
case "image/heic": "heic"
default: "jpg"
let source = AgentAttachment(kind: .image, name: "Image \(index + 1)", data: image.data)
let data: Data
let kind: AgentAttachment.Kind
let ext: String
switch source.imageMIMEType {
case "image/png": data = image.data; kind = .image; ext = "png"
case "image/jpeg": data = image.data; kind = .image; ext = "jpg"
case "image/gif": data = image.data; kind = .image; ext = "gif"
case "image/webp": data = image.data; kind = .image; ext = "webp"
default:
if let jpeg = UIImage(data: image.data)?.jpegData(compressionQuality: 0.85) {
data = jpeg; kind = .image; ext = "jpg"
} else {
data = image.data; kind = .file; ext = "bin"
}
}
return AgentAttachment(kind: .image, name: "Image \(index + 1).\(ext)", data: image.data)
return AgentAttachment(kind: kind, name: "Image \(index + 1).\(ext)", data: data)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,8 @@ private let tinyPNG = Data(base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAA
}

#if os(iOS)
import UIKit

@Suite @MainActor struct ChiefDirectoryTests {
func directory() -> ChiefDirectory {
let chief = ChiefDirectory.recipients(store: AppStore(teams: [], activeTeamID: "", workspaces: [], feed: []), identity: .standard, place: .mac)
Expand Down Expand Up @@ -762,6 +764,50 @@ private let tinyPNG = Data(base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAA
#expect(Set(prompt.attachments.map(\.id)).count == 2)
}

@Test func encodedImageBytesOverrideDeclaredMimeType() throws {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 2, height: 2))
let validPNG = renderer.pngData { context in
UIColor.red.setFill()
context.fill(CGRect(x: 0, y: 0, width: 2, height: 2))
}
let image = ConversationComposeImage(data: validPNG, width: 2, height: 2, mimeType: "image/heic")
let attachment = try #require(ChiefComposer.attachments([image]).first)
#expect(attachment.name.hasSuffix(".png"))
#expect(attachment.imageMIMEType == "image/png")
#expect(attachment.data == validPNG)
}

@Test func imageBytesWinOverTheDeclaredMimeType() throws {
let image = ConversationComposeImage(data: Self.png, width: 1, height: 1, mimeType: "image/heic")
let attachment = try #require(ChiefComposer.attachments([image]).first)
#expect(attachment.kind == .image)
#expect(attachment.name.hasSuffix(".png"))
#expect(attachment.imageMIMEType == "image/png")
}

@Test func unsupportedDecodedImageIsConvertedToJpeg() throws {
// A complete 1x1, 24-bit BMP (red pixel, padded four-byte row).
let bmp = try #require(Data(base64Encoded: "Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABABgAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AA=="))
try #require(UIImage(data: bmp) != nil)
#expect(AgentAttachment(kind: .image, name: "source", data: bmp).imageMIMEType == nil)
let attachment = try #require(ChiefComposer.attachments([
ConversationComposeImage(data: bmp, width: 1, height: 1, mimeType: "image/jpeg")
]).first)
#expect(attachment.kind == .image)
#expect(attachment.name.hasSuffix(".jpg"))
#expect(attachment.imageMIMEType == "image/jpeg")
#expect(attachment.data != bmp)
let jpeg = try #require(attachment.data)
#expect(UIImage(data: jpeg) != nil)
}

@Test func undecodableImageIsSentAsAFile() throws {
let image = ConversationComposeImage(data: Data([0x01, 0x02, 0x03]), width: 1, height: 1, mimeType: "image/png")
let attachment = try #require(ChiefComposer.attachments([image]).first)
#expect(attachment.kind == .file)
#expect(attachment.imageMIMEType == nil)
}

@Test func anImageOnlyMessageStillSends() throws {
let prompts = route(ConversationComposeMessage(text: "", images: [
ConversationComposeImage(data: Self.png, width: 1, height: 1, mimeType: "image/png"),
Expand Down
Loading