authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-16 13:04:41+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-16 13:04:41+02:00
log2a33a18716854b8d07378008cf3101ad999209ae
tree90ac69468b735187863c075bf415cec620dc75c5
parentbde0a4c0dd2d420a5fd71f39448d5db1bab078da
signature Signed by PGP key B5690EEEBB952194

fix(mobile): Send composer images in upload wire format (#4035)


4 files changed, 48 insertions(+), 4 deletions(-)

apps/mobile/src/lib/composerImages.test.ts+31-1
......@@ -36,7 +36,37 @@ vi.mock("./uuid", () => ({
3636 uuidv4: () => "attachment-id",
3737}));
3838
39import { convertPastedImagesToAttachments, isOwnedPastedImageUri } from "./composerImages";
39import {
40 convertPastedImagesToAttachments,
41 isOwnedPastedImageUri,
42 toUploadChatImageAttachments,
43} from "./composerImages";
44
45describe("toUploadChatImageAttachments", () => {
46 it("strips client draft id and previewUri for the startTurn wire shape", () => {
47 expect(
48 toUploadChatImageAttachments([
49 {
50 id: "client-draft-id",
51 type: "image",
52 name: "pasted-image.png",
53 mimeType: "image/png",
54 sizeBytes: 12,
55 dataUrl: "data:image/png;base64,AA==",
56 previewUri: "file:///tmp/preview.png",
57 },
58 ]),
59 ).toEqual([
60 {
61 type: "image",
62 name: "pasted-image.png",
63 mimeType: "image/png",
64 sizeBytes: 12,
65 dataUrl: "data:image/png;base64,AA==",
66 },
67 ]);
68 });
69});
4070
4171describe("native pasted image cleanup", () => {
4272 beforeEach(() => {
apps/mobile/src/lib/composerImages.ts+13
......@@ -10,6 +10,19 @@ export interface DraftComposerImageAttachment extends UploadChatImageAttachment
1010 readonly previewUri: string;
1111}
1212
13/** Wire shape for startTurn: pure uploads without client draft id / previewUri. */
14export function toUploadChatImageAttachments(
15 attachments: ReadonlyArray<DraftComposerImageAttachment>,
16): ReadonlyArray<UploadChatImageAttachment> {
17 return attachments.map((attachment) => ({
18 type: attachment.type,
19 name: attachment.name,
20 mimeType: attachment.mimeType,
21 sizeBytes: attachment.sizeBytes,
22 dataUrl: attachment.dataUrl,
23 }));
24}
25
1326const OWNED_PASTED_IMAGE_DIRECTORY = "t3-composer-paste";
1427
1528function estimateBase64ByteSize(base64: string): number {
apps/mobile/src/lib/projectThreadStartTurn.ts+2-2
......@@ -8,7 +8,7 @@ import {
88 type RuntimeMode,
99} from "@t3tools/contracts";
1010
11import type { DraftComposerImageAttachment } from "./composerImages";
11import { toUploadChatImageAttachments, type DraftComposerImageAttachment } from "./composerImages";
1212
1313export function deriveThreadTitleFromPrompt(value: string): string {
1414 const trimmed = value.trim();
......@@ -55,7 +55,7 @@ export function buildProjectThreadStartTurnInput(spec: ProjectThreadStartTurnSpe
5555 messageId: MessageId.make(spec.messageId),
5656 role: "user" as const,
5757 text: spec.text,
58 attachments: spec.attachments,
58 attachments: toUploadChatImageAttachments(spec.attachments),
5959 },
6060 modelSelection: spec.modelSelection,
6161 titleSeed: title,
apps/mobile/src/state/use-thread-outbox-drain.ts+2-1
......@@ -17,6 +17,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
1717
1818import { scopedThreadKey } from "../lib/scopedEntities";
1919import { buildProjectThreadStartTurnInput } from "../lib/projectThreadStartTurn";
20import { toUploadChatImageAttachments } from "../lib/composerImages";
2021import { randomHex } from "../lib/uuid";
2122import { appAtomRegistry } from "./atom-registry";
2223import { useProjects, useThreadShells } from "./entities";
......@@ -221,7 +222,7 @@ export function useThreadOutboxDrain(): void {
221222 messageId: queuedMessage.messageId,
222223 role: "user",
223224 text: queuedMessage.text,
224 attachments: queuedMessage.attachments,
225 attachments: toUploadChatImageAttachments(queuedMessage.attachments),
225226 },
226227 modelSelection: settings.modelSelection,
227228 runtimeMode: settings.runtimeMode,