Skip to content

feat(chat): add image attachment support#5045

Merged
rschlaefli merged 26 commits intov3from
chatbot_add_image_attachments
Apr 16, 2026
Merged

feat(chat): add image attachment support#5045
rschlaefli merged 26 commits intov3from
chatbot_add_image_attachments

Conversation

@TheHummel
Copy link
Copy Markdown
Collaborator

@TheHummel TheHummel commented Mar 26, 2026

Summary

  • add end-to-end image attachment support to chat, including composer upload flow, attachment validation, and support for multiple images per message
  • add AI-generated image descriptions for prior-turn model context, model capability/config updates, and the related credit-accounting and prompt/config wiring
  • persist attachments with stable ordering, preview-safe history payloads, lazy full-image hydration, edit/regenerate preservation, and a preview tile plus on-demand viewer UX for persisted history attachments
  • generate persisted preview bytes server-side with sharp so the /chat request only carries full image bytes and avoids the local dev parse failure we hit during verification

Verification

  • pnpm --filter @klicker-uzh/chat check
  • pnpm --filter @klicker-uzh/chat test:run
  • Browser verified on the seeded chatbot flow: login, attach a real PNG plus text, send successfully, reload the thread, and open the persisted history image in the viewer

Notes

  • includes Prisma schema/migration work for attachment ordering and preview persistence
  • includes the updated implementation plan in project/CHAT_IMAGE_IMPROVEMENTS.md

@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Mar 26, 2026
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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 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

Walkthrough

Adds end-to-end image attachment support: client adapter and composer UI, message/store/type updates, API request/response and persistence (Prisma model + migration), runtime wiring to propagate image data through message conversions, and credit accounting for image description generation.

Changes

Cohort / File(s) Summary
Database Schema & Migration
packages/prisma/src/prisma/schema/chat.prisma, packages/prisma/src/prisma/schema/migrations/20260317133854_add_chat_attachment/migration.sql
Add ChatAttachmentType enum (IMAGE), new ChatAttachment model (UUID PK, imageBase64, imageDescription, timestamps) and one-to-one relation from ChatMessage with cascade delete/update; migration SQL creates enum/table/indexes/FK.
API Routes
apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts, apps/chat/src/app/api/chatbots/[chatbotId]/threads/[threadId]/messages/route.ts
POST /chat accepts optional root imageBase64 (validated), generates image description, upserts ChatAttachment, includes prior image descriptions into prompts, and accounts credits for description cost; GET /threads/.../messages now returns attachment per message.
Client Attachment Adapter
apps/chat/src/lib/attachments/imageAttachmentAdapter.ts
New imageAttachmentAdapter: validates MIME, converts HEIC/HEIF via dynamic heic2any, enforces ~5 MB limit with canvas downscaling fallback, returns Data URL PendingAttachment; send marks complete; remove no-op.
Types & Store
apps/chat/src/lib/api/types.ts, apps/chat/src/stores/chatStore.ts
Add optional attachment to ApiMessage and ExtendedThreadMessageLike (image object with nullable imageBase64 and imageDescription); conversion maps API attachment into internal messages.
Runtime & Hooks
apps/chat/src/app/RuntimeProvider.tsx, apps/chat/src/hooks/useChatResponse.ts, apps/chat/src/hooks/useThreadManagement.ts
Register imageAttachmentAdapter in runtime adapters; convertMessage propagates attachment into metadata.custom; outgoing chat payloads include root imageBase64 when trigger has one; thread management extracts/preserves attachments on new/edit.
UI / Thread Component
apps/chat/src/components/thread.tsx
Composer layout changed, added attachments UI (ComposerAttachments, ComposerAttachButton, ComposerImageAttachment) with preview/remove, shows attached images in messages and edit composer, surfaces attachment errors.
Composer Store
apps/chat/src/stores/composerStore.ts
New client-only Zustand store useComposerStore exposing attachmentError and setAttachmentError for composer error handling.
Package deps
apps/chat/package.json
Add dependency heic2any@0.0.4 to support HEIC/HEIF conversion.
Minor updates
apps/chat/src/app/RuntimeProvider.tsx, apps/chat/src/lib/api/types.ts (convert helper), apps/chat/src/hooks/useChatResponse.ts
Message conversion extended to include attachment in runtime flow; outgoing requests include imageBase64 derived from trigger message.

Sequence Diagram

