-
Notifications
You must be signed in to change notification settings - Fork 854
Fix: Address potential bugs and improve error handling #863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughCentralized redaction and reconnection added to background; runtime messaging, webRequest/tab listeners, and side-panel guards expanded. Content script adds idempotent mounting, multi-token (Claude/Kimi/ChatGPT) management, selection/UI tooling, and new run/manage utilities. Changes
Sequence Diagram(s)sequenceDiagram
participant Port as Background Port
participant Manager as Reconnect Manager
participant Handler as Message Handler
participant Redactor as Redaction Layer
participant API as API Executor
Port->>Manager: connect
Manager->>Handler: deliver incoming message
Handler->>Redactor: redact sensitive fields
Redactor-->>Handler: redacted payload
Handler->>API: executeApi(request)
API-->>Handler: response / error
Handler->>Port: send response
alt Connection lost
Port--xManager: disconnect
Manager->>Manager: exponential backoff retry
Manager->>Port: reconnect attempt
end
sequenceDiagram
participant Runtime as Browser Runtime
participant Script as Content Script
participant TokenMgr as Token Manager
participant Claude as Claude Service
participant UI as UI Components
Runtime->>Script: inject / start
Script->>Script: run()
Script->>TokenMgr: detect / overwrite tokens
TokenMgr->>Claude: request sessionKey
Claude-->>TokenMgr: sessionKey or null
Script->>Script: mountComponent (with retry)
Script->>UI: init selection tools / static card
alt Jump-back required
Script->>TokenMgr: poll token readiness
TokenMgr-->>Script: token ready
Script->>UI: render WebJumpBackNotification
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 3fbed97
Previous suggestions✅ Suggestions up to commit 0b5e7a4
✅ Suggestions up to commit a986d2e
✅ Suggestions up to commit 789d8fe
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/background/index.mjs (1)
433-439: Returning{}on failure drops request headersIf an exception occurs we currently return an empty object, which strips all headers and can break the request:
- return {} // Return empty object or original headers on error? + return { requestHeaders: details.requestHeaders }src/content-script/index.jsx (2)
622-627: Dead code triggers “defined but never used” lint warning
prepareForForegroundRequestsis kept as a no-op placeholder but still defined.
Either delete it or export it for tests to silence the linter.🧰 Tools
🪛 GitHub Check: tests
[failure] 622-622:
'prepareForForegroundRequests' is defined but never used
878-916: Risk of multipleregisterPortListenerregistrations
registerPortListeneris invoked every page load; if the script is injected twice (e.g. SPA navigation) duplicate listeners will accumulate.Guard with a module-level flag:
+if (!window.__chatgptboxPortListenerRegistered) { registerPortListener(…) + window.__chatgptboxPortListenerRegistered = true +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/background/index.mjs(2 hunks)src/content-script/index.jsx(2 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/background/index.mjs
[error] 380-380: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 459-459: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
src/content-script/index.jsx
[error] 265-265: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 326-326: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 367-367: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 410-410: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 530-530: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 581-581: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 610-610: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🪛 GitHub Check: tests
src/background/index.mjs
[failure] 460-460:
'chrome' is not defined
src/content-script/index.jsx
[failure] 622-622:
'prepareForForegroundRequests' is defined but never used
🪛 GitHub Actions: pr-tests
src/background/index.mjs
[error] 460-460: ESLint: 'chrome' is not defined. (no-undef)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (4)
src/background/index.mjs (4)
72-88:⚠️ Potential issueProxy reconnection still risks infinite recursion & listener leaks
proxyOnDisconnectinvokessetPortProxyunconditionally and immediately, without:
- Removing the listeners attached to the previous proxy port.
- Capping the retry count / adding back-off delay.
The exact same concerns were raised in the previous review but remain unaddressed.
Leaving this as-is can snow-ball into dozens of lingering listeners and a tight retry loop on a flapping tab.
See the earlier suggestion for a bounded retry counter + exponential back-off.
103-104:⚠️ Potential issueSensitive configuration is still dumped to the console
console.debug('[background] Full config details:', config)will happily print API keys, custom URLs and tokens.
The prior review already flagged this leakage – please remove or redact before shipping.- console.debug('[background] Full config details:', config) + // Avoid leaking secrets – only log non-sensitive fields + console.debug('[background] Config loaded. modelName:', config.modelName)
432-439: 🛠️ Refactor suggestionReturning
{}on error strips all request headersIf an exception bubbles up we return an empty object, which drops every header and can break the request completely.
Safer fall-back: return the original headers so the request proceeds unchanged.- return {} // Return empty object or original headers on error? + return { requestHeaders: details.requestHeaders }
458-459:⚠️ Potential issue
chromeglobal is still unguarded – breaks Firefox & eslintAccessing
chrome.sidePanelwithout atypeof chrome !== 'undefined'guard triggersno-undefand fails on Firefox wherechromeis absent.- if (chrome && chrome.sidePanel) { + if (typeof chrome !== 'undefined' && chrome?.sidePanel) {🧰 Tools
🪛 Biome (1.9.4)
[error] 458-458: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
.eslintrc.json(1 hunks)package.json(1 hunks)src/background/index.mjs(2 hunks)src/content-script/index.jsx(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- .eslintrc.json
- package.json
🧰 Additional context used
🪛 Biome (1.9.4)
src/background/index.mjs
[error] 380-380: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 458-458: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
src/content-script/index.jsx
[error] 265-265: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 326-326: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 367-367: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 410-410: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 530-530: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 581-581: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 610-610: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
src/background/index.mjs (1)
545-556:⚠️ Potential issueChrome global usage still present despite past review feedback.
This code still uses the unguarded
chromeglobal that was flagged in previous reviews as causing ESLint errors and Firefox compatibility issues.The pipeline error persists due to unguarded
chromeusage. Apply the fix from the past review:-// eslint-disable-next-line no-undef -if (chrome && chrome.sidePanel) { - // eslint-disable-next-line no-undef - await chrome.sidePanel.setOptions({ +if (typeof chrome !== 'undefined' && chrome?.sidePanel) { + await chrome.sidePanel.setOptions({ tabId, path: 'IndependentPanel.html', enabled: true, }) - console.debug(`[background] Side panel options set for tab ${tabId} using chrome.sidePanel`) + console.debug(`[background] Side panel options set for tab ${tabId} using chrome.sidePanel`) } else { - console.debug('[background] chrome.sidePanel API also not available.') + console.debug('[background] chrome.sidePanel API also not available.') }🧰 Tools
🪛 Biome (1.9.4)
[error] 546-546: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
src/content-script/index.jsx (1)
719-758: 🛠️ Refactor suggestionKimi token polling has the same race condition issue.
The same timeout/polling race condition exists in the Kimi refresh token logic.
Apply the same cleanup pattern as suggested for the Claude session key polling logic above.
🧹 Nitpick comments (5)
src/background/index.mjs (3)
64-69: Previous listener cleanup logic has improved but could be more robust.The cleanup logic for old listeners is better than before, but there's still a potential issue: if
port._proxyOnMessageorport._proxyOnDisconnectare undefined, the removeListener calls will silently fail rather than throwing the expected error that the try-catch handles.Consider checking for listener existence before removal:
if (port.proxy) { try { - if (port._proxyOnMessage) port.proxy.onMessage.removeListener(port._proxyOnMessage); - if (port._proxyOnDisconnect) port.proxy.onDisconnect.removeListener(port._proxyOnDisconnect); + if (port._proxyOnMessage && port.proxy.onMessage.hasListener?.(port._proxyOnMessage)) { + port.proxy.onMessage.removeListener(port._proxyOnMessage); + } + if (port._proxyOnDisconnect && port.proxy.onDisconnect.hasListener?.(port._proxyOnDisconnect)) { + port.proxy.onDisconnect.removeListener(port._proxyOnDisconnect); + } } catch(e) {
93-124: Exponential backoff implementation is well-designed but could benefit from configuration.The reconnection logic with exponential backoff and retry limits addresses the infinite loop concerns from past reviews effectively. The implementation is solid with proper cleanup and error handling.
Consider making the retry configuration more visible:
+const RECONNECT_CONFIG = { + MAX_ATTEMPTS: 5, + BASE_DELAY_MS: 1000, + BACKOFF_MULTIPLIER: 2 +}; + -const MAX_RECONNECT_ATTEMPTS = 5; +const MAX_RECONNECT_ATTEMPTS = RECONNECT_CONFIG.MAX_ATTEMPTS; ... -const delay = Math.pow(2, port._reconnectAttempts - 1) * 1000; +const delay = Math.pow(RECONNECT_CONFIG.BACKOFF_MULTIPLIER, port._reconnectAttempts - 1) * RECONNECT_CONFIG.BASE_DELAY_MS;
488-503: Header modification logic is correct but could use optional chaining.The header modification logic is functionally correct, but Biome suggests using optional chaining for cleaner code.
Apply the static analysis suggestion:
-for (let i = 0; i < headers.length; i++) { - if (headers[i].name.toLowerCase() === 'origin') { +for (let i = 0; i < headers?.length; i++) { + if (headers[i]?.name?.toLowerCase() === 'origin') { headers[i].value = 'https://www.bing.com' modified = true - } else if (headers[i].name.toLowerCase() === 'referer') { + } else if (headers[i]?.name?.toLowerCase() === 'referer') { headers[i].value = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' modified = true } }src/content-script/index.jsx (2)
264-276: Selection element containment check could use optional chaining.The logic is correct but could benefit from the optional chaining suggested by static analysis tools.
-if (toolbarContainer && toolbarContainer.contains(e.target)) { +if (toolbarContainer?.contains(e.target)) { console.debug('[content] Mouseup inside toolbar, ignoring.') return } const selectionElement = window.getSelection()?.rangeCount > 0 && window.getSelection()?.getRangeAt(0).endContainer.parentElement -if (toolbarContainer && selectionElement && toolbarContainer.contains(selectionElement)) { +if (toolbarContainer?.contains(selectionElement)) { console.debug('[content] Mouseup selection is inside toolbar, ignoring.') return }🧰 Tools
🪛 Biome (1.9.4)
[error] 265-265: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
810-822: Storage change listener could be more specific.The storage change listener correctly handles config changes but could be more efficient by checking specific config keys that affect ChatGPT tab state.
-if (areaName === 'local' && (changes.userConfig || changes.config)) { +if (areaName === 'local' && + (changes.userConfig?.newValue?.modelName !== changes.userConfig?.oldValue?.modelName || + changes.userConfig?.newValue?.apiMode !== changes.userConfig?.oldValue?.apiMode || + changes.config)) { console.log( '[content] User config changed in storage, re-evaluating ChatGPT tab state.', ) await manageChatGptTabState() }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/background/index.mjs(2 hunks)src/content-script/index.jsx(1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/background/index.mjs
[error] 169-169: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 452-452: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 546-546: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
src/content-script/index.jsx
[error] 265-265: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 326-326: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 367-367: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 410-410: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 530-530: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 581-581: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 610-610: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🔇 Additional comments (6)
src/background/index.mjs (2)
450-461: Request body handling uses optional chaining correctly.The requestBody handling logic properly checks for existence before accessing properties, addressing potential null pointer issues.
🧰 Tools
🪛 Biome (1.9.4)
[error] 452-452: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
566-598: Try-catch blocks for initialization provide good error isolation.The wrapping of initialization functions (
registerPortListener,registerCommands,refreshMenu) in separate try-catch blocks is a good practice for preventing one failure from affecting others.src/content-script/index.jsx (4)
44-78: Retry logic for element detection is well-implemented.The retry mechanism with URL change detection prevents infinite loops and provides good user feedback through logging. The logic properly handles the case where elements aren't found after all retries.
862-872: Textarea value vs textContent issue has been properly fixed.The code now correctly uses
input.valueinstead ofinput.textContentfor the textarea element, which addresses the focus hack issue identified in past reviews.
896-947: Port listener registration logic is well-designed.The global flag to prevent duplicate port listener registration is a good pattern, and the error handling around the registration is comprehensive.
838-892: ChatGPT tab state management function is well-structured.The
manageChatGptTabStatefunction properly handles the ChatGPT-specific logic with appropriate guards and error handling. The textarea manipulation usingvalueinstead oftextContentis correctly implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (7)
src/background/index.mjs (3)
497-497: Apply optional chaining as suggested by static analysis.The code can be simplified using optional chaining to safely access nested properties.
- if (details.requestBody && details.requestBody.formData) { + if (details.requestBody?.formData) {🧰 Tools
🪛 Biome (1.9.4)
[error] 497-497: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
216-223: Consider reducing log verbosity for production environments.The extensive debug logging, while helpful for development, may impact performance and expose sensitive information in production environments. Consider implementing log level controls or environment-based logging.
+ const isDebugMode = process.env.NODE_ENV === 'development' || userConfig?.debugMode; + - console.debug('[background] Full session details (redacted):', redactSensitiveFields(session)) - console.debug('[background] Full config details (redacted):', redactSensitiveFields(config)) - if (session.apiMode) { - console.debug('[background] Session apiMode details (redacted):', redactSensitiveFields(session.apiMode)) - } + if (isDebugMode) { + console.debug('[background] Full session details (redacted):', redactSensitiveFields(session)) + console.debug('[background] Full config details (redacted):', redactSensitiveFields(config)) + if (session.apiMode) { + console.debug('[background] Session apiMode details (redacted):', redactSensitiveFields(session.apiMode)) + } + }
54-58: Document reconnection configuration constants.The reconnection constants lack documentation about their purpose and impact on user experience.
+/** + * Configuration for exponential backoff reconnection strategy + * @constant {Object} RECONNECT_CONFIG + * @property {number} MAX_ATTEMPTS - Maximum reconnection attempts before giving up + * @property {number} BASE_DELAY_MS - Initial delay between reconnection attempts + * @property {number} BACKOFF_MULTIPLIER - Multiplier for exponential backoff calculation + */ const RECONNECT_CONFIG = { MAX_ATTEMPTS: 5, - BASE_DELAY_MS: 1000, // Base delay in milliseconds - BACKOFF_MULTIPLIER: 2, // Multiplier for exponential backoff + BASE_DELAY_MS: 1000, + BACKOFF_MULTIPLIER: 2, };src/content-script/index.jsx (4)
529-529: Apply optional chaining as suggested by static analysis.The nested property access can be simplified using optional chaining.
- if (siteAdapterAction && siteAdapterAction.init) { + if (siteAdapterAction?.init) {🧰 Tools
🪛 Biome (1.9.4)
[error] 529-529: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
580-580: Apply optional chaining as suggested by static analysis.The property access can be simplified using optional chaining.
- if (preElement && preElement.textContent) { + if (preElement?.textContent) {🧰 Tools
🪛 Biome (1.9.4)
[error] 580-580: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
609-609: Apply optional chaining as suggested by static analysis.The nested property access can be simplified using optional chaining.
- if (data && data.accessToken) { + if (data?.accessToken) {🧰 Tools
🪛 Biome (1.9.4)
[error] 609-609: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
38-41: Consider reducing debug log verbosity in production.Similar to the background script, the extensive debug logging may impact performance in production environments. Consider implementing environment-based logging controls.
+ const isDebugMode = process.env.NODE_ENV === 'development'; + - console.debug('[content] mountComponent called with siteConfig:', siteConfig) + if (isDebugMode) { + console.debug('[content] mountComponent called with siteConfig:', siteConfig) + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/background/index.mjs(2 hunks)src/content-script/index.jsx(1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/background/index.mjs
[error] 497-497: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
src/content-script/index.jsx
[error] 529-529: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 580-580: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 609-609: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🔇 Additional comments (5)
src/background/index.mjs (1)
108-213: Excellent error handling and reconnection logic implementation.The
setPortProxyfunction demonstrates comprehensive error handling with proper listener cleanup, exponential backoff, and detailed logging. The implementation correctly addresses the infinite reconnection loop issues identified in previous reviews.src/content-script/index.jsx (4)
44-79: Robust retry logic with proper URL change detection.The retry mechanism for element detection is well-implemented with proper checks for URL changes during retries, preventing unnecessary work when navigation occurs.
655-697: Excellent promise timeout cleanup implementation.The Claude session key polling logic properly addresses the memory leak issues identified in previous reviews with proper cleanup of both timers and settled promise protection.
861-873: Correct textarea value manipulation.The code correctly uses the
valueproperty instead oftextContentfor textarea elements, addressing the issue identified in previous reviews.
895-946: Well-designed global port listener registration.The port listener registration includes proper guards to prevent duplicate registration and comprehensive error handling for the ChatGPT Web model processing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/background/index.mjs (1)
658-673: Chrome global usage still needs proper guarding despite ESLint disable.While the ESLint disable comment addresses the linting error, the code still has the underlying compatibility issue with Firefox. The chrome global should be properly guarded.
The existing review comment about this issue is still valid. Replace the ESLint disable approach with proper runtime checking:
- if (!sidePanelSet) { - // eslint-disable-next-line no-undef - if (typeof chrome !== 'undefined' && chrome.sidePanel && typeof chrome.sidePanel.setOptions === 'function') { + if (!sidePanelSet && typeof chrome !== 'undefined' && chrome?.sidePanel?.setOptions) {
🧹 Nitpick comments (3)
src/background/index.mjs (1)
76-133: Review the redactSensitiveFields function for potential edge cases and optimization.The function has good protection against circular references and deep recursion, but there are a few potential issues:
- The
seenWeakSet is passed by reference through recursive calls, which could cause issues if the same object appears in different branches of the recursion tree.- String value checking in arrays (lines 96-104) iterates through all sensitive keywords for every string, which could be inefficient for large arrays.
Consider these optimizations:
function redactSensitiveFields(obj, recursionDepth = 0, maxDepth = 5, seen = new WeakSet()) { if (recursionDepth > maxDepth) { return 'REDACTED_TOO_DEEP'; } if (obj === null || typeof obj !== 'object') { return obj; } if (seen.has(obj)) { return 'REDACTED_CIRCULAR_REFERENCE'; } seen.add(obj); + // Pre-compile sensitive keyword regex for better performance + const sensitiveRegex = new RegExp(SENSITIVE_KEYWORDS.join('|'), 'i'); + + const checkSensitive = (str) => sensitiveRegex.test(str.toLowerCase()); if (Array.isArray(obj)) { const redactedArray = []; for (let i = 0; i < obj.length; i++) { const item = obj[i]; if (item !== null && typeof item === 'object') { redactedArray[i] = redactSensitiveFields(item, recursionDepth + 1, maxDepth, seen); } else if (typeof item === 'string') { - let isItemSensitive = false; - const lowerItem = item.toLowerCase(); - for (const keyword of SENSITIVE_KEYWORDS) { - if (lowerItem.includes(keyword)) { - isItemSensitive = true; - break; - } - } - redactedArray[i] = isItemSensitive ? 'REDACTED' : item; + redactedArray[i] = checkSensitive(item) ? 'REDACTED' : item; } else { redactedArray[i] = item; } } + seen.delete(obj); // Clean up after processing return redactedArray; } else { const redactedObj = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const lowerKey = key.toLowerCase(); - let isKeySensitive = false; - for (const keyword of SENSITIVE_KEYWORDS) { - if (lowerKey.includes(keyword)) { - isKeySensitive = true; - break; - } - } - if (isKeySensitive) { + if (checkSensitive(key)) { redactedObj[key] = 'REDACTED'; } else if (obj[key] !== null && typeof obj[key] === 'object') { redactedObj[key] = redactSensitiveFields(obj[key], recursionDepth + 1, maxDepth, seen); } else { redactedObj[key] = obj[key]; } } } + seen.delete(obj); // Clean up after processing return redactedObj; } }src/content-script/index.jsx (2)
37-79: Retry mechanism in mountComponent could be optimized.The retry loop is well-implemented but could benefit from early termination on certain conditions and better performance:
async function mountComponent(siteConfig) { console.debug('[content] mountComponent called with siteConfig:', siteConfig) try { const userConfig = await getUserConfig() console.debug('[content] User config in mountComponent:', userConfig) if (!userConfig.alwaysFloatingSidebar) { const retry = 10 let oldUrl = location.href let elementFound = false + const selectors = [ + ...(siteConfig ? [siteConfig.sidebarContainerQuery, siteConfig.appendContainerQuery, siteConfig.resultsContainerQuery].filter(Boolean) : []), + userConfig.prependQuery, + userConfig.appendQuery + ].filter(Boolean); + for (let i = 1; i <= retry; i++) { console.debug(`[content] mountComponent retry ${i}/${retry} for element detection.`) if (location.href !== oldUrl) { console.log('[content] URL changed during retry, stopping mountComponent.') return } - const e = - (siteConfig && - (getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) || - getPossibleElementByQuerySelector(siteConfig.appendContainerQuery) || - getPossibleElementByQuerySelector(siteConfig.resultsContainerQuery))) || - getPossibleElementByQuerySelector([userConfig.prependQuery]) || - getPossibleElementByQuerySelector([userConfig.appendQuery]) + const e = getPossibleElementByQuerySelector(selectors); if (e) { console.log('[content] Element found for mounting component:', e) elementFound = true break } else { console.debug(`[content] Element not found on retry ${i}.`) if (i === retry) { console.warn('[content] Element not found after all retries for mountComponent.') return } - await new Promise((r) => setTimeout(r, 500)) + // Progressive delay: 500ms, 750ms, 1000ms, etc. + await new Promise((r) => setTimeout(r, 500 + (i - 1) * 250)) } }
721-770: Duplicate polling logic for Kimi token - consider extracting to utility function.The Kimi refresh token polling logic is nearly identical to the Claude session key polling, leading to code duplication.
Consider extracting the polling logic into a reusable utility function:
async function pollForCondition(conditionFn, options = {}) { const { interval = 500, timeout = 30000, errorHandler = null, onSuccess = null } = options; let promiseSettled = false; let timerId = null; let timeoutId = null; const cleanup = () => { if (timerId) clearInterval(timerId); if (timeoutId) clearTimeout(timeoutId); }; return new Promise((resolve, reject) => { timerId = setInterval(async () => { if (promiseSettled) { cleanup(); return; } try { const result = await conditionFn(); if (result) { if (!promiseSettled) { promiseSettled = true; cleanup(); if (onSuccess) await onSuccess(result); resolve(result); } } } catch (err) { if (errorHandler && await errorHandler(err) && !promiseSettled) { promiseSettled = true; cleanup(); reject(err); } } }, interval); timeoutId = setTimeout(() => { if (!promiseSettled) { promiseSettled = true; cleanup(); reject(new Error('Polling timeout exceeded')); } }, timeout); }).catch((err) => { cleanup(); throw err; }); }Then use it for both Claude and Kimi polling.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
.eslintrc.json(1 hunks)src/background/index.mjs(2 hunks)src/content-script/index.jsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- .eslintrc.json
🔇 Additional comments (5)
src/background/index.mjs (2)
135-259: LGTM! The setPortProxy function addresses previous infinite loop concerns.The extensive rewrite successfully addresses the past review comments about infinite reconnect loops and listener leaks by:
- Implementing proper retry limits with exponential backoff
- Cleaning up old listeners before adding new ones
- Using named listener functions stored on the port object for easier removal
- Comprehensive error handling with try-catch blocks around cleanup operations
The reconnection logic is now robust and prevents resource leaks.
271-391: Enhanced executeApi function improves observability and error handling.The additions provide excellent debugging capabilities with redacted logging and comprehensive model support. The error handling ensures that failures are properly communicated back to the caller.
src/content-script/index.jsx (3)
868-887: Textarea manipulation logic addresses previous textContent issue.The fix correctly uses the
valueproperty instead oftextContentfor textarea elements, which resolves the previous issue where the focus hack wasn't working properly.
903-954: Global port listener registration is well-structured.The approach of moving the port listener registration outside the
runfunction and using a global flag to prevent duplicate registration is a good design pattern. The comprehensive error handling ensures robustness.
651-698: 🛠️ Refactor suggestionPotential cleanup issue in Claude session key polling.
The promise polling logic has improved cleanup but there's still a potential race condition where the cleanup function might not be called if an exception occurs during the interval callback.
await new Promise((resolve, reject) => { timerId = setInterval(async () => { if (promiseSettled) { console.warn('[content] Promise already settled but Claude interval still running. This indicates a potential cleanup issue.'); cleanup() return } try { claudeSession = await getClaudeSessionKey() if (claudeSession) { if (!promiseSettled) { promiseSettled = true cleanup() console.log('[content] Claude session key found after waiting.') resolve() } } } catch (err) { console.error('[content] Error polling for Claude session key:', err) const errMsg = err.message.toLowerCase(); if ((errMsg.includes('network') || errMsg.includes('permission')) && !promiseSettled) { promiseSettled = true; cleanup(); reject(new Error(`Failed to get Claude session key due to: ${err.message}`)); + } else if (!promiseSettled) { + // For non-critical errors, continue polling but log the issue + console.debug('[content] Non-critical error in Claude polling, continuing...'); } } }, 500) timeoutId = setTimeout(() => { if (!promiseSettled) { promiseSettled = true cleanup() console.warn('[content] Timed out waiting for Claude session key.') reject(new Error('Timed out waiting for Claude session key.')) } }, 30000) - }).catch((err) => { + }) + .catch((err) => { + // Ensure cleanup happens even if promise chain fails + cleanup(); console.error('[content] Failed to get Claude session key for jump back notification:', err) return })Likely an incorrect or invalid review comment.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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 enhances the extension’s stability and robustness by improving error handling, logging, and development tooling.
- Updated ESLint commands in package.json to use npx, ensuring the local version of ESLint is used.
- Modified ESLint configuration by adding webextensions support and Node-specific overrides for build and script files.
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| package.json | Updated lint commands to use npx for consistent, local dependency usage. |
| .eslintrc.json | Added the webextensions environment and Node overrides to better target files. |
Comments suppressed due to low confidence (2)
package.json:8
- Using 'npx' ensures that the project's local ESLint version is used. Consider updating the development documentation to note that npx is required to run these lint commands.
"lint": "npx eslint --ext .js,.mjs,.jsx .",
.eslintrc.json:8
- [nitpick] Review the Node-specific override configuration to ensure it aligns with the intended setup for build and script files. It might be helpful to document how these overrides interact with the base config.
"overrides": [
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@codex review |
There was a problem hiding this comment.
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 2 out of 2 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) | ||
| } | ||
| } catch (e) { | ||
| console.error('[content] Error in port listener callback:', e, 'Session:', session) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error logging at line 1022 logs the entire session object to the console, which could contain sensitive information like API keys, tokens, or user data. While this is helpful for debugging, it poses a security risk in production. The background script has implemented a redaction mechanism (redactSensitiveFields function), but the content script doesn't use it. Consider applying similar redaction here or reducing what gets logged.
| .catch((e) => console.error('[background] Error saving Arkose req url and form:', e)) | ||
| } | ||
| } catch (error) { | ||
| console.error('[background] Error in onBeforeRequest listener callback:', error, details) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handler logs the entire details object which could contain sensitive request information. While this is useful for debugging, in production it could expose sensitive data. Consider implementing selective logging or using the redaction mechanism for error logs that include request details.
| let selectionToolsInitialized = false | ||
|
|
||
| async function prepareForSelectionTools() { | ||
| if (selectionToolsInitialized) { | ||
| console.debug('[content] Selection tools already initialized, skipping.') | ||
| return | ||
| } | ||
| selectionToolsInitialized = true | ||
| console.log('[content] Initializing selection tools.') | ||
| document.addEventListener('mouseup', (e) => { | ||
| if (toolbarContainer && toolbarContainer.contains(e.target)) return | ||
| const selectionElement = | ||
| window.getSelection()?.rangeCount > 0 && | ||
| window.getSelection()?.getRangeAt(0).endContainer.parentElement | ||
| if (toolbarContainer && selectionElement && toolbarContainer.contains(selectionElement)) return | ||
|
|
||
| deleteToolbar() | ||
| setTimeout(async () => { | ||
| const selection = window | ||
| .getSelection() | ||
| ?.toString() | ||
| .trim() | ||
| .replace(/^-+|-+$/g, '') | ||
| if (selection) { | ||
| let position | ||
|
|
||
| const config = await getUserConfig() | ||
| if (!config.selectionToolsNextToInputBox) position = { x: e.pageX + 20, y: e.pageY + 20 } | ||
| else { | ||
| const inputElement = selectionElement.querySelector('input, textarea') | ||
| if (inputElement) { | ||
| position = getClientPosition(inputElement) | ||
| position = { | ||
| x: position.x + window.scrollX + inputElement.offsetWidth + 50, | ||
| y: e.pageY + 30, | ||
| try { | ||
| if (toolbarContainer?.contains(e.target)) { | ||
| console.debug('[content] Mouseup inside toolbar, ignoring.') | ||
| return | ||
| } | ||
| const selectionElement = | ||
| window.getSelection()?.rangeCount > 0 && | ||
| window.getSelection()?.getRangeAt(0).endContainer.parentElement | ||
| if (selectionElement && toolbarContainer?.contains(selectionElement)) { | ||
| console.debug('[content] Mouseup selection is inside toolbar, ignoring.') | ||
| return | ||
| } | ||
|
|
||
| deleteToolbar() | ||
| setTimeout(async () => { | ||
| try { | ||
| const selection = window | ||
| .getSelection() | ||
| ?.toString() | ||
| .trim() | ||
| .replace(/^-+|-+$/g, '') | ||
| if (selection) { | ||
| console.debug('[content] Text selected:', selection) | ||
| let position | ||
|
|
||
| const config = await getUserConfig() | ||
| if (!config.selectionToolsNextToInputBox) { | ||
| position = { x: e.pageX + 20, y: e.pageY + 20 } | ||
| } else { | ||
| const activeElement = document.activeElement | ||
| const inputElement = | ||
| selectionElement?.querySelector('input, textarea') || | ||
| (activeElement?.matches('input, textarea') ? activeElement : null) | ||
|
|
||
| if (inputElement) { | ||
| console.debug( | ||
| '[content] Input element found for positioning toolbar:', | ||
| inputElement, | ||
| ) | ||
| const clientRect = getClientPosition(inputElement) | ||
| position = { | ||
| x: clientRect.x + window.scrollX + inputElement.offsetWidth + 50, | ||
| y: e.pageY + 30, | ||
| } | ||
| } else { | ||
| position = { x: e.pageX + 20, y: e.pageY + 20 } | ||
| } | ||
| } | ||
| console.debug('[content] Toolbar position:', position) | ||
| toolbarContainer = createElementAtPosition(position.x, position.y) | ||
| await createSelectionTools(toolbarContainer, selection) | ||
| } else { | ||
| position = { x: e.pageX + 20, y: e.pageY + 20 } | ||
| console.debug('[content] No text selected on mouseup.') | ||
| } | ||
| } catch (err) { | ||
| console.error('[content] Error in mouseup setTimeout callback for selection tools:', err) | ||
| } | ||
| toolbarContainer = createElementAtPosition(position.x, position.y) | ||
| await createSelectionTools(toolbarContainer, selection) | ||
| } | ||
| }) | ||
| }, 0) | ||
| } catch (error) { | ||
| console.error('[content] Error in mouseup listener for selection tools:', error) | ||
| } | ||
| }) | ||
| document.addEventListener('mousedown', (e) => { | ||
| if (toolbarContainer && toolbarContainer.contains(e.target)) return | ||
|
|
||
| document.querySelectorAll('.chatgptbox-toolbar-container').forEach((e) => e.remove()) | ||
| document.addEventListener('mousedown', (e) => { | ||
| try { | ||
| if (toolbarContainer?.contains(e.target)) { | ||
| console.debug('[content] Mousedown inside toolbar, ignoring.') | ||
| return | ||
| } | ||
| console.debug('[content] Mousedown outside toolbar, removing existing toolbars.') | ||
| document.querySelectorAll('.chatgptbox-toolbar-container').forEach((el) => el.remove()) | ||
| toolbarContainer = null | ||
| } catch (error) { | ||
| console.error('[content] Error in mousedown listener for selection tools:', error) | ||
| } | ||
| }) | ||
|
|
||
| document.addEventListener('keydown', (e) => { | ||
| if ( | ||
| toolbarContainer && | ||
| !toolbarContainer.contains(e.target) && | ||
| (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') | ||
| ) { | ||
| setTimeout(() => { | ||
| if (!window.getSelection()?.toString().trim()) deleteToolbar() | ||
| }) | ||
| try { | ||
| if ( | ||
| toolbarContainer && | ||
| !toolbarContainer.contains(e.target) && | ||
| (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') | ||
| ) { | ||
| console.debug('[content] Keydown in input/textarea outside toolbar.') | ||
| setTimeout(() => { | ||
| try { | ||
| if (!window.getSelection()?.toString().trim()) { | ||
| console.debug('[content] No selection after keydown, deleting toolbar.') | ||
| deleteToolbar() | ||
| } | ||
| } catch (err_inner) { | ||
| console.error('[content] Error in keydown setTimeout callback:', err_inner) | ||
| } | ||
| }, 0) | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error in keydown listener for selection tools:', error) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| let selectionToolsTouchInitialized = false | ||
|
|
||
| async function prepareForSelectionToolsTouch() { | ||
| if (selectionToolsTouchInitialized) { | ||
| console.debug('[content] Touch selection tools already initialized, skipping.') | ||
| return | ||
| } | ||
| selectionToolsTouchInitialized = true | ||
| console.log('[content] Initializing touch selection tools.') | ||
| document.addEventListener('touchend', (e) => { | ||
| if (toolbarContainer && toolbarContainer.contains(e.target)) return | ||
| if ( | ||
| toolbarContainer && | ||
| window.getSelection()?.rangeCount > 0 && | ||
| toolbarContainer.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement) | ||
| ) | ||
| return | ||
|
|
||
| deleteToolbar() | ||
| setTimeout(() => { | ||
| const selection = window | ||
| .getSelection() | ||
| ?.toString() | ||
| .trim() | ||
| .replace(/^-+|-+$/g, '') | ||
| if (selection) { | ||
| toolbarContainer = createElementAtPosition( | ||
| e.changedTouches[0].pageX + 20, | ||
| e.changedTouches[0].pageY + 20, | ||
| ) | ||
| createSelectionTools(toolbarContainer, selection) | ||
| try { | ||
| if (toolbarContainer?.contains(e.target)) { | ||
| console.debug('[content] Touchend inside toolbar, ignoring.') | ||
| return | ||
| } | ||
| }) | ||
| if ( | ||
| window.getSelection()?.rangeCount > 0 && | ||
| toolbarContainer?.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement) | ||
| ) { | ||
| console.debug('[content] Touchend selection is inside toolbar, ignoring.') | ||
| return | ||
| } | ||
|
|
||
| deleteToolbar() | ||
| setTimeout(async () => { | ||
| try { | ||
| const selection = window | ||
| .getSelection() | ||
| ?.toString() | ||
| .trim() | ||
| .replace(/^-+|-+$/g, '') | ||
| if (selection) { | ||
| console.debug('[content] Text selected via touch:', selection) | ||
| const touch = e.changedTouches[0] | ||
| toolbarContainer = createElementAtPosition(touch.pageX + 20, touch.pageY + 20) | ||
| await createSelectionTools(toolbarContainer, selection) | ||
| } else { | ||
| console.debug('[content] No text selected on touchend.') | ||
| } | ||
| } catch (err) { | ||
| console.error( | ||
| '[content] Error in touchend setTimeout callback for touch selection tools:', | ||
| err, | ||
| ) | ||
| } | ||
| }, 0) | ||
| } catch (error) { | ||
| console.error('[content] Error in touchend listener for touch selection tools:', error) | ||
| } | ||
| }) | ||
| document.addEventListener('touchstart', (e) => { | ||
| if (toolbarContainer && toolbarContainer.contains(e.target)) return | ||
|
|
||
| document.querySelectorAll('.chatgptbox-toolbar-container').forEach((e) => e.remove()) | ||
| document.addEventListener('touchstart', (e) => { | ||
| try { | ||
| if (toolbarContainer?.contains(e.target)) { | ||
| console.debug('[content] Touchstart inside toolbar, ignoring.') | ||
| return | ||
| } | ||
| console.debug('[content] Touchstart outside toolbar, removing existing toolbars.') | ||
| document.querySelectorAll('.chatgptbox-toolbar-container').forEach((el) => el.remove()) | ||
| toolbarContainer = null | ||
| } catch (error) { | ||
| console.error('[content] Error in touchstart listener for touch selection tools:', error) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| let menuX, menuY | ||
| let rightClickMenuInitialized = false | ||
|
|
||
| async function prepareForRightClickMenu() { | ||
| if (rightClickMenuInitialized) { | ||
| console.debug('[content] Right-click menu already initialized, skipping.') | ||
| return | ||
| } | ||
| rightClickMenuInitialized = true | ||
| console.log('[content] Initializing right-click menu handler.') | ||
| document.addEventListener('contextmenu', (e) => { | ||
| menuX = e.clientX | ||
| menuY = e.clientY | ||
| console.debug(`[content] Context menu opened at X: ${menuX}, Y: ${menuY}`) | ||
| }) | ||
|
|
||
| Browser.runtime.onMessage.addListener(async (message) => { | ||
| if (message.type === 'CREATE_CHAT') { | ||
| const data = message.data | ||
| let prompt = '' | ||
| if (data.itemId in toolsConfig) { | ||
| prompt = await toolsConfig[data.itemId].genPrompt(data.selectionText) | ||
| } else if (data.itemId in menuConfig) { | ||
| const menuItem = menuConfig[data.itemId] | ||
| if (!menuItem.genPrompt) return | ||
| else prompt = await menuItem.genPrompt() | ||
| if (prompt) prompt = await cropText(`Reply in ${await getPreferredLanguage()}.\n` + prompt) | ||
| } | ||
| console.log('[content] Received CREATE_CHAT message:', message) | ||
| try { | ||
| const data = message.data | ||
| let prompt = '' | ||
| if (data.itemId in toolsConfig) { | ||
| console.debug('[content] Generating prompt from toolsConfig for item:', data.itemId) | ||
| prompt = await toolsConfig[data.itemId].genPrompt(data.selectionText) | ||
| } else if (data.itemId in menuConfig) { | ||
| console.debug('[content] Generating prompt from menuConfig for item:', data.itemId) | ||
| const menuItem = menuConfig[data.itemId] | ||
| if (!menuItem.genPrompt) { | ||
| console.warn('[content] No genPrompt for menu item:', data.itemId) | ||
| return | ||
| } | ||
| prompt = await menuItem.genPrompt() | ||
| if (prompt) { | ||
| const preferredLanguage = await getPreferredLanguage() | ||
| prompt = await cropText(`Reply in ${preferredLanguage}.\n` + prompt) | ||
| } | ||
| } else { | ||
| console.warn('[content] Unknown itemId for CREATE_CHAT:', data.itemId) | ||
| return | ||
| } | ||
| console.debug('[content] Generated prompt:', prompt) | ||
|
|
||
| const position = data.useMenuPosition | ||
| ? { x: menuX, y: menuY } | ||
| : { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 } | ||
| const container = createElementAtPosition(position.x, position.y) | ||
| container.className = 'chatgptbox-toolbar-container-not-queryable' | ||
| const userConfig = await getUserConfig() | ||
| render( | ||
| <FloatingToolbar | ||
| session={initSession({ | ||
| modelName: userConfig.modelName, | ||
| apiMode: userConfig.apiMode, | ||
| extraCustomModelName: userConfig.customModelName, | ||
| })} | ||
| selection={data.selectionText} | ||
| container={container} | ||
| triggered={true} | ||
| closeable={true} | ||
| prompt={prompt} | ||
| />, | ||
| container, | ||
| ) | ||
| const useMenuPosition = | ||
| data.useMenuPosition && Number.isFinite(menuX) && Number.isFinite(menuY) | ||
| const position = useMenuPosition | ||
| ? { x: menuX, y: menuY } | ||
| : { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 } | ||
| console.debug('[content] Toolbar position for CREATE_CHAT:', position) | ||
| const container = createElementAtPosition(position.x, position.y) | ||
| container.className = 'chatgptbox-toolbar-container-not-queryable' | ||
| const userConfig = await getUserConfig() | ||
| render( | ||
| <FloatingToolbar | ||
| session={initSession({ | ||
| modelName: userConfig.modelName, | ||
| apiMode: userConfig.apiMode, | ||
| extraCustomModelName: userConfig.customModelName, | ||
| })} | ||
| selection={data.selectionText} | ||
| container={container} | ||
| triggered={true} | ||
| closeable={true} | ||
| prompt={prompt} | ||
| />, | ||
| container, | ||
| ) | ||
| console.log('[content] CREATE_CHAT toolbar rendered.') | ||
| } catch (error) { | ||
| console.error('[content] Error processing CREATE_CHAT message:', error, message) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| async function prepareForStaticCard() { | ||
| const userConfig = await getUserConfig() | ||
| let siteRegex | ||
| if (userConfig.useSiteRegexOnly) siteRegex = userConfig.siteRegex | ||
| else | ||
| siteRegex = new RegExp( | ||
| (userConfig.siteRegex && userConfig.siteRegex + '|') + Object.keys(siteConfig).join('|'), | ||
| ) | ||
|
|
||
| const matches = location.hostname.match(siteRegex) | ||
| if (matches) { | ||
| const siteName = matches[0] | ||
| console.log('[content] Initializing static card.') | ||
| try { | ||
| const userConfig = await getUserConfig() | ||
| let siteRegexPattern | ||
| if (userConfig.useSiteRegexOnly) { | ||
| siteRegexPattern = userConfig.siteRegex | ||
| } else { | ||
| siteRegexPattern = | ||
| (userConfig.siteRegex ? userConfig.siteRegex + '|' : '') + Object.keys(siteConfig).join('|') | ||
| } | ||
|
|
||
| if ( | ||
| userConfig.siteAdapters.includes(siteName) && | ||
| !userConfig.activeSiteAdapters.includes(siteName) | ||
| ) | ||
| if (!siteRegexPattern) { | ||
| console.debug('[content] No site regex pattern defined for static card.') | ||
| return | ||
| } | ||
| const siteRegex = new RegExp(siteRegexPattern) | ||
| console.debug('[content] Static card site regex:', siteRegex) | ||
|
|
||
| const matches = location.hostname.match(siteRegex) | ||
| if (matches) { | ||
| const siteName = matches[0] | ||
| console.log(`[content] Static card matched site: ${siteName}`) | ||
|
|
||
| let initSuccess = true | ||
| if (siteName in siteConfig) { | ||
| const siteAction = siteConfig[siteName].action | ||
| if (siteAction && siteAction.init) { | ||
| initSuccess = await siteAction.init(location.hostname, userConfig, getInput, mountComponent) | ||
| if ( | ||
| userConfig.siteAdapters.includes(siteName) && | ||
| !userConfig.activeSiteAdapters.includes(siteName) | ||
| ) { | ||
| console.log( | ||
| `[content] Site adapter for ${siteName} is installed but not active. Skipping static card.`, | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| let initSuccess = true | ||
| if (siteName in siteConfig) { | ||
| const siteAdapterAction = siteConfig[siteName].action | ||
| if (siteAdapterAction?.init) { | ||
| console.debug(`[content] Initializing site adapter action for ${siteName}.`) | ||
| initSuccess = await siteAdapterAction.init( | ||
| location.hostname, | ||
| userConfig, | ||
| getInput, | ||
| mountComponent, | ||
| ) | ||
| console.debug(`[content] Site adapter init success for ${siteName}: ${initSuccess}`) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (initSuccess) mountComponent(siteName, siteConfig[siteName]) | ||
| if (initSuccess) { | ||
| console.log(`[content] Mounting static card for site: ${siteName}`) | ||
| await mountComponent(siteName, siteConfig[siteName]) | ||
| } else { | ||
| console.warn(`[content] Static card init failed for site: ${siteName}`) | ||
| } | ||
| } else { | ||
| console.debug('[content] No static card match for current site:', location.hostname) | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error in prepareForStaticCard:', error) | ||
| } | ||
| } | ||
|
|
||
| async function overwriteAccessToken() { | ||
| if (location.hostname !== 'chatgpt.com') { | ||
| if (location.hostname === 'kimi.moonshot.cn' || location.hostname.includes('kimi.com')) { | ||
| setUserConfig({ | ||
| kimiMoonShotRefreshToken: window.localStorage.refresh_token, | ||
| }) | ||
| console.debug('[content] overwriteAccessToken called for hostname:', location.hostname) | ||
| try { | ||
| const isKimiHost = | ||
| location.hostname === 'kimi.moonshot.cn' || | ||
| location.hostname === 'kimi.com' || | ||
| location.hostname === 'www.kimi.com' | ||
| if (isKimiHost) { | ||
| console.log(`[content] On ${location.hostname}, attempting to save refresh token.`) | ||
| const refreshToken = window.localStorage.refresh_token | ||
| if (refreshToken) { | ||
| await setUserConfig({ kimiMoonShotRefreshToken: refreshToken }) | ||
| console.log('[content] Kimi Moonshot refresh token saved.') | ||
| } else { | ||
| console.warn('[content] Kimi Moonshot refresh token not found in localStorage.') | ||
| } | ||
| return | ||
| } | ||
| return | ||
| } | ||
|
|
||
| let data | ||
| if (location.pathname === '/api/auth/session') { | ||
| const response = document.querySelector('pre').textContent | ||
| try { | ||
| data = JSON.parse(response) | ||
| } catch (error) { | ||
| console.error('json error', error) | ||
| if (location.hostname !== 'chatgpt.com') { | ||
| console.debug('[content] Not on chatgpt.com, skipping access token overwrite.') | ||
| return | ||
| } | ||
|
|
||
| console.log('[content] On chatgpt.com, attempting to overwrite access token.') | ||
| let data | ||
| if (location.pathname === '/api/auth/session') { | ||
| console.debug('[content] On /api/auth/session page.') | ||
| const preElement = document.querySelector('pre') | ||
| if (preElement?.textContent) { | ||
| const response = preElement.textContent | ||
| try { | ||
| data = JSON.parse(response) | ||
| console.debug('[content] Parsed access token data from <pre> tag.') | ||
| } catch (error) { | ||
| console.error('[content] Failed to parse JSON from <pre> tag for access token:', error) | ||
| } | ||
| } else { | ||
| console.warn( | ||
| '[content] <pre> tag not found or empty for access token on /api/auth/session.', | ||
| ) | ||
| } | ||
| } else { | ||
| console.debug('[content] Not on /api/auth/session page, fetching token from API endpoint.') | ||
| try { | ||
| const resp = await fetch('https://chatgpt.com/api/auth/session') | ||
| if (resp.ok) { | ||
| data = await resp.json() | ||
| console.debug('[content] Fetched access token data from API endpoint.') | ||
| } else { | ||
| console.warn( | ||
| `[content] Failed to fetch access token, status: ${resp.status} ${resp.statusText}`, | ||
| ) | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error fetching access token from API:', error) | ||
| } | ||
| } | ||
| } else { | ||
| const resp = await fetch('https://chatgpt.com/api/auth/session') | ||
| data = await resp.json().catch(() => ({})) | ||
|
|
||
| if (data?.accessToken) { | ||
| await setAccessToken(data.accessToken) | ||
| console.log('[content] ChatGPT Access token has been set successfully from page data.') | ||
| } else { | ||
| console.warn('[content] No access token found in page data or fetch response.') | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error in overwriteAccessToken:', error) | ||
| } | ||
| if (data && data.accessToken) { | ||
| await setAccessToken(data.accessToken) | ||
| console.log(data.accessToken) | ||
| } | ||
|
|
||
| async function getClaudeSessionKey() { | ||
| console.debug('[content] getClaudeSessionKey called.') | ||
| try { | ||
| const sessionKey = await Browser.runtime.sendMessage({ | ||
| type: 'GET_COOKIE', | ||
| data: { url: 'https://claude.ai/', name: 'sessionKey' }, | ||
| }) | ||
| console.debug( | ||
| '[content] Claude session key from background:', | ||
| sessionKey ? 'found' : 'not found', | ||
| ) | ||
| return sessionKey | ||
| } catch (error) { | ||
| console.error('[content] Error in getClaudeSessionKey sending message:', error) | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| async function prepareForForegroundRequests() { | ||
| if (location.hostname !== 'chatgpt.com' || location.pathname === '/auth/login') return | ||
| async function prepareForJumpBackNotification() { | ||
| console.log('[content] Initializing jump back notification.') | ||
| try { | ||
| if ( | ||
| location.hostname === 'chatgpt.com' && | ||
| document.querySelector('button[data-testid=login-button]') | ||
| ) { | ||
| console.log('[content] ChatGPT login button found, user not logged in. Skipping jump back.') | ||
| return | ||
| } | ||
|
|
||
| const userConfig = await getUserConfig() | ||
| const url = new URL(window.location.href) | ||
| if (url.searchParams.has('chatgptbox_notification')) { | ||
| console.log('[content] chatgptbox_notification param found in URL.') | ||
|
|
||
| if ( | ||
| !chatgptWebModelKeys.some((model) => | ||
| getApiModesStringArrayFromConfig(userConfig, true).includes(model), | ||
| ) | ||
| ) | ||
| return | ||
| if (location.hostname === 'claude.ai') { | ||
| console.debug('[content] On claude.ai, checking login status.') | ||
| let claudeSession = await getClaudeSessionKey() | ||
| if (!claudeSession) { | ||
| console.log('[content] Claude session key not found, waiting for it...') | ||
| let promiseSettled = false | ||
| let timerId = null | ||
| let timeoutId = null | ||
| const cleanup = () => { | ||
| if (timerId) clearTimeout(timerId) | ||
| if (timeoutId) clearTimeout(timeoutId) | ||
| } | ||
|
|
||
| // if (location.pathname === '/') { | ||
| // const input = document.querySelector('#prompt-textarea') | ||
| // if (input) { | ||
| // input.textContent = ' ' | ||
| // input.dispatchEvent(new Event('input', { bubbles: true })) | ||
| // setTimeout(() => { | ||
| // input.textContent = '' | ||
| // input.dispatchEvent(new Event('input', { bubbles: true })) | ||
| // }, 300) | ||
| // } | ||
| // } | ||
|
|
||
| await Browser.runtime.sendMessage({ | ||
| type: 'SET_CHATGPT_TAB', | ||
| data: {}, | ||
| }) | ||
| try { | ||
| await new Promise((resolve, reject) => { | ||
| const poll = async () => { | ||
| if (promiseSettled) return | ||
| try { | ||
| claudeSession = await getClaudeSessionKey() | ||
| if (claudeSession && !promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.log('[content] Claude session key found after waiting.') | ||
| resolve() | ||
| return | ||
| } | ||
| } catch (err) { | ||
| console.error('[content] Error polling for Claude session key:', err) | ||
| const rawMessage = | ||
| typeof err?.message === 'string' ? err.message : String(err ?? '') | ||
| const errMsg = rawMessage.toLowerCase() | ||
| const isNetworkError = /\bnetwork\b/.test(errMsg) | ||
| const isPermissionError = /\bpermission\b/.test(errMsg) | ||
| if ((isNetworkError || isPermissionError) && !promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| reject(new Error(`Failed to get Claude session key due to: ${rawMessage}`)) | ||
| return | ||
| } | ||
| } | ||
| if (!promiseSettled) { | ||
| timerId = setTimeout(poll, 500) | ||
| } | ||
| } | ||
|
|
||
| registerPortListener(async (session, port) => { | ||
| if (isUsingChatgptWebModel(session)) { | ||
| const accessToken = await getChatGptAccessToken() | ||
| await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken) | ||
| } | ||
| }) | ||
| } | ||
| poll() | ||
|
|
||
| async function getClaudeSessionKey() { | ||
| return Browser.runtime.sendMessage({ | ||
| type: 'GET_COOKIE', | ||
| data: { url: 'https://claude.ai/', name: 'sessionKey' }, | ||
| }) | ||
| } | ||
| timeoutId = setTimeout(() => { | ||
| if (!promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.warn('[content] Timed out waiting for Claude session key.') | ||
| reject(new Error('Timed out waiting for Claude session key.')) | ||
| } | ||
| }, 30000) | ||
| }) | ||
| } catch (err) { | ||
| console.error( | ||
| '[content] Failed to get Claude session key for jump back notification:', | ||
| err, | ||
| ) | ||
| return | ||
| } | ||
| } else { | ||
| console.log('[content] Claude session key found immediately.') | ||
| } | ||
| } | ||
|
|
||
| async function prepareForJumpBackNotification() { | ||
| if ( | ||
| location.hostname === 'chatgpt.com' && | ||
| document.querySelector('button[data-testid=login-button]') | ||
| ) { | ||
| console.log('chatgpt not logged in') | ||
| return | ||
| } | ||
| const isKimiHost = | ||
| location.hostname === 'kimi.moonshot.cn' || | ||
| location.hostname === 'kimi.com' || | ||
| location.hostname === 'www.kimi.com' | ||
| if (isKimiHost) { | ||
| console.debug('[content] On Kimi host, checking login status.') | ||
| if (!window.localStorage.refresh_token) { | ||
| console.log('[content] Kimi refresh token not found, attempting to trigger login.') | ||
| setTimeout(() => { | ||
| try { | ||
| const loginContainer = document.querySelector('.user-info-container') | ||
| if (!loginContainer) { | ||
| console.warn('[content] Kimi login container not found, skipping auto-click.') | ||
| return | ||
| } | ||
| console.log('[content] Clicking Kimi login container.') | ||
| loginContainer.click() | ||
| } catch (err_click) { | ||
| console.error('[content] Error clicking Kimi login container:', err_click) | ||
| } | ||
| }, 1000) | ||
|
|
||
| const url = new URL(window.location.href) | ||
| if (url.searchParams.has('chatgptbox_notification')) { | ||
| if (location.hostname === 'claude.ai' && !(await getClaudeSessionKey())) { | ||
| console.log('claude not logged in') | ||
|
|
||
| await new Promise((resolve) => { | ||
| const timer = setInterval(async () => { | ||
| const token = await getClaudeSessionKey() | ||
| if (token) { | ||
| clearInterval(timer) | ||
| resolve() | ||
| let promiseSettled = false | ||
| let timerId = null | ||
| let timeoutId = null | ||
| const cleanup = () => { | ||
| if (timerId) clearTimeout(timerId) | ||
| if (timeoutId) clearTimeout(timeoutId) | ||
| } | ||
| }, 500) | ||
| }) | ||
| } | ||
|
|
||
| if ( | ||
| (location.hostname === 'kimi.moonshot.cn' || location.hostname.includes('kimi.com')) && | ||
| !window.localStorage.refresh_token | ||
| ) { | ||
| console.log('kimi not logged in') | ||
| setTimeout(() => { | ||
| document.querySelector('.user-info-container').click() | ||
| }, 1000) | ||
|
|
||
| await new Promise((resolve) => { | ||
| const timer = setInterval(() => { | ||
| const token = window.localStorage.refresh_token | ||
| if (token) { | ||
| setUserConfig({ | ||
| kimiMoonShotRefreshToken: token, | ||
| try { | ||
| await new Promise((resolve, reject) => { | ||
| const poll = async () => { | ||
| if (promiseSettled) return | ||
| try { | ||
| const token = window.localStorage.refresh_token | ||
| if (token) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.log('[content] Kimi refresh token found after waiting.') | ||
| await setUserConfig({ kimiMoonShotRefreshToken: token }) | ||
| console.log('[content] Kimi refresh token saved to config.') | ||
| resolve() | ||
| return | ||
| } | ||
| } catch (err_set) { | ||
| console.error('[content] Error setting Kimi refresh token from polling:', err_set) | ||
| // Do not reject on polling error, let timeout handle failure. | ||
| } | ||
| if (!promiseSettled) { | ||
| timerId = setTimeout(poll, 500) | ||
| } | ||
| } | ||
|
|
||
| poll() | ||
|
|
||
| timeoutId = setTimeout(() => { | ||
| if (!promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.warn('[content] Timed out waiting for Kimi refresh token.') | ||
| reject(new Error('Timed out waiting for Kimi refresh token.')) | ||
| } | ||
| }, 30000) | ||
| }) | ||
| clearInterval(timer) | ||
| resolve() | ||
| } catch (err) { | ||
| console.error( | ||
| '[content] Failed to get Kimi refresh token for jump back notification:', | ||
| err, | ||
| ) | ||
| return | ||
| } | ||
| }, 500) | ||
| } else { | ||
| console.log('[content] Kimi refresh token found in localStorage.') | ||
| await setUserConfig({ kimiMoonShotRefreshToken: window.localStorage.refresh_token }) | ||
| } | ||
| } | ||
|
|
||
| console.log('[content] Rendering WebJumpBackNotification.') | ||
| const div = document.createElement('div') | ||
| document.body.append(div) | ||
| render( | ||
| <WebJumpBackNotification | ||
| container={div} | ||
| chatgptMode={location.hostname === 'chatgpt.com'} | ||
| />, | ||
| div, | ||
| ) | ||
| console.log('[content] WebJumpBackNotification rendered.') | ||
| } else { | ||
| console.debug('[content] No chatgptbox_notification param in URL.') | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error in prepareForJumpBackNotification:', error) | ||
| } | ||
| } | ||
|
|
||
| async function run() { | ||
| console.log('[content] Script run started.') | ||
| try { | ||
| await getPreferredLanguageKey() | ||
| .then((lang) => { | ||
| console.log(`[content] Setting language to: ${lang}`) | ||
| changeLanguage(lang) | ||
| }) | ||
| .catch((err) => console.error('[content] Error setting preferred language:', err)) | ||
|
|
||
| Browser.runtime.onMessage.addListener(async (message) => { | ||
| console.debug('[content] Received runtime message:', message) | ||
| try { | ||
| if (message.type === 'CHANGE_LANG') { | ||
| console.log('[content] Processing CHANGE_LANG message:', message.data) | ||
| changeLanguage(message.data.lang) | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error in global runtime.onMessage listener:', error, message) | ||
| } | ||
| }) | ||
|
|
||
| await overwriteAccessToken() | ||
| const isChatGptHost = location.hostname === 'chatgpt.com' | ||
| if (isChatGptHost) { | ||
| await manageChatGptTabState() | ||
|
|
||
| Browser.storage.onChanged.addListener(async (changes, areaName) => { | ||
| console.debug('[content] Storage changed:', changes, 'in area:', areaName) | ||
| try { | ||
| if (areaName === 'local' && (changes.userConfig || changes.config)) { | ||
| console.log( | ||
| '[content] User config changed in storage, re-evaluating ChatGPT tab state.', | ||
| ) | ||
| await manageChatGptTabState() | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error in storage.onChanged listener:', error) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| const div = document.createElement('div') | ||
| document.body.append(div) | ||
| render( | ||
| <WebJumpBackNotification container={div} chatgptMode={location.hostname === 'chatgpt.com'} />, | ||
| div, | ||
| ) | ||
| await prepareForSelectionTools() | ||
| await prepareForSelectionToolsTouch() | ||
| prepareForStaticCard().catch((error) => { | ||
| console.error('[content] Error in prepareForStaticCard (unhandled):', error) | ||
| }) | ||
| await prepareForRightClickMenu() | ||
| prepareForJumpBackNotification().catch((error) => { | ||
| console.error('[content] Error in prepareForJumpBackNotification (unhandled):', error) | ||
| }) | ||
|
|
||
| console.log('[content] Script run completed successfully.') | ||
| } catch (error) { | ||
| console.error('[content] Error in run function:', error) | ||
| } | ||
| } | ||
|
|
||
| async function run() { | ||
| await getPreferredLanguageKey().then((lang) => { | ||
| changeLanguage(lang) | ||
| }) | ||
| Browser.runtime.onMessage.addListener(async (message) => { | ||
| if (message.type === 'CHANGE_LANG') { | ||
| const data = message.data | ||
| changeLanguage(data.lang) | ||
| let manageChatGptTabStatePromise = null | ||
| let chatGPTBoxPortListenerRegistered = false | ||
|
|
||
| async function manageChatGptTabState() { | ||
| if (manageChatGptTabStatePromise) { | ||
| console.debug('[content] manageChatGptTabState already running, waiting for in-flight call.') | ||
| return manageChatGptTabStatePromise | ||
| } | ||
|
|
||
| manageChatGptTabStatePromise = (async () => { | ||
| console.debug('[content] manageChatGptTabState called. Current location:', location.href) | ||
| try { | ||
| if (location.hostname !== 'chatgpt.com' || location.pathname === '/auth/login') { | ||
| console.debug( | ||
| '[content] Not on main chatgpt.com page, skipping manageChatGptTabState logic.', | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| const userConfig = await getUserConfig() | ||
| const isThisTabDesignatedForChatGptWeb = chatgptWebModelKeys.some((model) => | ||
| getApiModesStringArrayFromConfig(userConfig, true).includes(model), | ||
| ) | ||
| console.debug( | ||
| '[content] Is this tab designated for ChatGPT Web:', | ||
| isThisTabDesignatedForChatGptWeb, | ||
| ) | ||
|
|
||
| if (isThisTabDesignatedForChatGptWeb) { | ||
| if (location.pathname === '/') { | ||
| console.debug('[content] On chatgpt.com root path.') | ||
| const input = document.querySelector('#prompt-textarea') | ||
| if (input && input.value === '') { | ||
| console.log('[content] Manipulating #prompt-textarea for focus.') | ||
| if (document.activeElement === input) { | ||
| console.debug('[content] #prompt-textarea already focused; skipping injection.') | ||
| } else { | ||
| const injectedValue = ' ' | ||
| input.value = injectedValue | ||
| input.dispatchEvent(new Event('input', { bubbles: true })) | ||
| setTimeout(() => { | ||
| const currentInput = document.querySelector('#prompt-textarea') | ||
| if (!currentInput?.isConnected) { | ||
| console.warn( | ||
| '[content] #prompt-textarea no longer available in setTimeout callback.', | ||
| ) | ||
| return | ||
| } | ||
| if (document.activeElement === currentInput) { | ||
| console.debug('[content] #prompt-textarea focused; skipping injection cleanup.') | ||
| return | ||
| } | ||
| if (currentInput.value === injectedValue) { | ||
| currentInput.value = '' | ||
| currentInput.dispatchEvent(new Event('input', { bubbles: true })) | ||
| console.debug('[content] #prompt-textarea manipulation complete.') | ||
| } else if (currentInput.value.startsWith(injectedValue)) { | ||
| currentInput.value = currentInput.value.slice(injectedValue.length) | ||
| currentInput.dispatchEvent(new Event('input', { bubbles: true })) | ||
| console.debug('[content] Removed injected leading space from #prompt-textarea.') | ||
| } | ||
| }, 300) | ||
| } | ||
| } else { | ||
| console.debug( | ||
| '[content] #prompt-textarea not found, not empty (value: "' + | ||
| input?.value + | ||
| '"), or not on root path for manipulation.', | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| console.log('[content] Sending SET_CHATGPT_TAB message.') | ||
| await Browser.runtime.sendMessage({ | ||
| type: 'SET_CHATGPT_TAB', | ||
| data: {}, | ||
| }) | ||
| console.log('[content] SET_CHATGPT_TAB message sent successfully.') | ||
| } else { | ||
| console.log('[content] This tab is NOT configured for ChatGPT Web model processing.') | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error in manageChatGptTabState:', error) | ||
| } | ||
| }) | ||
| })() | ||
|
|
||
| await overwriteAccessToken() | ||
| await prepareForForegroundRequests() | ||
| try { | ||
| await manageChatGptTabStatePromise | ||
| } finally { | ||
| manageChatGptTabStatePromise = null | ||
| } | ||
| } | ||
|
|
||
| prepareForSelectionTools() | ||
| prepareForSelectionToolsTouch() | ||
| prepareForStaticCard() | ||
| prepareForRightClickMenu() | ||
| prepareForJumpBackNotification() | ||
| if (!chatGPTBoxPortListenerRegistered) { |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initialization flags (selectionToolsInitialized, selectionToolsTouchInitialized, rightClickMenuInitialized, chatGPTBoxPortListenerRegistered) prevent re-initialization, but they're never reset. This means if the script is reinjected or reloaded in the same context, these features won't be re-initialized, potentially leading to broken functionality. Consider whether these should be reset under certain conditions or if the script lifecycle guarantees they're only needed once.
| const delay = | ||
| Math.pow(RECONNECT_CONFIG.BACKOFF_MULTIPLIER, port._reconnectAttempts - 1) * | ||
| RECONNECT_CONFIG.BASE_DELAY_MS |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exponential backoff delay calculation uses Math.pow with attempt numbers, but there's no maximum delay cap. With MAX_ATTEMPTS of 5 and a backoff multiplier of 2, the delays will be 1s, 2s, 4s, 8s, which is reasonable. However, if these constants are ever changed to larger values, the delay could become excessively long. Consider adding a maximum delay cap for robustness.
| if (userConfig.triggerMode === 'always') triggered = true | ||
| else if ( | ||
| userConfig.triggerMode === 'questionMark' && | ||
| question && |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition checks if endsWithQuestionMark(question.trim()) but on line 120 the question variable is already checked for truthiness in the condition. However, looking at line 119-122, if question is falsy, the second part of the OR won't execute due to short-circuit evaluation, so this is safe. That said, for code clarity and to prevent potential issues if the code is refactored, consider adding an explicit null/undefined check before calling trim().
| question && | |
| typeof question === 'string' && |
| const hasMatchingProxy = Boolean(port.proxy && port._proxyTabId === tabId) | ||
| if (!hasMatchingProxy) { | ||
| if (port.proxy) { | ||
| console.debug( | ||
| `[background] Existing proxy tab ${port._proxyTabId} does not match ${tabId}; reconnecting.`, | ||
| ) | ||
| } else { | ||
| console.debug('[background] port.proxy not found, calling setPortProxy.') | ||
| } | ||
| setPortProxy(port, tabId) | ||
| } | ||
| if (port.proxy && port._proxyTabId === tabId) { | ||
| if (hasMatchingProxy) { | ||
| console.debug('[background] Proxy already established; forwarding session.') | ||
| } | ||
| console.debug('[background] Posting message to proxy tab:', { session: redactedSession }) | ||
| try { | ||
| port.proxy.postMessage({ session }) | ||
| } catch (e) { | ||
| console.warn( | ||
| '[background] Error posting message to existing proxy tab in executeApi (ChatGPT Web Model):', | ||
| e, | ||
| '. Attempting to reconnect.', | ||
| { session: redactedSession }, | ||
| ) | ||
| setPortProxy(port, tabId) | ||
| if (port.proxy) { | ||
| console.debug('[background] Proxy re-established. Attempting to post message again.') | ||
| try { | ||
| port.proxy.postMessage({ session }) | ||
| console.info('[background] Successfully posted session after proxy reconnection.') | ||
| } catch (e2) { | ||
| console.error( | ||
| '[background] Error posting message even after proxy reconnection:', | ||
| e2, | ||
| { session: redactedSession }, | ||
| ) | ||
| try { | ||
| port.postMessage({ | ||
| error: | ||
| 'Failed to communicate with ChatGPT tab after reconnection attempt. Try refreshing the page.', | ||
| }) | ||
| } catch (notifyError) { | ||
| console.error( | ||
| '[background] Error sending final communication failure notification back:', | ||
| notifyError, | ||
| ) | ||
| } | ||
| } | ||
| } else { | ||
| console.error( | ||
| '[background] Failed to re-establish proxy connection. Cannot send session.', | ||
| ) | ||
| try { | ||
| port.postMessage({ | ||
| error: | ||
| 'Could not re-establish connection to ChatGPT tab. Try refreshing the page.', | ||
| }) | ||
| } catch (notifyError) { | ||
| console.error( | ||
| '[background] Error sending re-establishment failure notification back:', | ||
| notifyError, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| console.error( | ||
| '[background] Failed to send message: port.proxy is still not available after initial setPortProxy attempt.', | ||
| ) | ||
| try { | ||
| port.postMessage({ | ||
| error: 'Failed to initialize connection to ChatGPT tab. Try refreshing the page.', | ||
| }) | ||
| } catch (notifyError) { | ||
| console.error( | ||
| '[background] Error sending initial connection failure notification back:', | ||
| notifyError, | ||
| ) | ||
| } | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition in the proxy setup logic. Between checking hasMatchingProxy on line 425 and calling setPortProxy on line 434, the proxy state could change (e.g., if a disconnect happens). Then on line 436, the code checks if port.proxy exists and matches the tabId, but this check happens after setPortProxy which is synchronous at the top level but sets up async reconnection logic. If setPortProxy fails to establish the connection immediately, the condition on line 436 might be false, leading to the error path on line 492. Consider making the proxy establishment more atomic or adding explicit waiting for the connection to be established.
| console.log( | ||
| '[content] Session is for ChatGPT Web Model, processing request for question:', | ||
| session.question, | ||
| ) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code logs the full session.question at line 1006, which could contain sensitive user queries or personal information. While the background script has a redaction mechanism for sensitive fields, the content script doesn't use it. User questions might contain personal data, passwords, or other sensitive information that shouldn't be logged. Consider either redacting sensitive parts of the question or reducing the logging level for production builds.
|
Codex Review: Didn't find any major issues. Swish! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
58bb91d to
ac9d6e7
Compare
|
@codex review |
There was a problem hiding this comment.
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 2 out of 2 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try { | ||
| port.proxy.postMessage({ session }) | ||
| } catch (e) { | ||
| console.warn( | ||
| '[background] Error posting message to existing proxy tab in executeApi (ChatGPT Web Model):', | ||
| e, | ||
| '. Attempting to reconnect.', | ||
| { session: redactedSession }, | ||
| ) | ||
| setPortProxy(port, tabId) | ||
| if (port.proxy) { | ||
| console.debug('[background] Proxy re-established. Attempting to post message again.') | ||
| try { | ||
| port.proxy.postMessage({ session }) | ||
| console.info('[background] Successfully posted session after proxy reconnection.') | ||
| } catch (e2) { | ||
| console.error( | ||
| '[background] Error posting message even after proxy reconnection:', | ||
| e2, | ||
| { session: redactedSession }, | ||
| ) | ||
| try { | ||
| port.postMessage({ | ||
| error: | ||
| 'Failed to communicate with ChatGPT tab after reconnection attempt. Try refreshing the page.', | ||
| }) | ||
| } catch (notifyError) { | ||
| console.error( | ||
| '[background] Error sending final communication failure notification back:', | ||
| notifyError, | ||
| ) | ||
| } | ||
| } | ||
| } else { | ||
| console.error( | ||
| '[background] Failed to re-establish proxy connection. Cannot send session.', | ||
| ) | ||
| try { | ||
| port.postMessage({ | ||
| error: | ||
| 'Could not re-establish connection to ChatGPT tab. Try refreshing the page.', | ||
| }) | ||
| } catch (notifyError) { | ||
| console.error( | ||
| '[background] Error sending re-establishment failure notification back:', | ||
| notifyError, | ||
| ) | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the ChatGPT Web Model handling, when the first attempt to post a message fails, the code calls setPortProxy to reconnect and then attempts to post the message again. However, if setPortProxy itself throws an error or fails to establish the proxy (e.g., the tab was closed), the second attempt will also fail. The nested error handling here is complex, and the fallback logic might not handle all error scenarios correctly. Consider simplifying this logic or adding a maximum retry count to prevent infinite loops in edge cases.
| if (details.requestBody?.formData) { | ||
| for (const k in details.requestBody.formData) { | ||
| const values = details.requestBody.formData[k] | ||
| if (Array.isArray(values)) { | ||
| for (const value of values) { | ||
| formData.append(k, value) | ||
| } | ||
| } else if (values != null) { | ||
| formData.append(k, values) | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Arkose form data handling has been improved to handle array values, but there's a potential issue. When details.requestBody.formData[k] contains multiple values (array), the code appends each value separately. However, the URLSearchParams API will serialize this as multiple parameters with the same key. When reconstructing the form data, this might not match the original request format if the server expects a different encoding for array values. Verify that this serialization approach is compatible with the Arkose API's expectations.
| if (location.pathname === '/') { | ||
| console.debug('[content] On chatgpt.com root path.') | ||
| const input = document.querySelector('#prompt-textarea') | ||
| if (input && input.value === '') { | ||
| console.log('[content] Manipulating #prompt-textarea for focus.') | ||
| if (document.activeElement === input) { | ||
| console.debug('[content] #prompt-textarea already focused; skipping injection.') | ||
| } else { | ||
| const injectedValue = ' ' | ||
| input.value = injectedValue | ||
| input.dispatchEvent(new Event('input', { bubbles: true })) | ||
| setTimeout(() => { | ||
| const currentInput = document.querySelector('#prompt-textarea') | ||
| if (!currentInput?.isConnected) { | ||
| console.warn( | ||
| '[content] #prompt-textarea no longer available in setTimeout callback.', | ||
| ) | ||
| return | ||
| } | ||
| if (document.activeElement === currentInput) { | ||
| console.debug('[content] #prompt-textarea focused; skipping injection cleanup.') | ||
| return | ||
| } | ||
| if (currentInput.value === injectedValue) { | ||
| currentInput.value = '' | ||
| currentInput.dispatchEvent(new Event('input', { bubbles: true })) | ||
| console.debug('[content] #prompt-textarea manipulation complete.') | ||
| } else if (currentInput.value.startsWith(injectedValue)) { | ||
| currentInput.value = currentInput.value.slice(injectedValue.length) | ||
| currentInput.dispatchEvent(new Event('input', { bubbles: true })) | ||
| console.debug('[content] Removed injected leading space from #prompt-textarea.') | ||
| } | ||
| }, 300) | ||
| } | ||
| } else { | ||
| console.debug( | ||
| '[content] #prompt-textarea not found, not empty (value: "' + | ||
| input?.value + | ||
| '"), or not on root path for manipulation.', | ||
| ) | ||
| } | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The prompt textarea manipulation logic in manageChatGptTabState has a race condition. Between checking if the textarea is focused in the outer check and the setTimeout callback, the user could have interacted with the page. The code checks document.activeElement === input twice but doesn't prevent concurrent executions. If manageChatGptTabState is called multiple times rapidly (e.g., from storage change events), multiple setTimeout callbacks could be queued, leading to unexpected behavior. Consider using the manageChatGptTabStatePromise guard more comprehensively or adding a flag to prevent concurrent textarea manipulations.
| port._reconnectAttempts = (port._reconnectAttempts || 0) + 1 | ||
| if (port._reconnectAttempts >= RECONNECT_CONFIG.MAX_ATTEMPTS) { | ||
| console.error( | ||
| `[background] Max reconnect attempts (${RECONNECT_CONFIG.MAX_ATTEMPTS}) reached for tab ${proxyTabId}. Giving up.`, | ||
| ) | ||
| if (port._portOnMessage) { | ||
| try { | ||
| port.onMessage.removeListener(port._portOnMessage) | ||
| } catch (e) { | ||
| console.warn('[background] Error removing _portOnMessage on max retries:', e) | ||
| } | ||
| } | ||
| if (port._portOnDisconnect) { | ||
| try { | ||
| port.onDisconnect.removeListener(port._portOnDisconnect) | ||
| } catch (e) { | ||
| console.warn('[background] Error removing _portOnDisconnect on max retries:', e) | ||
| } | ||
| } | ||
| try { | ||
| port.postMessage({ | ||
| error: `Connection to ChatGPT tab lost after ${RECONNECT_CONFIG.MAX_ATTEMPTS} attempts. Please refresh the page.`, | ||
| }) | ||
| } catch (e) { | ||
| console.warn('[background] Error sending final error message on max retries:', e) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| const delay = | ||
| Math.pow(RECONNECT_CONFIG.BACKOFF_MULTIPLIER, port._reconnectAttempts - 1) * | ||
| RECONNECT_CONFIG.BASE_DELAY_MS | ||
| console.log( | ||
| `[background] Attempting reconnect #${port._reconnectAttempts} in ${ | ||
| delay / 1000 | ||
| }s for tab ${proxyTabId}.`, | ||
| ) | ||
|
|
||
| port._reconnectTimerId = setTimeout(async () => { | ||
| if (port._isClosed) { | ||
| console.debug('[background] Main port closed; skipping proxy reconnect.') | ||
| return | ||
| } | ||
| port._reconnectTimerId = null | ||
| try { | ||
| await Browser.tabs.get(proxyTabId) | ||
| } catch (error) { | ||
| console.warn( | ||
| `[background] Proxy tab ${proxyTabId} no longer exists. Aborting reconnect.`, | ||
| error, | ||
| ) | ||
| return | ||
| } | ||
| console.debug( | ||
| `[background] Retrying connection to tab ${proxyTabId}, attempt ${port._reconnectAttempts}.`, | ||
| ) | ||
| try { | ||
| setPortProxy(port, proxyTabId) | ||
| } catch (error) { | ||
| console.warn(`[background] Error reconnecting to tab ${proxyTabId}:`, error) | ||
| } | ||
| }, delay) | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reconnection logic in setPortProxy schedules reconnection attempts even when the main port is still open. The check if (port._isClosed) at line 279 prevents reconnection after the main port closes, but there's no mechanism to cancel pending reconnection timers if the proxy successfully reconnects through other means (e.g., if another caller invokes setPortProxy before the timer fires). This could lead to redundant reconnection attempts. Consider storing and clearing the reconnection timer reference more carefully, or checking if the proxy is already connected when the timer fires.
| if (tabId) { | ||
| console.debug(`[background] ChatGPT Tab ID ${tabId} found.`) | ||
| const hasMatchingProxy = Boolean(port.proxy && port._proxyTabId === tabId) | ||
| if (!hasMatchingProxy) { | ||
| if (port.proxy) { | ||
| console.debug( | ||
| `[background] Existing proxy tab ${port._proxyTabId} does not match ${tabId}; reconnecting.`, | ||
| ) | ||
| } else { | ||
| console.debug('[background] port.proxy not found, calling setPortProxy.') | ||
| } | ||
| setPortProxy(port, tabId) | ||
| } | ||
| if (port.proxy && port._proxyTabId === tabId) { | ||
| if (hasMatchingProxy) { | ||
| console.debug('[background] Proxy already established; forwarding session.') | ||
| } | ||
| console.debug('[background] Posting message to proxy tab:', { session: redactedSession }) | ||
| try { | ||
| port.proxy.postMessage({ session }) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the PR description, this change ensures messages are sent to the ChatGPT tab even when port.proxy already exists. However, the logic at lines 436-442 will always attempt to send the message when port.proxy exists and matches the tabId, regardless of whether hasMatchingProxy was true initially. This is the intended fix, but the surrounding conditional logic with hasMatchingProxy is now somewhat redundant since the message is posted regardless of whether the proxy was just created or already existed. Consider simplifying this logic for clarity.
| const config = await getUserConfig() | ||
| if (config.kimiMoonShotRefreshToken) { | ||
| await setUserConfig({ kimiMoonShotRefreshToken: '' }) | ||
| console.log('[content] Kimi Moonshot refresh token cleared.') | ||
| } | ||
| console.warn('[content] Kimi Moonshot refresh token not found in localStorage.') | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the overwriteAccessToken function for Kimi hosts, when the refresh token is not found, the code clears the stored token only if it was previously set. However, the check if (config.kimiMoonShotRefreshToken) at line 590 could be bypassed if the config is an empty string (which is falsy). Consider using if (config.kimiMoonShotRefreshToken !== undefined && config.kimiMoonShotRefreshToken !== null) to be more explicit, or simply always call setUserConfig to ensure consistency.
There was a problem hiding this comment.
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: ac9d6e7ddd
ℹ️ 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".
| Browser.storage.onChanged.addListener(async (changes, areaName) => { | ||
| console.debug('[content] Storage changed:', changes, 'in area:', areaName) | ||
| try { | ||
| if (areaName === 'local' && (changes.userConfig || changes.config)) { | ||
| console.log( | ||
| '[content] User config changed in storage, re-evaluating ChatGPT tab state.', | ||
| ) | ||
| await manageChatGptTabState() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-evaluate ChatGPT tab state on actual config keys
The new storage.onChanged handler only runs when changes.userConfig or changes.config exist, but setUserConfig writes individual keys (e.g., modelName, apiMode, chatgptTabId) into storage.local, so those properties never appear. In practice, changing model settings in the popup while staying on chatgpt.com won’t trigger manageChatGptTabState, so the ChatGPT tab won’t be re-registered until a reload. Consider checking for any relevant key changes (or simply any areaName === 'local') instead of changes.userConfig/config.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
ac9d6e7 to
2294ea4
Compare
|
@codex review |
There was a problem hiding this comment.
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 2 out of 2 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (selectionToolsInitialized) { | ||
| console.debug('[content] Selection tools already initialized, skipping.') | ||
| return | ||
| } | ||
| selectionToolsInitialized = true |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for checking if selectionToolsInitialized prevents re-initialization, but the flag is set at the module level and never reset. If the content script is reloaded or re-injected (which can happen in single-page applications or when the extension is updated), the event listeners won't be registered again because the flag remains true. Consider resetting this flag when appropriate or using a more robust initialization check.
| setTimeout(() => { | ||
| try { | ||
| if (!window.getSelection()?.toString().trim()) { | ||
| console.debug('[content] No selection after keydown, deleting toolbar.') | ||
| deleteToolbar() | ||
| } | ||
| } catch (err_inner) { | ||
| console.error('[content] Error in keydown setTimeout callback:', err_inner) | ||
| } | ||
| }, 0) |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple setTimeout callbacks use arrow functions with additional try-catch blocks nested inside them (e.g., lines 354-363, 956-977). This creates deeply nested error handling that makes the code harder to read. Consider extracting these callback functions into named functions with their own error handling, which would improve readability and make the error handling structure clearer.
| if (selectionToolsTouchInitialized) { | ||
| console.debug('[content] Touch selection tools already initialized, skipping.') | ||
| return | ||
| } | ||
| selectionToolsTouchInitialized = true |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar issue as with selectionToolsInitialized - the selectionToolsTouchInitialized flag is set at module level and never reset. This prevents re-initialization of touch event listeners if the content script is reloaded or re-injected, which can happen in SPAs or during extension updates.
| try { | ||
| await new Promise((resolve, reject) => { | ||
| const poll = async () => { | ||
| if (promiseSettled) return | ||
| try { | ||
| const token = window.localStorage.refresh_token | ||
| if (token) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.log('[content] Kimi refresh token found after waiting.') | ||
| await setUserConfig({ kimiMoonShotRefreshToken: token }) | ||
| console.log('[content] Kimi refresh token saved to config.') | ||
| resolve() | ||
| return | ||
| } | ||
| } catch (err_set) { | ||
| console.error('[content] Error setting Kimi refresh token from polling:', err_set) | ||
| // Do not reject on polling error, let timeout handle failure. | ||
| } | ||
| if (!promiseSettled) { | ||
| timerId = setTimeout(poll, 500) | ||
| } | ||
| } | ||
|
|
||
| poll() | ||
|
|
||
| timeoutId = setTimeout(() => { | ||
| if (!promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.warn('[content] Timed out waiting for Kimi refresh token.') | ||
| reject(new Error('Timed out waiting for Kimi refresh token.')) | ||
| } | ||
| }, 30000) | ||
| }) |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The polling logic for Kimi refresh token has nearly identical structure to the Claude polling logic (lines 696-738), creating significant code duplication. Both implement the same polling pattern with promiseSettled flag, cleanup function, and timeout handling. Consider extracting this into a reusable utility function like 'pollForCondition(checkFn, timeoutMs, pollIntervalMs)' to improve maintainability and reduce the risk of bugs.
| try { | ||
| await new Promise((resolve, reject) => { | ||
| const poll = async () => { | ||
| if (promiseSettled) return | ||
| try { | ||
| claudeSession = await getClaudeSessionKey() | ||
| if (claudeSession && !promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.log('[content] Claude session key found after waiting.') | ||
| resolve() | ||
| return | ||
| } | ||
| } catch (err) { | ||
| console.error('[content] Error polling for Claude session key:', err) | ||
| const rawMessage = | ||
| typeof err?.message === 'string' ? err.message : String(err ?? '') | ||
| const errMsg = rawMessage.toLowerCase() | ||
| const isNetworkError = /\bnetwork\b/.test(errMsg) | ||
| const isPermissionError = /\bpermission\b/.test(errMsg) | ||
| if ((isNetworkError || isPermissionError) && !promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| reject(new Error(`Failed to get Claude session key due to: ${rawMessage}`)) | ||
| return | ||
| } | ||
| } | ||
| if (!promiseSettled) { | ||
| timerId = setTimeout(poll, 500) | ||
| } | ||
| } |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The polling implementation for Claude session key uses a recursive setTimeout pattern with error handling that rejects on network/permission errors. However, this pattern has a subtle issue: if an error occurs during poll(), it logs the error and checks if it's a network/permission error, but continues polling otherwise. This means transient errors could accumulate in the console. Consider adding a maximum error count threshold before giving up, or improving the error categorization logic.
| console.log('[content] Manipulating #prompt-textarea for focus.') | ||
| if (document.activeElement === input) { | ||
| console.debug('[content] #prompt-textarea already focused; skipping injection.') | ||
| } else { | ||
| const injectedValue = ' ' | ||
| input.value = injectedValue | ||
| input.dispatchEvent(new Event('input', { bubbles: true })) | ||
| setTimeout(() => { | ||
| const currentInput = document.querySelector('#prompt-textarea') | ||
| if (!currentInput?.isConnected) { | ||
| console.warn( | ||
| '[content] #prompt-textarea no longer available in setTimeout callback.', | ||
| ) | ||
| return | ||
| } | ||
| if (document.activeElement === currentInput) { | ||
| console.debug('[content] #prompt-textarea focused; skipping injection cleanup.') | ||
| return | ||
| } | ||
| if (currentInput.value === injectedValue) { | ||
| currentInput.value = '' | ||
| currentInput.dispatchEvent(new Event('input', { bubbles: true })) | ||
| console.debug('[content] #prompt-textarea manipulation complete.') | ||
| } else if (currentInput.value.startsWith(injectedValue)) { | ||
| currentInput.value = currentInput.value.slice(injectedValue.length) | ||
| currentInput.dispatchEvent(new Event('input', { bubbles: true })) | ||
| console.debug('[content] Removed injected leading space from #prompt-textarea.') | ||
| } | ||
| }, 300) | ||
| } | ||
| } else { | ||
| console.debug( | ||
| '[content] #prompt-textarea not found, not empty (value: "' + | ||
| input?.value + | ||
| '"), or not on root path for manipulation.', |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manageChatGptTabState function manipulates the #prompt-textarea element by injecting a space and then removing it after 300ms to work around focus issues. This approach is fragile and could interfere with user input or cause race conditions if the user interacts with the textarea during this window. The logic also has multiple nested checks for element availability and focus state. Consider using a more robust approach like directly calling focus() on the element or using the Textarea API if available, rather than manipulating its value.
| console.log('[content] Manipulating #prompt-textarea for focus.') | |
| if (document.activeElement === input) { | |
| console.debug('[content] #prompt-textarea already focused; skipping injection.') | |
| } else { | |
| const injectedValue = ' ' | |
| input.value = injectedValue | |
| input.dispatchEvent(new Event('input', { bubbles: true })) | |
| setTimeout(() => { | |
| const currentInput = document.querySelector('#prompt-textarea') | |
| if (!currentInput?.isConnected) { | |
| console.warn( | |
| '[content] #prompt-textarea no longer available in setTimeout callback.', | |
| ) | |
| return | |
| } | |
| if (document.activeElement === currentInput) { | |
| console.debug('[content] #prompt-textarea focused; skipping injection cleanup.') | |
| return | |
| } | |
| if (currentInput.value === injectedValue) { | |
| currentInput.value = '' | |
| currentInput.dispatchEvent(new Event('input', { bubbles: true })) | |
| console.debug('[content] #prompt-textarea manipulation complete.') | |
| } else if (currentInput.value.startsWith(injectedValue)) { | |
| currentInput.value = currentInput.value.slice(injectedValue.length) | |
| currentInput.dispatchEvent(new Event('input', { bubbles: true })) | |
| console.debug('[content] Removed injected leading space from #prompt-textarea.') | |
| } | |
| }, 300) | |
| } | |
| } else { | |
| console.debug( | |
| '[content] #prompt-textarea not found, not empty (value: "' + | |
| input?.value + | |
| '"), or not on root path for manipulation.', | |
| console.log('[content] Ensuring #prompt-textarea is focused.') | |
| if (document.activeElement === input) { | |
| console.debug('[content] #prompt-textarea already focused; skipping focus().') | |
| } else if (typeof input.focus === 'function') { | |
| input.focus() | |
| console.debug('[content] Called focus() on #prompt-textarea.') | |
| } else { | |
| console.warn('[content] #prompt-textarea does not support focus().') | |
| } | |
| } else { | |
| console.debug( | |
| '[content] #prompt-textarea not found, not empty (value: "' + | |
| input?.value + | |
| '"), or not on root path for focus.', |
| }) | ||
| } catch (e) { | ||
| console.warn('[background] Error sending final error message on max retries:', e) | ||
| } |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reconnection logic in setPortProxy implements exponential backoff with a maximum of 5 attempts, but the delay calculation uses Math.pow(BACKOFF_MULTIPLIER, port._reconnectAttempts - 1). When _reconnectAttempts is 1 (first retry), the delay is Math.pow(2, 0) * 1000 = 1000ms. However, after max retries are reached, the listeners are removed and an error is sent, but the port itself is not disconnected. This could leave the port in an inconsistent state. Consider explicitly calling port.disconnect() or ensuring the port is properly cleaned up after max retries.
| } | |
| } | |
| // Mark the port as closed and explicitly disconnect to avoid leaving it in an inconsistent state. | |
| port._isClosed = true | |
| try { | |
| port.disconnect() | |
| } catch (e) { | |
| console.warn('[background] Error disconnecting port on max retries:', e) | |
| } |
There was a problem hiding this comment.
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: 2294ea4380
ℹ️ 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".
| types: ['xmlhttprequest', 'websocket'], | ||
| }, | ||
| ['requestHeaders'], | ||
| ['requestHeaders', ...(Browser.runtime.getManifest().manifest_version < 3 ? ['blocking'] : [])], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add MV2 blocking permission or drop blocking option
For the Firefox/MV2 build, this now requests the blocking extraInfoSpec when manifest_version < 3, but src/manifest.v2.json does not include the webRequestBlocking permission. In MV2 this causes onBeforeSendHeaders.addListener to fail registration (permission error), so the Bing/Claude header overrides won’t run, and because the listeners are in the same try block it can also skip later listener setup. Either add the permission to the MV2 manifest or avoid using blocking there.
Useful? React with 👍 / 👎.
2294ea4 to
79a3d29
Compare
|
@codex review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/background/index.mjs`:
- Around line 850-873: The listener added to
Browser.webRequest.onBeforeSendHeaders logs the raw details.requestHeaders which
can contain sensitive tokens/cookies; change the debug calls in the
onBeforeSendHeaders handler (the callback that reads details.requestHeaders and
sets modified) to avoid printing header values directly—instead log only header
names or a redacted representation (e.g., map headers to names or to {name,
value: '<REDACTED>'}) when calling console.debug('[background] Modified headers
for Bing:', ...), and ensure any other debug of details.requestHeaders or
headers uses that sanitized view rather than the raw headers array.
🧹 Nitpick comments (1)
src/background/index.mjs (1)
988-1012: Manual regression testing reminder.Please re‑test popup flow, content‑script injection, and keyboard shortcuts after these changes. Based on learnings, core extension functionality should be validated after updates.
| Browser.webRequest.onBeforeSendHeaders.addListener( | ||
| (details) => { | ||
| const headers = details.requestHeaders | ||
| for (let i = 0; i < headers.length; i++) { | ||
| if (headers[i].name === 'Origin') { | ||
| headers[i].value = 'https://www.bing.com' | ||
| } else if (headers[i].name === 'Referer') { | ||
| headers[i].value = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' | ||
| try { | ||
| console.debug('[background] onBeforeSendHeaders triggered for URL:', details.url) | ||
| const headers = details.requestHeaders | ||
| let modified = false | ||
| for (let i = 0; i < headers.length; i++) { | ||
| const header = headers[i] | ||
| if (!header || !header.name) { | ||
| continue | ||
| } | ||
| const headerNameLower = header.name.toLowerCase() | ||
| if (headerNameLower === 'origin') { | ||
| header.value = 'https://www.bing.com' | ||
| modified = true | ||
| } else if (headerNameLower === 'referer') { | ||
| header.value = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' | ||
| modified = true | ||
| } | ||
| } | ||
| if (modified) { | ||
| console.debug('[background] Modified headers for Bing:', headers) | ||
| } | ||
| return { requestHeaders: headers } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid logging raw request headers (can leak cookies/tokens).
headers may contain sensitive values (e.g., cookies/authorization). Please log only names or a redacted view.
🔧 Safer logging
- if (modified) {
- console.debug('[background] Modified headers for Bing:', headers)
- }
+ if (modified) {
+ console.debug(
+ '[background] Modified headers for Bing (names only):',
+ headers.map((h) => h?.name).filter(Boolean),
+ )
+ }🤖 Prompt for AI Agents
In `@src/background/index.mjs` around lines 850 - 873, The listener added to
Browser.webRequest.onBeforeSendHeaders logs the raw details.requestHeaders which
can contain sensitive tokens/cookies; change the debug calls in the
onBeforeSendHeaders handler (the callback that reads details.requestHeaders and
sets modified) to avoid printing header values directly—instead log only header
names or a redacted representation (e.g., map headers to names or to {name,
value: '<REDACTED>'}) when calling console.debug('[background] Modified headers
for Bing:', ...), and ensure any other debug of details.requestHeaders or
headers uses that sanitized view rather than the raw headers array.
There was a problem hiding this comment.
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 3 out of 3 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| console.warn('[background] Rejecting FETCH message from untrusted sender:', sender) | ||
| return [null, { message: 'Unauthorized sender' }] | ||
| } | ||
|
|
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sender.id check compares against Browser.runtime.id, which is the extension's own ID. However, this check is insufficient because content scripts from the same extension will pass this check. Since FETCH can make arbitrary HTTP requests, this could be exploited if a malicious website can trigger the content script to send a FETCH message. Consider adding additional validation such as checking sender.tab to ensure the request comes from an expected tab, or implementing a whitelist of allowed URLs.
| console.warn('[background] Rejecting FETCH message from untrusted sender:', sender) | |
| return [null, { message: 'Unauthorized sender' }] | |
| } | |
| console.warn('[background] Rejecting FETCH message from untrusted sender (id mismatch):', sender) | |
| return [null, { message: 'Unauthorized sender' }] | |
| } | |
| const senderUrl = sender?.url | |
| const senderTabId = sender?.tab?.id | |
| const extensionBaseUrl = Browser.runtime.getURL('') | |
| const isFromExtensionPage = | |
| !senderTabId && | |
| typeof senderUrl === 'string' && | |
| senderUrl.startsWith(extensionBaseUrl) | |
| if (!isFromExtensionPage) { | |
| console.warn('[background] Rejecting FETCH message from non-extension context:', sender) | |
| return [null, { message: 'Unauthorized context' }] | |
| } |
| setTimeout(() => { | ||
| try { | ||
| const loginContainer = document.querySelector('.user-info-container') | ||
| if (!loginContainer) { | ||
| console.warn('[content] Kimi login container not found, skipping auto-click.') | ||
| return | ||
| } | ||
| console.log('[content] Clicking Kimi login container.') | ||
| loginContainer.click() | ||
| } catch (err_click) { | ||
| console.error('[content] Error clicking Kimi login container:', err_click) | ||
| } | ||
| }, 1000) |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Kimi login flow automatically clicks the login container after 1 second without user consent. This could be unexpected behavior for users and may violate user expectations about automated interactions. Additionally, the querySelector('.user-info-container') may not find the correct element or could match unintended elements. Consider either removing the auto-click behavior or adding a user confirmation step before automatically clicking login elements.
| dockable={true} | ||
| />, | ||
| toolbarContainer, | ||
| const createSelectionTools = async (toolbarContainerElement, selection) => { |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The toolbarContainer variable is module-scoped and nullable, but the deleteToolbar function sets it to null after removing it. However, in createSelectionTools, the parameter is named toolbarContainerElement but the module-level toolbarContainer is still used elsewhere. This could lead to confusion about which container variable is being used. Consider renaming the parameter to avoid shadowing concerns or making the naming more explicit.
| if (!fetchInput.startsWith('https://') && !fetchInput.startsWith('http://')) { | ||
| console.warn('[background] Rejecting FETCH for non-http(s) URL:', fetchInput) | ||
| return [null, { message: 'Unsupported fetch protocol' }] | ||
| } | ||
|
|
||
| console.log('[background] Processing FETCH message for URL:', fetchInput) | ||
| if (fetchInput.includes('bing.com')) { | ||
| console.debug('[background] Fetching Bing access token for FETCH message.') | ||
| const accessToken = await getBingAccessToken() | ||
| await setUserConfig({ bingAccessToken: accessToken }) | ||
| } | ||
|
|
||
| try { | ||
| const response = await fetch(fetchInput, message.data?.init) |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URL validation only checks that the URL starts with 'https://' or 'http://', but doesn't validate the actual URL format. A malformed URL string could still pass this check. Use URL constructor validation (which is done later) first, or wrap the URL string validation in a try-catch to ensure it's a valid URL before the protocol check.
| if (!fetchInput.startsWith('https://') && !fetchInput.startsWith('http://')) { | |
| console.warn('[background] Rejecting FETCH for non-http(s) URL:', fetchInput) | |
| return [null, { message: 'Unsupported fetch protocol' }] | |
| } | |
| console.log('[background] Processing FETCH message for URL:', fetchInput) | |
| if (fetchInput.includes('bing.com')) { | |
| console.debug('[background] Fetching Bing access token for FETCH message.') | |
| const accessToken = await getBingAccessToken() | |
| await setUserConfig({ bingAccessToken: accessToken }) | |
| } | |
| try { | |
| const response = await fetch(fetchInput, message.data?.init) | |
| let url | |
| try { | |
| url = new URL(fetchInput) | |
| } catch (e) { | |
| console.warn('[background] Rejecting FETCH for invalid URL:', fetchInput, e) | |
| return [null, { message: 'Invalid fetch URL' }] | |
| } | |
| if (url.protocol !== 'https:' && url.protocol !== 'http:') { | |
| console.warn('[background] Rejecting FETCH for non-http(s) URL:', fetchInput) | |
| return [null, { message: 'Unsupported fetch protocol' }] | |
| } | |
| const validatedUrl = url.toString() | |
| console.log('[background] Processing FETCH message for URL:', validatedUrl) | |
| if (validatedUrl.includes('bing.com')) { | |
| console.debug('[background] Fetching Bing access token for FETCH message.') | |
| const accessToken = await getBingAccessToken() | |
| await setUserConfig({ bingAccessToken: accessToken }) | |
| } | |
| try { | |
| const response = await fetch(validatedUrl, message.data?.init) |
| } | ||
| try { | ||
| port.postMessage({ | ||
| error: `Connection to ChatGPT tab lost after ${RECONNECT_CONFIG.MAX_ATTEMPTS} attempts. Please refresh the page.`, |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message "Connection to ChatGPT tab lost after X attempts. Please refresh the page." is somewhat ambiguous about which page to refresh. Users might not know whether to refresh the ChatGPT tab, the current tab, or the extension popup. Consider making the error message more specific, e.g., "Connection to ChatGPT tab lost. Please refresh both the ChatGPT tab and this page."
| error: `Connection to ChatGPT tab lost after ${RECONNECT_CONFIG.MAX_ATTEMPTS} attempts. Please refresh the page.`, | |
| error: `Connection to ChatGPT tab lost after ${RECONNECT_CONFIG.MAX_ATTEMPTS} attempts. Please refresh both the ChatGPT tab and this page.`, |
| document.addEventListener('mouseup', (e) => { | ||
| if (toolbarContainer && toolbarContainer.contains(e.target)) return | ||
| const selectionElement = | ||
| window.getSelection()?.rangeCount > 0 && | ||
| window.getSelection()?.getRangeAt(0).endContainer.parentElement | ||
| if (toolbarContainer && selectionElement && toolbarContainer.contains(selectionElement)) return | ||
|
|
||
| deleteToolbar() | ||
| setTimeout(async () => { | ||
| const selection = window | ||
| .getSelection() | ||
| ?.toString() | ||
| .trim() | ||
| .replace(/^-+|-+$/g, '') | ||
| if (selection) { | ||
| let position | ||
| try { | ||
| if (toolbarContainer?.contains(e.target)) { | ||
| console.debug('[content] Mouseup inside toolbar, ignoring.') | ||
| return | ||
| } | ||
| const selectionElement = | ||
| window.getSelection()?.rangeCount > 0 && | ||
| window.getSelection()?.getRangeAt(0).endContainer.parentElement | ||
| if (selectionElement && toolbarContainer?.contains(selectionElement)) { | ||
| console.debug('[content] Mouseup selection is inside toolbar, ignoring.') | ||
| return | ||
| } | ||
|
|
||
| const config = await getUserConfig() | ||
| if (!config.selectionToolsNextToInputBox) position = { x: e.pageX + 20, y: e.pageY + 20 } | ||
| else { | ||
| const inputElement = selectionElement.querySelector('input, textarea') | ||
| if (inputElement) { | ||
| position = getClientPosition(inputElement) | ||
| position = { | ||
| x: position.x + window.scrollX + inputElement.offsetWidth + 50, | ||
| y: e.pageY + 30, | ||
| deleteToolbar() | ||
| setTimeout(async () => { | ||
| try { | ||
| const selection = window | ||
| .getSelection() | ||
| ?.toString() | ||
| .trim() | ||
| .replace(/^-+|-+$/g, '') | ||
| if (selection) { | ||
| console.debug('[content] Text selected:', selection) | ||
| let position | ||
|
|
||
| const config = await getUserConfig() | ||
| if (!config.selectionToolsNextToInputBox) { | ||
| position = { x: e.pageX + 20, y: e.pageY + 20 } | ||
| } else { | ||
| const activeElement = document.activeElement | ||
| const inputElement = | ||
| selectionElement?.querySelector('input, textarea') || | ||
| (activeElement?.matches('input, textarea') ? activeElement : null) | ||
|
|
||
| if (inputElement) { | ||
| console.debug( | ||
| '[content] Input element found for positioning toolbar:', | ||
| inputElement, | ||
| ) | ||
| const clientRect = getClientPosition(inputElement) | ||
| position = { | ||
| x: clientRect.x + window.scrollX + inputElement.offsetWidth + 50, | ||
| y: e.pageY + 30, | ||
| } | ||
| } else { | ||
| position = { x: e.pageX + 20, y: e.pageY + 20 } | ||
| } | ||
| } | ||
| console.debug('[content] Toolbar position:', position) | ||
| toolbarContainer = createElementAtPosition(position.x, position.y) | ||
| await createSelectionTools(toolbarContainer, selection) | ||
| } else { | ||
| position = { x: e.pageX + 20, y: e.pageY + 20 } | ||
| console.debug('[content] No text selected on mouseup.') | ||
| } | ||
| } catch (err) { | ||
| console.error('[content] Error in mouseup setTimeout callback for selection tools:', err) | ||
| } | ||
| toolbarContainer = createElementAtPosition(position.x, position.y) | ||
| await createSelectionTools(toolbarContainer, selection) | ||
| } | ||
| }) | ||
| }, 0) | ||
| } catch (error) { | ||
| console.error('[content] Error in mouseup listener for selection tools:', error) | ||
| } |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR adds try-catch blocks around nearly every operation, but many of these simply log errors without proper recovery or user feedback. Over-aggressive error catching can mask real issues and make debugging harder. Consider being more selective about where to catch errors—catch them at boundaries where you can provide meaningful recovery or user feedback, rather than wrapping every operation. For example, the try-catch in mouseup/touchend listeners (lines 269-329) catches errors that should probably propagate or be handled more specifically.
| const senderId = sender?.id | ||
| if (!senderId || senderId !== Browser.runtime.id) { | ||
| console.warn('[background] Rejecting GET_COOKIE message from untrusted sender:', sender) | ||
| return null | ||
| } |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the FETCH handler, the GET_COOKIE sender validation only checks sender.id against Browser.runtime.id. This means any content script from this extension can request cookies for any URL. Combined with the FETCH handler, a compromised content script on a malicious website could potentially access sensitive cookies. Consider restricting cookie access to specific trusted URLs or implementing additional sender validation.
| const isKimiHost = | ||
| location.hostname === 'kimi.moonshot.cn' || | ||
| location.hostname === 'kimi.com' || | ||
| location.hostname === 'www.kimi.com' | ||
| if (isKimiHost) { | ||
| console.log(`[content] On ${location.hostname}, attempting to save refresh token.`) | ||
| const refreshToken = window.localStorage.refresh_token | ||
| if (refreshToken) { | ||
| await setUserConfig({ kimiMoonShotRefreshToken: refreshToken }) | ||
| console.log('[content] Kimi Moonshot refresh token saved.') | ||
| } else { | ||
| const config = await getUserConfig() | ||
| if (config.kimiMoonShotRefreshToken) { | ||
| await setUserConfig({ kimiMoonShotRefreshToken: '' }) | ||
| console.log('[content] Kimi Moonshot refresh token cleared.') | ||
| } | ||
| console.warn('[content] Kimi Moonshot refresh token not found in localStorage.') | ||
| } | ||
| return | ||
| } | ||
| return | ||
| } | ||
|
|
||
| let data | ||
| if (location.pathname === '/api/auth/session') { | ||
| const response = document.querySelector('pre').textContent | ||
| try { | ||
| data = JSON.parse(response) | ||
| } catch (error) { | ||
| console.error('json error', error) | ||
| if (location.hostname !== 'chatgpt.com') { | ||
| console.debug('[content] Not on chatgpt.com, skipping access token overwrite.') | ||
| return | ||
| } | ||
|
|
||
| console.log('[content] On chatgpt.com, attempting to overwrite access token.') | ||
| let data | ||
| if (location.pathname === '/api/auth/session') { | ||
| console.debug('[content] On /api/auth/session page.') | ||
| const preElement = document.querySelector('pre') | ||
| if (preElement?.textContent) { | ||
| const response = preElement.textContent | ||
| try { | ||
| data = JSON.parse(response) | ||
| console.debug('[content] Parsed access token data from <pre> tag.') | ||
| } catch (error) { | ||
| console.error('[content] Failed to parse JSON from <pre> tag for access token:', error) | ||
| } | ||
| } else { | ||
| console.warn( | ||
| '[content] <pre> tag not found or empty for access token on /api/auth/session.', | ||
| ) | ||
| } | ||
| } else { | ||
| console.debug('[content] Not on /api/auth/session page, fetching token from API endpoint.') | ||
| try { | ||
| const resp = await fetch('https://chatgpt.com/api/auth/session') | ||
| if (resp.ok) { | ||
| data = await resp.json() | ||
| console.debug('[content] Fetched access token data from API endpoint.') | ||
| } else { | ||
| console.warn( | ||
| `[content] Failed to fetch access token, status: ${resp.status} ${resp.statusText}`, | ||
| ) | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error fetching access token from API:', error) | ||
| } | ||
| } | ||
| } else { | ||
| const resp = await fetch('https://chatgpt.com/api/auth/session') | ||
| data = await resp.json().catch(() => ({})) | ||
|
|
||
| if (data?.accessToken) { | ||
| await setAccessToken(data.accessToken) | ||
| console.log('[content] ChatGPT Access token has been set successfully from page data.') | ||
| } else { | ||
| console.warn('[content] No access token found in page data or fetch response.') | ||
| } | ||
| } catch (error) { | ||
| console.error('[content] Error in overwriteAccessToken:', error) | ||
| } | ||
| if (data && data.accessToken) { | ||
| await setAccessToken(data.accessToken) | ||
| console.log(data.accessToken) | ||
| } | ||
|
|
||
| async function getClaudeSessionKey() { | ||
| console.debug('[content] getClaudeSessionKey called.') | ||
| try { | ||
| const sessionKey = await Browser.runtime.sendMessage({ | ||
| type: 'GET_COOKIE', | ||
| data: { url: 'https://claude.ai/', name: 'sessionKey' }, | ||
| }) | ||
| console.debug( | ||
| '[content] Claude session key from background:', | ||
| sessionKey ? 'found' : 'not found', | ||
| ) | ||
| return sessionKey | ||
| } catch (error) { | ||
| console.error('[content] Error in getClaudeSessionKey sending message:', error) | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| async function prepareForForegroundRequests() { | ||
| if (location.hostname !== 'chatgpt.com' || location.pathname === '/auth/login') return | ||
| async function prepareForJumpBackNotification() { | ||
| console.log('[content] Initializing jump back notification.') | ||
| try { | ||
| if ( | ||
| location.hostname === 'chatgpt.com' && | ||
| document.querySelector('button[data-testid=login-button]') | ||
| ) { | ||
| console.log('[content] ChatGPT login button found, user not logged in. Skipping jump back.') | ||
| return | ||
| } | ||
|
|
||
| const userConfig = await getUserConfig() | ||
| const url = new URL(window.location.href) | ||
| if (url.searchParams.has('chatgptbox_notification')) { | ||
| console.log('[content] chatgptbox_notification param found in URL.') | ||
|
|
||
| if ( | ||
| !chatgptWebModelKeys.some((model) => | ||
| getApiModesStringArrayFromConfig(userConfig, true).includes(model), | ||
| ) | ||
| ) | ||
| return | ||
| if (location.hostname === 'claude.ai') { | ||
| console.debug('[content] On claude.ai, checking login status.') | ||
| let claudeSession = await getClaudeSessionKey() | ||
| if (!claudeSession) { | ||
| console.log('[content] Claude session key not found, waiting for it...') | ||
| let promiseSettled = false | ||
| let timerId = null | ||
| let timeoutId = null | ||
| const cleanup = () => { | ||
| if (timerId) clearTimeout(timerId) | ||
| if (timeoutId) clearTimeout(timeoutId) | ||
| } | ||
|
|
||
| // if (location.pathname === '/') { | ||
| // const input = document.querySelector('#prompt-textarea') | ||
| // if (input) { | ||
| // input.textContent = ' ' | ||
| // input.dispatchEvent(new Event('input', { bubbles: true })) | ||
| // setTimeout(() => { | ||
| // input.textContent = '' | ||
| // input.dispatchEvent(new Event('input', { bubbles: true })) | ||
| // }, 300) | ||
| // } | ||
| // } | ||
|
|
||
| await Browser.runtime.sendMessage({ | ||
| type: 'SET_CHATGPT_TAB', | ||
| data: {}, | ||
| }) | ||
| try { | ||
| await new Promise((resolve, reject) => { | ||
| const poll = async () => { | ||
| if (promiseSettled) return | ||
| try { | ||
| claudeSession = await getClaudeSessionKey() | ||
| if (claudeSession && !promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.log('[content] Claude session key found after waiting.') | ||
| resolve() | ||
| return | ||
| } | ||
| } catch (err) { | ||
| console.error('[content] Error polling for Claude session key:', err) | ||
| const rawMessage = | ||
| typeof err?.message === 'string' ? err.message : String(err ?? '') | ||
| const errMsg = rawMessage.toLowerCase() | ||
| const isNetworkError = /\bnetwork\b/.test(errMsg) | ||
| const isPermissionError = /\bpermission\b/.test(errMsg) | ||
| if ((isNetworkError || isPermissionError) && !promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| reject(new Error(`Failed to get Claude session key due to: ${rawMessage}`)) | ||
| return | ||
| } | ||
| } | ||
| if (!promiseSettled) { | ||
| timerId = setTimeout(poll, 500) | ||
| } | ||
| } | ||
|
|
||
| registerPortListener(async (session, port) => { | ||
| if (isUsingChatgptWebModel(session)) { | ||
| const accessToken = await getChatGptAccessToken() | ||
| await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken) | ||
| } | ||
| }) | ||
| } | ||
| poll() | ||
|
|
||
| async function getClaudeSessionKey() { | ||
| return Browser.runtime.sendMessage({ | ||
| type: 'GET_COOKIE', | ||
| data: { url: 'https://claude.ai/', name: 'sessionKey' }, | ||
| }) | ||
| } | ||
| timeoutId = setTimeout(() => { | ||
| if (!promiseSettled) { | ||
| promiseSettled = true | ||
| cleanup() | ||
| console.warn('[content] Timed out waiting for Claude session key.') | ||
| reject(new Error('Timed out waiting for Claude session key.')) | ||
| } | ||
| }, 30000) | ||
| }) | ||
| } catch (err) { | ||
| console.error( | ||
| '[content] Failed to get Claude session key for jump back notification:', | ||
| err, | ||
| ) | ||
| return | ||
| } | ||
| } else { | ||
| console.log('[content] Claude session key found immediately.') | ||
| } | ||
| } | ||
|
|
||
| async function prepareForJumpBackNotification() { | ||
| if ( | ||
| location.hostname === 'chatgpt.com' && | ||
| document.querySelector('button[data-testid=login-button]') | ||
| ) { | ||
| console.log('chatgpt not logged in') | ||
| return | ||
| } | ||
| const isKimiHost = | ||
| location.hostname === 'kimi.moonshot.cn' || | ||
| location.hostname === 'kimi.com' || | ||
| location.hostname === 'www.kimi.com' |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The isKimiHost check is duplicated in two places (lines 578-581 and lines 751-754) with identical logic. This violates the DRY (Don't Repeat Yourself) principle and makes maintenance harder. Consider extracting this check into a helper function or constant defined once at the module level.
| 'kimimoonshotrefreshtoken', | ||
| 'credential', | ||
| 'jwt', | ||
| 'session', |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The keyword 'session' is too broad and will redact non-sensitive session data (like conversation sessions, model sessions, etc.) that may be useful for debugging. This could make the extensive logging less useful. Consider removing 'session' from the list or making it more specific (e.g., 'sessionkey', 'sessionid') to avoid over-redaction.
| 'session', | |
| 'sessionkey', | |
| 'sessionid', |
There was a problem hiding this comment.
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: 79a3d29448
ℹ️ 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".
| console.log('[content] Sending SET_CHATGPT_TAB message.') | ||
| await Browser.runtime.sendMessage({ | ||
| type: 'SET_CHATGPT_TAB', | ||
| data: {}, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent chatgptTabId ping-pong across tabs
In run() you now register a storage change listener that includes chatgptTabId and calls manageChatGptTabState() whenever it changes (around lines 875–892), and manageChatGptTabState() always sends SET_CHATGPT_TAB when this tab is configured for ChatGPT Web. With two or more chatgpt.com tabs open, any change to chatgptTabId will cause every tab to re‑assert itself, so the stored tab id can bounce back and forth indefinitely and the proxy may target the wrong tab. Consider removing chatgptTabId from the watched keys or short‑circuiting when the stored id already matches this tab.
Useful? React with 👍 / 👎.
User description
It's a large patch, and I'm unsure if we can get it merged. It's generated by Google's Jules Agent, which serves as a trial run and test lab. We can also review the feedback from other agents.
Jule's commit message:
GitHub Copilot's summary:
PR Type
Enhancement, Bug fix
Description
Major refactor to improve error handling and logging throughout background and content scripts.
Enhanced communication and state management between background and content scripts.
Improved web request interception and header modification logic.
Refined UI mounting and selection tool logic in the content script.
Changes walkthrough 📝
index.mjs
Robust error handling and logging in background scriptsrc/background/index.mjs
functions and listeners.
ChatGPT tab.
use of 'blocking' for header modification.
responses.
and menu refresh.
index.jsx
Refactor content script for reliability and diagnosticssrc/content-script/index.jsx
manageChatGptTabState with robust error handling.
and event-driven logic.
better DOM checks and error diagnostics.
configuration updates.
Summary by CodeRabbit
New Features
Bug Fixes & Reliability
Chores
✏️ Tip: You can customize this high-level summary in your review settings.