Skip to content

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

Merged
PeterDaveHello merged 1 commit into
ChatGPTBox-dev:masterfrom
PeterDaveHello:fix/stop-abort-ui-unstick
Jul 17, 2026
Merged

Fix UI stuck on Stop when request is slow or proxy disconnects#995
PeterDaveHello merged 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 sent only on normal completion, with consistent abort-aware behavior and guaranteed cleanup.
    • Reduced “stuck generating” and related reconnection issues by improving port disconnect/disconnect-notify handling.
    • Hardened port error handling to safely ignore disconnected/closed ports and reliably surface user-facing errors.
    • Improved the non-foreground “Clear Conversation” flow by more reliably swapping and reconnecting the port during reset.
  • Tests
    • Added unit tests covering abort behavior, callback/lifecycle error propagation, listener/controller cleanup, and port-error edge cases.

@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
  • ✅ Review completed - (🔄 Check again to review again)
📝 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 thread src/services/apis/openai-compatible-core.mjs Outdated
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 thread src/background/index.mjs Outdated
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 thread src/services/apis/openai-compatible-core.mjs Outdated

@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) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Stale disconnect requestId 🐞 Bug ≡ Correctness ⭐ New
Description
postProxySession() posts to port.proxy before recording the new request generation state, but
the proxy disconnect handler tags its {done:true, proxyDisconnected:true} using the stored
_proxyRequestGenerationId. If port.proxy.postMessage() throws or a disconnect happens before
markProxyGenerationStarted() runs, the disconnect completion can be tagged with a previous
requestGenerationId and be dropped by the UI’s superseded-request filter, leaving the current
request stuck.
Code

src/background/index.mjs[R64-71]

+function postProxySession(port, session, requestGenerationId) {
+  const proxyGenerationId = (port._proxyGenerationId ?? 0) + 1
+  port.proxy.postMessage({
+    session,
+    proxyGenerationId,
+    ...(requestGenerationId === undefined ? {} : { requestGenerationId }),
+  })
+  markProxyGenerationStarted(port, proxyGenerationId, requestGenerationId)
Evidence
postProxySession() posts to the proxy before calling markProxyGenerationStarted(), while the
proxy disconnect handler tags {done:true, proxyDisconnected:true} using
tagProxyRequestGeneration() (which reads port._proxyRequestGenerationId). The ConversationCard
drops messages whose requestGenerationId doesn’t match the current request, so a stale tag can
prevent the disconnect completion from unblocking the UI.

src/background/index.mjs[64-72]
src/background/index.mjs[184-200]
src/background/proxy-generation-state.mjs[8-12]
src/components/ConversationCard/session.mjs[34-39]

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

### Issue description
`postProxySession()` currently calls `port.proxy.postMessage(...)` before persisting the new request’s generation bookkeeping via `markProxyGenerationStarted(...)`. Disconnect/error paths (e.g. proxy disconnect completion) rely on `port._proxyRequestGenerationId` when tagging messages; if the proxy post fails or disconnect occurs before the bookkeeping is committed, the emitted completion can be tagged with a stale `requestGenerationId` and dropped by the UI.

### Issue Context
- Disconnect completion is tagged using `tagProxyRequestGeneration(port, ...)`, which reads `port._proxyRequestGenerationId`.
- The UI drops messages whose `requestGenerationId` doesn’t match the current request.

### Fix Focus Areas
- src/background/index.mjs[64-72]
- src/background/index.mjs[184-200]
- src/background/proxy-generation-state.mjs[1-12]

### Suggested fix
Make the request/proxy generation state update atomic with respect to disconnect/error tagging:
1) Move the state update ahead of `port.proxy.postMessage(...)` (or at least set `_proxyRequestGenerationId` and `_proxyGenerationId` before posting).
2) If you need `_generating` to reflect only *successful* dispatch, consider:
  - pre-setting IDs used for tagging (request/proxy generation ids),
  - then setting `_generating=true` only after a successful post,
  - and reverting/clearing IDs if the post fails.
3) Add/adjust unit tests to cover the failure path where `postMessage` throws and a subsequent disconnect/max-retry path emits a tagged message (ensuring it uses the current requestGenerationId).

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



Remediation recommended

2. Superseded msgs untagged ✓ Resolved 🐞 Bug ≡ Correctness
Description
ConversationCard ignores superseded messages only when stoppedGenerationId is present, but normal
provider completion messages (e.g., {done:true, session}) don’t include it. A late completion from
a cancelled generation can therefore still run the msg.done path and mark the card ready /
finalize the wrong session.
Code

