Skip to content

Fix UI stuck on Stop when request is slow or proxy disconnects#995

Open
PeterDaveHello wants to merge 1 commit into
ChatGPTBox-dev:masterfrom
PeterDaveHello:fix/stop-abort-ui-unstick
Open

Fix UI stuck on Stop when request is slow or proxy disconnects#995
PeterDaveHello wants to merge 1 commit into
ChatGPTBox-dev:masterfrom
PeterDaveHello:fix/stop-abort-ui-unstick

Conversation

@PeterDaveHello

@PeterDaveHello PeterDaveHello commented Jul 8, 2026

Copy link
Copy Markdown
Member
  • background: unlock the conversation UI (post {done:true}) when the ChatGPT/Claude/etc. proxy tab disconnects mid-generation; use a per-port _generating flag so only in-flight requests are affected.
  • fetch-sse: treat abort as onEnd(true) and swallow onEnd errors so a closed port on disconnect is not reported as a failure.
  • openai-compatible-core: on abort, persist the streamed partial answer into session history (pushRecord + post {session}) without re-sending the done signal.
  • moonshot-web/claude client: on abort, reject the stream promise so cancellation is treated as cancellation, not a successful completion.

Summary by CodeRabbit

  • Bug Fixes
    • Improved streaming completion handling across providers so { done: true } is only sent on normal completion, with consistent abort behavior and guaranteed cleanup.
    • Reduced “stuck generating” cases by clearing in-flight proxy generation state on disconnect and improving reconnection notifications.
    • Hardened port error handling to safely ignore disconnected/closed ports and reliably report user-facing errors.
    • Improved non-foreground conversation reset flow by replacing/reconnecting the port more reliably.
  • Tests
    • Added unit tests covering abort behavior, listener/controller cleanup, port-error edge cases, and callback error propagation.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR makes SSE termination abort-aware across shared utilities and API clients, updates disconnected-port error handling, and tracks proxy/session port lifecycle state in the background and conversation card flows.

Changes

Abort-aware stream termination and cleanup

Layer / File(s) Summary
fetchSSE abort control flow
src/utils/fetch-sse.mjs, tests/unit/utils/fetch-sse.test.mjs
fetchSSE now distinguishes abort errors from other failures in fetch, streaming, and parser callbacks, and the tests cover onEnd, onStart, and onMessage error propagation.
API clients honor aborted streams
src/services/apis/azure-openai-api.mjs, src/services/apis/chatgpt-web.mjs, src/services/apis/claude-api.mjs, src/services/apis/waylaidwanderer-api.mjs, src/services/apis/openai-compatible-core.mjs, src/services/apis/moonshot-web.mjs, src/services/clients/claude/index.mjs, tests/unit/services/apis/openai-api-compat.test.mjs
SSE handlers now accept aborted state, suppress normal completion on abort, preserve or reject in-flight work as appropriate, and clean up listeners/controllers in finally blocks. The compat tests cover partial-stream and pre-chunk abort paths.

Disconnected port error handling

Layer / File(s) Summary
Disconnected port error handling
src/services/wrappers.mjs, tests/unit/services/handle-port-error.test.mjs
handlePortError now ignores disconnected-port style failures with a warning, posts mapped errors through a guarded helper, and tests cover disconnected and closed-port cases.

Proxy and conversation port lifecycle

Layer / File(s) Summary
Proxy and conversation port lifecycle
src/background/index.mjs, src/components/ConversationCard/index.jsx
The background tracks ChatGPT Web proxy generation state across session posts and disconnects, and the conversation card reconnects its port during delete/reset flows while refreshing readiness state.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: Review effort 4/5

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing the UI getting stuck on Stop during slow requests or proxy disconnects.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request improves stream abort handling across various API clients and the fetchSSE utility by passing an aborted flag to the onEnd callback and tracking the port's generation state. Feedback focuses on ensuring robust error handling by wrapping port.postMessage calls in try...catch blocks to prevent crashes when ports disconnect, maintaining consistency across all API clients by persisting partial answers on abort, using optional chaining for safer message property access, and explicitly passing false to onEnd upon normal completion.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +147 to +151
if (aborted) {
if (answer) {
pushRecord(session, question, answer)
port.postMessage({ session })
}

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.

high

When the stream is aborted due to a port disconnection (e.g., when the popup or sidebar is closed), calling port.postMessage will throw an error because the port is already closed. We should wrap this call in a try...catch block to prevent unhandled exceptions.

        if (aborted) {
          if (answer) {
            pushRecord(session, question, answer)
            try {
              port.postMessage({ session })
            } catch (e) {
              console.warn('[openai-compatible-core] Failed to post session on abort:', e)
            }
          }
        }

Comment on lines +71 to 80
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
}
} finally {
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
}
},

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.

medium

To maintain consistency with openai-compatible-core.mjs, we should also persist the streamed partial answer into the session history when the request is aborted. We should wrap the port.postMessage call in a try...catch block in case the port is disconnected.

      async onEnd(aborted) {
        try {
          if (!aborted) {
            port.postMessage({ done: true })
          } else if (answer) {
            pushRecord(session, question, answer)
            try {
              port.postMessage({ session })
            } catch (e) {
              console.warn('[azure-openai-api] Failed to post session on abort:', e)
            }
          }
        } finally {
          port.onMessage.removeListener(messageListener)
          port.onDisconnect.removeListener(disconnectListener)
        }
      },

Comment on lines +81 to 90
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
}
} finally {
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
}
},

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.

medium

To maintain consistency with openai-compatible-core.mjs, we should also persist the streamed partial answer into the session history when the request is aborted. We should wrap the port.postMessage call in a try...catch block in case the port is disconnected.

Suggested change
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
}
} finally {
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
}
},
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
} else if (answer) {
pushRecord(session, question, answer)
try {
port.postMessage({ session })
} catch (e) {
console.warn('[claude-api] Failed to post session on abort:', e)
}
}
} finally {
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
}
},

Comment on lines +66 to 75
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
}
} finally {
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
}
},

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.

medium

To maintain consistency with openai-compatible-core.mjs, we should also persist the streamed partial answer into the session history when the request is aborted. We should wrap the port.postMessage call in a try...catch block in case the port is disconnected.

Suggested change
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
}
} finally {
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
}
},
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
} else if (answer) {
pushRecord(session, question, answer)
try {
port.postMessage({ session })
} catch (e) {
console.warn('[waylaidwanderer-api] Failed to post session on abort:', e)
}
}
} finally {
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
}
},

Comment thread src/services/apis/chatgpt-web.mjs Outdated
Comment on lines 380 to 388
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
}
} finally {
cleanController()
}
},

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.

medium

To maintain consistency with openai-compatible-core.mjs, we should also persist the streamed partial answer into the session history when the request is aborted. We should wrap the port.postMessage call in a try...catch block in case the port is disconnected.

      async onEnd(aborted) {
        try {
          if (!aborted) {
            port.postMessage({ done: true })
          } else if (answer) {
            pushRecord(session, question, answer)
            try {
              port.postMessage({ session })
            } catch (e) {
              console.warn('[chatgpt-web] Failed to post session on abort:', e)
            }
          }
        } finally {
          cleanController()
        }
      },

Comment thread src/background/index.mjs
console.debug('[background] Main port closed; skipping proxy message.')
return
}
if (msg.done || msg.error) port._generating = false

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.

medium