sequenceDiagram
    actor User
    participant Composer as Composer UI
    participant Adapter as Image Adapter
    participant Store as Composer Store
    participant ClientHook as useChatResponse
    participant API as Chat API
    participant DB as Database
    participant Thread as Thread UI

    User->>Composer: select image file
    Composer->>Adapter: add(file)
    Adapter->>Adapter: validate/convert HEIC, downscale if needed
    Adapter-->>Composer: PendingAttachment (data:image/... base64)
    Composer->>Store: set pending attachment (preview)

    User->>Composer: submit message with attachment
    Composer->>ClientHook: send (includes root imageBase64)
    ClientHook->>API: POST /chat { messages..., imageBase64? }

    API->>API: validate imageBase64, generate imageDescription
    API->>DB: upsert ChatMessage and ChatAttachment (imageBase64, imageDescription)
    DB-->>API: persisted record
    API-->>ClientHook: response stream with message + attachment metadata

    Thread->>API: GET /threads/.../messages
    API->>DB: fetch messages including attachment
    DB-->>API: messages + attachments
    API-->>Thread: messages with attachment objects
    Thread->>User: render message images and descriptions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rschlaefli
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding image attachment support to the chat application, matching the substantial refactoring across UI components, API endpoints, database schema, and store management.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@dosubot dosubot bot added the feature label Mar 26, 2026
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

Caution

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

⚠️ Outside diff range comments (2)
apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts (1)

935-990: ⚠️ Potential issue | 🟠 Major

Make the message and attachment write atomic.

The message create/update and chatAttachment.upsert() are separate writes inside the same try. If the attachment write fails after the message write succeeds, the request still completes but the attachment disappears on reload. Please persist these dependent changes in one transaction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts around lines 935 -
990, Wrap the message create/update and attachment upsert in a single Prisma
transaction so both succeed or both fail: replace the separate calls to
prisma.chatMessage.updateMany / prisma.chatMessage.create and the
upsertAttachment helper with a single prisma.$transaction block (or
prisma.$transaction(async (tx) => { ... })) and call tx.chatMessage.updateMany /
tx.chatMessage.create and tx.chatAttachment.upsert inside it (or refactor
upsertAttachment to accept the transaction client and use
tx.chatAttachment.upsert); ensure you use the same IDs (userMessageId or
lastMessage.id) inside the transaction and handle the conditional create vs
update logic within that transaction so the message and its attachment are
persisted atomically.
apps/chat/src/components/thread.tsx (1)

523-563: ⚠️ Potential issue | 🟠 Major

Edits can't remove or replace an attached image.

The main composer exposes add/remove attachment controls, but EditComposer only shows a preview of imageBase64. Once a user sends the wrong image, editing the message keeps reusing that attachment with no way to clear or replace it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/components/thread.tsx` around lines 523 - 563, The preview of
imageBase64 inside ComposerPrimitive.Root has no control to clear or replace the
attachment, so EditComposer re-sends the old image; add a visible remove/replace
control (e.g., a small "Remove" or "Replace" button/icon next to the <img>
preview) that clears the local imageBase64 state and invokes the existing edit
callbacks (e.g., call a passed-in onClearAttachment or onUpdateAttachment prop)
so ComposerPrimitive.Send uses the updated state when submitting; update
EditComposer to accept and wire those handlers and ensure
ComposerPrimitive.Input or an upload control can set a new imageBase64 when
replacing.
🧹 Nitpick comments (4)
apps/chat/src/app/RuntimeProvider.tsx (1)

18-18: Consider using path alias for import consistency.

The import uses a relative path (../lib/attachments/imageAttachmentAdapter) while other imports in this file use src/ prefix paths (lines 10-16). As per coding guidelines, prefer @ or ~ path aliases for imports.

♻️ Suggested change
-import { imageAttachmentAdapter } from '../lib/attachments/imageAttachmentAdapter'
+import { imageAttachmentAdapter } from 'src/lib/attachments/imageAttachmentAdapter'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/app/RuntimeProvider.tsx` at line 18, Replace the relative
import of imageAttachmentAdapter with the same path-alias style used by other
imports in this file: update the import of imageAttachmentAdapter (symbol:
imageAttachmentAdapter) to use the project's alias-based path (e.g., the same
'src/...' prefix used on lines 10-16) so imports are consistent; verify the
alias exists in tsconfig/webpack and adjust the import string accordingly.
apps/chat/src/lib/attachments/imageAttachmentAdapter.ts (1)

9-35: LGTM with optional enhancement.

The adapter correctly implements the AttachmentAdapter interface with appropriate size validation and error messaging. The use of satisfies PendingAttachment ensures type safety while preserving literal types.

Consider adding explicit file type validation in add(), since the accept property only hints to the file picker but doesn't prevent programmatic additions of unsupported file types.