src/components/ConversationCard/index.jsx[R188-213]

  const portMessageListener = (msg) => {
+    if (isSupersededGenerationMessage(msg, retryGenerationIdRef.current)) return
+
    if (msg.answer) {
+      partialAnswerRef.current = msg.answer
      updateAnswer(msg.answer, false, 'answer')
    }
    if (msg.session) {
-      if (msg.done) msg.session = { ...msg.session, isRetry: false }
-      setSession(msg.session)
+      setSession(msg.done ? { ...msg.session, isRetry: false } : msg.session)
    }
    if (msg.done) {
-      updateAnswer('', true, 'answer', true)
+      const partialAnswer = partialAnswerRef.current
+      const retryRecord = retryRecordRef.current
+      const completionState = getInterruptedCompletionState(msg, partialAnswer, retryRecord)
+      if (completionState.shouldFinalize) {
+        setSession((currentSession) =>
+          finalizeInterruptedSession(currentSession, partialAnswer, retryRecord),
+        )
+      }
+      partialAnswerRef.current = ''
+      retryRecordRef.current = null
+      const answerUpdate = getCompletedAnswerUpdate(completionState.restoredRetryAnswer)
+      updateAnswer(answerUpdate.value, answerUpdate.appended, 'answer', true)
      setIsReady(true)
    }
    if (msg.error) {
Evidence
The UI filtering logic drops only messages that contain stoppedGenerationId, while several
provider completion messages do not attach that field; therefore those completions cannot be
filtered as superseded by the current logic.

src/components/ConversationCard/index.jsx[188-213]
src/components/ConversationCard/session.mjs[27-31]
src/services/apis/shared.mjs[18-26]
src/services/apis/openai-compatible-core.mjs[110-117]
src/services/apis/chatgpt-web.mjs[490-496]

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` uses `isSupersededGenerationMessage()` to drop messages from cancelled generations, but the predicate only matches when `message.stoppedGenerationId` exists. Many providers emit completion messages without any `stoppedGenerationId`, so those messages will not be filtered and can still trigger `msg.done` handling (setting `isReady` / finalizing session state) after a retry/cancel replacement has started.

### Issue Context
- UI filter only checks `stoppedGenerationId`.
- `stoppedGenerationId` is currently only guaranteed on stop-ack messages (and a few abort-session updates), not on normal `{done:true}` provider completions.

### Fix Focus Areas
Implement a consistent request/generation identity that is attached to **all** background->UI port messages (answer/done/error/session), and have `ConversationCard` ignore messages whose id is not the current one.

Suggested approach:
- Create a `generationId` per `session` request (incrementing counter).
- Include `generationId` in `createConversationPortMessage({ session, ... })`.
- Store current `generationId` on the background port (or in the executor scope) and attach it to every `port.postMessage(...)` emitted by providers.
- Update `ConversationCard` to ignore any message with `generationId` that doesn’t match the active generation.

Code references:
- `src/components/ConversationCard/index.jsx` (message handling / superseded filtering)
- `src/components/ConversationCard/session.mjs` (current `isSupersededGenerationMessage` predicate)
- `src/services/apis/shared.mjs` (stop acknowledgement only tags `stoppedGenerationId`)
- `src/services/apis/openai-compatible-core.mjs` (normal `finish()` posts done without stopped id)
- `src/services/apis/chatgpt-web.mjs` (normal completion posts done without stopped id)

### Fix Focus Areas (file/lines)
- src/components/ConversationCard/index.jsx[186-268]
- src/components/ConversationCard/session.mjs[27-40]
- src/services/apis/shared.mjs[18-33]
- src/services/apis/openai-compatible-core.mjs[110-117]
- src/services/apis/chatgpt-web.mjs[490-496]

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


3. Done lacks session update ✓ Resolved 🐞 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


4. 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


View more (3)
5. 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


6. 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


7. 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 4421eee

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. 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


2. 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


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 (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Done lacks session update ✓ Resolved 🐞 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


Results up to commit f56ab05


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


Remediation recommended
1. Superseded msgs untagged 🐞 Bug ≡ Correctness
Description
ConversationCard ignores superseded messages only when stoppedGenerationId is present, but normal
provider completion messages (e.g., {done:true, session}) don’t include it. A late completion from
a cancelled generation can therefore still run the msg.done path and mark the card ready /
finalize the wrong session.
Code

src/components/ConversationCard/index.jsx[R188-213]

  const portMessageListener = (msg) => {
+    if (isSupersededGenerationMessage(msg, retryGenerationIdRef.current)) return
+
    if (msg.answer) {
+      partialAnswerRef.current = msg.answer
      updateAnswer(msg.answer, false, 'answer')
    }
    if (msg.session) {
-      if (msg.done) msg.session = { ...msg.session, isRetry: false }
-      setSession(msg.session)
+      setSession(msg.done ? { ...msg.session, isRetry: false } : msg.session)
    }
    if (msg.done) {
-      updateAnswer('', true, 'answer', true)
+      const partialAnswer = partialAnswerRef.current
+      const retryRecord = retryRecordRef.current
+      const completionState = getInterruptedCompletionState(msg, partialAnswer, retryRecord)
+      if (completionState.shouldFinalize) {
+        setSession((currentSession) =>
+          finalizeInterruptedSession(currentSession, partialAnswer, retryRecord),
+        )
+      }
+      partialAnswerRef.current = ''
+      retryRecordRef.current = null
+      const answerUpdate = getCompletedAnswerUpdate(completionState.restoredRetryAnswer)
+      updateAnswer(answerUpdate.value, answerUpdate.appended, 'answer', true)
      setIsReady(true)
    }
    if (msg.error) {
Evidence
The UI filtering logic drops only messages that contain stoppedGenerationId, while several
provider completion messages do not attach that field; therefore those completions cannot be
filtered as superseded by the current logic.

src/components/ConversationCard/index.jsx[188-213]
src/components/ConversationCard/session.mjs[27-31]
src/services/apis/shared.mjs[18-26]
src/services/apis/openai-compatible-core.mjs[110-117]
src/services/apis/chatgpt-web.mjs[490-496]

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` uses `isSupersededGenerationMessage()` to drop messages from cancelled generations, but the predicate only matches when `message.stoppedGenerationId` exists. Many providers emit completion messages without any `stoppedGenerationId`, so those messages will not be filtered and can still trigger `msg.done` handling (setting `isReady` / finalizing session state) after a retry/cancel replacement has started.

### Issue Context
- UI filter only checks `stoppedGenerationId`.
- `stoppedGenerationId` is currently only guaranteed on stop-ack messages (and a few abort-session updates), not on normal `{done:true}` provider completions.

### Fix Focus Areas
Implement a consistent request/generation identity that is attached to **all** background->UI port messages (answer/done/error/session), and have `ConversationCard` ignore messages whose id is not the current one.

Suggested approach:
- Create a `generationId` per `session` request (incrementing counter).
- Include `generationId` in `createConversationPortMessage({ session, ... })`.
- Store current `generationId` on the background port (or in the executor scope) and attach it to every `port.postMessage(...)` emitted by providers.
- Update `ConversationCard` to ignore any message with `generationId` that doesn’t match the active generation.

Code references:
- `src/components/ConversationCard/index.jsx` (message handling / superseded filtering)
- `src/components/ConversationCard/session.mjs` (current `isSupersededGenerationMessage` predicate)
- `src/services/apis/shared.mjs` (stop acknowledgement only tags `stoppedGenerationId`)
- `src/services/apis/openai-compatible-core.mjs` (normal `finish()` posts done without stopped id)
- `src/services/apis/chatgpt-web.mjs` (normal completion posts done without stopped id)

### Fix Focus Areas (file/lines)
- src/components/ConversationCard/index.jsx[186-268]
- src/components/ConversationCard/session.mjs[27-40]
- src/services/apis/shared.mjs[18-33]
- src/services/apis/openai-compatible-core.mjs[110-117]
- src/services/apis/chatgpt-web.mjs[490-496]

ⓘ 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 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.

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 thread src/services/apis/openai-compatible-core.mjs Outdated
@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 Outdated
}
}
}, [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 thread src/components/ConversationCard/index.jsx Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

| File | Line | Issue |
| src/utils/fetch-sse.mjs | 94 | Normal-completion await onEnd() is not wrapped in try/catch (unlike the abort-path onEnd(true) calls at lines 48-52). A thrown port.postMessage on a disconnected port during onEnd (e.g. finish() in openai-compatible-core.mjs:121, {done:true} in claude-api.mjs:84 / chatgpt-web.mjs:422) rejects fetchSSE() and propagates to handlePortError, surfacing a spurious error / leaving the UI stuck. This contradicts the PR's stated goal of swallowing disconnect errors. |
| src/background/proxy-generation-state.mjs | 34 | markProxyGenerationFinishedFromMessage short-circuits when message.stoppedGenerationId !== undefined (line 37), so a clean proxy stop ack {done:true, stoppedGenerationId} never clears port._generating. A later _proxyOnDisconnect (background/index.mjs:190) then posts a spurious {done:true, proxyDisconnected:true} to the UI. (Carried from prior review; still present.) |

Files Reviewed (changed in this increment)
  • src/utils/fetch-sse.mjs - 1 issue (new)
  • src/background/proxy-generation-state.mjs - 1 issue (carried forward)
  • src/background/index.mjs - verified, no new issues
  • src/components/ConversationCard/index.jsx - verified, no new issues
  • src/components/ConversationCard/session.mjs - prior WARNING resolved (guard now !session.isRetry && !retryRecord); no blank-record push on interrupted retry
  • src/services/apis/openai-compatible-core.mjs - verified, no new issues
  • src/services/apis/chatgpt-web.mjs - verified, no new issues
  • src/services/apis/claude-api.mjs - verified, no new issues
  • src/services/apis/moonshot-web.mjs - verified, no new issues (stop ack via acknowledgePortStop unsticks UI)
  • src/services/clients/claude/index.mjs - verified, no new issues
  • src/services/wrappers.mjs - verified, no new issues
  • src/services/apis/shared.mjs - verified, no new issues
  • src/services/apis/azure-openai-api.mjs - verified, no new issues
  • src/services/apis/waylaidwanderer-api.mjs - verified, no new issues
  • src/services/apis/bard-web.mjs - verified, no new issues
  • src/services/apis/claude-web.mjs - verified, no new issues
  • src/content-script/index.jsx - verified, no new issues

Fix these issues in Kilo Cloud

Previous Review Summaries (6 snapshots, latest commit 9a038f6)

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

Previous review (commit 9a038f6)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

| File | Line | Issue |
| src/background/proxy-generation-state.mjs | 34 | Clean proxy stop leaks port._generating; markProxyGenerationFinishedFromMessage short-circuits on stoppedGenerationId (line 37) so a clean stop ack never clears _generating, causing a spurious {done:true, proxyDisconnected:true} on later disconnect. Re-verified present in HEAD. |
| src/components/ConversationCard/session.mjs | 5 | Interrupted retry with no partial answer appends a blank conversation record because the early return session guard requires !session.isRetry. Re-verified present in HEAD. |

Files Reviewed (20 files)
  • src/background/index.mjs
  • src/background/proxy-generation-state.mjs
  • src/components/ConversationCard/index.jsx
  • src/components/ConversationCard/session.mjs
  • src/content-script/index.jsx
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/bard-web.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/claude-web.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/shared.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • tests/unit/background/proxy-generation-state.test.mjs
  • tests/unit/components/conversation-card-session.test.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • tests/unit/services/apis/shared.test.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs

Fix these issues in Kilo Cloud

Previous review (commit f56ab05)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

| File | Line | Issue |
| src/background/proxy-generation-state.mjs | 19 | Clean proxy stop leaks port._generating; markProxyGenerationFinishedFromMessage short-circuits on stoppedGenerationId so a clean stop ack never clears _generating, causing a spurious {done:true, proxyDisconnected:true} on later disconnect. |
| src/components/ConversationCard/session.mjs | 5 | Interrupted retry with no partial answer appends a blank conversation record because the early return session guard requires !session.isRetry. |

Files Reviewed (20 files)
  • src/background/index.mjs
  • src/background/proxy-generation-state.mjs
  • src/components/ConversationCard/index.jsx
  • src/components/ConversationCard/session.mjs
  • src/content-script/index.jsx
  • src/services/apis/azure-openai-api.mjs
  • src/services/apis/chatgpt-web.mjs
  • src/services/apis/claude-api.mjs
  • src/services/apis/claude-web.mjs
  • src/services/apis/moonshot-web.mjs
  • src/services/apis/openai-compatible-core.mjs
  • src/services/apis/shared.mjs
  • src/services/apis/waylaidwanderer-api.mjs
  • src/services/clients/claude/index.mjs
  • src/services/wrappers.mjs
  • src/utils/fetch-sse.mjs
  • tests/unit/background/proxy-generation-state.test.mjs
  • tests/unit/components/conversation-card-session.test.mjs
  • tests/unit/services/apis/openai-api-compat.test.mjs
  • tests/unit/services/apis/shared.test.mjs
  • tests/unit/services/handle-port-error.test.mjs
  • tests/unit/utils/fetch-sse.test.mjs

Fix these issues in Kilo Cloud

Previous review (commit a4e1939)

Status: No New Issues Found (incremental pass) | Recommendation: Address existing active reviewer comments before merge

Overview

This is an incremental review of commits 1aed9042..a4e19390 (review ID def6a7cc).

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Incremental Analysis

  • No new defects were introduced in the changed lines since the previous review commit (1aed9042).
  • Two earlier code-level findings were resolved in HEAD:
    • src/services/apis/openai-compatible-core.mjs: sticky session.isRetry on abort is now reset (session.isRetry = false) in the abort branch.
    • src/utils/fetch-sse.mjs: the normal-completion await onEnd() is no longer wrapped in a swallowing try/catch, so real completion-path errors surface again.
  • The remaining active concerns are already tracked by other reviewers (gemini-code-assist, Copilot, Qodo, Codex, CodeRabbit) and are not duplicated here:
    • src/background/index.mjs:114msg.done || msg.error should use optional chaining (msg?.done || msg?.error).
    • src/background/index.mjs:169 — proxy-disconnect done path posts no updated session.
    • src/utils/fetch-sse.mjs:7isAbortError only checks AbortError name; align with wrappers.mjs.
    • src/components/ConversationCard/index.jsx:337conversationItemData in effect deps re-registers listener per token.
    • src/components/ConversationCard/index.jsx:505 — Clear Conversation double-reconnects the port.
    • src/components/ConversationCard/index.jsx:296replacedPortRef can retain a stale port.
    • src/services/apis/openai-compatible-core.mjs:150 — abort can repost a stale session after Clear Conversation.
    • src/services/wrappers.mjspostError catch parameter shadows the outer postError function.
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 1aed904)

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 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 hy3:free · Input: 107.5K · Output: 9.9K · Cached: 774K

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