Use optional chaining msg?.done and msg?.error to prevent potential TypeError if msg is null or undefined, adhering to defensive programming guidelines.

Suggested change
if (msg.done || msg.error) port._generating = false
if (msg?.done || msg?.error) port._generating = false

Comment thread src/utils/fetch-sse.mjs Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Unstick Stop UI on abort/proxy disconnect during streaming

🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Unblocks UI by emitting done when proxy tab disconnects mid-stream.
• Propagates abort through fetchSSE so adapters skip done and always clean up.
• Persists partial streamed answers on abort for OpenAI-compatible sessions.
Diagram

graph TD
  ui["Conversation UI"] --> bg["Background bridge"] --> adapters["API adapters"] --> sse["fetchSSE"] --> providers{"LLM provider"}
  providers --> adapters --> bg --> ui
  bg --> proxy["Proxy tab"] --> bg
  proxy -. "disconnect" .-> bg
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Request-scoped generation IDs (vs per-port _generating flag)
  • ➕ Handles overlapping/concurrent generations per port without false done/unlock
  • ➕ Makes lifecycle transitions explicit and debuggable (id in every message)
  • ➖ More invasive protocol change (UI/background/proxy must all propagate IDs)
  • ➖ Higher surface area for regressions than the current minimal fix
2. Explicit aborted/cancelled message to UI (not done:true)
  • ➕ UI can differentiate completion vs cancellation and present clearer state/telemetry
  • ➕ Avoids overloading done:true semantics
  • ➖ Requires UI state machine changes and backwards compatibility handling
  • ➖ May complicate existing adapter behavior that already keys off done:true

Recommendation: Keep the PR’s approach: it is a minimal, targeted fix that restores the UI’s ability to exit the “Stop” state on disconnect/abort while keeping existing done-based UI wiring intact. Consider a follow-up to introduce request-scoped IDs or an explicit aborted signal if concurrent generations or richer UX around cancellation become requirements.

Files changed (9) +121 / -39

Bug fix (9) +121 / -39
index.mjsTrack in-flight generation per port and unlock UI on proxy disconnect +13/-0

Track in-flight generation per port and unlock UI on proxy disconnect

• Introduces a per-port '_generating' flag set when a session is posted to the proxy and cleared on 'done'/'error'. When the proxy tab disconnects mid-generation, the background now posts '{done:true}' (best-effort) to unblock the UI only for in-flight requests.

src/background/index.mjs

azure-openai-api.mjsAvoid posting done on abort; always remove listeners +9/-4

Avoid posting done on abort; always remove listeners

• Updates 'onEnd' to accept an 'aborted' flag and only emits '{done:true}' on normal completion. Listener cleanup is moved into a 'finally' block so it runs regardless of abort or errors.

src/services/apis/azure-openai-api.mjs

chatgpt-web.mjsTreat abort as non-completion and ensure controller cleanup +8/-3

Treat abort as non-completion and ensure controller cleanup

• Changes SSE 'onEnd' to take an 'aborted' parameter and suppress '{done:true}' when aborted. Ensures abort controller cleanup always runs via 'finally'.

src/services/apis/chatgpt-web.mjs

claude-api.mjsSuppress done on abort and guarantee listener cleanup +9/-4

Suppress done on abort and guarantee listener cleanup

• Adds 'aborted' parameter to 'onEnd' and only posts '{done:true}' for non-aborted endings. Moves listener removal into 'finally' for consistent cleanup.

src/services/apis/claude-api.mjs

moonshot-web.mjsReject stream promise on abort instead of resolving +5/-1

Reject stream promise on abort instead of resolving

• Updates 'Conversation' streaming 'onEnd' to reject with an 'AbortError' when aborted, so cancellation is treated as cancellation rather than a successful completion.

src/services/apis/moonshot-web.mjs

openai-compatible-core.mjsPersist partial answer to session history on abort without re-sending done +9/-2

Persist partial answer to session history on abort without re-sending done

• Extends 'onEnd' with an 'aborted' flag. On abort, it saves any accumulated partial answer into session history and posts the updated '{session}' without emitting a completion signal; on normal end it retains existing 'finish()' behavior.

src/services/apis/openai-compatible-core.mjs

waylaidwanderer-api.mjsSkip done on abort; always remove listeners +9/-4

Skip done on abort; always remove listeners

• Updates 'onEnd' signature to accept 'aborted' and suppresses '{done:true}' when aborted. Listener cleanup now happens in 'finally' to avoid leaks on abort paths.

src/services/apis/waylaidwanderer-api.mjs

index.mjsReject Claude web client stream promise on abort +5/-1

Reject Claude web client stream promise on abort

• Changes 'Conversation' streaming 'onEnd' to reject with an 'AbortError' when aborted. This prevents aborts from being interpreted as successful completions by callers.

src/services/clients/claude/index.mjs

fetch-sse.mjsPropagate abort to onEnd(true) and swallow onEnd abort errors +54/-20

Propagate abort to onEnd(true) and swallow onEnd abort errors

• Adds abort detection and treats aborts during 'fetch()' or 'reader.read()' as 'onEnd(true)' rather than an error. Wraps 'onEnd' in try/catch to avoid reporting closed-port/disconnect situations as failures while still calling 'onError' for non-abort exceptions.

src/utils/fetch-sse.mjs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a UX issue where the conversation UI can remain stuck in a “Stop”/generating state when a streaming request is interrupted (slow request, abort, or proxy tab disconnect). It does so by treating aborts as a controlled end-of-stream across SSE handling, API clients, and the background proxy bridge so the UI reliably unlocks.

Changes:

  • Update fetchSSE to treat AbortError as onEnd(true) and to swallow onEnd errors so disconnect-related cleanup doesn’t surface as failures.
  • Update multiple API clients to avoid posting { done: true } on abort, and in some cases persist partial answers / reject stream promises on abort.
  • Add a per-port _generating flag in the background proxy bridge to post { done: true } if the proxy disconnects mid-generation, unlocking the UI.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/utils/fetch-sse.mjs Treat abort as a normal end (onEnd(true)) and swallow onEnd errors to avoid false failures on disconnect.