♻️ Optional file type validation
 async add({ file }) {
+  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
+  if (!allowedTypes.includes(file.type)) {
+    throw new Error(
+      `Unsupported image type: ${file.type}. Allowed types: ${allowedTypes.join(', ')}`
+    )
+  }
+
   if (file.size > MAX_IMAGE_SIZE_BYTES) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/lib/attachments/imageAttachmentAdapter.ts` around lines 9 - 35,
The add() method in imageAttachmentAdapter currently only enforces size limits
but not MIME type validation, so add an explicit check inside
imageAttachmentAdapter.add that verifies file.type against the allowed MIME
types (use the existing accept value or a small array of allowed types) before
reading the file; if the type is not allowed, throw a descriptive Error (similar
to the size check). Keep the error messaging consistent with the size error,
reference file.name/file.type in the message, and preserve the returned
PendingAttachment shape and use of crypto.randomUUID().
packages/prisma/src/prisma/schema/migrations/20260317133854_add_chat_attachment/migration.sql (1)

17-21: Remove redundant index on messageId.

The schema declares both @unique on the messageId field and @@index([messageId]), resulting in duplicate indexes in the migration (lines 18 and 21). A unique constraint already provides index functionality for efficient lookups, making the additional index redundant and wasteful of storage.

Remove @@index([messageId]) from the ChatAttachment model in the schema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/prisma/src/prisma/schema/migrations/20260317133854_add_chat_attachment/migration.sql`
around lines 17 - 21, The ChatAttachment model has a redundant non-unique index
on messageId producing duplicate indexes in the migration: remove the explicit
@@index([messageId]) from the ChatAttachment model so only the `@unique` on
messageId remains (which already creates the necessary unique index), then
regenerate the migration so the duplicate index ("ChatAttachment_messageId_idx")
is not created; look for references to messageId, the ChatAttachment model,
@@index and `@unique` when making the change.
apps/chat/src/components/thread.tsx (1)

280-370: Use function declarations for the new composer components.

ComposerAttachments, ComposerImageAttachment, and ComposerAttachButton were added in a components/** file, so they should follow the local component declaration convention instead of const ...: FC =.

As per coding guidelines, **/{components,app}/**/*.{ts,tsx}: Component files should use PascalCase naming with function keyword for component declarations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/components/thread.tsx` around lines 280 - 370, Replace the
three arrow-function component declarations with function declarations to follow
the project's component convention: change "const ComposerAttachments: FC = ()
=> { ... }" to "function ComposerAttachments() { ... }", change "const
ComposerImageAttachment: FC = () => { ... }" to "function
ComposerImageAttachment() { ... }", and change "const ComposerAttachButton: FC =
() => { ... }" to "function ComposerAttachButton() { ... }"; keep all internal
logic, hooks (useChatUi, useThreadComposerAttachment), props (none) and JSX
unchanged, and remove the FC type annotations so the components match the
PascalCase function-style component pattern used in components/** files.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts:
- Around line 822-842: The generateText(...) call that creates imageDescription
is not being accounted for in usage/cost tracking; update the flow so that the
result from generateText (function generateText) is fed into the same cost
accounting path as streamText: calculate its cost via calcCost(...) and call
CreditsService.decrementCredits(...) with the computed cost (or aggregate it
with the later streamText cost) before proceeding, ensuring imageDescription
generation consumes credits and is included in usage reporting.
- Around line 591-597: The current validation for imageBase64
(z.string().max(...).refine(...).optional()) is too permissive; update the
refine to only accept data URLs for the exact MIME types we support (jpeg, png,
gif, webp) and require the ';base64,' marker and a base64 payload. In the
imageBase64 validator in route.ts, replace the startsWith check with a strict
regex/refine that enforces
/^data:image\/(jpeg|png|gif|webp);base64,[A-Za-z0-9+/=]+$/ (or equivalent logic)
so SVG and non-base64 payloads are rejected while keeping the same max length
and optional flag.

---

Outside diff comments:
In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts:
- Around line 935-990: Wrap the message create/update and attachment upsert in a
single Prisma transaction so both succeed or both fail: replace the separate
calls to prisma.chatMessage.updateMany / prisma.chatMessage.create and the
upsertAttachment helper with a single prisma.$transaction block (or
prisma.$transaction(async (tx) => { ... })) and call tx.chatMessage.updateMany /
tx.chatMessage.create and tx.chatAttachment.upsert inside it (or refactor
upsertAttachment to accept the transaction client and use
tx.chatAttachment.upsert); ensure you use the same IDs (userMessageId or
lastMessage.id) inside the transaction and handle the conditional create vs
update logic within that transaction so the message and its attachment are
persisted atomically.

In `@apps/chat/src/components/thread.tsx`:
- Around line 523-563: The preview of imageBase64 inside ComposerPrimitive.Root
has no control to clear or replace the attachment, so EditComposer re-sends the
old image; add a visible remove/replace control (e.g., a small "Remove" or
"Replace" button/icon next to the <img> preview) that clears the local
imageBase64 state and invokes the existing edit callbacks (e.g., call a
passed-in onClearAttachment or onUpdateAttachment prop) so
ComposerPrimitive.Send uses the updated state when submitting; update
EditComposer to accept and wire those handlers and ensure
ComposerPrimitive.Input or an upload control can set a new imageBase64 when
replacing.

---

Nitpick comments:
In `@apps/chat/src/app/RuntimeProvider.tsx`:
- Line 18: Replace the relative import of imageAttachmentAdapter with the same
path-alias style used by other imports in this file: update the import of
imageAttachmentAdapter (symbol: imageAttachmentAdapter) to use the project's
alias-based path (e.g., the same 'src/...' prefix used on lines 10-16) so
imports are consistent; verify the alias exists in tsconfig/webpack and adjust
the import string accordingly.

In `@apps/chat/src/components/thread.tsx`:
- Around line 280-370: Replace the three arrow-function component declarations
with function declarations to follow the project's component convention: change
"const ComposerAttachments: FC = () => { ... }" to "function
ComposerAttachments() { ... }", change "const ComposerImageAttachment: FC = ()
=> { ... }" to "function ComposerImageAttachment() { ... }", and change "const
ComposerAttachButton: FC = () => { ... }" to "function ComposerAttachButton() {
... }"; keep all internal logic, hooks (useChatUi, useThreadComposerAttachment),
props (none) and JSX unchanged, and remove the FC type annotations so the
components match the PascalCase function-style component pattern used in
components/** files.

In `@apps/chat/src/lib/attachments/imageAttachmentAdapter.ts`:
- Around line 9-35: The add() method in imageAttachmentAdapter currently only
enforces size limits but not MIME type validation, so add an explicit check
inside imageAttachmentAdapter.add that verifies file.type against the allowed
MIME types (use the existing accept value or a small array of allowed types)
before reading the file; if the type is not allowed, throw a descriptive Error
(similar to the size check). Keep the error messaging consistent with the size
error, reference file.name/file.type in the message, and preserve the returned
PendingAttachment shape and use of crypto.randomUUID().

In
`@packages/prisma/src/prisma/schema/migrations/20260317133854_add_chat_attachment/migration.sql`:
- Around line 17-21: The ChatAttachment model has a redundant non-unique index
on messageId producing duplicate indexes in the migration: remove the explicit
@@index([messageId]) from the ChatAttachment model so only the `@unique` on
messageId remains (which already creates the necessary unique index), then
regenerate the migration so the duplicate index ("ChatAttachment_messageId_idx")
is not created; look for references to messageId, the ChatAttachment model,
@@index and `@unique` when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 91204053-cbe9-4b01-9856-b98ad2830508

📥 Commits

Reviewing files that changed from the base of the PR and between 23588c9 and dbdedaa.

📒 Files selected for processing (11)
  • apps/chat/src/app/RuntimeProvider.tsx
  • apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts
  • apps/chat/src/app/api/chatbots/[chatbotId]/threads/[threadId]/messages/route.ts
  • apps/chat/src/components/thread.tsx
  • apps/chat/src/hooks/useChatResponse.ts
  • apps/chat/src/hooks/useThreadManagement.ts
  • apps/chat/src/lib/api/types.ts
  • apps/chat/src/lib/attachments/imageAttachmentAdapter.ts
  • apps/chat/src/stores/chatStore.ts
  • packages/prisma/src/prisma/schema/chat.prisma
  • packages/prisma/src/prisma/schema/migrations/20260317133854_add_chat_attachment/migration.sql

Comment thread apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts Outdated
Comment thread apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts Outdated
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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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.

🧹 Nitpick comments (2)
apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts (2)

981-991: Use optional chaining for cleaner guard clause.

The condition can be simplified with optional chaining as suggested by static analysis.

♻️ Suggested refactor
   for (let i = 0; i < messages.length; i++) {
     const msg = messages[i]
-    if (!msg || msg.role !== 'user' || !msg.imageDescription) continue
+    if (msg?.role !== 'user' || !msg?.imageDescription) continue
     // skip the current message — already handled by the describe-once block above
     if (imageDescription && msg.id === lastMessage?.id) continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts around lines 981 -
991, Replace the verbose null checks in the loop with optional chaining: instead
of checking "!msg || msg.role !== 'user' || !msg.imageDescription" use a single
guard that leverages msg?.role and msg?.imageDescription (e.g., if (msg?.role
!== 'user' || !msg?.imageDescription) continue) so the loop over messages, the
skip-for-current-message check (comparing imageDescription && msg.id ===
lastMessage?.id), and the modelMessages[i] augmentation (spreading existing
content and appending the attached image description) remain unchanged; update
the guard in the for loop accordingly to make the code cleaner and safer.

901-904: Refactor nested template literals for readability.

The prompt construction uses nested template literals which reduces clarity.

♻️ Suggested refactor
           {
             type: 'text',
-            text: `${lastMessage?.content ? `User message context: ${lastMessage.content}\n\n` : ''}Describe this image in detail. Include all visible text, diagrams, charts, equations, labels, and notable visual elements. This description will serve as context for an ongoing conversation.`,
+            text: [
+              lastMessage?.content
+                ? `User message context: ${lastMessage.content}`
+                : '',
+              'Describe this image in detail. Include all visible text, diagrams, charts, equations, labels, and notable visual elements. This description will serve as context for an ongoing conversation.',
+            ]
+              .filter(Boolean)
+              .join('\n\n'),
           },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts around lines 901 -
904, The prompt's nested template literal (in the object with type: 'text' where
text currently uses `${lastMessage?.content ? `User message context:
${lastMessage.content}\n\n` : ''}Describe this image...`) should be simplified
for readability: extract the optional prefix into a local variable (e.g.,
userContext or prefix) that computes lastMessage?.content ? `User message
context: ${lastMessage.content}\n\n` : '' and then set the text field by
concatenating or a single template `${userContext}Describe this image in
detail...`, keeping the rest of the prompt unchanged; update the object where
text is defined to use that variable instead of the nested template literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts:
- Around line 981-991: Replace the verbose null checks in the loop with optional
chaining: instead of checking "!msg || msg.role !== 'user' ||
!msg.imageDescription" use a single guard that leverages msg?.role and
msg?.imageDescription (e.g., if (msg?.role !== 'user' || !msg?.imageDescription)
continue) so the loop over messages, the skip-for-current-message check
(comparing imageDescription && msg.id === lastMessage?.id), and the
modelMessages[i] augmentation (spreading existing content and appending the
attached image description) remain unchanged; update the guard in the for loop
accordingly to make the code cleaner and safer.
- Around line 901-904: The prompt's nested template literal (in the object with
type: 'text' where text currently uses `${lastMessage?.content ? `User message
context: ${lastMessage.content}\n\n` : ''}Describe this image...`) should be
simplified for readability: extract the optional prefix into a local variable
(e.g., userContext or prefix) that computes lastMessage?.content ? `User message
context: ${lastMessage.content}\n\n` : '' and then set the text field by
concatenating or a single template `${userContext}Describe this image in
detail...`, keeping the rest of the prompt unchanged; update the object where
text is defined to use that variable instead of the nested template literal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 62ed2f64-3d80-43c4-9dda-781ba598e2c8

📥 Commits

Reviewing files that changed from the base of the PR and between dbdedaa and 189dd0e.

📒 Files selected for processing (1)
  • apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts:
- Around line 1040-1042: upsertAttachment currently returns early when both
imageBase64 and imageDescription are empty, leaving any previous chatAttachment
orphaned; update upsertAttachment to first check the DB for an existing
attachment for the given messageId and, if no new image data is provided, delete
that existing chatAttachment (and update any related flags on chatMessage, e.g.,
hasAttachment) before returning; otherwise proceed with the existing upsert
logic to create/update the attachment. Apply the same delete-if-empty logic to
the other similar upsertAttachment occurrence referenced in the review.
- Around line 899-916: The image captioning branch should only run when the most
recent transcript message is from the user: guard uses of imageBase64 by
checking lastMessage?.role === 'user' before calling generateText (and before
any other image-related calls), rejecting/ignoring imageBase64 otherwise to
avoid unnecessary captioning/charges; update the blocks that call generateText
(referencing imageBase64 and generateText) and the other similar branches you
noted (around the imageBase64 usages at the other locations) to early-return or
clear imageBase64 when lastMessage?.role !== 'user'.
- Around line 919-943: The code currently decrements imageDescriptionCost
immediately inside the captioning branch (CreditsService.decrementCredits with
participantId, chatbotId, imageDescriptionCost) and then also folds
imageDescriptionCost into creditsUsed which is decremented again on
finish/abort, causing double billing; remove the immediate decrement calls in
the captioning/image-description branches (the places that compute
descriptionResult and set imageDescriptionCost) and instead only add
imageDescriptionCost to the creditsUsed accumulator (the creditsUsed variable)
so the single CreditsService.decrementCredits call at the unified finish/abort
path handles billing once; ensure any try/catch around
CreditsService.decrementCredits is preserved only where the final ledger
decrement happens.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5d2be13-b4c3-4446-ac4e-edad52340186

📥 Commits

Reviewing files that changed from the base of the PR and between 189dd0e and f389085.

📒 Files selected for processing (1)
  • apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts

Comment thread apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts Outdated
Comment thread apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts Outdated
Comment thread apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts Outdated
@cypress
Copy link
Copy Markdown

cypress bot commented Mar 26, 2026

klicker-uzh    Run #6689

Run Properties:  status check passed Passed #6689  •  git commit ddce0b88cf ℹ️: Merge 2ac25252ea618c8428832b693f0ce29e95d8b53d into a241faa73c3045a6cd87cb428baa...
Project klicker-uzh
Branch Review chatbot_add_image_attachments
Run status status check passed Passed #6689
Run duration 09m 18s
Commit git commit ddce0b88cf ℹ️: Merge 2ac25252ea618c8428832b693f0ce29e95d8b53d into a241faa73c3045a6cd87cb428baa...
Committer Jannis Alsbach
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 758
View all changes introduced in this branch ↗︎

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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

♻️ Duplicate comments (3)
apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts (3)

1062-1064: ⚠️ Potential issue | 🟡 Minor

Delete stale attachments when a resend no longer carries image data.

The early return here preserves any existing chatAttachment row for that messageId, so editing or resending a message without an image keeps the old preview/description alive.

Suggested fix
       const upsertAttachment = async (messageId: string) => {
-        if (!imageBase64 && !imageDescription) return
+        if (!imageBase64 && !imageDescription) {
+          await prisma.chatAttachment.deleteMany({
+            where: { messageId },
+          })
+          return
+        }
 
         await prisma.chatAttachment.upsert({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts around lines 1062 -
1064, The upsertAttachment function currently returns early if both imageBase64
and imageDescription are falsy, which leaves any existing chatAttachment row for
that messageId intact; instead, when imageBase64 and imageDescription are both
empty you should delete the stale attachment for that messageId. Update
upsertAttachment to, when !imageBase64 && !imageDescription, call the code path
that removes the existing chatAttachment (e.g., delete or deleteMany on
chatAttachment filtered by messageId) before returning; otherwise proceed with
the existing create/update logic to upsert the attachment. Ensure you reference
upsertAttachment, messageId, imageBase64, imageDescription and the
chatAttachment delete operation in your change.

898-915: ⚠️ Potential issue | 🟠 Major

Reject imageBase64 unless the latest message is from the user.

This still calls generateText() for any request with imageBase64. If the transcript ends with an assistant message, the server can caption and bill an image that never gets attached or persisted by the later user-only paths.

Suggested fix
-  if (imageBase64) {
+  if (imageBase64) {
+    if (lastMessage?.role !== 'user') {
+      return NextResponse.json(
+        {
+          error:
+            'Image attachments are only supported on the latest user message',
+        },
+        { status: 400 }
+      )
+    }
+
     try {
       const descriptionResult = await generateText({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts around lines 898 -
915, The current flow calls generateText whenever imageBase64 is present, even
if the transcript's lastMessage is from the assistant; update the logic so image
handling only proceeds when the latest message is from the user (check
lastMessage?.role === 'user' or equivalent) — if the last message is not from
the user then reject/ignore imageBase64 (e.g., early return or skip the
generateText/image-caption branch) to prevent captioning/billing images that
aren't tied to a user message; adjust the conditional around the generateText
call and any downstream use of the generated description to reference
imageBase64 only when lastMessage?.role === 'user'.

918-941: ⚠️ Potential issue | 🟠 Major

Don't decrement imageDescriptionCost inside the captioning branch.

imageDescriptionCost is already folded into creditsUsed later, so this immediate decrement can bill successful or aborted image requests twice. Keep the cost in imageDescriptionCost and let the unified finish/abort path charge once.

Suggested fix
-      // deduct credits for image description generation
       if (descriptionResult.usage) {
         imageDescriptionCost = calcCost(
           selectedModelConfig.cost,
           descriptionResult.usage.inputTokens || 0,
           descriptionResult.usage.outputTokens || 0
         )
-        if (imageDescriptionCost > 0) {
-          try {
-            await CreditsService.decrementCredits(
-              participantId,
-              chatbotId,
-              imageDescriptionCost
-            )
-          } catch (creditsError) {
-            console.error(
-              'Failed to decrement credits for image description:',
-              {
-                requestId,
-                error: creditsError,
-              }
-            )
-          }
-        }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts around lines 918 -
941, The code currently decrements credits inside the image captioning branch by
calling CreditsService.decrementCredits(participantId, chatbotId,
imageDescriptionCost) which causes double-billing because imageDescriptionCost
is also added into creditsUsed and charged in the unified finish/abort path;
remove the try/catch block and the call to CreditsService.decrementCredits in
the block that handles descriptionResult.usage (the section computing
imageDescriptionCost) and leave imageDescriptionCost assigned so the later
unified billing logic charges it exactly once.
🧹 Nitpick comments (2)
apps/chat/src/components/thread.tsx (2)

26-26: Use the repo alias for the new composer store import.

Line 26 introduces another relative import in a file type where the project standard is @ / ~ aliases.

Suggested fix
-import { useComposerStore } from '../stores/composerStore'
+import { useComposerStore } from '@/src/stores/composerStore'
As per coding guidelines, `**/*.{ts,tsx,js,jsx}`: Use `@` and `~` path aliases for imports instead of relative paths.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/components/thread.tsx` at line 26, Replace the relative import
of useComposerStore with the project path alias (use @ or ~) to follow the repo
import convention; update the import that currently reads import {
useComposerStore } from '../stores/composerStore' so it imports from the aliased
module (e.g. '@/stores/composerStore' or '~/stores/composerStore') and ensure
any IDE/tsconfig paths are already configured so useComposerStore resolves
correctly in thread.tsx.

300-390: Switch the newly added components to function declarations.

ComposerAttachments, ComposerImageAttachment, and ComposerAttachButton are all new in this hunk, but they're still declared as const ...: FC =. This directory's convention is function ComponentName().

Example refactor
-const ComposerAttachments: FC = () => {
+function ComposerAttachments() {
   return (
     <div className="flex w-full flex-wrap gap-2 py-2 empty:hidden">
       <ComposerPrimitive.Attachments
         components={{
           Image: ComposerImageAttachment,
@@
       />
     </div>
   )
 }
As per coding guidelines, `**/{components,app}/**/*.{ts,tsx}`: Component files should use PascalCase naming with `function` keyword for component declarations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/components/thread.tsx` around lines 300 - 390, Replace the
three arrow-const component declarations with function declarations to follow
project convention: change "const ComposerAttachments: FC = () => { ... }",
"const ComposerImageAttachment: FC = () => { ... }", and "const
ComposerAttachButton: FC = () => { ... }" into "function ComposerAttachments() {
... }", "function ComposerImageAttachment() { ... }", and "function
ComposerAttachButton() { ... }" respectively, keeping the bodies, hooks
(useChatUi, useThreadComposerAttachment), JSX, and any local variables
unchanged; remove the explicit ": FC" typing (no props change needed) so the
components remain named exactly the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts:
- Around line 1012-1029: The Prisma lookup for prior image descriptions
(prisma.chatAttachment.findMany) only filters by messageId and can return
attachments from other threads; narrow the query to the current thread by also
filtering on the owning thread id (use the already-fetched owningThread
identifier) and ensure owningThread is present before running the query so only
attachments with messageId IN priorMessageIds AND threadId === owningThread.id
are returned; update the findMany call that builds descriptionsByMsgId to
include this thread-scoped where clause.

---

Duplicate comments:
In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts:
- Around line 1062-1064: The upsertAttachment function currently returns early
if both imageBase64 and imageDescription are falsy, which leaves any existing
chatAttachment row for that messageId intact; instead, when imageBase64 and
imageDescription are both empty you should delete the stale attachment for that
messageId. Update upsertAttachment to, when !imageBase64 && !imageDescription,
call the code path that removes the existing chatAttachment (e.g., delete or
deleteMany on chatAttachment filtered by messageId) before returning; otherwise
proceed with the existing create/update logic to upsert the attachment. Ensure
you reference upsertAttachment, messageId, imageBase64, imageDescription and the
chatAttachment delete operation in your change.
- Around line 898-915: The current flow calls generateText whenever imageBase64
is present, even if the transcript's lastMessage is from the assistant; update
the logic so image handling only proceeds when the latest message is from the
user (check lastMessage?.role === 'user' or equivalent) — if the last message is
not from the user then reject/ignore imageBase64 (e.g., early return or skip the
generateText/image-caption branch) to prevent captioning/billing images that
aren't tied to a user message; adjust the conditional around the generateText
call and any downstream use of the generated description to reference
imageBase64 only when lastMessage?.role === 'user'.
- Around line 918-941: The code currently decrements credits inside the image
captioning branch by calling CreditsService.decrementCredits(participantId,
chatbotId, imageDescriptionCost) which causes double-billing because
imageDescriptionCost is also added into creditsUsed and charged in the unified
finish/abort path; remove the try/catch block and the call to
CreditsService.decrementCredits in the block that handles
descriptionResult.usage (the section computing imageDescriptionCost) and leave
imageDescriptionCost assigned so the later unified billing logic charges it
exactly once.

---

Nitpick comments:
In `@apps/chat/src/components/thread.tsx`:
- Line 26: Replace the relative import of useComposerStore with the project path
alias (use @ or ~) to follow the repo import convention; update the import that
currently reads import { useComposerStore } from '../stores/composerStore' so it
imports from the aliased module (e.g. '@/stores/composerStore' or
'~/stores/composerStore') and ensure any IDE/tsconfig paths are already
configured so useComposerStore resolves correctly in thread.tsx.
- Around line 300-390: Replace the three arrow-const component declarations with
function declarations to follow project convention: change "const
ComposerAttachments: FC = () => { ... }", "const ComposerImageAttachment: FC =
() => { ... }", and "const ComposerAttachButton: FC = () => { ... }" into
"function ComposerAttachments() { ... }", "function ComposerImageAttachment() {
... }", and "function ComposerAttachButton() { ... }" respectively, keeping the
bodies, hooks (useChatUi, useThreadComposerAttachment), JSX, and any local
variables unchanged; remove the explicit ": FC" typing (no props change needed)
so the components remain named exactly the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 45dfc268-d01d-414b-9b1a-6cbb2849c423

📥 Commits

Reviewing files that changed from the base of the PR and between f389085 and a094a96.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • apps/chat/package.json
  • apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts
  • apps/chat/src/components/thread.tsx
  • apps/chat/src/hooks/useChatResponse.ts
  • apps/chat/src/lib/attachments/imageAttachmentAdapter.ts
  • apps/chat/src/stores/composerStore.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/chat/package.json
  • apps/chat/src/hooks/useChatResponse.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/chat/src/lib/attachments/imageAttachmentAdapter.ts

Comment thread apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts Outdated
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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@TheHummel TheHummel self-assigned this Mar 31, 2026
@TheHummel TheHummel requested a review from rschlaefli March 31, 2026 15:21
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.

🧹 Nitpick comments (1)
apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts (1)

906-911: Extract nested template literal for readability.

The nested template literal makes the prompt harder to read. Consider extracting the context prefix.

♻️ Suggested refactor
+            const contextPrefix = lastMessage?.content
+              ? `User message context: ${lastMessage.content}\n\n`
+              : ''
             content: [
               { type: 'image', image: imageBase64 },
               {
                 type: 'text',
-                text: `${lastMessage?.content ? `User message context: ${lastMessage.content}\n\n` : ''}Describe this image in detail. Include all visible text, diagrams, charts, equations, labels, and notable visual elements. This description will serve as context for an ongoing conversation.`,
+                text: `${contextPrefix}Describe this image in detail. Include all visible text, diagrams, charts, equations, labels, and notable visual elements. This description will serve as context for an ongoing conversation.`,
               },
             ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts around lines 906 -
911, The nested template literal inside the text message makes the prompt hard
to read; extract the context prefix into a separate variable (e.g., const
contextPrefix = lastMessage?.content ? `User message context:
${lastMessage.content}\n\n` : '') and then use that variable when building the
text message in the messages array (the object with type: 'text' in route.ts),
keeping the rest of the prompt unchanged; reference lastMessage and imageBase64
to locate the surrounding code and replace the inline `${lastMessage?.content ?
...}` expression with the new contextPrefix for clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/chat/src/app/api/chatbots/`[chatbotId]/chat/route.ts:
- Around line 906-911: The nested template literal inside the text message makes
the prompt hard to read; extract the context prefix into a separate variable
(e.g., const contextPrefix = lastMessage?.content ? `User message context:
${lastMessage.content}\n\n` : '') and then use that variable when building the
text message in the messages array (the object with type: 'text' in route.ts),
keeping the rest of the prompt unchanged; reference lastMessage and imageBase64
to locate the surrounding code and replace the inline `${lastMessage?.content ?
...}` expression with the new contextPrefix for clarity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 248e2fa4-83c6-4a29-a185-8750fea25bb0

📥 Commits

Reviewing files that changed from the base of the PR and between a094a96 and 4777795.

📒 Files selected for processing (1)
  • apps/chat/src/app/api/chatbots/[chatbotId]/chat/route.ts

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@rschlaefli rschlaefli requested review from jabbadizzleCode and removed request for rschlaefli April 8, 2026 11:38
Copy link
Copy Markdown
Collaborator

@jabbadizzleCode jabbadizzleCode left a comment

Choose a reason for hiding this comment

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

Looks good to me :). Before merging, maybe consider using Azure Blob Storage for the images instead of PostgreSQL (see comment).

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Apr 15, 2026
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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@rschlaefli rschlaefli changed the title feat(apps/chat): add support for image attachments feat(chat): make image attachments lazy and reliable Apr 15, 2026
@rschlaefli rschlaefli changed the title feat(chat): make image attachments lazy and reliable feat(chat): add image attachment support Apr 15, 2026
…ot_add_image_attachments

# Conflicts:
#	AGENTS.md
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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@sonarqubecloud
Copy link
Copy Markdown

@rschlaefli rschlaefli merged commit 9e90bf0 into v3 Apr 16, 2026
24 of 25 checks passed
@rschlaefli rschlaefli deleted the chatbot_add_image_attachments branch April 16, 2026 06:25
@cypress
Copy link
Copy Markdown

cypress bot commented Apr 16, 2026

klicker-uzh    Run #6690

Run Properties:  status check failed Failed #6690  •  git commit 9e90bf0d2d: feat(chat): add image attachment support (#5045)
Project klicker-uzh
Branch Review v3
Run status status check failed Failed #6690
Run duration 09m 22s
Commit git commit 9e90bf0d2d: feat(chat): add image attachment support (#5045)
Committer Jannis Alsbach
View all properties for this run ↗︎

Test results
Tests that failed  Failures 1
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 757
View all changes introduced in this branch ↗︎

Tests for review

Failed  cypress/e2e/U-catalog-workflow.cy.ts • 1 failed test

View Output Video

Test Artifacts
Test all functionalities of catalog collections and objects contained therein > Remove a member and an admin from the group and verify that the corresponding users lost access Test Replay Screenshots Video

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature size:XXL This PR changes 1000+ lines, ignoring generated files.

Development

Successfully merging this pull request may close these issues.

3 participants