@qodo-code-review

Copy link
Copy Markdown
Contributor

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

@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

@PeterDaveHello
PeterDaveHello force-pushed the fix/stop-abort-ui-unstick branch 2 times, most recently from a4e1939 to f56ab05 Compare July 16, 2026 23:56
@PeterDaveHello
PeterDaveHello requested a review from Copilot July 16, 2026 23:56

@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 state management for proxy generation and session handling, adding mechanisms to track active generations, suppress reconnect errors on interruption, and handle superseded session requests across various web APIs. It also updates SSE fetching to handle abort signals gracefully and adds comprehensive unit tests. The review feedback focuses on defensive programming, recommending safety checks to ensure message objects are defined before accessing their properties and suggesting a fallback default for session.conversationRecords to prevent potential runtime errors.

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 +18 to +22
export function markProxyGenerationFinishedFromMessage(port, message) {
if (message.stoppedGenerationId !== undefined || (!message.error && !message.done)) return false
markProxyGenerationFinished(port)
return true
}

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 prevent potential runtime TypeError exceptions, add a safety check to ensure message is defined before accessing its properties. This is a good defensive programming practice.

Suggested change
export function markProxyGenerationFinishedFromMessage(port, message) {
if (message.stoppedGenerationId !== undefined || (!message.error && !message.done)) return false
markProxyGenerationFinished(port)
return true
}
export function markProxyGenerationFinishedFromMessage(port, message) {
if (!message || message.stoppedGenerationId !== undefined || (!message.error && !message.done)) return false
markProxyGenerationFinished(port)
return true
}

Comment on lines +27 to +32
export function isSupersededGenerationMessage(message, latestSupersededGenerationId) {
return (
message.stoppedGenerationId !== undefined &&
message.stoppedGenerationId <= latestSupersededGenerationId
)
}

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 message?.stoppedGenerationId to safely handle cases where message might be null or undefined, preventing potential runtime crashes.

Suggested change
export function isSupersededGenerationMessage(message, latestSupersededGenerationId) {
return (
message.stoppedGenerationId !== undefined &&
message.stoppedGenerationId <= latestSupersededGenerationId
)
}
export function isSupersededGenerationMessage(message, latestSupersededGenerationId) {
return (
message?.stoppedGenerationId !== undefined &&
message.stoppedGenerationId <= latestSupersededGenerationId
)
}

Comment on lines +3 to +25
export function finalizeInterruptedSession(session, answer, retryRecord = null) {
if (!answer) {
if (!session.isRetry && !retryRecord) return session
const lastRecord = session.conversationRecords.at(-1)
const shouldRestoreRetryRecord =
retryRecord &&
(lastRecord?.question !== retryRecord.question || lastRecord?.answer !== retryRecord.answer)
return {
...session,
conversationRecords: shouldRestoreRetryRecord
? [...session.conversationRecords, { ...retryRecord }]
: session.conversationRecords,
isRetry: false,
}
}
const updatedSession = {
...session,
conversationRecords: session.conversationRecords.map((record) => ({ ...record })),
}
pushRecord(updatedSession, session.question, answer)
updatedSession.isRetry = false
return updatedSession
}

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 ensure robustness and prevent potential runtime errors, default session.conversationRecords to an empty array if it is undefined or null. This is a recommended defensive programming practice.

export function finalizeInterruptedSession(session, answer, retryRecord = null) {
  const conversationRecords = session.conversationRecords || []
  if (!answer) {
    if (!session.isRetry && !retryRecord) return session
    const lastRecord = conversationRecords.at(-1)
    const shouldRestoreRetryRecord = 
      retryRecord &&
      (lastRecord?.question !== retryRecord.question || lastRecord?.answer !== retryRecord.answer)
    return {
      ...session,
      conversationRecords: shouldRestoreRetryRecord
        ? [...conversationRecords, { ...retryRecord }]
        : conversationRecords,
      isRetry: false,
    }
  }
  const updatedSession = {
    ...session,
    conversationRecords: conversationRecords.map((record) => ({ ...record })),
  }
  pushRecord(updatedSession, session.question, answer)
  updatedSession.isRetry = false
  return updatedSession
}

Comment thread src/services/wrappers.mjs
Comment on lines 146 to +148
const onMessage = async (msg) => {
console.debug('received msg', msg)
if (msg.stop) {

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

Add a safety check to ensure msg is defined before checking msg.stop to prevent potential runtime TypeError exceptions if a null or undefined message is received on the port.

Suggested change
const onMessage = async (msg) => {
console.debug('received msg', msg)
if (msg.stop) {
const onMessage = async (msg) => {
console.debug('received msg', msg)
if (!msg) return
if (msg.stop) {

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 22 out of 22 changed files in this pull request and generated 1 comment.

Comment on lines +418 to +437
async onEnd(aborted) {
try {
if (!aborted) {
port.postMessage({ done: true })
}
} finally {
cleanController()
}
},
async onError(resp) {
cleanController()
if (resp instanceof Error) throw resp
if (resp.status === 403) {
throw new Error('CLOUDFLARE')
}
const error = await resp.json().catch(() => ({}))
throw new Error(
!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`,
)
},
Comment thread src/background/proxy-generation-state.mjs Outdated

export function finalizeInterruptedSession(session, answer, retryRecord = null) {
if (!answer) {
if (!session.isRetry && !retryRecord) return 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.

WARNING: Interrupted retry with no partial answer appends a blank record

When a done (proxy disconnect or stop) arrives while session.isRetry is true and no partial answer/retry record is present (partialAnswerRef and retryRecordRef are null/empty — the retry case where createRetrySession set isRetry: retryRecord === null), the early return session guard is skipped because it requires !session.isRetry, and the code falls through to pushRecord(updatedSession, session.question, ''), appending a blank answer record to conversation history.

Fix by also bailing out when there is no actual answer to persist, e.g. change the guard to if (!answer && !retryRecord) { ... } or add && !answer handling so an interrupted retry without any streamed content does not record an empty answer.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

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 f56ab05

@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 request and generation tracking across the extension's background scripts, content scripts, and UI components. By tagging messages with unique proxy and request generation IDs, the system can now reliably identify and discard superseded or stale requests, prevent duplicate or out-of-order responses, and gracefully handle aborted streams. It also adds robust error handling for disconnected ports, enhances the SSE client to support abort signals, and introduces comprehensive unit tests covering these new synchronization states. I have no feedback to provide as there are no review comments.

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.

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 26 out of 26 changed files in this pull request and generated 2 comments.

Comment on lines 376 to +379
port.onMessage.removeListener(portMessageListener)
}
}
}, [conversationItemData])
}, [port, conversationItemData])
Comment thread src/services/wrappers.mjs
postError(t('Please login at https://bing.com first'))
else postError(message)
} else {
const errMsg = JSON.stringify(err) ?? 'unknown error'
Comment thread src/background/index.mjs
Comment on lines +64 to +71
function postProxySession(port, session, requestGenerationId) {
const proxyGenerationId = (port._proxyGenerationId ?? 0) + 1
port.proxy.postMessage({
session,
proxyGenerationId,
...(requestGenerationId === undefined ? {} : { requestGenerationId }),
})
markProxyGenerationStarted(port, proxyGenerationId, requestGenerationId)

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.

Action required

1. Stale disconnect requestid 🐞 Bug ≡ Correctness

postProxySession() posts to port.proxy before recording the new request generation state, but
the proxy disconnect handler tags its {done:true, proxyDisconnected:true} using the stored
_proxyRequestGenerationId. If port.proxy.postMessage() throws or a disconnect happens before
markProxyGenerationStarted() runs, the disconnect completion can be tagged with a previous
requestGenerationId and be dropped by the UI’s superseded-request filter, leaving the current
request stuck.
Agent Prompt
### Issue description
`postProxySession()` currently calls `port.proxy.postMessage(...)` before persisting the new request’s generation bookkeeping via `markProxyGenerationStarted(...)`. Disconnect/error paths (e.g. proxy disconnect completion) rely on `port._proxyRequestGenerationId` when tagging messages; if the proxy post fails or disconnect occurs before the bookkeeping is committed, the emitted completion can be tagged with a stale `requestGenerationId` and dropped by the UI.

### Issue Context
- Disconnect completion is tagged using `tagProxyRequestGeneration(port, ...)`, which reads `port._proxyRequestGenerationId`.
- The UI drops messages whose `requestGenerationId` doesn’t match the current request.

### Fix Focus Areas
- src/background/index.mjs[64-72]
- src/background/index.mjs[184-200]
- src/background/proxy-generation-state.mjs[1-12]

### Suggested fix
Make the request/proxy generation state update atomic with respect to disconnect/error tagging:
1) Move the state update ahead of `port.proxy.postMessage(...)` (or at least set `_proxyRequestGenerationId` and `_proxyGenerationId` before posting).
2) If you need `_generating` to reflect only *successful* dispatch, consider:
   - pre-setting IDs used for tagging (request/proxy generation ids),
   - then setting `_generating=true` only after a successful post,
   - and reverting/clearing IDs if the post fails.
3) Add/adjust unit tests to cover the failure path where `postMessage` throws and a subsequent disconnect/max-retry path emits a tagged message (ensuring it uses the current requestGenerationId).

ⓘ 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 9a038f6

- 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.
@PeterDaveHello
PeterDaveHello force-pushed the fix/stop-abort-ui-unstick branch from 9a038f6 to 4421eee Compare July 17, 2026 15:49
@PeterDaveHello
PeterDaveHello merged commit 745fdf5 into ChatGPTBox-dev:master Jul 17, 2026
1 check passed
@PeterDaveHello
PeterDaveHello deleted the fix/stop-abort-ui-unstick branch July 17, 2026 15:49
Comment thread src/utils/fetch-sse.mjs
await handleCallbackError(err)
}
}
await onEnd()

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.

WARNING: Normal-completion onEnd() is not guarded, contradicting the PR's stated goal

Unlike the abort-path onEnd(true) calls (lines 48-52), the final await onEnd() here is unwrapped. If an onEnd implementation throws after the stream ends — e.g. finish() calls port.postMessage({ answer: null, done: true, session }) (openai-compatible-core.mjs:121) or { done: true } (claude-api.mjs:84, chatgpt-web.mjs:422) on a port that was just disconnected — the rejection propagates out of fetchSSE(), reaching registerPortListener's executor catch → handlePortError, which can surface a spurious error to the UI or leave it in a stuck state. This is exactly the disconnect-after-done failure the PR claims to fix.

Wrap it to match the abort path, e.g.:

Suggested change
await onEnd()
try {
await onEnd()
} catch (e) {
console.warn('[fetch-sse] onEnd threw during completion:', e)
}

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

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