src/services/clients/claude/index.mjs Reject the stream promise on abort so cancellation is handled as cancellation.
src/services/apis/waylaidwanderer-api.mjs Avoid posting {done:true} on abort; ensure listeners are removed in finally.
src/services/apis/openai-compatible-core.mjs On abort, persist streamed partial answer into session history without re-sending done.
src/services/apis/moonshot-web.mjs Reject the stream promise on abort so cancellation is handled as cancellation.
src/services/apis/claude-api.mjs Avoid posting {done:true} on abort; ensure listeners are removed in finally.
src/services/apis/chatgpt-web.mjs Avoid posting {done:true} on abort; ensure controller cleanup runs in finally.
src/services/apis/azure-openai-api.mjs Avoid posting {done:true} on abort; ensure listeners are removed in finally.
src/background/index.mjs Track in-flight generation via _generating and post {done:true} on proxy disconnect to unlock UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +145 to +149
async onEnd(aborted = false) {
if (!finished) {
finish()
if (aborted) {
if (answer) {
pushRecord(session, question, answer)

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
src/utils/fetch-sse.mjs (1)

3-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid abort-aware refactor; minor duplication to consider.

The isAbortErroronEnd(true) / else onError(err) pattern is repeated verbatim in the fetch try/catch (lines 12-25) and the reader-loop try/catch (lines 60-71). Extracting a small helper would reduce duplication and ensure future edits to abort handling stay in sync.

♻️ Optional extraction
+async function handleTermination(err, onEnd, onError) {
+  if (isAbortError(err)) {
+    try {
+      await onEnd(true)
+    } catch (e) {
+      console.warn('[fetch-sse] onEnd threw during abort:', e)
+    }
+    return
+  }
+  await onError(err)
+}

Then replace both catch blocks with return await handleTermination(err, onEnd, onError).

Also applies to: 38-76

🤖 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/utils/fetch-sse.mjs` around lines 3 - 25, The abort-handling logic is
duplicated in fetchSSE’s fetch and reader-loop catch paths, so extract it into a
shared helper to keep behavior in sync. Add a small termination helper near
isAbortError that takes the error plus onEnd/onError, handles AbortError by
calling onEnd(true) with the same warning guard, and otherwise forwards to
onError(err). Then replace both catch blocks in fetchSSE and the reader loop
with that helper so the abort-aware flow is centralized.
🤖 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.

Nitpick comments:
In `@src/utils/fetch-sse.mjs`:
- Around line 3-25: The abort-handling logic is duplicated in fetchSSE’s fetch
and reader-loop catch paths, so extract it into a shared helper to keep behavior
in sync. Add a small termination helper near isAbortError that takes the error
plus onEnd/onError, handles AbortError by calling onEnd(true) with the same
warning guard, and otherwise forwards to onError(err). Then replace both catch
blocks in fetchSSE and the reader loop with that helper so the abort-aware flow
is centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 03267618-0a1d-49a2-acf9-34e73abcc2e1

📥 Commits

Reviewing files that changed from the base of the PR and between ff3d5f7 and a78600b.

📒 Files selected for processing (9)
  • src/background/index.mjs
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/utils/fetch-sse.mjs

@qodo-code-review

qodo-code-review Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Done lacks session update 🐞 Bug ≡ Correctness
Description
When the proxy tab disconnects mid-generation, setPortProxy() posts { done: true } without a `{
session }`, so the UI marks the answer complete and becomes ready while
session.conversationRecords remains stale. As a result, saving/exporting or persisting the
conversation can omit the partial answer the user saw.
Code

src/background/index.mjs[R160-168]

+      if (port._generating) {
+        port._generating = false
+        if (!port._isClosed) {
+          try {
+            port.postMessage({ done: true })
+          } catch (e) {
+            console.warn('[background] Error posting done on proxy disconnect:', e)
+          }
+        }
Evidence
The background unstick path posts a terminal done message with no session; the UI only persists
session state when msg.session exists, yet features like Save Conversation and remount rehydration
read from session.conversationRecords, so the partial streamed answer can be lost from persisted
history.

src/background/index.mjs[154-169]
src/components/ConversationCard/index.jsx[175-186]
src/components/ConversationCard/index.jsx[97-114]
src/components/ConversationCard/index.jsx[562-574]
src/pages/IndependentPanel/App.jsx[169-179]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
On proxy disconnect during an in-flight generation, the background sends `{ done: true }` to the UI to unstick it, but does not provide an updated `{ session }`. The UI only updates `session` when `msg.session` is present, so the partial assistant answer that was streamed to the UI is not persisted into `session.conversationRecords`, which is later used for “Save Conversation” and for rehydration/persistence.

### Issue Context
- Background disconnect path sends done-only.
- UI marks done/ready on `msg.done`, but does not update `session` unless `msg.session` exists.
- Save/export and initial rehydration are based on `session.conversationRecords`.

### Fix Focus Areas
- src/background/index.mjs[154-169]
- src/components/ConversationCard/index.jsx[97-114]
- src/components/ConversationCard/index.jsx[175-186]
- src/components/ConversationCard/index.jsx[562-574]
- src/pages/IndependentPanel/App.jsx[169-179]

### Suggested fix
Implement a way to persist the currently displayed partial answer into the session when a proxy disconnect triggers a done-only terminal signal.

Two viable approaches:
1) **UI-side (recommended):** In `ConversationCard.portMessageListener`, when receiving `msg.done === true` **and** `msg.session` is missing, derive the last Q/A from current UI state (or track latest answer text in a ref) and `pushRecord`-equivalent into a cloned `session`, then `setSession(updatedSession)` before setting ready.
  - Guard against double-persist if a subsequent `{session}` arrives.
2) **Protocol change:** Add a marker flag from background, e.g. `{ done: true, proxyDisconnected: true }`, so the UI can distinguish this path from other done-only signals (e.g., stop) and apply special persistence logic only for proxy disconnects.

Either approach should ensure `session.conversationRecords` reflects the partial answer that remains visible in the UI, so Save/rehydration/persistence does not drop it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Duplicate port reconnect ✓ Resolved 🐞 Bug ☼ Reliability
Description
ConversationCard's Clear Conversation handler calls port.disconnect() and immediately creates a new
port, but the existing port.onDisconnect handler also reconnects. This can race and create an extra
redundant runtime Port, causing transient duplicate connections and potential listener/state churn.
Code

src/components/ConversationCard/index.jsx[R496-499]

+              if (!useForegroundFetch) {
+                port.disconnect()
+                setPort(Browser.runtime.connect())
+              }
Evidence
The component registers a disconnect listener that reconnects the port on any disconnect; the Clear
Conversation handler now also disconnects and reconnects explicitly, creating a duplicate reconnect
path.

src/components/ConversationCard/index.jsx[290-322]
src/components/ConversationCard/index.jsx[491-499]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ConversationCard` reconnects twice when clearing a conversation:
1) the explicit `setPort(Browser.runtime.connect())` in the Clear Conversation handler, and
2) the existing `port.onDisconnect` listener (`portListener`) which also calls `setPort(Browser.runtime.connect())`.

This can lead to two new ports being created for a single user action, with the “first” new port potentially being replaced in state by the second, leaving an unnecessary open connection and racy listener re-binding.

## Issue Context
- Clear Conversation now does `port.disconnect()` + `setPort(connect())`.
- Separately, there is a `port.onDisconnect` listener that always reconnects and sets ready state.

## Fix Focus Areas
- src/components/ConversationCard/index.jsx[290-322]
- src/components/ConversationCard/index.jsx[494-499]

### Suggested fix
Pick exactly one reconnection mechanism:
- **Option A (simplest):** remove the explicit `setPort(Browser.runtime.connect())` in the Clear Conversation handler and rely on the existing `onDisconnect` listener to reconnect.
- **Option B:** keep the explicit reconnect, but guard the `onDisconnect` handler with a ref/flag (e.g., `isIntentionalDisconnectRef`) so intentional disconnects don’t trigger an additional reconnect.

Ensure the chosen approach still sets `isReady` appropriately after clearing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Abort detection inconsistent 🐞 Bug ☼ Reliability
Description
fetchSSE()’s new isAbortError() only checks err.name === 'AbortError', which is inconsistent with
other repo abort handling that also matches 'aborted' messages; this increases the risk that some
cancellations are treated as errors (onError) instead of the new abort end-path (onEnd(true)).
Code

src/utils/fetch-sse.mjs[R3-7]

+function isAbortError(err) {
+  if (!err || typeof err !== 'object') return false
+  const name = typeof err.name === 'string' ? err.name : ''
+  return name === 'AbortError'
+}
Evidence
The repo already treats aborts as non-fatal using message-based detection (not just name), so
fetch-sse’s stricter check is a behavioral inconsistency that can prevent aborts from being handled
via the new onEnd(true) path.

src/utils/fetch-sse.mjs[3-25]
src/services/wrappers.mjs[62-71]
src/services/clients/bing/index.mjs[429-447]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`src/utils/fetch-sse.mjs` introduces `isAbortError()` that only checks `err.name === 'AbortError'`. Elsewhere in the codebase, abort detection is broader (name OR message contains “aborted/aborterror”), indicating aborts are not always represented with the canonical DOMException name.

### Issue Context
If aborts are misclassified, `fetchSSE()` will route them to `onError()` rather than `onEnd(true)`, undermining the PR’s new “abort behaves like end” semantics.

### Fix Focus Areas
- src/utils/fetch-sse.mjs[3-7]

### Suggested fix
Align `fetch-sse` abort detection with the repo’s existing pattern:
- Also inspect `err.message` (lowercased) for substrings like `aborted` / `aborterror`.
- Keep the `AbortError` name check.
Optionally factor this into a shared util (to avoid divergent abort detection across files).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. onEnd failures hidden ✓ Resolved 🐞 Bug ☼ Reliability
Description
fetchSSE() now swallows exceptions thrown by onEnd() on the normal completion path, so failures to
post terminal signals (done/session) may no longer propagate into registerPortListener’s error
handling and can leave the UI without a done or error event.
Code

src/utils/fetch-sse.mjs[R72-76]

+  try {
+    await onEnd()
+  } catch (e) {
+    console.warn('[fetch-sse] onEnd threw:', e)
  }
-  await onEnd()
Evidence
The UI only transitions back to ready state on msg.done or msg.error; by swallowing onEnd()
exceptions, the system can lose the main mechanism (exception propagation) that triggers
handlePortError to emit msg.error when terminal signaling fails.

src/utils/fetch-sse.mjs[60-77]
src/services/wrappers.mjs[111-134]
src/components/ConversationCard/index.jsx[182-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`fetchSSE()` wraps the final `await onEnd()` in a try/catch and only logs. If `onEnd()` fails before emitting terminal UI signals (e.g., `{done:true}`), that failure will not reach the existing outer error-reporting path, risking a stuck UI with neither `done` nor `error`.

### Issue Context
Previously, `onEnd()` exceptions would reject `fetchSSE()` and propagate to `registerPortListener(...).catch(handlePortError)`, which would at least emit an error to the UI.

### Fix Focus Areas
- src/utils/fetch-sse.mjs[72-76]
- src/services/wrappers.mjs[111-134]

### Suggested fix
Restore propagation for non-abort `onEnd()` failures:
- Remove the try/catch around the final `await onEnd()` (or rethrow after logging).
- Keep the abort-path swallowing (the earlier `onEnd(true)` calls) if you still need to avoid noise on disconnect.
This preserves the PR goal (don’t treat disconnect abort as failure) while keeping real completion-path bugs visible and recoverable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Retry flag not cleared ✓ Resolved 🐞 Bug ≡ Correctness
Description
In generateAnswersWithOpenAICompatible(), the abort path persists partial history and posts
{session} without clearing session.isRetry; the UI only clears isRetry when it receives a session
together with msg.done, so an aborted retry can leave isRetry=true and later cause pushRecord() to
overwrite the wrong record.
Code

src/services/apis/openai-compatible-core.mjs[R145-152]

+    async onEnd(aborted = false) {
      if (!finished) {
-        finish()
+        if (aborted) {
+          if (answer) {
+            pushRecord(session, question, answer)
+            port.postMessage({ session })
+          }
+        } else {
Evidence
The abort path posts only {session} (no {done:true}), but the UI only clears isRetry when a
session arrives with done, and pushRecord() uses isRetry to decide whether to overwrite the
last entry—so a cancelled retry can leave the session in retry-mode and affect subsequent
persistence behavior.

src/services/apis/openai-compatible-core.mjs[109-158]
src/components/ConversationCard/index.jsx[174-187]
src/components/ConversationCard/index.jsx[334-353]
src/services/apis/shared.mjs[51-58]
src/services/init-session.mjs[76-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`generateAnswersWithOpenAICompatible()` posts an updated `session` on abort without resetting `session.isRetry`. The UI only forces `isRetry=false` when `{done:true}` accompanies a session payload, so after an aborted retry the session can remain in retry-mode and affect later history writes.

### Issue Context
- `ConversationCard` sets `isRetry=true` when the user clicks retry, and only clears it when `msg.done && msg.session`.
- `pushRecord()` uses `session.isRetry` to decide whether to overwrite the last record.

### Fix Focus Areas
- src/services/apis/openai-compatible-core.mjs[145-155]

### Suggested fix
In the `aborted` branch, keep the current overwrite behavior for the aborted retry **but** reset the retry flag before posting the session back:
- Call `pushRecord(session, question, answer)`
- Then set `session.isRetry = false` (or post a cloned session with `isRetry:false`)
- Then `port.postMessage({ session })`
This preserves the intent (persist partial answer) while preventing sticky retry state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 1aed904

Results up to commit a78600b


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. onEnd failures hidden ✓ Resolved 🐞 Bug ☼ Reliability
Description
fetchSSE() now swallows exceptions thrown by onEnd() on the normal completion path, so failures to
post terminal signals (done/session) may no longer propagate into registerPortListener’s error
handling and can leave the UI without a done or error event.
Code

src/utils/fetch-sse.mjs[R72-76]

+  try {
+    await onEnd()
+  } catch (e) {
+    console.warn('[fetch-sse] onEnd threw:', e)
  }
-  await onEnd()
Evidence
The UI only transitions back to ready state on msg.done or msg.error; by swallowing onEnd()
exceptions, the system can lose the main mechanism (exception propagation) that triggers
handlePortError to emit msg.error when terminal signaling fails.

src/utils/fetch-sse.mjs[60-77]
src/services/wrappers.mjs[111-134]
src/components/ConversationCard/index.jsx[182-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`fetchSSE()` wraps the final `await onEnd()` in a try/catch and only logs. If `onEnd()` fails before emitting terminal UI signals (e.g., `{done:true}`), that failure will not reach the existing outer error-reporting path, risking a stuck UI with neither `done` nor `error`.

### Issue Context
Previously, `onEnd()` exceptions would reject `fetchSSE()` and propagate to `registerPortListener(...).catch(handlePortError)`, which would at least emit an error to the UI.

### Fix Focus Areas
- src/utils/fetch-sse.mjs[72-76]
- src/services/wrappers.mjs[111-134]

### Suggested fix
Restore propagation for non-abort `onEnd()` failures:
- Remove the try/catch around the final `await onEnd()` (or rethrow after logging).
- Keep the abort-path swallowing (the earlier `onEnd(true)` calls) if you still need to avoid noise on disconnect.
This preserves the PR goal (don’t treat disconnect abort as failure) while keeping real completion-path bugs visible and recoverable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Abort detection inconsistent 🐞 Bug ☼ Reliability
Description
fetchSSE()’s new isAbortError() only checks err.name === 'AbortError', which is inconsistent with
other repo abort handling that also matches 'aborted' messages; this increases the risk that some
cancellations are treated as errors (onError) instead of the new abort end-path (onEnd(true)).
Code

src/utils/fetch-sse.mjs[R3-7]

+function isAbortError(err) {
+  if (!err || typeof err !== 'object') return false
+  const name = typeof err.name === 'string' ? err.name : ''
+  return name === 'AbortError'
+}
Evidence
The repo already treats aborts as non-fatal using message-based detection (not just name), so
fetch-sse’s stricter check is a behavioral inconsistency that can prevent aborts from being handled
via the new onEnd(true) path.

src/utils/fetch-sse.mjs[3-25]
src/services/wrappers.mjs[62-71]
src/services/clients/bing/index.mjs[429-447]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`src/utils/fetch-sse.mjs` introduces `isAbortError()` that only checks `err.name === 'AbortError'`. Elsewhere in the codebase, abort detection is broader (name OR message contains “aborted/aborterror”), indicating aborts are not always represented with the canonical DOMException name.

### Issue Context
If aborts are misclassified, `fetchSSE()` will route them to `onError()` rather than `onEnd(true)`, undermining the PR’s new “abort behaves like end” semantics.

### Fix Focus Areas
- src/utils/fetch-sse.mjs[3-7]

### Suggested fix
Align `fetch-sse` abort detection with the repo’s existing pattern:
- Also inspect `err.message` (lowercased) for substrings like `aborted` / `aborterror`.
- Keep the `AbortError` name check.
Optionally factor this into a shared util (to avoid divergent abort detection across files).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Retry flag not cleared ✓ Resolved 🐞 Bug ≡ Correctness
Description
In generateAnswersWithOpenAICompatible(), the abort path persists partial history and posts
{session} without clearing session.isRetry; the UI only clears isRetry when it receives a session
together with msg.done, so an aborted retry can leave isRetry=true and later cause pushRecord() to
overwrite the wrong record.
Code

src/services/apis/openai-compatible-core.mjs[R145-152]

+    async onEnd(aborted = false) {
      if (!finished) {
-        finish()
+        if (aborted) {
+          if (answer) {
+            pushRecord(session, question, answer)
+            port.postMessage({ session })
+          }
+        } else {
Evidence
The abort path posts only {session} (no {done:true}), but the UI only clears isRetry when a
session arrives with done, and pushRecord() uses isRetry to decide whether to overwrite the
last entry—so a cancelled retry can leave the session in retry-mode and affect subsequent
persistence behavior.

src/services/apis/openai-compatible-core.mjs[109-158]
src/components/ConversationCard/index.jsx[174-187]
src/components/ConversationCard/index.jsx[334-353]
src/services/apis/shared.mjs[51-58]
src/services/init-session.mjs[76-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`generateAnswersWithOpenAICompatible()` posts an updated `session` on abort without resetting `session.isRetry`. The UI only forces `isRetry=false` when `{done:true}` accompanies a session payload, so after an aborted retry the session can remain in retry-mode and affect later history writes.

### Issue Context
- `ConversationCard` sets `isRetry=true` when the user clicks retry, and only clears it when `msg.done && msg.session`.
- `pushRecord()` uses `session.isRetry` to decide whether to overwrite the last record.

### Fix Focus Areas
- src/services/apis/openai-compatible-core.mjs[145-155]

### Suggested fix
In the `aborted` branch, keep the current overwrite behavior for the aborted retry **but** reset the retry flag before posting the session back:
- Call `pushRecord(session, question, answer)`
- Then set `session.isRetry = false` (or post a cloned session with `isRetry:false`)
- Then `port.postMessage({ session })`
This preserves the intent (persist partial answer) while preventing sticky retry state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 2689a9a


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Duplicate port reconnect ✓ Resolved 🐞 Bug ☼ Reliability
Description
ConversationCard's Clear Conversation handler calls port.disconnect() and immediately creates a new
port, but the existing port.onDisconnect handler also reconnects. This can race and create an extra
redundant runtime Port, causing transient duplicate connections and potential listener/state churn.
Code

src/components/ConversationCard/index.jsx[R496-499]

+              if (!useForegroundFetch) {
+                port.disconnect()
+                setPort(Browser.runtime.connect())
+              }
Evidence
The component registers a disconnect listener that reconnects the port on any disconnect; the Clear
Conversation handler now also disconnects and reconnects explicitly, creating a duplicate reconnect
path.

src/components/ConversationCard/index.jsx[290-322]
src/components/ConversationCard/index.jsx[491-499]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ConversationCard` reconnects twice when clearing a conversation:
1) the explicit `setPort(Browser.runtime.connect())` in the Clear Conversation handler, and
2) the existing `port.onDisconnect` listener (`portListener`) which also calls `setPort(Browser.runtime.connect())`.

This can lead to two new ports being created for a single user action, with the “first” new port potentially being replaced in state by the second, leaving an unnecessary open connection and racy listener re-binding.

## Issue Context
- Clear Conversation now does `port.disconnect()` + `setPort(connect())`.
- Separately, there is a `port.onDisconnect` listener that always reconnects and sets ready state.

## Fix Focus Areas
- src/components/ConversationCard/index.jsx[290-322]
- src/components/ConversationCard/index.jsx[494-499]

### Suggested fix
Pick exactly one reconnection mechanism:
- **Option A (simplest):** remove the explicit `setPort(Browser.runtime.connect())` in the Clear Conversation handler and rely on the existing `onDisconnect` listener to reconnect.
- **Option B:** keep the explicit reconnect, but guard the `onDisconnect` handler with a ref/flag (e.g., `isIntentionalDisconnectRef`) so intentional disconnects don’t trigger an additional reconnect.

Ensure the chosen approach still sets `isReady` appropriately after clearing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 12ea56a


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Done lacks session update 🐞 Bug ≡ Correctness
Description
When the proxy tab disconnects mid-generation, setPortProxy() posts { done: true } without a `{
session }`, so the UI marks the answer complete and becomes ready while
session.conversationRecords remains stale. As a result, saving/exporting or persisting the
conversation can omit the partial answer the user saw.
Code

src/background/index.mjs[R160-168]

+      if (port._generating) {
+        port._generating = false
+        if (!port._isClosed) {
+          try {
+            port.postMessage({ done: true })
+          } catch (e) {
+            console.warn('[background] Error posting done on proxy disconnect:', e)
+          }
+        }
Evidence
The background unstick path posts a terminal done message with no session; the UI only persists
session state when msg.session exists, yet features like Save Conversation and remount rehydration
read from session.conversationRecords, so the partial streamed answer can be lost from persisted
history.

src/background/index.mjs[154-169]
src/components/ConversationCard/index.jsx[175-186]
src/components/ConversationCard/index.jsx[97-114]
src/components/ConversationCard/index.jsx[562-574]
src/pages/IndependentPanel/App.jsx[169-179]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
On proxy disconnect during an in-flight generation, the background sends `{ done: true }` to the UI to unstick it, but does not provide an updated `{ session }`. The UI only updates `session` when `msg.session` is present, so the partial assistant answer that was streamed to the UI is not persisted into `session.conversationRecords`, which is later used for “Save Conversation” and for rehydration/persistence.

### Issue Context
- Background disconnect path sends done-only.
- UI marks done/ready on `msg.done`, but does not update `session` unless `msg.session` exists.
- Save/export and initial rehydration are based on `session.conversationRecords`.

### Fix Focus Areas
- src/background/index.mjs[154-169]
- src/components/ConversationCard/index.jsx[97-114]
- src/components/ConversationCard/index.jsx[175-186]
- src/components/ConversationCard/index.jsx[562-574]
- src/pages/IndependentPanel/App.jsx[169-179]

### Suggested fix
Implement a way to persist the currently displayed partial answer into the session when a proxy disconnect triggers a done-only terminal signal.

Two viable approaches:
1) **UI-side (recommended):** In `ConversationCard.portMessageListener`, when receiving `msg.done === true` **and** `msg.session` is missing, derive the last Q/A from current UI state (or track latest answer text in a ref) and `pushRecord`-equivalent into a cloned `session`, then `setSession(updatedSession)` before setting ready.
  - Guard against double-persist if a subsequent `{session}` arrives.
2) **Protocol change:** Add a marker flag from background, e.g. `{ done: true, proxyDisconnected: true }`, so the UI can distinguish this path from other done-only signals (e.g., stop) and apply special persistence logic only for proxy disconnects.

Either approach should ensure `session.conversationRecords` reflects the partial answer that remains visible in the UI, so Save/rehydration/persistence does not drop it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread src/services/apis/openai-compatible-core.mjs Outdated
Comment thread src/utils/fetch-sse.mjs
Comment on lines +3 to +7
function isAbortError(err) {
if (!err || typeof err !== 'object') return false
const name = typeof err.name === 'string' ? err.name : ''
return name === 'AbortError'
}

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.

Remediation recommended

2. Abort detection inconsistent 🐞 Bug ☼ Reliability

fetchSSE()’s new isAbortError() only checks err.name === 'AbortError', which is inconsistent with
other repo abort handling that also matches 'aborted' messages; this increases the risk that some
cancellations are treated as errors (onError) instead of the new abort end-path (onEnd(true)).
Agent Prompt
### Issue description
`src/utils/fetch-sse.mjs` introduces `isAbortError()` that only checks `err.name === 'AbortError'`. Elsewhere in the codebase, abort detection is broader (name OR message contains “aborted/aborterror”), indicating aborts are not always represented with the canonical DOMException name.

### Issue Context
If aborts are misclassified, `fetchSSE()` will route them to `onError()` rather than `onEnd(true)`, undermining the PR’s new “abort behaves like end” semantics.

### Fix Focus Areas
- src/utils/fetch-sse.mjs[3-7]

### Suggested fix
Align `fetch-sse` abort detection with the repo’s existing pattern:
- Also inspect `err.message` (lowercased) for substrings like `aborted` / `aborterror`.
- Keep the `AbortError` name check.
Optionally factor this into a shared util (to avoid divergent abort detection across files).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/utils/fetch-sse.mjs Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request improves error handling and state management during stream aborts and port disconnections across various API services and the background script. It introduces tracking for active generation states, gracefully handles disconnected ports, and updates fetchSSE to trigger abort cleanups. The review feedback suggests using optional chaining when checking incoming port messages to prevent potential TypeError crashes, and recommends making the isAbortError check in fetch-sse.mjs more robust to match the implementation in wrappers.mjs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/background/index.mjs
console.debug('[background] Main port closed; skipping proxy message.')
return
}
if (msg.done || msg.error) port._generating = false

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.

high

If msg is null or undefined, accessing msg.done or msg.error will throw a TypeError and crash the port's message listener. Since msg can potentially be null or non-object in extension messaging, it is safer to use optional chaining here.

Suggested change
if (msg.done || msg.error) port._generating = false
if (msg?.done || msg?.error) port._generating = false

Comment thread src/utils/fetch-sse.mjs
Comment on lines +3 to +7
function isAbortError(err) {
if (!err || typeof err !== 'object') return false
const name = typeof err.name === 'string' ? err.name : ''
return name === 'AbortError'
}

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.

medium

This implementation of isAbortError is less robust than the one defined in src/services/wrappers.mjs. In some environments or under certain network conditions, abort errors might be wrapped or thrown as generic errors with messages containing 'aborted' rather than having the exact name 'AbortError'. If not recognized as an abort, it will trigger onError and show a disruptive error message to the user. Consider aligning this with the more robust version.

Suggested change
function isAbortError(err) {
if (!err || typeof err !== 'object') return false
const name = typeof err.name === 'string' ? err.name : ''
return name === 'AbortError'
}
function isAbortError(err) {
if (!err || typeof err !== 'object') return false
const name = typeof err.name === 'string' ? err.name : ''
const message = typeof err.message === 'string' ? err.message.toLowerCase() : ''
return name === 'AbortError' || message.includes('aborted') || message.includes('aborterror')
}

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
src/services/wrappers.mjs (1)

86-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the catch-block parameter to avoid shadowing postError.

The catch parameter postError shadows the outer postError arrow function. While not a bug today (the catch block only logs), a future retry attempt inside the catch would silently fail since postError would resolve to the caught error object, not the function.

♻️ Proposed refactor
   const postError = (error) => {
     try {
       port.postMessage({ error })
-    } catch (postError) {
-      console.warn('[handlePortError] Failed to post error:', postError)
+    } catch (postErr) {
+      console.warn('[handlePortError] Failed to post error:', postErr)
     }
   }
🤖 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/services/wrappers.mjs` around lines 86 - 92, The catch parameter inside
postError currently shadows the outer postError function, which can cause
confusion and future retry attempts to reference the wrong value. Rename the
catch-block parameter in the postError helper to a distinct name, and keep the
logic in the port.postMessage error handling unchanged so the outer postError
symbol remains callable.
🤖 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.

Nitpick comments:
In `@src/services/wrappers.mjs`:
- Around line 86-92: The catch parameter inside postError currently shadows the
outer postError function, which can cause confusion and future retry attempts to
reference the wrong value. Rename the catch-block parameter in the postError
helper to a distinct name, and keep the logic in the port.postMessage error
handling unchanged so the outer postError symbol remains callable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 744fd4af-3c65-45c7-90d6-acd8dad9a719

📥 Commits

Reviewing files that changed from the base of the PR and between a78600b and 24b663f.

📒 Files selected for processing (13)
  • src/background/index.mjs
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/background/index.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24b663f398

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +149 to +150
const shouldPostSession = Boolean(answer) || session.isRetry
if (shouldPostSession) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid reposting stale sessions after Stop/Clear

When an OpenAI-compatible stream is aborted after producing any partial text, this branch always posts the old session back to the UI. The same stop message is used by controls that immediately replace the session, such as Clear Conversation in ConversationCard; because postMessage({ stop: true }) resolves before the background abort finishes, the late { session } here can restore the just-cleared/in-flight conversation and make the next prompt include stale history even though the UI was reset.

Useful? React with 👍 / 👎.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 24b663f

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request improves abort and error handling across various API clients and SSE fetching, ensuring proper cleanup of listeners and preventing crashes when ports are disconnected. Key changes include tracking port generation state, handling aborted states in onEnd callbacks, and safely catching disconnected port errors. The review feedback highlights a potential TypeError in the background script if a message is null or undefined, and suggests optimizing a React useEffect hook by using a useRef to prevent unnecessary listener churn during stream updates.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/background/index.mjs
console.debug('[background] Main port closed; skipping proxy message.')
return
}
if (msg.done || msg.error) port._generating = false

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.

high

If msg is null or undefined, accessing msg.done or msg.error will throw a TypeError and crash the background script's message listener. Since msg can potentially be null/undefined in extension message passing, you should use optional chaining here to prevent runtime crashes.

Suggested change
if (msg.done || msg.error) port._generating = false
if (msg?.done || msg?.error) port._generating = false

}
}
}, [conversationItemData])
}, [port, conversationItemData])

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.

medium

Including conversationItemData in the dependency array of this useEffect causes the message listener to be removed and re-registered on every single stream chunk (since conversationItemData updates continuously during generation).\n\nTo optimize performance and avoid unnecessary listener churn, you can store the latest conversationItemData in a useRef (e.g., conversationItemDataRef.current = conversationItemData on every render) and reference conversationItemDataRef.current inside portMessageListener. This allows you to safely remove conversationItemData from the useEffect dependency array so the listener is only registered once per port.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment on lines 495 to +499
await postMessage({ stop: true })
if (!useForegroundFetch) {
port.disconnect()
setPort(Browser.runtime.connect())
}
@kilo-code-bot

kilo-code-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (14 files)
  • src/background/index.mjs
  • src/components/ConversationCard/index.jsx
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs
Previous Review Summaries (2 snapshots, latest commit 12ea56a)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 12ea56a)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (14 files)
  • src/background/index.mjs
  • src/components/ConversationCard/index.jsx
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs

Previous review (commit 2689a9a)

Status: No New Issues Found | Recommendation: Merge

Files Reviewed (14 files)
  • src/background/index.mjs
  • src/components/ConversationCard/index.jsx
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs

Reviewed by step-3.7-flash · Input: 343.8K · Output: 40K · Cached: 1.2M

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
src/components/ConversationCard/index.jsx (1)

496-499: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding port to the onUpdate effect dependency array.

The onUpdate effect at lines 119–121 calls props.onUpdate(port, session, conversationItemData) but only depends on [session, conversationItemData]. This PR introduces a new code path (lines 496–499) where port changes via setPort(Browser.runtime.connect()). In the current clear-conversation flow the parent still receives the new port because setConversationItemData([]) (line 506) and setSession(newSession) (line 513) also fire, triggering the effect. However, this is fragile — if a future change removes either of those state updates, the parent would retain a stale, disconnected port reference in currentPort.current.

Adding port to the dependency array is a one-line fix that makes the parent-child port contract robust regardless of what else changes in the flow.

♻️ Proposed fix for `onUpdate` effect deps
   useEffect(() => {
     if (props.onUpdate) props.onUpdate(port, session, conversationItemData)
-  }, [session, conversationItemData])
+  }, [port, session, conversationItemData])
🤖 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/components/ConversationCard/index.jsx` around lines 496 - 499, The
onUpdate effect in ConversationCard is using a stale port because it only
depends on session and conversationItemData even though the clear-conversation
flow can replace port via setPort(Browser.runtime.connect()). Update the
dependency array for the effect that calls props.onUpdate(port, session,
conversationItemData) to include port so the parent always receives the current
connection reference. Use the onUpdate effect in ConversationCard and the port
state update path around Browser.runtime.connect() as the key symbols to locate
the fix.
🤖 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.

Nitpick comments:
In `@src/components/ConversationCard/index.jsx`:
- Around line 496-499: The onUpdate effect in ConversationCard is using a stale
port because it only depends on session and conversationItemData even though the
clear-conversation flow can replace port via setPort(Browser.runtime.connect()).
Update the dependency array for the effect that calls props.onUpdate(port,
session, conversationItemData) to include port so the parent always receives the
current connection reference. Use the onUpdate effect in ConversationCard and
the port state update path around Browser.runtime.connect() as the key symbols
to locate the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24c7a4bf-32b3-4a1d-8a36-9a663af8d596

📥 Commits

Reviewing files that changed from the base of the PR and between 24b663f and 2689a9a.

📒 Files selected for processing (14)
  • src/background/index.mjs
  • src/components/ConversationCard/index.jsx
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs
✅ Files skipped from review due to trivial changes (1)
  • src/services/apis/moonshot-web.mjs
🚧 Files skipped from review as they are similar to previous changes (12)
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/background/index.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs

Comment thread src/components/ConversationCard/index.jsx
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 2689a9a

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request improves error handling, connection cleanup, and abort behavior across background scripts, API clients, and SSE fetching utilities. It introduces state tracking for active generation, safely handles disconnected ports, and ensures proper cleanup of listeners when streams are aborted. The review feedback highlights a performance issue in ConversationCard where useEffect re-runs on every streamed token, and suggests defensive programming improvements such as optional chaining for msg and a default fallback for onEnd in fetchSSE.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

}
}
}, [conversationItemData])
}, [port, conversationItemData])

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.

high

Including conversationItemData in the dependency array of this useEffect causes the message listener to be removed and re-added on every single streamed token, which is highly inefficient and can cause performance issues or race conditions.\n\nTo optimize this, you can remove conversationItemData from the dependency array and use a useRef to keep track of the latest conversationItemData without triggering effect re-runs.\n\nFor example, add a ref at the top of the component:\njavascript\nconst conversationItemDataRef = useRef(conversationItemData);\nconversationItemDataRef.current = conversationItemData;\n\nAnd then update portMessageListener to use conversationItemDataRef.current instead of conversationItemData.

Suggested change
}, [port, conversationItemData])
}, [port])

Comment thread src/background/index.mjs
console.debug('[background] Main port closed; skipping proxy message.')
return
}
if (msg.done || msg.error) port._generating = false

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.

medium

Use optional chaining (msg?.done and msg?.error) to prevent potential TypeError if msg is null or undefined, ensuring robust defensive programming.

Suggested change
if (msg.done || msg.error) port._generating = false
if (msg?.done || msg?.error) port._generating = false

Comment thread src/utils/fetch-sse.mjs
}

export async function fetchSSE(resource, options) {
const { onMessage, onStart, onEnd, onError, ...fetchOptions } = options

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.

medium

Provide a default empty function for onEnd in the destructuring assignment to prevent runtime TypeError crashes if onEnd is omitted from the options object.

Suggested change
const { onMessage, onStart, onEnd, onError, ...fetchOptions } = options
const { onMessage, onStart, onEnd = () => {}, onError, ...fetchOptions } = options

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread src/utils/fetch-sse.mjs Outdated
Comment on lines +38 to +42
try {
while (!(result = await reader.read()).done) {
const chunk = result.value
if (!hasStarted) {
const str = new TextDecoder().decode(chunk)
Comment thread src/background/index.mjs
Comment on lines +160 to +168
if (port._generating) {
port._generating = false
if (!port._isClosed) {
try {
port.postMessage({ done: true })
} catch (e) {
console.warn('[background] Error posting done on proxy disconnect:', e)
}
}

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.

Remediation recommended

1. Done lacks session update 🐞 Bug ≡ Correctness

When the proxy tab disconnects mid-generation, setPortProxy() posts { done: true } without a `{
session }`, so the UI marks the answer complete and becomes ready while
session.conversationRecords remains stale. As a result, saving/exporting or persisting the
conversation can omit the partial answer the user saw.
Agent Prompt
### Issue description
On proxy disconnect during an in-flight generation, the background sends `{ done: true }` to the UI to unstick it, but does not provide an updated `{ session }`. The UI only updates `session` when `msg.session` is present, so the partial assistant answer that was streamed to the UI is not persisted into `session.conversationRecords`, which is later used for “Save Conversation” and for rehydration/persistence.

### Issue Context
- Background disconnect path sends done-only.
- UI marks done/ready on `msg.done`, but does not update `session` unless `msg.session` exists.
- Save/export and initial rehydration are based on `session.conversationRecords`.

### Fix Focus Areas
- src/background/index.mjs[154-169]
- src/components/ConversationCard/index.jsx[97-114]
- src/components/ConversationCard/index.jsx[175-186]
- src/components/ConversationCard/index.jsx[562-574]
- src/pages/IndependentPanel/App.jsx[169-179]

### Suggested fix
Implement a way to persist the currently displayed partial answer into the session when a proxy disconnect triggers a done-only terminal signal.

Two viable approaches:
1) **UI-side (recommended):** In `ConversationCard.portMessageListener`, when receiving `msg.done === true` **and** `msg.session` is missing, derive the last Q/A from current UI state (or track latest answer text in a ref) and `pushRecord`-equivalent into a cloned `session`, then `setSession(updatedSession)` before setting ready.
   - Guard against double-persist if a subsequent `{session}` arrives.
2) **Protocol change:** Add a marker flag from background, e.g. `{ done: true, proxyDisconnected: true }`, so the UI can distinguish this path from other done-only signals (e.g., stop) and apply special persistence logic only for proxy disconnects.

Either approach should ensure `session.conversationRecords` reflects the partial answer that remains visible in the UI, so Save/rehydration/persistence does not drop it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 12ea56a

- background: unlock the conversation UI (post {done:true}) when the
  ChatGPT/Claude/etc. proxy tab disconnects mid-generation; use a
  per-port _generating flag so only in-flight requests are affected.
- fetch-sse: treat abort as onEnd(true) and swallow onEnd errors so a
  closed port on disconnect is not reported as a failure.
- openai-compatible-core: on abort, persist the streamed partial answer
  into session history (pushRecord + post {session}) without re-sending
  the done signal.
- moonshot-web/claude client: on abort, reject the stream promise so
  cancellation is treated as cancellation, not a successful completion.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
tests/unit/utils/fetch-sse.test.mjs (1)

115-175: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting shared mock setup to reduce duplication.

All three new tests repeat the same t.mock.method(console, 'debug', ...) and t.mock.method(globalThis, 'fetch', ...) boilerplate. A small helper or beforeEach/t.before hook could reduce this to a single setup call per test.

♻️ Optional refactor
+function setupSseMock(t, chunk = 'data: {"delta":"A"}\n\n') {
+  t.mock.method(console, 'debug', () => {})
+  t.mock.method(globalThis, 'fetch', async () => createMockSseResponse([chunk]))
+}
+
 test('fetchSSE propagates onEnd errors on normal completion', async (t) => {
-  t.mock.method(console, 'debug', () => {})
-  t.mock.method(globalThis, 'fetch', async () => createMockSseResponse(['data: {"delta":"A"}\n\n']))
+  setupSseMock(t)
 test('fetchSSE propagates onStart errors', async (t) => {
-  t.mock.method(console, 'debug', () => {})
-  t.mock.method(globalThis, 'fetch', async () => createMockSseResponse(['data: {"delta":"A"}\n\n']))
+  setupSseMock(t)
 test('fetchSSE propagates onMessage errors', async (t) => {
-  t.mock.method(console, 'debug', () => {})
-  t.mock.method(globalThis, 'fetch', async () => createMockSseResponse(['data: {"delta":"A"}\n\n']))
+  setupSseMock(t)
🤖 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 `@tests/unit/utils/fetch-sse.test.mjs` around lines 115 - 175, The new fetchSSE
tests duplicate the same console.debug and globalThis.fetch mock setup across
multiple cases; extract that boilerplate into a shared helper or a test hook so
each test only defines its specific onStart/onMessage/onEnd/onError behavior.
Update the fetchSSE test block to reuse the common setup around
createMockSseResponse and t.mock.method, keeping the individual assertions
focused on the error propagation scenarios.
🤖 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.

Nitpick comments:
In `@tests/unit/utils/fetch-sse.test.mjs`:
- Around line 115-175: The new fetchSSE tests duplicate the same console.debug
and globalThis.fetch mock setup across multiple cases; extract that boilerplate
into a shared helper or a test hook so each test only defines its specific
onStart/onMessage/onEnd/onError behavior. Update the fetchSSE test block to
reuse the common setup around createMockSseResponse and t.mock.method, keeping
the individual assertions focused on the error propagation scenarios.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4fa04106-dc1a-4a63-9b09-5f5cef0393f1

📥 Commits

Reviewing files that changed from the base of the PR and between 12ea56a and 1aed904.

📒 Files selected for processing (14)
  • src/background/index.mjs
  • src/components/ConversationCard/index.jsx
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/services/apis/waylaidwanderer-api.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • src/services/apis/azure-openai-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/components/ConversationCard/index.jsx
  • src/services/apis/moonshot-web.mjs
  • src/background/index.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces robust error handling, abort state propagation, and connection management across various API services, background scripts, and SSE utilities. It ensures that aborted streams are handled gracefully, partial responses are preserved when appropriate, and disconnected port errors are ignored or caught safely. The reviewer feedback focuses on optimizing the React components in ConversationCard. Specifically, it suggests avoiding potential memory leaks by setting a flag directly on the port object instead of using replacedPortRef, and highlights a performance issue where the message listener is re-registered on every stream chunk update due to conversationItemData being in the dependency array.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +293 to +296
if (replacedPortRef.current === port) {
replacedPortRef.current = null
return
}

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.

medium

Instead of using a useRef (replacedPortRef) to keep track of the manually disconnected port (which can lead to a memory leak if the old port object is retained in the ref indefinitely when onDisconnect does not fire locally), you can simply attach a temporary flag directly to the port object itself (e.g., port._replaced = true).

This avoids the need for replacedPortRef entirely and prevents any potential memory leaks.

      if (port._replaced) return

Comment on lines +501 to +505
if (!useForegroundFetch) {
replacedPortRef.current = port
port.disconnect()
setPort(Browser.runtime.connect())
}

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.

medium

To accompany the simplified port replacement logic and avoid memory leaks from holding onto the old port in a ref, you can set a flag directly on the port object before disconnecting.

Suggested change
if (!useForegroundFetch) {
replacedPortRef.current = port
port.disconnect()
setPort(Browser.runtime.connect())
}
if (!useForegroundFetch) {
port._replaced = true
port.disconnect()
setPort(Browser.runtime.connect())
}

}
}
}, [conversationItemData])
}, [port, conversationItemData])

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.

medium

Including conversationItemData in the dependency array of this useEffect causes the message listener to be removed and re-registered on port.onMessage on every single stream chunk update (since conversationItemData changes with every new token/chunk). This is highly inefficient and can cause performance degradation or race conditions during rapid streaming.

To fix this, you can use a useRef to hold the latest portMessageListener (or the latest conversationItemData) and remove conversationItemData from the dependency array so that the listener is only registered once per port connection.

For example, you can define a ref for the listener:

const listenerRef = useRef(portMessageListener)
listenerRef.current = portMessageListener

And then in the useEffect:

useEffect(() => {
  if (useForegroundFetch) {
    return () => {}
  } else {
    const listener = (msg) => listenerRef.current(msg)
    port.onMessage.addListener(listener)
    return () => {
      port.onMessage.removeListener(listener)
    }
  }
}, [port])
Suggested change
}, [port, conversationItemData])
}, [port])

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread src/utils/fetch-sse.mjs
Comment on lines +3 to +7
function isAbortError(err) {
if (!err || typeof err !== 'object') return false
const name = typeof err.name === 'string' ? err.name : ''
return name === 'AbortError'
}
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 1aed904

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