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", () => ({...@@ -36,7 +36,37 @@ vi.mock("./uuid", () => ({
36 uuidv4: () => "attachment-id",36 uuidv4: () => "attachment-id",
37}));37}));
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
41describe("native pasted image cleanup", () => {71describe("native pasted image cleanup", () => {
42 beforeEach(() => {72 beforeEach(() => {
apps/mobile/src/lib/composerImages.ts+13
...@@ -10,6 +10,19 @@ export interface DraftComposerImageAttachment extends UploadChatImageAttachment...@@ -10,6 +10,19 @@ export interface DraftComposerImageAttachment extends UploadChatImageAttachment
10 readonly previewUri: string;10 readonly previewUri: string;
11}11}
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
13const OWNED_PASTED_IMAGE_DIRECTORY = "t3-composer-paste";26const OWNED_PASTED_IMAGE_DIRECTORY = "t3-composer-paste";
1427
15function estimateBase64ByteSize(base64: string): number {28function estimateBase64ByteSize(base64: string): number {
apps/mobile/src/lib/projectThreadStartTurn.ts+2-2
...@@ -8,7 +8,7 @@ import {...@@ -8,7 +8,7 @@ import {
8 type RuntimeMode,8 type RuntimeMode,
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
1010
11import type { DraftComposerImageAttachment } from "./composerImages";11import { toUploadChatImageAttachments, type DraftComposerImageAttachment } from "./composerImages";
1212
13export function deriveThreadTitleFromPrompt(value: string): string {13export function deriveThreadTitleFromPrompt(value: string): string {
14 const trimmed = value.trim();14 const trimmed = value.trim();
...@@ -55,7 +55,7 @@ export function buildProjectThreadStartTurnInput(spec: ProjectThreadStartTurnSpe...@@ -55,7 +55,7 @@ export function buildProjectThreadStartTurnInput(spec: ProjectThreadStartTurnSpe
55 messageId: MessageId.make(spec.messageId),55 messageId: MessageId.make(spec.messageId),
56 role: "user" as const,56 role: "user" as const,
57 text: spec.text,57 text: spec.text,
58 attachments: spec.attachments,58 attachments: toUploadChatImageAttachments(spec.attachments),
59 },59 },
60 modelSelection: spec.modelSelection,60 modelSelection: spec.modelSelection,
61 titleSeed: title,61 titleSeed: title,
apps/mobile/src/state/use-thread-outbox-drain.ts+2-1
...@@ -17,6 +17,7 @@ import { useCallback, useEffect, useRef, useState } from "react";...@@ -17,6 +17,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
1717
18import { scopedThreadKey } from "../lib/scopedEntities";18import { scopedThreadKey } from "../lib/scopedEntities";
19import { buildProjectThreadStartTurnInput } from "../lib/projectThreadStartTurn";19import { buildProjectThreadStartTurnInput } from "../lib/projectThreadStartTurn";
20import { toUploadChatImageAttachments } from "../lib/composerImages";
20import { randomHex } from "../lib/uuid";21import { randomHex } from "../lib/uuid";
21import { appAtomRegistry } from "./atom-registry";22import { appAtomRegistry } from "./atom-registry";
22import { useProjects, useThreadShells } from "./entities";23import { useProjects, useThreadShells } from "./entities";
...@@ -221,7 +222,7 @@ export function useThreadOutboxDrain(): void {...@@ -221,7 +222,7 @@ export function useThreadOutboxDrain(): void {
221 messageId: queuedMessage.messageId,222 messageId: queuedMessage.messageId,
222 role: "user",223 role: "user",
223 text: queuedMessage.text,224 text: queuedMessage.text,
224 attachments: queuedMessage.attachments,225 attachments: toUploadChatImageAttachments(queuedMessage.attachments),
225 },226 },
226 modelSelection: settings.modelSelection,227 modelSelection: settings.modelSelection,
227 runtimeMode: settings.runtimeMode,228 runtimeMode: settings.runtimeMode,