Skip to content

Images#15

Merged
juliusmarminge merged 6 commits intomainfrom
images
Feb 12, 2026
Merged

Images#15
juliusmarminge merged 6 commits intomainfrom
images

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 12, 2026

CleanShot 2026-02-12 at 15 50 16@2x CleanShot 2026-02-12 at 15 50 18@2x CleanShot 2026-02-12 at 15 50 29@2x

Summary

  • improve image handling so pasting and drag-and-drop insert content without needing a button
  • expand images into a larger dialog on click and fix the dialog close button hit area
  • ensure UI state + persistence/types/tests cover the updated behavior across server and web layers

Testing

  • Not run (not requested)

Open with Devin

Summary by CodeRabbit

  • New Features

    • Send multiple image attachments (picker, drag-and-drop, paste); images can be sent alone or with text. Image previews in composer and timeline with expandable modal. Attachments included in chat history/bootstrap and persisted transcripts (preview URLs not stored).
  • Bug Fixes / UX

    • Clear validation and errors for empty sends, non-image or oversized files, and max-attachment limits. Preview URLs revoked after removal or send.
  • Tests

    • Added coverage for image handling, validation, and persistence.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds multi-item turn inputs and image-attachment end-to-end support: client UI for attaching/previewing images, types and store updates, persistence/hydration changes, provider contract and validation for attachments, server-side multi-item turn construction, and new tests covering image scenarios.

Changes

Cohort / File(s) Summary
Server turn input & tests
apps/server/src/codexAppServerManager.ts, apps/server/src/codexAppServerManager.test.ts
sendTurn now builds a unified turnInput array of text and image items (uses attachment.dataUrl for images), validates non-empty input, and adds tests for text+image, image-only, and empty-input rejection with a test harness.
Web chat UI / composer
apps/web/src/components/ChatView.tsx
Adds image attach UI (paste/drag-drop), file→dataURL reading, previews with cleanup, removal and expanded preview modal, attachment validation (type/size/count), and sends images alongside text.
Client types & state
apps/web/src/types.ts, apps/web/src/store.ts
Introduces ChatImageAttachment / ChatAttachment, adds attachments?: ChatAttachment[] to ChatMessage, and extends PUSH_USER_MESSAGE action to accept optional attachments (reducer persists attachments when present).
Persistence schema & tests
apps/web/src/persistenceSchema.ts, apps/web/src/persistenceSchema.test.ts
Adds optional attachments to persisted message schema; serialization strips previewUrl and persists image metadata (type, id, name, mimeType, sizeBytes); hydration restores attachments when present; tests updated.
Transcript / bootstrap
apps/web/src/historyBootstrap.ts, apps/web/src/historyBootstrap.test.ts
Adds attachmentSummary and integrates image summaries into message blocks (handles text+attachments, text-only, attachments-only); uses toReversed() for chronological assembly; test added for image context.
Provider contracts & tests
packages/contracts/src/provider.ts, packages/contracts/src/provider.test.ts
Adds image-related constants and attachment schemas, makes input optional and adds attachments array with max/default, enforces cross-field validation (text or attachments required), and extends tests for image validation and limits.
UI layout tweaks
apps/web/src/components/DiffPanel.tsx, apps/web/src/components/Sidebar.tsx
Minor className/layout adjustments to merge Electron drag-region styling into existing containers.
Manifests / package
manifest_file, package.json
Referenced manifest/package file adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ChatView as ChatView (Client)
    participant Store as State Store
    participant Codex as CodexAppServerManager (Server)
    participant Backend as Backend Service

    User->>ChatView: Attach image(s) and/or enter text
    ChatView->>ChatView: Validate attachments, read files as data URLs, create previews
    User->>ChatView: Click Send
    ChatView->>Store: PUSH_USER_MESSAGE (text + attachments metadata)
    Store-->>ChatView: State updated
    ChatView->>Codex: sendTurn(threadId, { input?, attachments? })
    Codex->>Codex: Build turnInput array (text + image items), validate non-empty
    Codex->>Backend: POST turn/start (includes text + image data URLs)
    Backend-->>Codex: Return threadId/turnId
    Codex->>Store: Update session (running, activeTurnId)
    Codex-->>ChatView: Resolve send result
    ChatView->>ChatView: Revoke previews and clear attachments
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title addresses image paste/drag and dialog behavior, which are covered in ChatView.tsx changes and related refactoring, but the title partially overlooks the substantial image attachment feature (multi-turn input, persistence, types, contracts) that constitutes the main bulk of changes.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch images

Comment @coderabbitai help to get the list of available commands and usage tips.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 12, 2026

Add image attachments support to CodexAppServerManager.sendTurn and propagate image-only turns through web UI, persistence, and provider validation with limits (max attachments, bytes, data URL chars)

sendTurn builds mixed text/image input and rejects empty turns; web ChatView composes, previews, and sends image-only messages; persistence and types include attachment metadata; provider input schema validates image attachments and enforces limits; tests cover server, web, history bootstrap, persistence, and provider cases.

📍Where to Start

Start with CodexAppServerManager.sendTurn in codexAppServerManager.ts to see payload assembly for mixed text/image turns, then review provider validation in provider.ts and UI sending in ChatView.tsx.


Macroscope summarized ea16f6f.

Comment thread packages/contracts/src/provider.ts Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 12, 2026

Greptile Overview

Greptile Summary

Added image paste/drag-and-drop support and expanded image preview dialog to the chat interface. Images can now be attached via paste or drag-and-drop without clicking a button, and clicking on images opens them in a full-screen dialog with proper close button hit area.

Key Changes:

  • Image attachments flow end-to-end from web UI through server to Codex app-server
  • Paste and drag-and-drop handlers automatically add images to composer
  • Image-only turns supported with bootstrap prompt fallback
  • Expanded image dialog with backdrop click-to-close and escape key handler
  • Blob URL memory management with cleanup on thread switch and unmount
  • Persistence layer strips ephemeral previewUrl from stored messages
  • Bootstrap transcript includes image attachment context
  • Comprehensive test coverage for schema validation and turn handling

Issue Found:

  • Memory leak in ChatView.tsx: blob URLs not revoked when clearing composer images after successful send (line 764)

Confidence Score: 4/5

  • Safe to merge after fixing the memory leak in ChatView.tsx
  • Implementation is thorough with proper validation, tests, and persistence handling. One memory leak issue found where blob URLs aren't revoked when clearing composer images after send. This is a resource leak that should be fixed before merging, though it won't cause functional errors.
  • apps/web/src/components/ChatView.tsx requires attention for the blob URL cleanup issue at line 764

Important Files Changed

Filename Overview
apps/web/src/components/ChatView.tsx Added comprehensive image paste/drag-and-drop with dialog expansion; memory cleanup could be improved when sending images
apps/server/src/codexAppServerManager.ts Implemented image support in turn/start by building input array with text and image objects
packages/contracts/src/provider.ts Added image schema validation with proper MIME type checking and size limits
apps/web/src/persistenceSchema.ts Added image persistence schema excluding previewUrl (ephemeral blob URLs)

Sequence Diagram

sequenceDiagram
    participant User
    participant ChatView
    participant Composer
    participant Store
    participant API
    participant Server
    participant Codex

    User->>Composer: Paste/Drag image
    Composer->>Composer: Create blob URL
    Composer->>ChatView: addComposerImages()
    ChatView->>ChatView: Validate size/type/count
    ChatView->>Composer: Display preview thumbnails
    
    User->>Composer: Click send
    Composer->>ChatView: onSend()
    ChatView->>Store: PUSH_USER_MESSAGE (with images)
    ChatView->>ChatView: readFileAsDataUrl()
    ChatView->>API: providers.sendTurn()
    API->>Server: WebSocket message
    Server->>Codex: turn/start (text + image dataUrls)
    Codex-->>Server: turn response
    Server-->>API: WebSocket push events
    API-->>ChatView: Provider events
    ChatView->>ChatView: Clear composer
    
    User->>ChatView: Click image thumbnail
    ChatView->>ChatView: openImageDialog()
    ChatView->>User: Show expanded dialog
    User->>ChatView: Click backdrop/Escape
    ChatView->>ChatView: setExpandedImage(null)
Loading

