Conversation
📝 WalkthroughWalkthroughThis 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. ChangesDependency and Model Configuration Updates
Image Save and Copy Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.jsonMicrosoft 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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorAI SDK upgrade needs error handling for NoImageGeneratedError.
The
generateImagecall insrc/main/presenter/llmProviderPresenter/aiSdk/runtime.ts(line 341) does not handle the newNoImageGeneratedErrorthat 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 valueSame
handleImageDialogOpenAutoFocusduplication as inMessageBlockImage.vue.See the extraction suggestion on
MessageBlockImage.vuelines 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 valueExtract
handleImageDialogOpenAutoFocusto a shared composable — duplicated verbatim inMessageBlockToolCallImagePreview.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
📒 Files selected for processing (26)
package.jsonresources/acp-registry/registry.jsonresources/model-db/providers.jsonsrc/main/presenter/configPresenter/modelConfig.tssrc/main/presenter/filePresenter/FilePresenter.tssrc/main/routes/index.tssrc/renderer/api/FileClient.tssrc/renderer/src/components/message/ImageActionContextMenu.vuesrc/renderer/src/components/message/MessageBlockImage.vuesrc/renderer/src/components/message/MessageBlockToolCallImagePreview.vuesrc/renderer/src/composables/useImageActions.tssrc/renderer/src/i18n/da-DK/image.jsonsrc/renderer/src/i18n/en-US/image.jsonsrc/renderer/src/i18n/fa-IR/image.jsonsrc/renderer/src/i18n/fr-FR/image.jsonsrc/renderer/src/i18n/he-IL/image.jsonsrc/renderer/src/i18n/ja-JP/image.jsonsrc/renderer/src/i18n/ko-KR/image.jsonsrc/renderer/src/i18n/pt-BR/image.jsonsrc/renderer/src/i18n/ru-RU/image.jsonsrc/renderer/src/i18n/zh-CN/image.jsonsrc/renderer/src/i18n/zh-HK/image.jsonsrc/renderer/src/i18n/zh-TW/image.jsonsrc/shared/contracts/routes.tssrc/shared/contracts/routes/file.routes.tssrc/shared/types/presenters/legacy.presenters.d.ts
| { | ||
| "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" | ||
| }, |
There was a problem hiding this comment.
🧩 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:
- 1: https://inworld.ai/models/xai-grok-3-mini-fast-beta
- 2: https://docs.x.ai/docs/models/grok-3-mini-fast
- 3: https://docs.x.ai/docs/models?cluster=us-west-1
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:
- Pricing – input should be
0.30(not0.33) and output should be0.50(not2.20011) - Context window – should be
131072tokens (not8192) - Output limit – should be
131072tokens (not8192)
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.
| { | ||
| "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" |
There was a problem hiding this comment.
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.
| { | |
| "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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
🧩 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:
- 1: https://www.electronjs.org/docs/api/net/
- 2: https://www.electronjs.org/docs/api/net
- 3: https://github.com/electron/electron/blob/1ad832a4/lib/browser/api/net-fetch.ts
- 4: feat: implement net.fetch electron/electron#36733
- 5: https://github.com/electron/electron/releases/tag/v40.0.0
- 6: http://electron.atom.io/blog/electron-40-0
- 7: https://github.com/electron/electron/blob/main/lib/browser/api/net.ts
🏁 Script executed:
# Check the imports and full context of the FilePresenter.ts file
head -50 src/main/presenter/filePresenter/FilePresenter.tsRepository: 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 2Repository: 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 -50Repository: 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.tsRepository: 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 5Repository: 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 1Repository: 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 -100Repository: 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.
| 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.
| 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' | ||
| }) |
There was a problem hiding this comment.
🧩 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...", |
There was a problem hiding this comment.
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.
| "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.
| mimeType: z.string().optional(), | ||
| suggestedName: z.string().optional() |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
New Features
Chores