Skip to content

feat(platform): image-generation agents with FLUX and Gemini#1585

Merged
larryro merged 3 commits into
mainfrom
feat/image-generation-agents
Apr 20, 2026
Merged

feat(platform): image-generation agents with FLUX and Gemini#1585
larryro merged 3 commits into
mainfrom
feat/image-generation-agents

Conversation

@larryro
Copy link
Copy Markdown
Collaborator

@larryro larryro commented Apr 20, 2026

Summary

  • New primaryBehavior: 'image-generation' for agents: bypasses the tool loop and routes prompts directly to an image model via OpenRouter.
  • Adds FLUX.2 (pro/max/flex) and Gemini 2.5 Flash Image to the OpenRouter provider config; extends provider/model schemas with image-generation tags, per-image cost, and chat-multimodal generation mode.
  • Chat UI additions: thumbnail picker, editing banner, thread image gallery, and updated model selector/badges.
  • Billing: run_image_generation reads OpenRouter usage.cost (via usage.include) and prefers it over a flat per-image rate — FLUX.2 prices per megapixel, so static rates under/over-bill at non-default resolutions.

Test plan

  • Create an agent with primaryBehavior: 'image-generation' bound to a FLUX.2 model; send a prompt; verify an image is generated and attached to the message.
  • Same flow with Gemini 2.5 Flash Image.
  • Verify billing rows use gateway-reported cost for FLUX.2 at non-default resolutions (not the flat per-image rate).
  • Verify radio control in the searchable-select vertically centers against the label + badge row.
  • Thumbnail picker + editing banner + thread images render correctly in chat.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added image-generation agent type with built-in editing support.
    • Introduced new image generation models: FLUX.2 (pro, max, flex) and Gemini 2.5 Flash Image.
    • Added image editing interface and thumbnail picker for managing generated and uploaded images.
    • Enhanced model selector with provider badges and image generation capabilities.
  • Improvements

    • Expanded localization support to German and French for image workflows.
    • Added send-blocking UI with custom messaging for constrained operations.
    • Extended component APIs for better customization options.

larryro added 3 commits April 20, 2026 16:29
Introduce a new `primaryBehavior: 'image-generation'` for agents that
bypasses the tool loop and routes prompts directly to an image model.
Extends provider/model schemas with image-generation tags, per-image
cost, and chat-multimodal generation mode. Adds FLUX.2 (pro/max/flex)
and Gemini 2.5 Flash Image to OpenRouter, rewires the image-creator
agent, and adds chat UI for thumbnails, editing banners, and thread
images.
…ge row

- run_image_generation: read OpenRouter `usage.cost` (via `usage.include`) and
  prefer it over flat `imageCentsPerImage` — FLUX.2 prices per megapixel, so a
  static per-image rate under/over-bills at non-default resolutions.
- searchable-select: wrap the radio in a 24px-tall flex row so it vertically
  centers against the label+badge row instead of top-aligning above the badge.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

This PR introduces image-generation support alongside the existing chat agent system. It expands agent configuration to include a primaryBehavior field (chat or image-generation) and updates provider models with image-generation tags, modes, and per-image pricing. The backend adds image model resolution and a new runImageGeneration action. The chat UI extends to detect image-generation agents, display an editing banner for managing reference images, and block sends when incompatible. Message bubbles gain edit overlays on assistant-generated images. Schemas, localization (German, English, French), and configuration management are updated throughout to support these new capabilities.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature addition—introducing image-generation agents with support for FLUX and Gemini models. It accurately reflects the primary change across the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/image-generation-agents

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


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

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

Caution

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

⚠️ Outside diff range comments (3)
services/platform/convex/agents/file_actions.ts (1)

209-228: ⚠️ Potential issue | 🟠 Major

Fail fast when an unqualified model matches no providers.

If matches.length is 0, resolvedProviderName becomes undefined and the loop keeps going, so an invalid supportedModels entry can still be saved. This should throw UNKNOWN_MODEL here rather than surfacing later during resolution.

🐛 Proposed fix
       } else {
         const matches = [...byProvider.entries()]
           .filter(([, s]) => s.has(modelId))
           .map(([p]) => p);
+        if (matches.length === 0) {
+          throw new ConvexError({
+            code: 'UNKNOWN_MODEL',
+            message: `Model "${modelId}" not found in any provider`,
+          });
+        }
         if (matches.length > 1) {
           warnings.push(
             `"${modelId}" matches ${matches.length} providers (${matches.join(', ')}); pinning to "${matches[0]}". Use "${matches[0]}:${modelId}" to pin explicitly.`,
           );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/file_actions.ts` around lines 209 - 228, The
code currently assumes an unqualified modelId will match at least one provider;
if matches.length === 0 you must fail fast: inside the block that computes
matches from byProvider (where matches, resolvedProviderName are set), detect
matches.length === 0 and throw a ConvexError with code 'UNKNOWN_MODEL' and a
clear message referencing the offending modelId/ref so the invalid
supportedModels entry cannot be saved; keep the existing behavior for
matches.length > 1 (warnings and pin to matches[0]) and then set
resolvedProviderName = matches[0] as before, and leave the later
requireImageGenerationTag/modelTagLookup logic unchanged.
services/platform/app/features/chat/components/model-selector.tsx (1)

64-103: ⚠️ Potential issue | 🟠 Major

Provider metadata lookup is ambiguous for duplicate model IDs.

modelInfoMap is keyed only by plain model.id, so the last provider wins for unqualified refs. That makes the badge, display name, and requiredTag filtering come from whichever provider was iterated last, not the provider the backend actually resolves.

Also applies to: 115-119, 216-229

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

In `@services/platform/app/features/chat/components/model-selector.tsx` around
lines 64 - 103, The modelInfoMap built in the useMemo is keyed only by model.id,
causing collisions when multiple providers expose the same model id so the last
provider wins; change the map to use a fully-qualified key (e.g.,
`${provider.name}/${model.id}`) or store provider-specific entries (or arrays)
so metadata is provider-scoped, then update getProviderSlug to look up by the
qualified key when parseModelRef yields a providerName or to resolve ambiguous
unqualified refs by checking all provider-scoped entries; touch the modelInfoMap
construction in the useMemo and the getProviderSlug callback (and any other
lookup sites at lines referenced: 115-119, 216-229) to use the new qualified-key
strategy so badges, displayName, and requiredTag filtering are sourced from the
correct provider.
services/platform/app/features/chat/components/message-bubble/file-displays.tsx (1)

3-13: ⚠️ Potential issue | 🟠 Major

Replace bare <img> elements with the custom Image component and fix accessibility issues on the edit button.

The file uses bare <img> at lines 186 and 287. Replace with the project's Image component from @/app/components/ui/data-display/image. Additionally, the edit button at line 291 uses size-5 (20px), which violates the 24×24 minimum touch target requirement. Increase to size-6 or larger. Remove focus:outline-none to restore focus visibility.

🛠️ Proposed refactor
-import { Image, Code2, Download, ...} from 'lucide-react';
+import { Image as ImageIcon, Code2, Download, ...} from 'lucide-react';
+import { Image } from '@/app/components/ui/data-display/image';

 // Line 66
-    return { Icon: Image, bgColor: 'bg-blue-50', iconColor: 'text-blue-600' };
+    return { Icon: ImageIcon, bgColor: 'bg-blue-50', iconColor: 'text-blue-600' };

 // Line 186
-        <img
+        <Image
           src={displayUrl}
           alt={attachment.fileName}
           className="size-full object-cover"
         />

 // Line 287
-          <img
+          <Image
             src={filePart.url}
             alt={filePart.filename || t('fileTypes.image')}
             className={imgClasses}
           />

 // Line 294 - fix touch target and focus ring
  const editButtonClasses = isLarge
    ? 'bg-background/90 ring-border text-foreground hover:bg-background absolute top-2 right-2 flex size-8 items-center justify-center rounded-full shadow-sm ring-1 transition-opacity focus:ring-2 focus:ring-offset-2 focus:outline-none'
-   : 'bg-background/95 ring-border text-foreground hover:bg-background absolute -top-1 -right-1 flex size-5 items-center justify-center rounded-full opacity-0 shadow-sm ring-1 transition-opacity group-hover:opacity-100 focus:opacity-100 focus:outline-none';
+   : 'bg-background/95 ring-border text-foreground hover:bg-background absolute -top-1 -right-1 flex size-6 items-center justify-center rounded-full opacity-0 shadow-sm ring-1 transition-opacity group-hover:opacity-100 focus:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/chat/components/message-bubble/file-displays.tsx`
around lines 3 - 13, Replace the two bare <img> usages in file-displays.tsx with
the project's Image component imported from
'@/app/components/ui/data-display/image' (update imports at the top), passing
the same src and alt props and appropriate width/height or className to preserve
styling; also locate the edit button (the button using classes including
"size-5" and "focus:outline-none") and change "size-5" to "size-6" (or larger)
to meet the 24×24 touch target and remove "focus:outline-none" so focus styles
remain visible. Ensure you keep any existing aria-label/text for accessibility
and preserve other event handlers on the edit button.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/platform/app/features/chat/components/chat-input.tsx`:
- Around line 443-475: Wrap the Button trigger inside a non-disabled wrapper
element so the Tooltip can receive pointer events when the Button is disabled:
inside the Tooltip component that currently renders Button (identify by Tooltip
+ Button with props isLoading, onStopGenerating, handleSendMessage and disabled
computed from
value.trim/attachments/inputDisabled/isUploading/isIndexing/sendBlocked), place
a <span> (or other inline wrapper) as the Tooltip child and keep the Button as a
child of that span (leave the Button disabled attribute and all props/aria-label
unchanged). This ensures sendBlocked/sendBlockedReason still control the tooltip
content while preserving the Button behavior for isLoading and stop/generate
actions.

In `@services/platform/app/features/chat/components/chat-interface.tsx`:
- Around line 295-310: The model lookup is using the hardcoded default provider
list and stripping provider qualifiers which can mis-resolve model metadata;
update the logic so useListProviders is called for the correct org/provider
(derive the org/provider from activeModelRef or the selected agent/organization
context instead of 'default') and change the activeModelInfo lookup to first try
matching the full model reference (including provider qualifier) against
provider.models entries and only fall back to stripModelRefQualifier/plain id if
no exact match is found; update references in this block (useListProviders,
activeModelRef, stripModelRefQualifier, providersForEdit, activeModelInfo) so
the UI reflects the backend-resolved model metadata.
- Around line 1022-1033: sendBlocked currently only tests model capability, but
the send path requires activeEditingImage.ref.fileId to attach the reference;
update the send-blocking logic to also block when there is an explicit edit
target lacking a storage id (i.e., when activeEditingImage is truthy but
activeEditingImage.ref?.fileId is empty). Change the checks used where
sendBlocked and sendBlockedReason are set (referencing isImageGenAgent,
activeEditingImage, currentModelSupportsEdit) so sendBlocked becomes true when
the fileId is missing and provide an appropriate sendBlockedReason (use a
new/i18n key like imageEdit.missingReference or similar) instead of allowing
silent fallback to text-only generation.

In `@services/platform/app/features/chat/components/editing-banner.tsx`:
- Around line 150-154: Replace the bare <img> preview in the editing-banner
component with the repo's shared Image component (import Image from
'@/components/ui/image') so previews go through the centralized wrapper; pass
the same props (src={ref.url}, alt={ref.fileName ?? ''}) and preserve className
("size-full object-cover") or equivalent props the Image component expects, and
add the import for Image at the top of the file.
- Around line 60-65: The banner dismissal isn't persisted for explicit images
because the code uses a synthetic explicitKey
(`explicit:${editingImageRef.fileId || editingImageRef.url}`) when building the
active ref but dismissal logic compares against `threadImages[0].key`; update
the dismissal flow to record and compare the same synthetic key: compute the
same explicitKey in the hook/component (used where `active: { ref:
editingImageRef, key: explicitKey }` is created and also where
`threadImages[0].key` is checked), make `handleDismiss()` store that explicitKey
in the dismissed state (e.g., `dismissedKey`) and use that dismissedKey to set
`isDismissed` so the banner stays hidden after dismissing explicit images; apply
the same change to the other occurrence around the 107-110 block.

In `@services/platform/app/features/chat/components/message-bubble.tsx`:
- Around line 179-187: The component currently calls useEffectiveAgent and
useChatAgents inside MessageBubble to compute isImageGenAgent, causing each
message row to subscribe to thread-level agent data; move that computation out
of MessageBubble into the parent (chat layout) where those hooks are already
used, compute the boolean once (using useEffectiveAgent and useChatAgents) and
pass it down as a prop named e.g. isImageGenAgent to MessageBubble; then remove
the useEffectiveAgent/useChatAgents logic and the isImageGenAgent derivation
from message-bubble.tsx so MessageBubble simply consumes the passed prop (retain
existing setEditingImageRef/setDismissedImageKey usage only).
- Around line 488-491: The assistant toolbar is being rendered for messages that
only have fileParts, causing the Copy button (which calls handleCopy() and
copies message.content) to be a no-op for image-only responses; update the
toolbar rendering or Copy button logic so Copy is only shown/active when there
is actual text to copy: modify the condition around the toolbar (the block using
isUser, isAssistantStreaming, displayContent, and message.fileParts) to require
displayContent (or non-empty message.content) before showing the Copy action, or
alternatively extend handleCopy() to detect image-only messages
(message.fileParts) and implement image-specific copy behavior (e.g., copy image
URL or fallback text) so copying is meaningful for image responses.
- Around line 191-195: The empty catch in the block that parses fileId from
part.url (where code does fileId = new URL(part.url).searchParams.get('id') ??
'') must not silently swallow failures; update the catch to capture the error
(e.g. catch (err)) and either log a warning with context (console.warn or
processLogger) including part.url, the failing value, and err, or re-throw a
descriptive error so the image-edit flow fails loudly; keep the existing
fallback behavior only after logging so debugging information is preserved —
locate this in message-bubble.tsx around the fileId/new URL(part.url) parsing
logic.

In
`@services/platform/app/features/chat/components/message-bubble/file-displays.tsx`:
- Around line 269-277: The small overlay edit button defined by
editButtonClasses when isLarge is false uses size-5 (20px) and removes a visible
focus ring; change it to a minimum size of size-6 (24px) or larger and restore
accessible focus styles (e.g., add focus-visible:ring-2 and
focus-visible:ring-offset-2 with ring color like ring-ring) so keyboard focus is
always visible and meets contrast; update any negative position offsets (e.g.,
-top-1, -right-1) as needed to keep the button visually aligned after increasing
size; keep the opacity/group-hover behavior but ensure focus-visible overrides
opacity to remain visible.

In `@services/platform/app/features/chat/components/model-selector.tsx`:
- Around line 154-171: currentModelId currently displays filteredModels[0] for
image-gen agents but doesn't persist it as an override, causing the backend to
still use the agent default; add logic to persist this UI fallback as the
selected override so sends match the displayed model. Specifically, when
isImageGenAgent is true and effectiveAgent?.name exists and there is no
selectedModelOverrides[effectiveAgent.name], write
selectedModelOverrides[effectiveAgent.name] = filteredModels[0] (or AUTO_MODEL
if filteredModels is empty) via the same setter/localStorage mechanism used
elsewhere (the code path that manages selectedModelOverrides), e.g. in a
useEffect that depends on [effectiveAgent?.name, isImageGenAgent,
filteredModels, selectedModelOverrides], so the fallback model is stored for the
agent before send. Ensure you reference and update the same state/localStorage
API that other overrides use rather than only changing currentModelId.

In `@services/platform/app/features/chat/components/thumbnail-picker.tsx`:
- Around line 80-84: Replace the bare <img> usage in ThumbnailPicker with the
repo's shared Image component: import Image from '@/components/ui/image' and
swap the <img src={img.url} alt={img.fileName ?? ''} className="size-full
object-cover" /> element for <Image ... /> while preserving src (img.url), alt
(img.fileName ?? ''), and the className ("size-full object-cover"); keep the
surrounding JSX and any key/iteration logic unchanged and ensure any required
props the shared Image requires (e.g., width/height or layout props) are
provided to match behavior.

In `@services/platform/app/features/chat/hooks/use-message-processing.ts`:
- Around line 326-340: The loop that attaches file parts (inside
use-message-processing.ts) only breaks when both msg.order and next.order are
present and different, which allows attachment when either order is missing;
change the guard so the loop bails unless both orders exist and are equal —
i.e., check msg.order and next.order for non-null equality before attaching to
extraFileParts (update the conditional around the turn-check in the for j loop
that currently references msg.order and next.order).

In `@services/platform/app/features/chat/hooks/use-thread-images.ts`:
- Around line 37-42: The extractStorageFileId function currently swallows URL
parsing errors; instead, catch the exception in the catch block and log a
warning or error that includes the original url and the caught error so there’s
a breadcrumb when extraction fails (e.g., use console.warn or console.error
inside extractStorageFileId and include the url and error.message), then
continue to return undefined as before.

In `@services/platform/app/features/settings/providers/utils/model-tag-label.ts`:
- Around line 13-15: The function modelTagLabel uses a broad cast to access
TAG_KEYS; remove the "as" cast and add a type guard (e.g., an isModelTag(tag):
tag is ModelTag helper or use "tag in TAG_KEYS") so you only index TAG_KEYS when
the compiler knows tag is a ModelTag; then look up the value (const key =
TAG_KEYS[tag]) inside that guarded branch and return t(key) or fallback to tag
otherwise, keeping the cast confined to the narrow, validated access site and
eliminating any use of "as".

In
`@services/platform/app/routes/dashboard/`$id/settings/providers/$providerName.tsx:
- Around line 902-907: The JSX contains hardcoded user-facing strings in the
provider <select> options and the "Cost per image (USD)" label; replace these
literals with translation hook calls (e.g., use t('providers.imagesApi'),
t('providers.chatMultimodal'), t('providers.defaultSuffix') and
t('providers.costPerImageUsd')) wherever they appear (notably the <select>
options in the provider selection block and the cost label around lines
~944-958), update the translation keys in your i18n resource files, and ensure
the default option concatenates the translated suffix via t rather than a
hardcoded English string so all locales render correctly.
- Around line 884-910: The select for imageGenerationMode is not associated with
its label or helper text and doesn't expose validation state; add an id (e.g.
"imageGenerationMode"), set the label's htmlFor to that id, give the helper Text
an id and reference it from the select via aria-describedby, and when there are
validation errors for form.imageGenerationMode use aria-invalid="true" on the
select and render the error node with role="alert" and include that error node's
id in aria-describedby so screen readers announce both helper text and errors;
update the setForm usage and any validation logic that produces the error
message so the select consumes those states.

In `@services/platform/convex/agents/image_generation/run_image_generation.ts`:
- Around line 139-158: The images-api branch (resolved.kind === 'images-api')
calls generateImage(...) but doesn't use provider-reported billing, so
costCentsOverride still falls back to imageCentsPerImage; update the image
generation flow (the generateImage call handling and any subsequent billing
logic that reads costCentsOverride) to extract and pass through provider usage
cost (e.g., usage.cost or equivalent returned from generateImage) when present,
and only fall back to imageCentsPerImage if the provider doesn't return a cost;
locate symbols like resolved.imageModel, generateImage, costCentsOverride, and
imageCentsPerImage to wire the returned usage.cost into the billing path used
elsewhere in this file (and on lines around 246-252) so megapixel-priced models
are billed correctly.
- Around line 466-470: In run_image_generation.ts, the try/catch that parses the
base64 payload in the helper currently swallows parse failures and returns null;
update the catch to either re-throw or log the error (e.g., console.error with
the error and payload/mediaType) before returning so malformed data-URI issues
are diagnosable — locate the block that constructs bytes using new
Uint8Array(Buffer.from(payload, 'base64')) and change the empty catch to surface
the error (throw the caught error or console.error and then return null) so
provider regressions are logged.
- Around line 390-397: The direct fetch call that posts to /chat/completions can
hang; wrap it with an AbortController and a timeout so the request is aborted if
it exceeds a chosen duration (e.g., opts.requestTimeout or a safe default).
Create const controller = new AbortController(), pass signal: controller.signal
into the fetch options (where response is created), start a timer via const
timeoutId = setTimeout(() => controller.abort(), timeoutMs), and
clearTimeout(timeoutId) after fetch resolves or throws so the finally block
still runs and generation status is cleared; make sure to handle AbortError/
fetch rejection appropriately. Use the existing variables from this diff (url,
body, opts.apiKey, response) and ensure the controller/timer are scoped around
that fetch call.
- Around line 35-39: The validator attachmentImageValidator currently uses
v.string() for fileId which permits malformed storage ids and forces an unsafe
cast later; change the fileId schema to v.id('_storage') inside the v.object so
fileId is a typed storage id at the action boundary and remove the downstream
unsafe cast (the code that casts the fileId before calling the action). Update
any call sites to pass the typed _storage id value into the action (or convert
earlier) so attachmentImageValidator, and subsequent uses of fileId, always
carry the correct v.id('_storage') type.

In `@services/platform/convex/lib/agent_chat/start_agent_chat.ts`:
- Around line 346-370: The code filters actionAttachments into imageAttachments
and silently ignores non-image files before scheduling runImageGeneration;
instead validate actionAttachments in start_agent_chat.ts and reject the request
if any attachment has a non-image MIME type. Before calling
ctx.scheduler.runAfter (where imageAttachments is built), check
actionAttachments for items where a.fileType does not startWith('image/'), and
return or throw a clear validation error (including which file/fileName and
MIME) so clients are informed rather than silently dropping attachments; update
the logic around imageAttachments and runImageGeneration to rely on this
validation.

In `@services/platform/lib/shared/schemas/agents.ts`:
- Around line 101-111: The superRefine validator currently only checks
systemInstructions.length === 0 allowing whitespace-only prompts; update the
chat-agent check to trim before validating: inside .superRefine((data, ctx) => {
... }) compute const si = data.systemInstructions?.trim(); and treat
null/undefined or si.length === 0 as invalid (ctx.addIssue on
['systemInstructions']). Alternatively, adjust the schema for systemInstructions
used for chat agents to z.string().trim().min(1) and conditionally apply it when
(data.primaryBehavior ?? 'chat') === 'chat' so whitespace-only strings are
rejected by parseAgentJson.

In `@services/platform/lib/shared/schemas/providers.ts`:
- Around line 29-36: The numeric cost fields inputCentsPerMillion,
outputCentsPerMillion, and imageCentsPerImage in providers.ts currently allow
negative values; update their Zod schemas to enforce non-negativity (e.g., use
z.number().min(0).optional() or an equivalent .refine(...) check) so any
provided value must be >= 0, and keep them optional as before; apply this change
to the schema declarations for those three symbols to prevent negative costs
from being accepted.
- Line 26: The schema currently allows imageGenerationMode to be optional which
lets providers with tags containing 'image-generation' omit it; update the
provider schema (where imageGenerationMode: imageGenerationModeSchema.optional()
is declared) to add a conditional validation: if the tags array includes
'image-generation' then imageGenerationMode must be present and valid. Implement
this via a Zod .refine or .superRefine on the provider schema (referencing tags
and imageGenerationMode) and return a clear error message when
imageGenerationMode is missing for providers that include 'image-generation'.

In `@services/platform/messages/de.json`:
- Line 1578: The JSON contains a duplicated key "default" under the same object
(settings.providers.default) — remove the second occurrence at the shown diff so
there is only a single settings.providers.default entry; locate the duplicate
"default" key in the same JSON object (the one at Line 1578 in the diff) and
delete that redundant key/value pair to avoid silent shadowing and keep the
single canonical settings.providers.default definition.
- Around line 1576-1577: The translation text for imageGenerationModeHelp
incorrectly instructs that FLUX models use `images-api`; update the copy
referenced by the `imageGenerationMode` / `imageGenerationModeHelp` keys so FLUX
models are described as using `chat-multimodal` (matching how bundled OpenRouter
configures FLUX.2 and avoiding directing admins to `images-api`), and adjust the
endpoint wording to indicate FLUX uses /v1/chat/completions with image content
parts rather than /v1/images/generations.

In `@services/platform/messages/en.json`:
- Line 1578: The JSON contains a duplicate key "providers.default" which
overwrites the earlier value; locate both occurrences of the "providers.default"
key (search for the exact key string "providers.default") and remove or rename
the later duplicate so the original provider label defined earlier remains
unchanged; if the later entry was meant to be a different label, give it a
distinct key name and update any code/usage to reference the new key instead of
creating a conflicting duplicate.

In `@services/platform/messages/fr.json`:
- Around line 2897-2909: The French strings use formal imperatives; change them
to the file's informal "tu" register by updating the values for the keys
"placeholder" -> use "Décris la modification…", "placeholderCreate" -> "Décris
une image à créer…", "modelCannotEdit" -> adjust the verb to informal e.g. "Ce
modèle crée uniquement de nouvelles images. Passe à un modèle d'édition pour
appliquer des modifications.", and "pickAnImage" -> "Choisis une image à
modifier"; keep other keys but ensure all imperative verbs use "tu" forms (e.g.,
replace "Passez", "Décrivez", "Choisir" with "Passe", "Décris", "Choisis").
- Around line 1570-1574: There is a duplicate translation key "default" inside
the same object (settings.providers.default) that overwrites the earlier entry;
locate the second "default" occurrence (the one currently set to "défaut") and
either remove it or replace it with the intended unique key, or restore the
original translation value ("Par défaut") so the earlier UI copy isn't lost;
ensure only one settings.providers.default exists and that its value matches the
intended French string.
- Around line 1570-1574: fr.json is missing 149 keys that exist in en.json
(breaking locale parity); update fr.json to include all keys present in en.json
(use de.json as a parity reference) so every en.json key appears in fr.json in
the same commit — specifically ensure keys such as tagImageGeneration,
tagImageEdit, imageGenerationMode, imageGenerationModeHelp, default and entire
key groups analytics.usage.* and governance.twoFactorPolicy.* are added; for
missing entries add proper French translations or safe placeholder strings
matching the en.json key names/structure and verify JSON validity and key
ordering matches en.json/de.json.

---

Outside diff comments:
In
`@services/platform/app/features/chat/components/message-bubble/file-displays.tsx`:
- Around line 3-13: Replace the two bare <img> usages in file-displays.tsx with
the project's Image component imported from
'@/app/components/ui/data-display/image' (update imports at the top), passing
the same src and alt props and appropriate width/height or className to preserve
styling; also locate the edit button (the button using classes including
"size-5" and "focus:outline-none") and change "size-5" to "size-6" (or larger)
to meet the 24×24 touch target and remove "focus:outline-none" so focus styles
remain visible. Ensure you keep any existing aria-label/text for accessibility
and preserve other event handlers on the edit button.

In `@services/platform/app/features/chat/components/model-selector.tsx`:
- Around line 64-103: The modelInfoMap built in the useMemo is keyed only by
model.id, causing collisions when multiple providers expose the same model id so
the last provider wins; change the map to use a fully-qualified key (e.g.,
`${provider.name}/${model.id}`) or store provider-specific entries (or arrays)
so metadata is provider-scoped, then update getProviderSlug to look up by the
qualified key when parseModelRef yields a providerName or to resolve ambiguous
unqualified refs by checking all provider-scoped entries; touch the modelInfoMap
construction in the useMemo and the getProviderSlug callback (and any other
lookup sites at lines referenced: 115-119, 216-229) to use the new qualified-key
strategy so badges, displayName, and requiredTag filtering are sourced from the
correct provider.

In `@services/platform/convex/agents/file_actions.ts`:
- Around line 209-228: The code currently assumes an unqualified modelId will
match at least one provider; if matches.length === 0 you must fail fast: inside
the block that computes matches from byProvider (where matches,
resolvedProviderName are set), detect matches.length === 0 and throw a
ConvexError with code 'UNKNOWN_MODEL' and a clear message referencing the
offending modelId/ref so the invalid supportedModels entry cannot be saved; keep
the existing behavior for matches.length > 1 (warnings and pin to matches[0])
and then set resolvedProviderName = matches[0] as before, and leave the later
requireImageGenerationTag/modelTagLookup logic unchanged.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 317e0c40-18ac-420b-8f75-0a90fe6caaa3

📥 Commits

Reviewing files that changed from the base of the PR and between e7293a0 and 167c5c3.

⛔ Files ignored due to path filters (1)
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (35)
  • examples/agents/image-creator.json
  • examples/providers/openrouter.json
  • services/platform/app/components/ui/forms/searchable-select.tsx
  • services/platform/app/components/ui/overlays/sheet.tsx
  • services/platform/app/features/chat/components/chat-input.tsx
  • services/platform/app/features/chat/components/chat-interface.tsx
  • services/platform/app/features/chat/components/editing-banner.tsx
  • services/platform/app/features/chat/components/message-bubble.tsx
  • services/platform/app/features/chat/components/message-bubble/file-displays.tsx
  • services/platform/app/features/chat/components/model-selector.tsx
  • services/platform/app/features/chat/components/model-tag-icons.tsx
  • services/platform/app/features/chat/components/thumbnail-picker.tsx
  • services/platform/app/features/chat/context/chat-layout-context.tsx
  • services/platform/app/features/chat/hooks/queries.ts
  • services/platform/app/features/chat/hooks/use-message-processing.ts
  • services/platform/app/features/chat/hooks/use-thread-images.ts
  • services/platform/app/features/settings/providers/components/provider-add-panel.tsx
  • services/platform/app/features/settings/providers/components/provider-edit-panel.tsx
  • services/platform/app/features/settings/providers/utils/model-tag-label.ts
  • services/platform/app/routes/dashboard/$id/agents/$agentId/instructions.tsx
  • services/platform/app/routes/dashboard/$id/settings/providers/$providerName.tsx
  • services/platform/convex/agents/config.ts
  • services/platform/convex/agents/file_actions.ts
  • services/platform/convex/agents/file_utils.ts
  • services/platform/convex/agents/image_generation/run_image_generation.ts
  • services/platform/convex/lib/agent_chat/start_agent_chat.ts
  • services/platform/convex/lib/agent_chat/types.ts
  • services/platform/convex/lib/agent_completion/on_agent_complete.ts
  • services/platform/convex/providers/file_actions.ts
  • services/platform/convex/providers/resolve_model.ts
  • services/platform/lib/shared/schemas/agents.ts
  • services/platform/lib/shared/schemas/providers.ts
  • services/platform/messages/de.json
  • services/platform/messages/en.json
  • services/platform/messages/fr.json

Comment on lines +443 to +475
<Tooltip
content={
sendBlocked && sendBlockedReason && !isLoading
? sendBlockedReason
: ''
}
size="icon"
className="rounded-full"
aria-label={isLoading ? tChat('stopGenerating') : tChat('send')}
side="top"
>
{isLoading ? (
<CircleStop className="size-4" />
) : (
<ArrowUp className="size-4" />
)}
</Button>
<Button
type="button"
onClick={isLoading ? onStopGenerating : handleSendMessage}
disabled={
isLoading
? !onStopGenerating
: (!value.trim() && attachments.length === 0) ||
inputDisabled ||
isUploading ||
isIndexing ||
sendBlocked
}
size="icon"
className="rounded-full"
aria-label={
isLoading ? tChat('stopGenerating') : tChat('send')
}
>
{isLoading ? (
<CircleStop className="size-4" />
) : (
<ArrowUp className="size-4" />
)}
</Button>
</Tooltip>
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i 'tooltip.tsx' services/platform/app/components/ui | while read -r file; do
  echo "=== $file ==="
  sed -n '1,240p' "$file"
done

echo
echo "=== trigger usage ==="
rg -n "TooltipTrigger|asChild|disabled" services/platform/app/components/ui

Repository: tale-project/tale

Length of output: 31720


Wrap the disabled button in a <span> so the tooltip receives pointer events.

When a <button> is disabled, it suppresses pointerdown, hover, and focus events. Because Tooltip uses asChild on its trigger, the disabled state prevents the tooltip from ever opening—even when sendBlockedReason is available. The codebase already documents this pattern: see tooltip.stories.tsx line 126 ("Wrap disabled elements in a span for tooltip to work on disabled buttons").

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

In `@services/platform/app/features/chat/components/chat-input.tsx` around lines
443 - 475, Wrap the Button trigger inside a non-disabled wrapper element so the
Tooltip can receive pointer events when the Button is disabled: inside the
Tooltip component that currently renders Button (identify by Tooltip + Button
with props isLoading, onStopGenerating, handleSendMessage and disabled computed
from value.trim/attachments/inputDisabled/isUploading/isIndexing/sendBlocked),
place a <span> (or other inline wrapper) as the Tooltip child and keep the
Button as a child of that span (leave the Button disabled attribute and all
props/aria-label unchanged). This ensures sendBlocked/sendBlockedReason still
control the tooltip content while preserving the Button behavior for isLoading
and stop/generate actions.

Comment on lines +295 to +310
const { providers: providersForEdit } = useListProviders('default');
const activeModelRef = effectiveAgent?.name
? (selectedModelOverrides[effectiveAgent.name] ??
activeAgentMeta?.supportedModels?.[0])
: undefined;
const activeModelInfo = useMemo(() => {
if (!activeModelRef) return undefined;
const plain = stripModelRefQualifier(activeModelRef);
for (const p of providersForEdit) {
if (!p || !('models' in p) || !Array.isArray(p.models)) continue;
for (const model of p.models) {
if (model.id === plain) return model;
}
}
return undefined;
}, [activeModelRef, providersForEdit]);
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

Resolve edit capability from the selected org/model, not the default provider list.

This lookup hardcodes the "default" org and strips the provider qualifier before matching on model.id. If an org overrides model metadata, or two providers share the same id, currentModelSupportsEdit/currentModelLabel can drift from what the backend actually resolves, so the UI may block valid edits or allow invalid ones.

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

In `@services/platform/app/features/chat/components/chat-interface.tsx` around
lines 295 - 310, The model lookup is using the hardcoded default provider list
and stripping provider qualifiers which can mis-resolve model metadata; update
the logic so useListProviders is called for the correct org/provider (derive the
org/provider from activeModelRef or the selected agent/organization context
instead of 'default') and change the activeModelInfo lookup to first try
matching the full model reference (including provider qualifier) against
provider.models entries and only fall back to stripModelRefQualifier/plain id if
no exact match is found; update references in this block (useListProviders,
activeModelRef, stripModelRefQualifier, providersForEdit, activeModelInfo) so
the UI reflects the backend-resolved model metadata.

Comment on lines +1022 to +1033
sendBlocked={
isImageGenAgent &&
!!activeEditingImage &&
!currentModelSupportsEdit
}
sendBlockedReason={
isImageGenAgent &&
!!activeEditingImage &&
!currentModelSupportsEdit
? t('imageEdit.modelCannotEdit')
: undefined
}
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

Block sends when the active edit reference has no storage id.

sendBlocked only checks model capability, but the send path only attaches the reference image when activeEditingImage.ref.fileId is present. That means an explicit edit target with an empty fileId can still be submitted and silently degrades into plain text-to-image generation.

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

In `@services/platform/app/features/chat/components/chat-interface.tsx` around
lines 1022 - 1033, sendBlocked currently only tests model capability, but the
send path requires activeEditingImage.ref.fileId to attach the reference; update
the send-blocking logic to also block when there is an explicit edit target
lacking a storage id (i.e., when activeEditingImage is truthy but
activeEditingImage.ref?.fileId is empty). Change the checks used where
sendBlocked and sendBlockedReason are set (referencing isImageGenAgent,
activeEditingImage, currentModelSupportsEdit) so sendBlocked becomes true when
the fileId is missing and provide an appropriate sendBlockedReason (use a
new/i18n key like imageEdit.missingReference or similar) instead of allowing
silent fallback to text-only generation.

Comment on lines +60 to +65
if (editingImageRef) {
const explicitKey = `explicit:${editingImageRef.fileId || editingImageRef.url}`;
return {
active: { ref: editingImageRef, key: explicitKey },
isDismissed: false,
};
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

Dismiss won't stick after selecting an explicit image.

Explicit refs get synthetic keys like explicit:${fileId}, but dismissal only suppresses threadImages[0].key. After handleDismiss() clears editingImageRef, the hook falls back to the latest thread image and the banner reappears immediately instead of staying dismissed.

Proposed fix
 export function useEffectiveEditingImage(threadImages: ThreadImage[]): {
   active: { ref: EditingImageRef; key: string } | null;
   isDismissed: boolean;
 } {
   const { editingImageRef, dismissedImageKey } = useChatLayout();

   return useMemo(() => {
     if (editingImageRef) {
-      const explicitKey = `explicit:${editingImageRef.fileId || editingImageRef.url}`;
+      const matchingThreadImage = threadImages.find(
+        (img) =>
+          (editingImageRef.fileId && img.fileId === editingImageRef.fileId) ||
+          img.url === editingImageRef.url,
+      );
+      const explicitKey =
+        matchingThreadImage?.key ??
+        `explicit:${editingImageRef.fileId || editingImageRef.url}`;
       return {
         active: { ref: editingImageRef, key: explicitKey },
         isDismissed: false,
       };
     }

Also applies to: 107-110

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

In `@services/platform/app/features/chat/components/editing-banner.tsx` around
lines 60 - 65, The banner dismissal isn't persisted for explicit images because
the code uses a synthetic explicitKey (`explicit:${editingImageRef.fileId ||
editingImageRef.url}`) when building the active ref but dismissal logic compares
against `threadImages[0].key`; update the dismissal flow to record and compare
the same synthetic key: compute the same explicitKey in the hook/component (used
where `active: { ref: editingImageRef, key: explicitKey }` is created and also
where `threadImages[0].key` is checked), make `handleDismiss()` store that
explicitKey in the dismissed state (e.g., `dismissedKey`) and use that
dismissedKey to set `isDismissed` so the banner stays hidden after dismissing
explicit images; apply the same change to the other occurrence around the
107-110 block.

Comment on lines +150 to +154
<img
src={ref.url}
alt={ref.fileName ?? ''}
className="size-full object-cover"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use the shared Image component for the preview thumbnail.

This new preview path bypasses the repo's image wrapper by rendering a bare <img>. Based on learnings "Applies to **/*.{tsx} : Images go through the custom Image component from @/components/ui/image. Never bare <img>."

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

In `@services/platform/app/features/chat/components/editing-banner.tsx` around
lines 150 - 154, Replace the bare <img> preview in the editing-banner component
with the repo's shared Image component (import Image from
'@/components/ui/image') so previews go through the centralized wrapper; pass
the same props (src={ref.url}, alt={ref.fileName ?? ''}) and preserve className
("size-full object-cover") or equivalent props the Image component expects, and
add the import for Image at the top of the file.

Comment on lines +1576 to +1577
"imageGenerationMode": "Bildgenerierungs-Modus",
"imageGenerationModeHelp": "Wie dieses Bildmodell aufgerufen wird. `images-api` nutzt den Standard-Endpunkt /v1/images/generations (FLUX, Imagen; Bearbeitung via /v1/images/edits). `chat-multimodal` nutzt /v1/chat/completions mit Bild-Inhaltsteilen (Nano Banana, GPT-Image; Bearbeitung via Bild-Teile in der Nutzernachricht).",
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

Correct the FLUX mode guidance.

Line 1577 says FLUX models use images-api, but the bundled OpenRouter examples configure every new FLUX.2 model with imageGenerationMode: "chat-multimodal". That will push admins toward the wrong mode when they add FLUX models manually.

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

In `@services/platform/messages/de.json` around lines 1576 - 1577, The translation
text for imageGenerationModeHelp incorrectly instructs that FLUX models use
`images-api`; update the copy referenced by the `imageGenerationMode` /
`imageGenerationModeHelp` keys so FLUX models are described as using
`chat-multimodal` (matching how bundled OpenRouter configures FLUX.2 and
avoiding directing admins to `images-api`), and adjust the endpoint wording to
indicate FLUX uses /v1/chat/completions with image content parts rather than
/v1/images/generations.

"tagImageEdit": "Bildbearbeitung",
"imageGenerationMode": "Bildgenerierungs-Modus",
"imageGenerationModeHelp": "Wie dieses Bildmodell aufgerufen wird. `images-api` nutzt den Standard-Endpunkt /v1/images/generations (FLUX, Imagen; Bearbeitung via /v1/images/edits). `chat-multimodal` nutzt /v1/chat/completions mit Bild-Inhaltsteilen (Nano Banana, GPT-Image; Bearbeitung via Bild-Teile in der Nutzernachricht).",
"default": "Standard",
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 | 🟡 Minor

Remove the duplicated default key.

settings.providers.default is already defined earlier in this object at Line 1518. Keeping a second copy means one value silently shadows the other during JSON parsing, which is easy to let diverge later.

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

In `@services/platform/messages/de.json` at line 1578, The JSON contains a
duplicated key "default" under the same object (settings.providers.default) —
remove the second occurrence at the shown diff so there is only a single
settings.providers.default entry; locate the duplicate "default" key in the same
JSON object (the one at Line 1578 in the diff) and delete that redundant
key/value pair to avoid silent shadowing and keep the single canonical
settings.providers.default definition.

"tagImageEdit": "Image edit",
"imageGenerationMode": "Image generation mode",
"imageGenerationModeHelp": "How this image model is invoked. `images-api` uses the standard /v1/images/generations endpoint (FLUX, Imagen; edit mode via /v1/images/edits). `chat-multimodal` uses /v1/chat/completions with image content parts (Nano Banana, GPT-Image; edit mode via image parts in the user message).",
"default": "default",
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

Duplicate JSON key overwrites the existing provider default label.

At Line 1578, providers.default is redefined, which overrides the earlier providers.default at Line 1518. JSON keeps only the last value, so this silently changes existing UI text.

💡 Suggested fix
-      "default": "default",
📝 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
"default": "default",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/messages/en.json` at line 1578, The JSON contains a
duplicate key "providers.default" which overwrites the earlier value; locate
both occurrences of the "providers.default" key (search for the exact key string
"providers.default") and remove or rename the later duplicate so the original
provider label defined earlier remains unchanged; if the later entry was meant
to be a different label, give it a distinct key name and update any code/usage
to reference the new key instead of creating a conflicting duplicate.

Comment on lines +1570 to +1574
"tagImageGeneration": "Génération d'images",
"tagImageEdit": "Édition d'images",
"imageGenerationMode": "Mode de génération d'images",
"imageGenerationModeHelp": "Comment ce modèle d'image est invoqué. `images-api` utilise le point de terminaison standard /v1/images/generations (FLUX, Imagen ; édition via /v1/images/edits). `chat-multimodal` utilise /v1/chat/completions avec des parties d'image dans le contenu (Nano Banana, GPT-Image ; édition via les parties image dans le message utilisateur).",
"default": "défaut",
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

Duplicate settings.providers.default key overwrites an existing translation.

Line 1574 redefines default, which is already defined at Line 1514 in the same object. In JSON, the later value wins, so this silently changes existing UI copy from Par défaut to défaut.

🛠️ Proposed fix
-      "default": "défaut",
📝 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
"tagImageGeneration": "Génération d'images",
"tagImageEdit": "Édition d'images",
"imageGenerationMode": "Mode de génération d'images",
"imageGenerationModeHelp": "Comment ce modèle d'image est invoqué. `images-api` utilise le point de terminaison standard /v1/images/generations (FLUX, Imagen ; édition via /v1/images/edits). `chat-multimodal` utilise /v1/chat/completions avec des parties d'image dans le contenu (Nano Banana, GPT-Image ; édition via les parties image dans le message utilisateur).",
"default": "défaut",
"tagImageGeneration": "Génération d'images",
"tagImageEdit": "Édition d'images",
"imageGenerationMode": "Mode de génération d'images",
"imageGenerationModeHelp": "Comment ce modèle d'image est invoqué. `images-api` utilise le point de terminaison standard /v1/images/generations (FLUX, Imagen ; édition via /v1/images/edits). `chat-multimodal` utilise /v1/chat/completions avec des parties d'image dans le contenu (Nano Banana, GPT-Image ; édition via les parties image dans le message utilisateur).",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/messages/fr.json` around lines 1570 - 1574, There is a
duplicate translation key "default" inside the same object
(settings.providers.default) that overwrites the earlier entry; locate the
second "default" occurrence (the one currently set to "défaut") and either
remove it or replace it with the intended unique key, or restore the original
translation value ("Par défaut") so the earlier UI copy isn't lost; ensure only
one settings.providers.default exists and that its value matches the intended
French string.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import json
from pathlib import Path

files = {
    "en": Path("services/platform/messages/en.json"),
    "de": Path("services/platform/messages/de.json"),
    "fr": Path("services/platform/messages/fr.json"),
}

def flatten(obj, prefix=""):
    out = set()
    if isinstance(obj, dict):
        for k, v in obj.items():
            p = f"{prefix}.{k}" if prefix else k
            out.add(p)
            out |= flatten(v, p)
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            p = f"{prefix}[{i}]"
            out.add(p)
            out |= flatten(v, p)
    return out

keysets = {}
for locale, p in files.items():
    data = json.loads(p.read_text(encoding="utf-8"))
    keysets[locale] = flatten(data)

base = keysets["en"]
for locale in ("de", "fr"):
    missing = sorted(base - keysets[locale])
    extra = sorted(keysets[locale] - base)
    print(f"\n=== {locale} ===")
    print(f"Missing vs en: {len(missing)}")
    for k in missing[:80]:
        print("  -", k)
    print(f"Extra vs en: {len(extra)}")
    for k in extra[:80]:
        print("  +", k)
PY

Repository: tale-project/tale

Length of output: 3159


Fix critical locale key parity gap in fr.json — missing 149 keys present in en.json.

The fr.json file is missing 149 keys that exist in en.json (de.json maintains perfect parity). This includes keys for analytics.usage., governance.twoFactorPolicy., and others. Per the coding guideline, every key in en.json must exist in de.json and fr.json on the same commit. Align fr.json with en.json and de.json.

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

In `@services/platform/messages/fr.json` around lines 1570 - 1574, fr.json is
missing 149 keys that exist in en.json (breaking locale parity); update fr.json
to include all keys present in en.json (use de.json as a parity reference) so
every en.json key appears in fr.json in the same commit — specifically ensure
keys such as tagImageGeneration, tagImageEdit, imageGenerationMode,
imageGenerationModeHelp, default and entire key groups analytics.usage.* and
governance.twoFactorPolicy.* are added; for missing entries add proper French
translations or safe placeholder strings matching the en.json key
names/structure and verify JSON validity and key ordering matches
en.json/de.json.

Comment on lines +2897 to +2909
"imageEdit": {
"editThis": "Modifier cette image",
"previewReference": "Prévisualiser l'image de référence",
"editingLatest": "Modifier la dernière image",
"editing": "Modifier",
"dismiss": "Ne pas référencer cette image",
"change": "Changer",
"placeholder": "Décrivez la modification…",
"placeholderCreate": "Décrivez une image à créer…",
"modelCannotEdit": "Ce modèle ne crée que de nouvelles images. Passez à un modèle d'édition pour appliquer des modifications.",
"pickAnImage": "Choisir une image à modifier",
"noOtherImages": "Aucune autre image dans ce fil."
},
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 | 🟡 Minor

Keep French register consistent with the file’s existing “tu” tone.

The new strings use formal imperatives (Décrivez, Passez, Choisir) while nearby chat copy uses informal phrasing.

✍️ Suggested copy adjustment
-      "placeholder": "Décrivez la modification…",
-      "placeholderCreate": "Décrivez une image à créer…",
-      "modelCannotEdit": "Ce modèle ne crée que de nouvelles images. Passez à un modèle d'édition pour appliquer des modifications.",
-      "pickAnImage": "Choisir une image à modifier",
+      "placeholder": "Décris la modification…",
+      "placeholderCreate": "Décris une image à créer…",
+      "modelCannotEdit": "Ce modèle ne crée que de nouvelles images. Passe à un modèle d'édition pour appliquer des modifications.",
+      "pickAnImage": "Choisis une image à modifier",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/messages/fr.json` around lines 2897 - 2909, The French
strings use formal imperatives; change them to the file's informal "tu" register
by updating the values for the keys "placeholder" -> use "Décris la
modification…", "placeholderCreate" -> "Décris une image à créer…",
"modelCannotEdit" -> adjust the verb to informal e.g. "Ce modèle crée uniquement
de nouvelles images. Passe à un modèle d'édition pour appliquer des
modifications.", and "pickAnImage" -> "Choisis une image à modifier"; keep other
keys but ensure all imperative verbs use "tu" forms (e.g., replace "Passez",
"Décrivez", "Choisir" with "Passe", "Décris", "Choisis").

@larryro larryro merged commit 10539d1 into main Apr 20, 2026
14 of 15 checks passed
@larryro larryro deleted the feat/image-generation-agents branch April 20, 2026 09:10
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