Skip to content

fix: correct OpenAI image model handling#1590

Merged
zerob13 merged 3 commits intodevfrom
fix/openai-image-model-config
May 7, 2026
Merged

fix: correct OpenAI image model handling#1590
zerob13 merged 3 commits intodevfrom
fix/openai-image-model-config

Conversation

@yyhhyyyyyy
Copy link
Copy Markdown
Collaborator

@yyhhyyyyyy yyhhyyyyyy commented May 7, 2026

  • correct OpenAI image model handling
  • update ai sdk version
  • add image save and copy actions

Summary by CodeRabbit

  • New Features

    • Added ability to save images with a save-as dialog
    • Added ability to copy images to clipboard
    • Added context menu for images with copy and save actions
  • Chores

    • Updated AI SDK and related dependencies to latest versions
    • Updated provider registry entries with new versions and pricing information
    • Added image action translations across 11 languages

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces image save and copy functionality via Electron file dialogs and clipboard, plus updates AI SDK dependencies, provider database models, and model type inference for image generation providers. The feature spans shared route contracts, a main-process file presenter with multiple image source handlers, renderer-bridge integration, composables with toast notifications, Vue components with context menus, and localization for twelve locales.

Changes

Dependency and Model Configuration Updates

Layer / File(s) Summary
Dependency Versions
package.json
Bumped @ai-sdk/* packages (amazon-bedrock, anthropic, azure, google, google-vertex, openai, openai-compatible, provider) and ai package to newer releases.
Agent Registry Updates
resources/acp-registry/registry.json
Updated Junie (1468.30.0 → 1543.24.0), Qoder CLI (0.2.7 → 0.2.8), and Qwen Code (0.15.6 → 0.15.7) with corresponding binary and NPX package references.
Provider Model Database
resources/model-db/providers.json
Added reasoning capability flags, updated pricing (set output costs to 0 for rerank/embedding), added new models (grok-4.20, grok-3-mini-fast-beta, wan2.5-t2v-preview), adjusted modalities, and reordered provider entries.
Model Type Inference
src/main/presenter/configPresenter/modelConfig.ts
Compute modelType upfront via inferModelType, mapping imageGeneration provider type to ModelType.ImageGeneration; select apiEndpoint (Image or Chat) based on modelType.

Image Save and Copy Feature

Layer / File(s) Summary
Route Contracts and Interfaces
src/shared/contracts/routes/file.routes.ts, src/shared/types/presenters/legacy.presenters.d.ts
Define fileSaveImageRoute and fileCopyImageRoute with shared FileImageActionInputSchema (source, mimeType?, suggestedName?) and distinct outputs; add IFilePresenter interface methods.
Core Image Handling
src/main/presenter/filePresenter/FilePresenter.ts
Implement saveImage (Electron dialog save) and copyImage (clipboard) methods; add resolveImageData dispatcher with handlers for data URLs, cached imgcache:// paths, remote http/https URLs, and raw base64; include MIME type normalization and filename sanitization.
Main Process Routing
src/main/routes/index.ts
Register fileSaveImageRoute and fileCopyImageRoute handlers; parse input and delegate to FilePresenter methods.
Route Catalog
src/shared/contracts/routes.ts
Update imports and register fileSaveImageRoute and fileCopyImageRoute in DEEPCHAT_ROUTE_CATALOG.
Renderer Bridge Client
src/renderer/api/FileClient.ts
Expose saveImage and copyImage methods delegating to fileSaveImageRoute and fileCopyImageRoute via IPC bridge.
Composable Actions
src/renderer/src/composables/useImageActions.ts
Create useImageActions composable; implement saveImage and copyImage with FileClient calls, localized success toasts, and error handling with destructive-variant failure toasts.
Context Menu Component
src/renderer/src/components/message/ImageActionContextMenu.vue
New Vue component rendering ContextMenu with Copy and Save actions, wiring handlers to useImageActions.
Message Image Integration
src/renderer/src/components/message/MessageBlockImage.vue
Wrap inline and dialog image renders with ImageActionContextMenu; add dialog save button; compute resolvedImageSrc and resolvedImageMimeType.
Tool Call Preview Integration
src/renderer/src/components/message/MessageBlockToolCallImagePreview.vue
Wrap thumbnail and selected image previews with ImageActionContextMenu; add download button to dialog; compute selectedPreviewSrc and selectedPreviewMimeType with deepchat/image-url normalization.
Internationalization
src/renderer/src/i18n/*/image.json
Add preview, save, saveAs, saveSuccess, saveFailed keys to all 12 locales (da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ThinkInAIXYZ/deepchat#1553: Extends i18n image translation bundles with the same namespace keys used in this PR.
  • ThinkInAIXYZ/deepchat#1568: Adds file-handling utilities and extension support to FilePresenter that complements the image save logic introduced here.
  • ThinkInAIXYZ/deepchat#1535: Integrates FilePresenter functionality into RemoteControlPresenter for remote media delivery, directly building on the IFilePresenter interface extended in this PR.

Suggested reviewers

  • zerob13

Poem

🐰 Hoppin' through the pixels bright,
Save and copy—pure delight!
Electron dialogs dance and play,
Images hop the IPC way.
Context menus, toasts that cheer,
Twelve-tongue translations crystal clear!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'fix: correct OpenAI image model handling' describes only a portion of the changes. The changeset includes dependency updates, registry updates, provider model database changes, image handling utilities, file operations, UI components, internationalization additions, and route contracts. The title focuses on OpenAI image models but does not capture the main scope of changes which is adding comprehensive image save/copy functionality. Revise the title to reflect the primary changes: 'feat: add image save and copy functionality with updated dependencies' or similar, or clarify if the OpenAI image model handling is truly the main focus.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 fix/openai-image-model-config

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Microsoft Presidio Analyzer (2.2.362)
resources/model-db/providers.json

Microsoft Presidio Analyzer failed to scan this file


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.

Copy link
Copy Markdown
Contributor

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

Caution

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

⚠️ Outside diff range comments (1)
package.json (1)

82-98: ⚠️ Potential issue | 🟠 Major

AI SDK upgrade needs error handling for NoImageGeneratedError.

The generateImage call in src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts (line 341) does not handle the new NoImageGeneratedError that AI SDK 6 throws when no predictions are returned. The current code will crash with an unhandled exception if image generation fails. Wrap this call in a try-catch block to gracefully handle this error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 82 - 98, The generateImage call in
src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts can throw the new
NoImageGeneratedError from AI SDK v6 and is currently unhandled; modify the code
around the generateImage invocation (the function/method that calls
generateImage in runtime.ts) to wrap the call in a try-catch, explicitly catch
NoImageGeneratedError (by name) to handle it gracefully (e.g., log a clear
message via the existing logger, return a safe value or propagate a sanitized
error), and only rethrow unexpected errors; ensure the catch references
NoImageGeneratedError so other errors continue to surface.
🧹 Nitpick comments (2)
src/renderer/src/components/message/MessageBlockToolCallImagePreview.vue (1)

158-162: 💤 Low value

Same handleImageDialogOpenAutoFocus duplication as in MessageBlockImage.vue.

See the extraction suggestion on MessageBlockImage.vue lines 262–266.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/message/MessageBlockToolCallImagePreview.vue`
around lines 158 - 162, The handleImageDialogOpenAutoFocus function is
duplicated; extract it into a shared utility/composable (e.g.,
useImageDialogFocus or focusOnEvent) and replace the inline definitions in
MessageBlockToolCallImagePreview.vue and MessageBlockImage.vue with imports;
ensure the extracted function accepts an Event, calls preventDefault(), and
focuses the event.target (matching the existing behavior), export it from the
new module and update both components to import and use that single
implementation.
src/renderer/src/components/message/MessageBlockImage.vue (1)

262-266: 💤 Low value

Extract handleImageDialogOpenAutoFocus to a shared composable — duplicated verbatim in MessageBlockToolCallImagePreview.vue.

The same four-line handler appears identically in both image dialog components. A small shared utility (e.g., useDialogAutoFocus) would remove the duplication.

♻️ Proposed extraction
// src/renderer/src/composables/useDialogAutoFocus.ts
export function useDialogAutoFocus() {
  const handleOpenAutoFocus = (event: Event) => {
    event.preventDefault()
    const target = event.target as HTMLElement | null
    target?.focus()
  }
  return { handleOpenAutoFocus }
}

Then in each component:

-const handleImageDialogOpenAutoFocus = (event: Event) => {
-  event.preventDefault()
-  const target = event.target as HTMLElement | null
-  target?.focus()
-}
+import { useDialogAutoFocus } from '@/composables/useDialogAutoFocus'
+const { handleOpenAutoFocus: handleImageDialogOpenAutoFocus } = useDialogAutoFocus()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/message/MessageBlockImage.vue` around lines 262 -
266, Extract the duplicated handler into a shared composable: create a new
composable (e.g., useDialogAutoFocus) that exports a function name
(useDialogAutoFocus) which returns the handler (e.g., handleOpenAutoFocus)
implementing the same logic as handleImageDialogOpenAutoFocus (preventDefault,
cast target to HTMLElement|null, call focus()). Replace the inline
handleImageDialogOpenAutoFocus in MessageBlockImage.vue and the identical
handler in MessageBlockToolCallImagePreview.vue by importing useDialogAutoFocus
and using the returned handleOpenAutoFocus for the dialog open autofocus.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@resources/model-db/providers.json`:
- Around line 193675-193704: The entry for model id/name
"grok-4.20-beta-0309-non-reasoning" incorrectly enables reasoning; update its
reasoning and extra_capabilities to disable reasoning (set "reasoning":
{"supported": false, "default": false} and either remove or set
"extra_capabilities": {"reasoning": {"supported": false}}) so the non-reasoning
variant does not advertise or default to reasoning.
- Around line 192794-192811: The JSON entry for model id/name
"grok-3-mini-fast-beta" has wrong cost and limit values; update the "limit"
object to context: 131072 and output: 131072, adjust the "cost" object to input:
0.30 and output: 0.50 and add cache_read: 0.075, and keep "tool_call",
"reasoning" and "type" unchanged so the entry matches the xAI docs.

In `@src/main/presenter/filePresenter/FilePresenter.ts`:
- Around line 429-459: In resolveRemoteImage, net.fetch can hang indefinitely;
wrap the fetch with an AbortSignal created via AbortSignal.timeout(30000) and
pass that signal to net.fetch (e.g., net.fetch(url.toString(), { signal })) so
the request aborts after 30s; ensure the thrown AbortError/TimeoutError bubbles
up (no swallowing) so existing callers like useImageActions.saveImage can catch
and handle it.
- Around line 429-459: In resolveRemoteImage, net.fetch can hang; wrap the fetch
in an AbortController with a 30_000 ms timeout: create an AbortController, pass
controller.signal to net.fetch(url.toString(), { signal }), set a timer to call
controller.abort() after 30_000 ms, and ensure the timer is cleared in a finally
block so it never leaks; handle the aborted fetch as before (let the existing
non-ok/error checks throw) and reference resolveRemoteImage, net.fetch, and
AbortController when making the change.

In `@src/renderer/src/composables/useImageActions.ts`:
- Around line 43-53: The locale files are missing translation keys referenced in
useImageActions.ts: add the keys common.copyImageSuccess,
common.copyImageSuccessDesc, common.copyFailed, and common.copyFailedDesc to
every locale's common.json (da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR,
pt-BR, ru-RU, zh-CN, zh-HK, zh-TW) with appropriate translated strings; ensure
the JSON structure matches existing common.json format and run a quick i18n
lint/validation to confirm no syntax errors and that the toast messages render
the translations instead of key paths.

In `@src/renderer/src/i18n/en-US/image.json`:
- Line 6: The "saveAs" translation value uses title case while "save" uses
sentence case; update the "saveAs" value for key "saveAs" in
src/renderer/src/i18n/en-US/image.json to "Save image as..." (sentence case,
with ellipsis), and apply the same change to the other locale files (fa-IR,
ru-RU, he-IL, ja-JP) so all "save" and "saveAs" strings follow the same
sentence-case pattern.

In `@src/shared/contracts/routes/file.routes.ts`:
- Around line 7-8: The schema fields mimeType and suggestedName currently use
z.string().optional() which allows empty strings; update both to require
non-empty strings when present (e.g., use Zod's nonempty() or min(1) on the
string schema) so that mimeType and suggestedName reject '' while remaining
optional, and ensure you update the schema definition where mimeType and
suggestedName are declared in file.routes.ts.

---

Outside diff comments:
In `@package.json`:
- Around line 82-98: The generateImage call in
src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts can throw the new
NoImageGeneratedError from AI SDK v6 and is currently unhandled; modify the code
around the generateImage invocation (the function/method that calls
generateImage in runtime.ts) to wrap the call in a try-catch, explicitly catch
NoImageGeneratedError (by name) to handle it gracefully (e.g., log a clear
message via the existing logger, return a safe value or propagate a sanitized
error), and only rethrow unexpected errors; ensure the catch references
NoImageGeneratedError so other errors continue to surface.

---

Nitpick comments:
In `@src/renderer/src/components/message/MessageBlockImage.vue`:
- Around line 262-266: Extract the duplicated handler into a shared composable:
create a new composable (e.g., useDialogAutoFocus) that exports a function name
(useDialogAutoFocus) which returns the handler (e.g., handleOpenAutoFocus)
implementing the same logic as handleImageDialogOpenAutoFocus (preventDefault,
cast target to HTMLElement|null, call focus()). Replace the inline
handleImageDialogOpenAutoFocus in MessageBlockImage.vue and the identical
handler in MessageBlockToolCallImagePreview.vue by importing useDialogAutoFocus
and using the returned handleOpenAutoFocus for the dialog open autofocus.

In `@src/renderer/src/components/message/MessageBlockToolCallImagePreview.vue`:
- Around line 158-162: The handleImageDialogOpenAutoFocus function is
duplicated; extract it into a shared utility/composable (e.g.,
useImageDialogFocus or focusOnEvent) and replace the inline definitions in
MessageBlockToolCallImagePreview.vue and MessageBlockImage.vue with imports;
ensure the extracted function accepts an Event, calls preventDefault(), and
focuses the event.target (matching the existing behavior), export it from the
new module and update both components to import and use that single
implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09067f2a-da3d-41fa-9837-dc6af14a6f95

📥 Commits

Reviewing files that changed from the base of the PR and between 530750a and 23028e3.

📒 Files selected for processing (26)
  • package.json
  • resources/acp-registry/registry.json
  • resources/model-db/providers.json
  • src/main/presenter/configPresenter/modelConfig.ts
  • src/main/presenter/filePresenter/FilePresenter.ts
  • src/main/routes/index.ts
  • src/renderer/api/FileClient.ts
  • src/renderer/src/components/message/ImageActionContextMenu.vue
  • src/renderer/src/components/message/MessageBlockImage.vue
  • src/renderer/src/components/message/MessageBlockToolCallImagePreview.vue
  • src/renderer/src/composables/useImageActions.ts
  • src/renderer/src/i18n/da-DK/image.json
  • src/renderer/src/i18n/en-US/image.json
  • src/renderer/src/i18n/fa-IR/image.json
  • src/renderer/src/i18n/fr-FR/image.json
  • src/renderer/src/i18n/he-IL/image.json
  • src/renderer/src/i18n/ja-JP/image.json
  • src/renderer/src/i18n/ko-KR/image.json
  • src/renderer/src/i18n/pt-BR/image.json
  • src/renderer/src/i18n/ru-RU/image.json
  • src/renderer/src/i18n/zh-CN/image.json
  • src/renderer/src/i18n/zh-HK/image.json
  • src/renderer/src/i18n/zh-TW/image.json
  • src/shared/contracts/routes.ts
  • src/shared/contracts/routes/file.routes.ts
  • src/shared/types/presenters/legacy.presenters.d.ts

Comment on lines +192794 to +192811
{
"id": "grok-3-mini-fast-beta",
"name": "grok-3-mini-fast-beta",
"display_name": "grok-3-mini-fast-beta",
"limit": {
"context": 8192,
"output": 8192
},
"tool_call": false,
"reasoning": {
"supported": false
},
"cost": {
"input": 0.33,
"output": 2.20011
},
"type": "chat"
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

grok-3-mini-fast-beta API pricing and context window limit

💡 Result:

Grok 3 Mini Fast Beta (xAI) API pricing (as of 2026-05-07): - Input: $0.30 per 1M tokens [1] - Output: $0.50 per 1M tokens [1] - Cached input: $0.075 per 1M tokens (listed on the model page as “Cached input”) [2] Grok 3 Mini Fast Beta context window limit: - Context window: 131,072 tokens [2] Notes: - Pricing above is for token usage; xAI also applies additional pricing components for server-side tools and offers Batch API discounts, but the core token rates are as listed above. [3]

Citations:


Fix incorrect pricing and context limits for grok-3-mini-fast-beta

The entry contains multiple data errors that conflict with official xAI API documentation:

  1. Pricing – input should be 0.30 (not 0.33) and output should be 0.50 (not 2.20011)
  2. Context window – should be 131072 tokens (not 8192)
  3. Output limit – should be 131072 tokens (not 8192)

Update the entry to match the official xAI documentation:

Expected fix
{
  "id": "grok-3-mini-fast-beta",
  "name": "grok-3-mini-fast-beta",
  "display_name": "grok-3-mini-fast-beta",
  "limit": {
    "context": 131072,
    "output": 131072
  },
  "tool_call": false,
  "reasoning": {
    "supported": false
  },
  "cost": {
    "input": 0.30,
    "output": 0.50,
    "cache_read": 0.075
  },
  "type": "chat"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/model-db/providers.json` around lines 192794 - 192811, The JSON
entry for model id/name "grok-3-mini-fast-beta" has wrong cost and limit values;
update the "limit" object to context: 131072 and output: 131072, adjust the
"cost" object to input: 0.30 and output: 0.50 and add cache_read: 0.075, and
keep "tool_call", "reasoning" and "type" unchanged so the entry matches the xAI
docs.

Comment on lines +193675 to +193704
{
"id": "grok-4.20-beta-0309-non-reasoning",
"name": "grok-4.20-beta-0309-non-reasoning",
"display_name": "grok-4.20-beta-0309-non-reasoning",
"modalities": {
"input": [
"text",
"image"
]
},
"limit": {
"context": 2000000,
"output": 2000000
},
"tool_call": true,
"reasoning": {
"supported": true,
"default": true
},
"extra_capabilities": {
"reasoning": {
"supported": true
}
},
"cost": {
"input": 2,
"output": 6,
"cache_read": 0.2
},
"type": "chat"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

grok-4.20-beta-0309-non-reasoning has reasoning enabled by default — likely copy-paste error

The "non-reasoning" model entry is identical to the reasoning variant: both carry "reasoning": {"supported": true, "default": true} and "extra_capabilities": {"reasoning": {"supported": true}}. With default: true, the application will enable reasoning for this model despite its name explicitly indicating it should not use reasoning.

The non-reasoning variant should have reasoning disabled:

🐛 Proposed fix
         {
           "id": "grok-4.20-beta-0309-non-reasoning",
           ...
-          "reasoning": {
-            "supported": true,
-            "default": true
-          },
-          "extra_capabilities": {
-            "reasoning": {
-              "supported": true
-            }
-          },
+          "reasoning": {
+            "supported": false
+          },
📝 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
{
"id": "grok-4.20-beta-0309-non-reasoning",
"name": "grok-4.20-beta-0309-non-reasoning",
"display_name": "grok-4.20-beta-0309-non-reasoning",
"modalities": {
"input": [
"text",
"image"
]
},
"limit": {
"context": 2000000,
"output": 2000000
},
"tool_call": true,
"reasoning": {
"supported": true,
"default": true
},
"extra_capabilities": {
"reasoning": {
"supported": true
}
},
"cost": {
"input": 2,
"output": 6,
"cache_read": 0.2
},
"type": "chat"
{
"id": "grok-4.20-beta-0309-non-reasoning",
"name": "grok-4.20-beta-0309-non-reasoning",
"display_name": "grok-4.20-beta-0309-non-reasoning",
"modalities": {
"input": [
"text",
"image"
]
},
"limit": {
"context": 2000000,
"output": 2000000
},
"tool_call": true,
"reasoning": {
"supported": false
},
"cost": {
"input": 2,
"output": 6,
"cache_read": 0.2
},
"type": "chat"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/model-db/providers.json` around lines 193675 - 193704, The entry
for model id/name "grok-4.20-beta-0309-non-reasoning" incorrectly enables
reasoning; update its reasoning and extra_capabilities to disable reasoning (set
"reasoning": {"supported": false, "default": false} and either remove or set
"extra_capabilities": {"reasoning": {"supported": false}}) so the non-reasoning
variant does not advertise or default to reasoning.

Comment on lines +429 to +459
private async resolveRemoteImage(
source: string,
fallbackMimeType?: string
): Promise<ResolvedImageData> {
const url = new URL(source)
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Unsupported image URL')
}

const response = await net.fetch(url.toString())
if (!response.ok) {
throw new Error(`Failed to download image: ${response.status}`)
}

const responseMimeType = normalizeImageMimeType(
response.headers.get('content-type') ?? undefined
)
const mimeType =
responseMimeType ||
normalizeImageMimeType(fallbackMimeType) ||
inferImageMimeTypeFromPath(url.pathname)

if (!mimeType) {
throw new Error('Remote URL is not an image')
}

return {
data: Buffer.from(await response.arrayBuffer()),
mimeType
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

net.fetch has no timeout — saveImage/copyImage silently hangs on unresponsive servers.

A slow or stalled remote server leaves the user's save/copy operation in a permanent pending state with no feedback or error. AbortSignal.timeout() creates a signal that automatically aborts after a set duration, which is the cleanest fix here.

🛡️ Proposed fix — abort after 30 s using AbortSignal.timeout
  private async resolveRemoteImage(
    source: string,
    fallbackMimeType?: string
  ): Promise<ResolvedImageData> {
    const url = new URL(source)
    if (url.protocol !== 'http:' && url.protocol !== 'https:') {
      throw new Error('Unsupported image URL')
    }

-    const response = await net.fetch(url.toString())
+    const response = await net.fetch(url.toString(), {
+      signal: AbortSignal.timeout(30_000)
+    })
    if (!response.ok) {
      throw new Error(`Failed to download image: ${response.status}`)
    }

The abort will surface as an AbortError/TimeoutError, which the try/catch in useImageActions.saveImage already catches and maps to the saveFailed toast.

📝 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
private async resolveRemoteImage(
source: string,
fallbackMimeType?: string
): Promise<ResolvedImageData> {
const url = new URL(source)
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Unsupported image URL')
}
const response = await net.fetch(url.toString())
if (!response.ok) {
throw new Error(`Failed to download image: ${response.status}`)
}
const responseMimeType = normalizeImageMimeType(
response.headers.get('content-type') ?? undefined
)
const mimeType =
responseMimeType ||
normalizeImageMimeType(fallbackMimeType) ||
inferImageMimeTypeFromPath(url.pathname)
if (!mimeType) {
throw new Error('Remote URL is not an image')
}
return {
data: Buffer.from(await response.arrayBuffer()),
mimeType
}
}
private async resolveRemoteImage(
source: string,
fallbackMimeType?: string
): Promise<ResolvedImageData> {
const url = new URL(source)
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Unsupported image URL')
}
const response = await net.fetch(url.toString(), {
signal: AbortSignal.timeout(30_000)
})
if (!response.ok) {
throw new Error(`Failed to download image: ${response.status}`)
}
const responseMimeType = normalizeImageMimeType(
response.headers.get('content-type') ?? undefined
)
const mimeType =
responseMimeType ||
normalizeImageMimeType(fallbackMimeType) ||
inferImageMimeTypeFromPath(url.pathname)
if (!mimeType) {
throw new Error('Remote URL is not an image')
}
return {
data: Buffer.from(await response.arrayBuffer()),
mimeType
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/filePresenter/FilePresenter.ts` around lines 429 - 459, In
resolveRemoteImage, net.fetch can hang indefinitely; wrap the fetch with an
AbortSignal created via AbortSignal.timeout(30000) and pass that signal to
net.fetch (e.g., net.fetch(url.toString(), { signal })) so the request aborts
after 30s; ensure the thrown AbortError/TimeoutError bubbles up (no swallowing)
so existing callers like useImageActions.saveImage can catch and handle it.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Electron net.fetch support AbortController signal option in Electron 40?

💡 Result:

Yes, Electron's net.fetch supports the AbortController signal option in Electron 40. The official documentation states that net.fetch accepts a RequestInit object (extended with bypassCustomProtocolHandlers), which includes the standard 'signal' property from the Fetch API [1][2]. The implementation explicitly handles the signal: it checks if req.signal.aborted and rejects the promise with an AbortError; it also adds an 'abort' event listener to the signal that rejects the promise, cancels the body if present, and calls r.abort on the underlying request [3]. This feature was added in the initial net.fetch implementation (PR #36733, merged 2023) [4], and Electron 40 release notes show no breaking changes or removals affecting it [5][6]. The source code confirms ongoing support [7].

Citations:


🏁 Script executed:

# Check the imports and full context of the FilePresenter.ts file
head -50 src/main/presenter/filePresenter/FilePresenter.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 1660


🏁 Script executed:

# Search for all net.fetch usages in the codebase to check if this is an isolated case
rg "net\.fetch" --type ts --type tsx -B 2 -A 2

Repository: ThinkInAIXYZ/deepchat

Length of output: 93


🏁 Script executed:

# Check if there's a pattern of timeouts being used elsewhere in the codebase
rg "AbortController|setTimeout.*abort|timeout" src/main --type ts -A 1 -B 1 | head -50

Repository: ThinkInAIXYZ/deepchat

Length of output: 3134


🏁 Script executed:

# Read the specific method at the mentioned lines
sed -n '429,459p' src/main/presenter/filePresenter/FilePresenter.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 923


🏁 Script executed:

# Search for net.fetch specifically in FilePresenter.ts
rg "net\.fetch" src/main/presenter/filePresenter/FilePresenter.ts -B 5 -A 5

Repository: ThinkInAIXYZ/deepchat

Length of output: 423


🏁 Script executed:

# Check if there are other image download/fetch methods in this file
rg "resolveRemoteImage|downloadImage|fetchImage" src/main/presenter/filePresenter/FilePresenter.ts -A 3 -B 1

Repository: ThinkInAIXYZ/deepchat

Length of output: 390


🏁 Script executed:

# Verify if there are any other timeouts or abort mechanisms around image operations
rg "saveImage|copyImage" src/main/presenter/filePresenter/FilePresenter.ts -B 10 -A 5 | head -100

Repository: ThinkInAIXYZ/deepchat

Length of output: 1115


Add timeout to remote image fetch to prevent indefinite hangs.

net.fetch in resolveRemoteImage can hang indefinitely on slow or unresponsive servers, causing saveImage and copyImage to freeze with no user feedback. Implement the proposed fix using AbortController with a 30-second timeout and proper cleanup in a try-finally block. Electron 40's net.fetch supports the signal option via the Fetch API standard.

Proposed fix — add 30 s timeout to remote image fetch
  private async resolveRemoteImage(
    source: string,
    fallbackMimeType?: string
  ): Promise<ResolvedImageData> {
    const url = new URL(source)
    if (url.protocol !== 'http:' && url.protocol !== 'https:') {
      throw new Error('Unsupported image URL')
    }

-    const response = await net.fetch(url.toString())
+    const controller = new AbortController()
+    const timeoutId = setTimeout(() => controller.abort(), 30_000)
+    let response: Response
+    try {
+      response = await net.fetch(url.toString(), { signal: controller.signal })
+    } finally {
+      clearTimeout(timeoutId)
+    }
    if (!response.ok) {
      throw new Error(`Failed to download image: ${response.status}`)
    }
📝 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
private async resolveRemoteImage(
source: string,
fallbackMimeType?: string
): Promise<ResolvedImageData> {
const url = new URL(source)
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Unsupported image URL')
}
const response = await net.fetch(url.toString())
if (!response.ok) {
throw new Error(`Failed to download image: ${response.status}`)
}
const responseMimeType = normalizeImageMimeType(
response.headers.get('content-type') ?? undefined
)
const mimeType =
responseMimeType ||
normalizeImageMimeType(fallbackMimeType) ||
inferImageMimeTypeFromPath(url.pathname)
if (!mimeType) {
throw new Error('Remote URL is not an image')
}
return {
data: Buffer.from(await response.arrayBuffer()),
mimeType
}
}
private async resolveRemoteImage(
source: string,
fallbackMimeType?: string
): Promise<ResolvedImageData> {
const url = new URL(source)
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Unsupported image URL')
}
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 30_000)
let response: Response
try {
response = await net.fetch(url.toString(), { signal: controller.signal })
} finally {
clearTimeout(timeoutId)
}
if (!response.ok) {
throw new Error(`Failed to download image: ${response.status}`)
}
const responseMimeType = normalizeImageMimeType(
response.headers.get('content-type') ?? undefined
)
const mimeType =
responseMimeType ||
normalizeImageMimeType(fallbackMimeType) ||
inferImageMimeTypeFromPath(url.pathname)
if (!mimeType) {
throw new Error('Remote URL is not an image')
}
return {
data: Buffer.from(await response.arrayBuffer()),
mimeType
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/filePresenter/FilePresenter.ts` around lines 429 - 459, In
resolveRemoteImage, net.fetch can hang; wrap the fetch in an AbortController
with a 30_000 ms timeout: create an AbortController, pass controller.signal to
net.fetch(url.toString(), { signal }), set a timer to call controller.abort()
after 30_000 ms, and ensure the timer is cleared in a finally block so it never
leaks; handle the aborted fetch as before (let the existing non-ok/error checks
throw) and reference resolveRemoteImage, net.fetch, and AbortController when
making the change.

Comment on lines +43 to +53
toast({
title: t('common.copyImageSuccess'),
description: t('common.copyImageSuccessDesc')
})
} catch (error) {
console.error('Failed to copy image:', error)
toast({
title: t('common.copyFailed'),
description: t('common.copyFailedDesc'),
variant: 'destructive'
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the four common.* keys used by copyImage exist in every locale
echo "=== Searching for copyImageSuccess in common locale files ==="
rg -n "copyImageSuccess\|copyImageSuccessDesc\|copyFailed\|copyFailedDesc" --type json src/renderer/src/i18n/

echo "=== Listing common.json files present ==="
fd -e json --full-path "common" src/renderer/src/i18n/

Repository: ThinkInAIXYZ/deepchat

Length of output: 649


Add missing i18n keys to all locale common.json files.

The code references common.copyImageSuccess, common.copyImageSuccessDesc, common.copyFailed, and common.copyFailedDesc, but these keys are absent from all 12 locale files (da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW). Without them, users will see raw key paths as toast text instead of translated messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/composables/useImageActions.ts` around lines 43 - 53, The
locale files are missing translation keys referenced in useImageActions.ts: add
the keys common.copyImageSuccess, common.copyImageSuccessDesc,
common.copyFailed, and common.copyFailedDesc to every locale's common.json
(da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK,
zh-TW) with appropriate translated strings; ensure the JSON structure matches
existing common.json format and run a quick i18n lint/validation to confirm no
syntax errors and that the toast messages render the translations instead of key
paths.

"preview": "Preview image"
"preview": "Preview image",
"save": "Save image",
"saveAs": "Save Image As...",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Capitalization inconsistency: saveAs vs save.

"save": "Save image" uses sentence case, but "saveAs": "Save Image As..." capitalises "Image". This inconsistency carries through all 5 locale files. Should be "Save image as...".

✏️ Proposed fix
-  "saveAs": "Save Image As...",
+  "saveAs": "Save image as...",

Apply the same pattern to the other affected locale files (fa-IR, ru-RU, he-IL, ja-JP).

📝 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
"saveAs": "Save Image As...",
"saveAs": "Save image as...",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/i18n/en-US/image.json` at line 6, The "saveAs" translation
value uses title case while "save" uses sentence case; update the "saveAs" value
for key "saveAs" in src/renderer/src/i18n/en-US/image.json to "Save image as..."
(sentence case, with ellipsis), and apply the same change to the other locale
files (fa-IR, ru-RU, he-IL, ja-JP) so all "save" and "saveAs" strings follow the
same sentence-case pattern.

Comment on lines +7 to +8
mimeType: z.string().optional(),
suggestedName: z.string().optional()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten optional string validation to reject empty values.

On Line 7 and Line 8, mimeType and suggestedName accept '' today. That can pass invalid metadata downstream (e.g., empty filename suggestion). Consider requiring non-empty strings when provided.

Suggested diff
 const FileImageActionInputSchema = z.object({
   source: z.string().min(1),
-  mimeType: z.string().optional(),
-  suggestedName: z.string().optional()
+  mimeType: z.string().min(1).optional(),
+  suggestedName: z.string().min(1).optional()
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/contracts/routes/file.routes.ts` around lines 7 - 8, The schema
fields mimeType and suggestedName currently use z.string().optional() which
allows empty strings; update both to require non-empty strings when present
(e.g., use Zod's nonempty() or min(1) on the string schema) so that mimeType and
suggestedName reject '' while remaining optional, and ensure you update the
schema definition where mimeType and suggestedName are declared in
file.routes.ts.

@zerob13 zerob13 merged commit a57b8bf into dev May 7, 2026
3 checks passed
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.

2 participants