Last reviewed commit: 3cf58f8

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment on lines +763 to +764
text: trimmed,
...(messageImages.length > 0 ? { images: messageImages } : {}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak: blob URLs not revoked when clearing composer images after send

Suggested change
text: trimmed,
...(messageImages.length > 0 ? { images: messageImages } : {}),
setPrompt("");
revokePreviewUrls(composerImages);
setComposerImages([]);

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/server/src/codexAppServerManager.ts`:
- Around line 260-292: The code builds turnInput in sendTurn without validating
image count or size; add server-side validation before constructing turnInput
using a Zod schema from packages/contracts (or create a local schema that
imports shared types) to enforce maximum image count (e.g. MAX_IMAGES) and
per-image size (e.g. MAX_IMAGE_SIZE_BYTES), and reject the request (throw an
Error or return a 4xx) when limits are exceeded; validate input.images and each
image.dataUrl length/size, then only map validated images into turnInput (refer
to sendTurn, turnInput, turnStartParams and input.images to locate the logic) so
oversized/excessive payloads are blocked before calling codex.
🧹 Nitpick comments (1)
packages/contracts/src/provider.ts (1)

62-82: Validate dataUrl media type against mimeType (case‑insensitive).
Keeps metadata consistent and avoids mismatched content handling.

♻️ Suggested tweak
   .superRefine((value, ctx) => {
-    if (!value.dataUrl.startsWith("data:image/")) {
+    const dataUrlLower = value.dataUrl.toLowerCase();
+    if (!dataUrlLower.startsWith("data:image/")) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
         message: "Image dataUrl must start with data:image/",
         path: ["dataUrl"],
       });
     }
+    const match = value.dataUrl.match(/^data:([^;]+);/i);
+    const dataMime = match?.[1]?.toLowerCase();
+    if (dataMime && dataMime !== value.mimeType.toLowerCase()) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: "mimeType must match dataUrl media type",
+        path: ["mimeType"],
+      });
+    }
   });

Comment on lines +260 to 292
const turnInput: Array<
| { type: "text"; text: string; text_elements: [] }
| { type: "image"; url: string }
> = [];
if (input.input) {
turnInput.push({
type: "text",
text: input.input,
text_elements: [],
});
}
for (const image of input.images ?? []) {
turnInput.push({
type: "image",
url: image.dataUrl,
});
}
if (turnInput.length === 0) {
throw new Error("Turn input must include text or image attachments.");
}

const turnStartParams: {
threadId: string;
input: Array<{ type: "text"; text: string; text_elements: [] }>;
input: Array<
| { type: "text"; text: string; text_elements: [] }
| { type: "image"; url: string }
>;
model?: string;
effort?: string;
} = {
threadId: context.session.threadId,
input: [
{
type: "text",
text: input.input,
text_elements: [],
},
],
input: turnInput,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add server-side validation for image limits before calling codex.

sendTurn now forwards image data URLs but does not enforce max image count/size. A single oversized or excessive payload can destabilize the app-server and breaks “predictable behavior under load.” Validate against shared contract limits (or a Zod schema from packages/contracts) before building turnInput.

🔧 Suggested guardrails
-    const turnInput: Array<
+    const turnInput: Array<
       | { type: "text"; text: string; text_elements: [] }
       | { type: "image"; url: string }
     > = [];
+    const images = input.images ?? [];
+    if (images.length > PROVIDER_SEND_TURN_MAX_IMAGES) {
+      throw new Error(`Too many images (max ${PROVIDER_SEND_TURN_MAX_IMAGES}).`);
+    }
     if (input.input) {
       turnInput.push({
         type: "text",
         text: input.input,
         text_elements: [],
       });
     }
-    for (const image of input.images ?? []) {
+    for (const image of images) {
+      if (image.sizeBytes > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) {
+        throw new Error(`Image "${image.name}" exceeds size limit.`);
+      }
       turnInput.push({
         type: "image",
         url: image.dataUrl,
       });
     }

As per coding guidelines: “apps/server/src//*.{ts,js}: Maintain predictable behavior under load and during failures (session restarts, reconnects, partial streams)” and “apps//*.{ts,tsx}: Use Zod schemas from packages/contracts for shared type contracts covering provider events, WebSocket protocol, and model/session types.”

🤖 Prompt for AI Agents
In `@apps/server/src/codexAppServerManager.ts` around lines 260 - 292, The code
builds turnInput in sendTurn without validating image count or size; add
server-side validation before constructing turnInput using a Zod schema from
packages/contracts (or create a local schema that imports shared types) to
enforce maximum image count (e.g. MAX_IMAGES) and per-image size (e.g.
MAX_IMAGE_SIZE_BYTES), and reject the request (throw an Error or return a 4xx)
when limits are exceeded; validate input.images and each image.dataUrl
length/size, then only map validated images into turnInput (refer to sendTurn,
turnInput, turnStartParams and input.images to locate the logic) so
oversized/excessive payloads are blocked before calling codex.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/ChatView.tsx (1)

696-807: ⚠️ Potential issue | 🟠 Major

Preserve attachments for retry on send failure.
Line 767/768 clears the composer before readFileAsDataUrl and sendTurn. If either fails, users must reattach images to retry. Consider restoring the draft on error (or clearing only after success).

💡 Suggested fix (restore draft on error)
-    const trimmed = prompt.trim();
+    const trimmed = prompt.trim();
+    const promptSnapshot = prompt;
     if (!trimmed && composerImages.length === 0) return;
@@
     setPrompt("");
     setComposerImages([]);
@@
     } catch (err) {
+      setPrompt(promptSnapshot);
+      setComposerImages(composerImagesSnapshot);
       setThreadError(
         activeThread.id,
         err instanceof Error ? err.message : "Failed to send message.",
       );
     } finally {
🧹 Nitpick comments (2)
packages/contracts/src/provider.ts (1)

72-94: Consider potential mimeType/dataUrl validation mismatch.

The schema validates mimeType with a regex (/^image\//i) and separately validates dataUrl starts with "data:image/". However, there's no cross-validation ensuring the mimeType in the schema matches the actual MIME type embedded in the dataUrl. For example, a payload with mimeType: "image/png" but dataUrl: "data:image/jpeg;base64,..." would pass validation.

If strict consistency is desired, consider adding a refinement that extracts and compares the MIME type from the dataUrl prefix against the declared mimeType field.

packages/contracts/src/provider.test.ts (1)

91-104: Consider adding a boundary test for exactly max attachments.

The test correctly rejects PROVIDER_SEND_TURN_MAX_ATTACHMENTS + 1 attachments. Consider also adding a test that verifies exactly PROVIDER_SEND_TURN_MAX_ATTACHMENTS attachments are accepted, to fully cover the boundary condition.

📝 Suggested boundary test
it("accepts exactly the max attachment count", () => {
  const parsed = providerSendTurnInputSchema.parse({
    sessionId: "sess_1",
    attachments: Array.from({ length: PROVIDER_SEND_TURN_MAX_ATTACHMENTS }, (_, index) => ({
      type: "image" as const,
      name: `image-${index}.png`,
      mimeType: "image/png",
      sizeBytes: 1_024,
      dataUrl: "data:image/png;base64,AAAA",
    })),
  });
  expect(parsed.attachments).toHaveLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS);
});

- Update `ChatView` so Electron uses a fixed 52px drag titlebar, including empty-thread state.
- Make `DiffPanel` header use the same Electron drag-region sizing.
- Consolidate Sidebar drag-region into branding row and remove separate spacer div.
- Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/historyBootstrap.ts (1)

93-109: ⚠️ Potential issue | 🔴 Critical

Use slice().reverse() instead of toReversed() for broader browser support.

Array.prototype.toReversed() is not supported in Firefox 114, which is within your build target range (Vite 8's baseline-widely-available defaults to Chrome 111, Edge 111, Firefox 114, and Safari 16.4). This will cause runtime errors for Firefox users. Use slice().reverse() as a compatible fallback on lines 97 and 109.

Suggested fix
-    const nextChronological = nextNewestFirst.toReversed();
+    const nextChronological = nextNewestFirst.slice().reverse();
...
-  let includedChronological = includedNewestFirst.toReversed();
+  let includedChronological = includedNewestFirst.slice().reverse();

});
const previousMessages = activeThread.messages;
setPrompt("");
setComposerImages([]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low

components/ChatView.tsx:770 Blob URLs created via URL.createObjectURL in addComposerImages are copied to messageAttachments when sending, but setComposerImages([]) clears the state without revoking them. Consider calling revokePreviewUrls(composerImagesSnapshot) before clearing the state.

Suggested change
setComposerImages([]);
revokePreviewUrls(composerImagesSnapshot);
setComposerImages([]);

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/web/src/components/ChatView.tsx around line 770:

Blob URLs created via `URL.createObjectURL` in `addComposerImages` are copied to `messageAttachments` when sending, but `setComposerImages([])` clears the state without revoking them. Consider calling `revokePreviewUrls(composerImagesSnapshot)` before clearing the state.

@juliusmarminge juliusmarminge changed the title Handle image paste/drag and resize dialog behavior Images Feb 12, 2026
@juliusmarminge juliusmarminge merged commit 1dca701 into main Feb 12, 2026
2 checks passed
@juliusmarminge juliusmarminge deleted the images branch February 12, 2026 23:52
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/web/src/components/ChatView.tsx`:
- Around line 752-800: The persisted messageAttachments are using
image.previewUrl (a blob URL) which breaks hydration and safe revocation; change
the flow so composerImagesSnapshot is converted to a dataUrl (via
readFileAsDataUrl) once and use that dataUrl for both the UI preview and the
Provider send payload (turnAttachments), update the creation of
messageAttachments to set previewUrl to the dataUrl instead of the blob URL, and
ensure any blob URLs are revoked immediately after creating the dataUrl; locate
symbols composerImagesSnapshot, messageAttachments, turnAttachments,
readFileAsDataUrl, and previewUrl to make these changes and clear blob URLs
after conversion.

In `@packages/contracts/src/provider.ts`:
- Around line 62-73: The providerSendTurnImageAttachmentSchema currently only
length-validates dataUrl allowing invalid or mismatched MIME headers; update
providerSendTurnImageAttachmentSchema to add a z.superRefine that (1) verifies
dataUrl matches the base64 data URL pattern
/^data:([^;]+);base64,([A-Za-z0-9+/=]+)$/ and extracts the MIME type and
payload, (2) checks the extracted MIME type equals the mimeType field, and (3)
fails with clear errors via ctx.addIssue when the format is invalid or MIME
types mismatch; reference providerSendTurnImageAttachmentSchema and its mimeType
and dataUrl fields when making the change.

Comment on lines +752 to +800
setThreadError(activeThread.id, null);
const messageAttachments: ChatImageAttachment[] = composerImagesSnapshot.map((image) => ({
type: "image",
id: image.id,
name: image.name,
mimeType: image.mimeType,
sizeBytes: image.sizeBytes,
previewUrl: image.previewUrl,
}));
dispatch({
type: "PUSH_USER_MESSAGE",
threadId: activeThread.id,
id: crypto.randomUUID(),
text: trimmed,
...(messageAttachments.length > 0 ? { attachments: messageAttachments } : {}),
});
const previousMessages = activeThread.messages;
setPrompt("");
setComposerImages([]);

const sessionInfo = await ensureSession(sessionCwd);
if (!sessionInfo) return;

setIsSending(true);
try {
const turnAttachments = await Promise.all(
composerImagesSnapshot.map(async (image): Promise<ProviderSendTurnAttachmentInput> => ({
type: "image",
name: image.name,
mimeType: image.mimeType,
sizeBytes: image.sizeBytes,
dataUrl: await readFileAsDataUrl(image.file),
})),
);
const shouldBootstrap =
previousMessages.length > 0 &&
(sessionInfo.continuityState === "new" || sessionInfo.continuityState === "fallback_new");
const latestPromptForBootstrap = trimmed || IMAGE_ONLY_BOOTSTRAP_PROMPT;
const input = shouldBootstrap
? buildBootstrapInput(previousMessages, trimmed, PROVIDER_SEND_TURN_MAX_INPUT_CHARS).text
: trimmed;
? buildBootstrapInput(
previousMessages,
latestPromptForBootstrap,
PROVIDER_SEND_TURN_MAX_INPUT_CHARS,
).text
: (trimmed || undefined);
await api.providers.sendTurn({
sessionId: sessionInfo.sessionId,
input,
...(input ? { input } : {}),
...(turnAttachments.length > 0 ? { attachments: turnAttachments } : {}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persisting blob previewUrl will break hydration and prevents safe revocation.
Blob URLs are session-scoped; if messages are persisted/hydrated, previews will be invalid after reload. It also blocks revoking without losing previews, which can leak memory across many sends. Consider using the already-read dataUrl for message previews and revoking blob URLs right after.

💡 Suggested approach (reuse dataUrl for UI + send)
-    const messageAttachments: ChatImageAttachment[] = composerImagesSnapshot.map((image) => ({
-      type: "image",
-      id: image.id,
-      name: image.name,
-      mimeType: image.mimeType,
-      sizeBytes: image.sizeBytes,
-      previewUrl: image.previewUrl,
-    }));
+    const turnAttachments = await Promise.all(
+      composerImagesSnapshot.map(async (image): Promise<ProviderSendTurnAttachmentInput> => ({
+        type: "image",
+        name: image.name,
+        mimeType: image.mimeType,
+        sizeBytes: image.sizeBytes,
+        dataUrl: await readFileAsDataUrl(image.file),
+      })),
+    );
+    const messageAttachments: ChatImageAttachment[] = composerImagesSnapshot.map((image, index) => ({
+      type: "image",
+      id: image.id,
+      name: image.name,
+      mimeType: image.mimeType,
+      sizeBytes: image.sizeBytes,
+      previewUrl: turnAttachments[index]?.dataUrl,
+    }));
+    revokePreviewUrls(composerImagesSnapshot);
🤖 Prompt for AI Agents
In `@apps/web/src/components/ChatView.tsx` around lines 752 - 800, The persisted
messageAttachments are using image.previewUrl (a blob URL) which breaks
hydration and safe revocation; change the flow so composerImagesSnapshot is
converted to a dataUrl (via readFileAsDataUrl) once and use that dataUrl for
both the UI preview and the Provider send payload (turnAttachments), update the
creation of messageAttachments to set previewUrl to the dataUrl instead of the
blob URL, and ensure any blob URLs are revoked immediately after creating the
dataUrl; locate symbols composerImagesSnapshot, messageAttachments,
turnAttachments, readFileAsDataUrl, and previewUrl to make these changes and
clear blob URLs after conversion.

Comment on lines +62 to 73
export const providerSendTurnImageAttachmentSchema = z.object({
type: z.literal("image"),
name: z.string().trim().min(1).max(255),
mimeType: z
.string()
.trim()
.min(1)
.max(100)
.regex(/^image\//i, "mimeType must be an image/* MIME type"),
sizeBytes: z.number().int().min(1).max(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES),
dataUrl: z.string().trim().min(1).max(PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let me check the file structure and find the provider.ts file
find . -type f -name "provider.ts" -path "*/packages/contracts/*" 2>/dev/null | head -5

Repository: pingdotgg/codething-mvp

Length of output: 103


🏁 Script executed:

# Read the provider.ts file to see the current state
wc -l packages/contracts/src/provider.ts

Repository: pingdotgg/codething-mvp

Length of output: 105


🏁 Script executed:

# Get the full context around lines 62-73 and 90-98
head -n 110 packages/contracts/src/provider.ts | tail -n +50

Repository: pingdotgg/codething-mvp

Length of output: 2168


🏁 Script executed:

# Also check what constants are defined for the max values
grep -n "PROVIDER_SEND_TURN_MAX_IMAGE" packages/contracts/src/provider.ts

Repository: pingdotgg/codething-mvp

Length of output: 381


🏁 Script executed:

# Search for how dataUrl is used downstream to understand importance of validation
rg "dataUrl" --type ts packages/contracts/src/ -A 2 -B 2

Repository: pingdotgg/codething-mvp

Length of output: 1487


🏁 Script executed:

# Check how the schema is used - look for imports or usage of providerSendTurnImageAttachmentSchema
rg "providerSendTurnImageAttachmentSchema|providerSendTurnInputSchema" --type ts -B 2 -A 2

Repository: pingdotgg/codething-mvp

Length of output: 5133


🏁 Script executed:

# Check provider.test.ts more thoroughly to see all test cases
cat -n packages/contracts/src/provider.test.ts | head -100

Repository: pingdotgg/codething-mvp

Length of output: 3675


🏁 Script executed:

# Look for any code that processes/parses dataUrl to see if validation is critical
rg "dataUrl" --type ts packages/ -B 3 -A 3 | head -60

Repository: pingdotgg/codething-mvp

Length of output: 2034


🏁 Script executed:

# Check if there are any validation utilities for data URLs elsewhere
fd -e ts -e js | xargs rg "data:" -l --type ts 2>/dev/null | head -10

Repository: pingdotgg/codething-mvp

Length of output: 391


🏁 Script executed:

# Check lines 75-98 to understand "Also applies to" reference
sed -n '75,98p' packages/contracts/src/provider.ts

Repository: pingdotgg/codething-mvp

Length of output: 888


Validate data URL format and MIME type consistency in attachment schema.

dataUrl is only length-validated, allowing non-data URLs or mismatched MIME headers to pass while downstream trusts mimeType. Add a superRefine check to ensure dataUrl is a base64 data URL with the correct MIME type prefix.

🔧 Suggested fix
-export const providerSendTurnImageAttachmentSchema = z.object({
+export const providerSendTurnImageAttachmentSchema = z
+  .object({
     type: z.literal("image"),
     name: z.string().trim().min(1).max(255),
     mimeType: z
       .string()
       .trim()
       .min(1)
       .max(100)
       .regex(/^image\//i, "mimeType must be an image/* MIME type"),
     sizeBytes: z.number().int().min(1).max(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES),
     dataUrl: z.string().trim().min(1).max(PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS),
-});
+  })
+  .superRefine((value, ctx) => {
+    const header = value.dataUrl.split(",", 1)[0]?.toLowerCase() ?? "";
+    const expected = `data:${value.mimeType.toLowerCase()}`;
+    if (!header.startsWith(expected) || !header.includes(";base64")) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: "dataUrl must be a base64 data URL matching mimeType",
+        path: ["dataUrl"],
+      });
+    }
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const providerSendTurnImageAttachmentSchema = z.object({
type: z.literal("image"),
name: z.string().trim().min(1).max(255),
mimeType: z
.string()
.trim()
.min(1)
.max(100)
.regex(/^image\//i, "mimeType must be an image/* MIME type"),
sizeBytes: z.number().int().min(1).max(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES),
dataUrl: z.string().trim().min(1).max(PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS),
});
export const providerSendTurnImageAttachmentSchema = z
.object({
type: z.literal("image"),
name: z.string().trim().min(1).max(255),
mimeType: z
.string()
.trim()
.min(1)
.max(100)
.regex(/^image\//i, "mimeType must be an image/* MIME type"),
sizeBytes: z.number().int().min(1).max(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES),
dataUrl: z.string().trim().min(1).max(PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS),
})
.superRefine((value, ctx) => {
const header = value.dataUrl.split(",", 1)[0]?.toLowerCase() ?? "";
const expected = `data:${value.mimeType.toLowerCase()}`;
if (!header.startsWith(expected) || !header.includes(";base64")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "dataUrl must be a base64 data URL matching mimeType",
path: ["dataUrl"],
});
}
});
🤖 Prompt for AI Agents
In `@packages/contracts/src/provider.ts` around lines 62 - 73, The
providerSendTurnImageAttachmentSchema currently only length-validates dataUrl
allowing invalid or mismatched MIME headers; update
providerSendTurnImageAttachmentSchema to add a z.superRefine that (1) verifies
dataUrl matches the base64 data URL pattern
/^data:([^;]+);base64,([A-Za-z0-9+/=]+)$/ and extracts the MIME type and
payload, (2) checks the extracted MIME type equals the mimeType field, and (3)
fails with clear errors via ctx.addIssue when the format is invalid or MIME
types mismatch; reference providerSendTurnImageAttachmentSchema and its mimeType
and dataUrl fields when making the change.

jjalangtry pushed a commit to jjalangtry/t3code that referenced this pull request Mar 16, 2026
smraikai pushed a commit to smraikai/t3code that referenced this pull request Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant