Skip to content

fix(extension): stop iframe hangs and add debug mode - #229

Merged
arunSunnyKVS merged 1 commit into
masterfrom
fix/extension-hang-timeouts-and-debug
Aug 6, 2026
Merged

fix(extension): stop iframe hangs and add debug mode#229
arunSunnyKVS merged 1 commit into
masterfrom
fix/extension-hang-timeouts-and-debug

Conversation

@arunSunnyKVS

@arunSunnyKVS arunSunnyKVS commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

On pages with unresponsive cross-origin iframes (e.g. Google Maps embeds), chrome.scripting.executeScript({ allFrames: true }) could block forever. The extension hung at "Detecting chat widget…" / agent-context detection with no timeout or visibility into what failed.

Separately, Chat360/Draft.js widgets nest message text deeper than the snapshot walker’s depth-15 limit, so bot replies were never extracted even when sends succeeded.

Solution

  • Wrap allFrames scripting paths in Promise.race timeouts with main-frame-only fallbacks (collectFrames, AX snapshots, shadow patch, response scan, agent-context summarize).
  • Raise frame_snapshot.js collect depth from 15 → 25 so deeply nested message bodies are captured.
  • Add an extension debug mode (debugLog.js + popup toggle / export / clear) that logs locate, LLM UI actions, send, and extract steps to the service worker + storage.
  • Improve frame_actuate.js failure details when submit is not accepted.

Changes

  • runners/extension/debugLog.js (new)
  • runners/extension/{agentContext,chatLocator,domActions,domTarget,frameDiscovery,frame_actuate,frame_snapshot,llm,llmUiActions,orchestrator,popup,responseExtractor,service_worker}.*

Issue

N/A

How to test

  1. Load unpacked runners/extension in Chrome.
  2. Open Advanced → enable Debug mode.
  3. Run against a page that previously hung (e.g. hospital site with Maps iframe) — locate should progress past allFrames within ~15–45s via fallback instead of hanging forever.
  4. Run against a Chat360 widget (e.g. hyundai.com/in) — after send, response extraction should capture bot text (depth fix).
  5. Export logs from Advanced and confirm locate / LLM / send / extract events appear.

Screenshots

N/A — debug toggle lives under Advanced settings in the side panel.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added an Advanced-panel Debug mode toggle.
    • Added options to export and clear diagnostic logs.
    • Added configurable diagnostic logging throughout browser automation workflows.
  • Improvements

    • Added timeout handling and fallback behavior for frame discovery, page scanning, and content extraction.
    • Expanded error details for failed actions and submissions.
    • Improved text detection across deeper page structures.

chrome.scripting.executeScript({ allFrames: true }) could block forever on
unresponsive cross-origin frames; add timeouts with main-frame fallbacks,
raise snapshot depth for Draft.js widgets, and ship a debug-log UI for runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The extension adds a persistent debug logger with popup controls. Runtime flows now emit diagnostics and use timed frame operations with main-frame fallbacks. DOM actions and response extraction return more detailed failure information.

Changes

Extension observability and execution resilience

Layer / File(s) Summary
Debug logging and controls
runners/extension/debugLog.js, runners/extension/service_worker.js, runners/extension/popup.html, runners/extension/popup.js
Adds buffered, sanitized debug logs with storage persistence, export, clearing, and popup controls.
Frame collection and chat location
runners/extension/frameDiscovery.js, runners/extension/chatLocator.js, runners/extension/frame_snapshot.js
Adds timeout-based frame and accessibility collection with main-frame fallbacks, detailed chat-location logging, and deeper text traversal.
LLM and run diagnostics
runners/extension/agentContext.js, runners/extension/llm.js, runners/extension/llmUiActions.js, runners/extension/orchestrator.js
Adds summarization timeouts and diagnostics around LLM calls, UI planning, business-context resolution, run configuration, errors, and completion.
DOM actuation and submission diagnostics
runners/extension/domActions.js, runners/extension/domTarget.js, runners/extension/frame_actuate.js
Adds injection fallbacks, guarded actuation, recovery logging, and structured submission and send failures.
Response scanning diagnostics
runners/extension/responseExtractor.js
Adds timed scan fallbacks and diagnostics for streaming, extraction, partial results, polling exhaustion, and timeout.

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

Possibly related PRs

Suggested reviewers: jithin23-kv, achuvyas-kv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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
Title check ✅ Passed The title clearly summarizes the main changes: preventing iframe hangs and adding debug mode.
Description check ✅ Passed The description includes all template sections and provides clear problem, solution, affected files, testing steps, and screenshot details.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/extension-hang-timeouts-and-debug

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.

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

Actionable comments posted: 6

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

Inline comments:
In `@runners/extension/agentContext.js`:
- Around line 16-35: Update summarizeAgentFromTab and
_summarizeAgentFromTabInner so the timeout-owned AbortController signal is
passed through to callLlm, aborting the request when CONTEXT_TIMEOUT_MS expires.
Replace Promise.race with explicit timer cleanup on success, timeout, or error,
while preserving any existing user-stop cancellation behavior.

In `@runners/extension/debugLog.js`:
- Around line 76-103: Serialize flush, clearDebugLogs, getDebugLogs, and
exportDebugLogs through a shared promise chain so storage read-modify-write
operations cannot overlap. Ensure each queued operation drains _buffer before
retrieving or exporting logs, and that clearDebugLogs runs in order after any
in-flight flush so cleared logs are not restored.
- Around line 20-23: Update the initialization around FLAG_KEY to expose a
readiness promise that resolves after chrome.storage.local.get completes. Await
that promise in the OPFOR_DEBUG_STATUS and toggle operation handlers, and defer
initial dbg entries until readiness resolves so stored state is applied before
any status, mutation, or diagnostic output.

In `@runners/extension/domActions.js`:
- Around line 19-24: Bound both main-frame fallback injections with the existing
timeout mechanism: in runners/extension/domActions.js (lines 19-24), update
preparePageForChat’s main-frame shadow-patch executeScript call; in
runners/extension/responseExtractor.js (lines 63-67), update scanBestFrame’s
main-frame snapshot injection. Race each call against a bounded timeout while
preserving the existing fallback behavior.

In `@runners/extension/popup.html`:
- Around line 2805-2818: Add an accessible name to the button identified by
debugToggle, using aria-label="Debug mode" or associating it with the visible
Debug mode label via aria-labelledby; preserve its existing switch semantics and
behavior.

In `@runners/extension/service_worker.js`:
- Around line 308-343: Update runners/extension/service_worker.js lines 308-343
in the OPFOR_DEBUG_TOGGLE, OPFOR_DEBUG_EXPORT, OPFOR_DEBUG_CLEAR, and related
async handlers to catch rejected storage operations and call sendResponse with {
ok: false, error }, while preserving successful responses. Update
runners/extension/popup.js lines 3016-3039 so the debug switch changes only
after a successful { ok: true } response, reverts on failure, and handles
chrome.runtime.lastError; apply equivalent failure handling to export and clear
commands.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e76117d9-cb96-4daa-8925-2d6acc93f287

📥 Commits

Reviewing files that changed from the base of the PR and between 254cd24 and 604dc0f.

📒 Files selected for processing (15)
  • runners/extension/agentContext.js
  • runners/extension/chatLocator.js
  • runners/extension/debugLog.js
  • runners/extension/domActions.js
  • runners/extension/domTarget.js
  • runners/extension/frameDiscovery.js
  • runners/extension/frame_actuate.js
  • runners/extension/frame_snapshot.js
  • runners/extension/llm.js
  • runners/extension/llmUiActions.js
  • runners/extension/orchestrator.js
  • runners/extension/popup.html
  • runners/extension/popup.js
  • runners/extension/responseExtractor.js
  • runners/extension/service_worker.js

Comment on lines +16 to +35
const CONTEXT_TIMEOUT_MS = 45_000;

/**
* Use the reader LLM to infer what the on-page chat agent is from DOM snapshots.
*/
export async function summarizeAgentFromTab(tabId, readerCfg, siteUrl = "") {
dbg("context", "summarizeAgentFromTab called", { tabId, siteUrl: String(siteUrl).slice(0, 120) });

return Promise.race([
_summarizeAgentFromTabInner(tabId, readerCfg, siteUrl),
new Promise((_, reject) =>
setTimeout(
() =>
reject(
new Error(`Agent context detection timed out after ${CONTEXT_TIMEOUT_MS / 1000}s`)
),
CONTEXT_TIMEOUT_MS
)
),
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Abort the LLM request when the context timeout expires.

Promise.race returns the timeout error, but _summarizeAgentFromTabInner continues its callLlm request. The request can consume provider capacity and cost after the run has moved to manual input or failed.

Create a timeout-owned AbortController, pass its signal to callLlm, abort it when the timeout expires, and clear the timer. Preserve user-stop cancellation.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 26-32: Avoid using the initial state variable in setState
Context: setTimeout(
() =>
reject(
new Error(Agent context detection timed out after ${CONTEXT_TIMEOUT_MS / 1000}s)
),
CONTEXT_TIMEOUT_MS
)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 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 `@runners/extension/agentContext.js` around lines 16 - 35, Update
summarizeAgentFromTab and _summarizeAgentFromTabInner so the timeout-owned
AbortController signal is passed through to callLlm, aborting the request when
CONTEXT_TIMEOUT_MS expires. Replace Promise.race with explicit timer cleanup on
success, timeout, or error, while preserving any existing user-stop cancellation
behavior.

Comment on lines +20 to +23
// Boot: read the stored flag once so hot-path checks are synchronous.
chrome.storage.local.get([FLAG_KEY], (data) => {
_enabled = !!data?.[FLAG_KEY];
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wait for the stored debug state before using it.

Lines 20-23 leave _enabled as false until the storage callback completes. A restarted worker can drop initial diagnostics and return false for OPFOR_DEBUG_STATUS when opforDebug is already enabled. A toggle in this interval can also be overwritten by the stale read.

Expose a readiness promise. Await it before status and toggle operations. Defer initial dbg entries until it resolves.

🤖 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 `@runners/extension/debugLog.js` around lines 20 - 23, Update the
initialization around FLAG_KEY to expose a readiness promise that resolves after
chrome.storage.local.get completes. Await that promise in the OPFOR_DEBUG_STATUS
and toggle operation handlers, and defer initial dbg entries until readiness
resolves so stored state is applied before any status, mutation, or diagnostic
output.

Comment on lines +76 to +103
function scheduleFlush() {
if (_flushTimer) return;
_flushTimer = setTimeout(flush, 300);
}

async function flush() {
_flushTimer = null;
if (!_buffer.length) return;

const batch = _buffer.splice(0);
try {
const data = await chrome.storage.local.get(STORAGE_KEY);
const existing = Array.isArray(data?.[STORAGE_KEY]) ? data[STORAGE_KEY] : [];
const merged = [...existing, ...batch].slice(-MAX_LOG_ENTRIES);
await chrome.storage.local.set({ [STORAGE_KEY]: merged });
} catch {
// Storage full or unavailable — logs are still in the console.
}
}

export async function getDebugLogs() {
const data = await chrome.storage.local.get(STORAGE_KEY);
return Array.isArray(data?.[STORAGE_KEY]) ? data[STORAGE_KEY] : [];
}

export async function clearDebugLogs() {
_buffer = [];
await chrome.storage.local.remove(STORAGE_KEY);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Serialize log storage operations.

Lines 85-90 perform an asynchronous read-modify-write. A second flush can read the same prior value and overwrite the first batch. clearDebugLogs() can also remove the key between the read and write, then an in-flight flush restores cleared logs. exportDebugLogs() can omit entries still in _buffer.

Queue flush, clear, retrieval, and export operations through one promise chain. Drain the buffer before retrieval or export.

Also applies to: 109-110

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 77-77: React's useState should not be directly called
Context: setTimeout(flush, 300)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)


[warning] 77-77: Avoid using the initial state variable in setState
Context: setTimeout(flush, 300)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 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 `@runners/extension/debugLog.js` around lines 76 - 103, Serialize flush,
clearDebugLogs, getDebugLogs, and exportDebugLogs through a shared promise chain
so storage read-modify-write operations cannot overlap. Ensure each queued
operation drains _buffer before retrieving or exporting logs, and that
clearDebugLogs runs in order after any in-flight flush so cleared logs are not
restored.

Comment on lines +19 to +24
try {
await chrome.scripting.executeScript({
target: { tabId, frameIds: [0] },
files: ["frame_shadow_patch.js"],
world: "MAIN",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound both main-frame fallback calls.

The all-frame timeout does not protect these fallback calls. If the main frame is unresponsive, preparePageForChat or scanBestFrame can still wait indefinitely.

  • runners/extension/domActions.js#L19-L24: race the main-frame shadow-patch injection against a bounded timeout.
  • runners/extension/responseExtractor.js#L63-L67: race the main-frame snapshot injection against a bounded timeout.
📍 Affects 2 files
  • runners/extension/domActions.js#L19-L24 (this comment)
  • runners/extension/responseExtractor.js#L63-L67
🤖 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 `@runners/extension/domActions.js` around lines 19 - 24, Bound both main-frame
fallback injections with the existing timeout mechanism: in
runners/extension/domActions.js (lines 19-24), update preparePageForChat’s
main-frame shadow-patch executeScript call; in
runners/extension/responseExtractor.js (lines 63-67), update scanBestFrame’s
main-frame snapshot injection. Race each call against a bounded timeout while
preserving the existing fallback behavior.

Comment on lines +2805 to +2818
<div class="toggle-row">
<div class="body">
<div class="label">Debug mode</div>
<div class="help">Log every step to the service-worker console and storage.</div>
</div>
<button
id="debugToggle"
class="toggle"
role="switch"
aria-checked="false"
type="button"
>
<span class="knob"></span>
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add an accessible name to debugToggle.

The adjacent “Debug mode” div does not label the switch. Screen readers can announce an unnamed switch.

Add aria-label="Debug mode" or associate the visible label with aria-labelledby.

🤖 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 `@runners/extension/popup.html` around lines 2805 - 2818, Add an accessible
name to the button identified by debugToggle, using aria-label="Debug mode" or
associating it with the visible Debug mode label via aria-labelledby; preserve
its existing switch semantics and behavior.

Comment on lines +308 to +343
if (message?.type === "OPFOR_DEBUG_TOGGLE") {
(async () => {
const on = !!message.enabled;
await setDebugEnabled(on);
sendResponse({ ok: true, enabled: on });
})();
return true;
}

if (message?.type === "OPFOR_DEBUG_STATUS") {
sendResponse({ ok: true, enabled: isDebugEnabled() });
return true;
}

if (message?.type === "OPFOR_DEBUG_EXPORT") {
(async () => {
const text = await exportDebugLogs();
sendResponse({ ok: true, text });
})();
return true;
}

if (message?.type === "OPFOR_DEBUG_CLEAR") {
(async () => {
await clearDebugLogs();
sendResponse({ ok: true });
})();
return true;
}

if (message?.type === "OPFOR_DEBUG_GET_LOGS") {
(async () => {
const logs = await getDebugLogs();
sendResponse({ ok: true, logs });
})();
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return debug-command failures and confirm state before updating the UI.

When a storage operation rejects, the service-worker async handler exits without calling sendResponse. The popup updates debugToggle before it confirms persistence and ignores runtime errors. The switch can show enabled while logging remains disabled.

  • runners/extension/service_worker.js#L308-L343: catch each asynchronous command failure and return { ok: false, error }.
  • runners/extension/popup.js#L3016-L3039: update the switch only after { ok: true }; revert it and handle chrome.runtime.lastError on failure. Handle export and clear failures similarly.
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 310-310: Avoid using the initial state variable in setState
Context: setDebugEnabled(on)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

📍 Affects 2 files
  • runners/extension/service_worker.js#L308-L343 (this comment)
  • runners/extension/popup.js#L3016-L3039
🤖 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 `@runners/extension/service_worker.js` around lines 308 - 343, Update
runners/extension/service_worker.js lines 308-343 in the OPFOR_DEBUG_TOGGLE,
OPFOR_DEBUG_EXPORT, OPFOR_DEBUG_CLEAR, and related async handlers to catch
rejected storage operations and call sendResponse with { ok: false, error },
while preserving successful responses. Update runners/extension/popup.js lines
3016-3039 so the debug switch changes only after a successful { ok: true }
response, reverts on failure, and handles chrome.runtime.lastError; apply
equivalent failure handling to export and clear commands.

@arunSunnyKVS
arunSunnyKVS merged commit 919e48c into master Aug 6, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants