From 46acd1c01bc6cc4556cce67145fdc8685b4bce53 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 20 Apr 2026 09:43:06 +0800 Subject: [PATCH 1/6] fix: accept Claude session keys in sk-ant-sid02 format (fixes #935) Anthropic now issues session keys with the prefix sk-ant-sid02- in addition to the previous sk-ant-sid01- format. The validation was too strict, rejecting valid new-format keys with an error. Relaxed the check to match any sk-ant-sid prefix to be forward-compatible with future key format versions. --- src/services/clients/claude/index.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/clients/claude/index.mjs b/src/services/clients/claude/index.mjs index c026f710..b20a487b 100644 --- a/src/services/clients/claude/index.mjs +++ b/src/services/clients/claude/index.mjs @@ -74,8 +74,8 @@ export class Claude { if (!sessionKey) { throw new Error('Session key required') } - if (!sessionKey.startsWith('sk-ant-sid01')) { - throw new Error('Session key invalid: Must be in the format sk-ant-sid01-*****') + if (!sessionKey.startsWith('sk-ant-sid')) { + throw new Error('Session key invalid: Must be in the format sk-ant-sid01-***** or sk-ant-sid02-*****') } if (fetch) { this.fetch = fetch From 792b6bce2cff3683d77ce27bb4dfc7cd50243176 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 20 Apr 2026 09:44:52 +0800 Subject: [PATCH 2/6] fix: call sidePanel.open() synchronously to fix context menu side panel (fixes #857) Chrome's sidePanel.open() must be called synchronously within a user gesture handler. Previously, the call was inside a Browser.tabs.query() Promise callback, which breaks the user gesture chain and causes: Error: sidePanel.open() may only be called in response to a user gesture. Fixed by extracting the itemId before the async query and handling the openSidePanel action synchronously at the top of the event handler, before any async operations. --- src/background/menus.mjs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/background/menus.mjs b/src/background/menus.mjs index a92bd7ba..d742ea11 100644 --- a/src/background/menus.mjs +++ b/src/background/menus.mjs @@ -5,10 +5,21 @@ import { config as menuConfig } from '../content-script/menu-tools/index.mjs' const menuId = 'ChatGPTBox-Menu' const onClickMenu = (info, tab) => { + const itemId = info.menuItemId.replace(menuId, '') + + // sidePanel.open() must be called synchronously within the user gesture handler. + // Calling it inside a Promise callback (e.g. Browser.tabs.query().then()) breaks + // Chrome's user gesture requirement and causes the error: + // "sidePanel.open() may only be called in response to a user gesture." + if (itemId === 'openSidePanel' && menuConfig.openSidePanel?.action) { + menuConfig.openSidePanel.action(true, tab) + return + } + Browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { const currentTab = tabs[0] const message = { - itemId: info.menuItemId.replace(menuId, ''), + itemId, selectionText: info.selectionText, useMenuPosition: tab.id === currentTab.id, } From 327c967aadb973411e4a7a307c6c7812c99f9f7c Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 20 Apr 2026 13:11:29 +0800 Subject: [PATCH 3/6] fix(menus): observe sidePanel.open promise to avoid unhandled rejections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The synchronous menuConfig.openSidePanel.action(true, tab) call is intentional — chrome.sidePanel.open() must run inside the user-gesture callback or it throws 'sidePanel.open() may only be called in response to a user gesture'. We don't want to break that. But the action used to be async, so the chrome.sidePanel.open Promise was discarded and any rejection (e.g. an invalid tab/window combo) would surface as an unhandled rejection in the background script. Two small changes: - menu-tools/index.mjs: drop the async wrapper and return the chrome.sidePanel.open() promise directly so the caller can observe it. - background/menus.mjs: still call the action synchronously (gesture preserved) but, if it returned a thenable, attach a .catch that logs the failure instead of letting it become unhandled. Addresses review feedback from CodeRabbit and gemini-code-assist. --- src/background/menus.mjs | 10 +++++++++- src/content-script/menu-tools/index.mjs | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/background/menus.mjs b/src/background/menus.mjs index d742ea11..0565836b 100644 --- a/src/background/menus.mjs +++ b/src/background/menus.mjs @@ -12,7 +12,15 @@ const onClickMenu = (info, tab) => { // Chrome's user gesture requirement and causes the error: // "sidePanel.open() may only be called in response to a user gesture." if (itemId === 'openSidePanel' && menuConfig.openSidePanel?.action) { - menuConfig.openSidePanel.action(true, tab) + // Keep the call synchronous to preserve the user-gesture requirement, + // but observe the returned Promise so a rejected sidePanel.open() does + // not become an unhandled rejection in the background script. + const result = menuConfig.openSidePanel.action(true, tab) + if (result && typeof result.catch === 'function') { + result.catch((error) => { + console.error('failed to open side panel', error) + }) + } return } diff --git a/src/content-script/menu-tools/index.mjs b/src/content-script/menu-tools/index.mjs index 0f46392e..77fdc634 100644 --- a/src/content-script/menu-tools/index.mjs +++ b/src/content-script/menu-tools/index.mjs @@ -59,14 +59,14 @@ export const config = { }, openSidePanel: { label: 'Open Side Panel', - action: async (fromBackground, tab) => { + action: (fromBackground, tab) => { console.debug('action is from background', fromBackground) if (fromBackground) { // eslint-disable-next-line no-undef - chrome.sidePanel.open({ windowId: tab.windowId, tabId: tab.id }) - } else { - // side panel is not supported + return chrome.sidePanel.open({ windowId: tab.windowId, tabId: tab.id }) } + // side panel is not supported + return undefined }, }, closeAllChats: { From 810e8d3efeb95ced9b75441d5680ad15a7ae3f60 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 20 Apr 2026 13:21:47 +0800 Subject: [PATCH 4/6] fix(menus): observe sidePanel.open promise in command handler & guard against missing chrome.sidePanel Addresses two follow-up review findings on PR #963: 1. devin-ai-integration[bot]: src/background/commands.mjs:15 had the same unhandled-rejection pattern as menus.mjs. The keyboard command path now mirrors the menus.mjs treatment: call the action synchronously to preserve user-gesture context, then attach a .catch() to the returned thenable so a rejected sidePanel.open() does not bubble up as an unhandled rejection. 2. coderabbitai[bot]: src/content-script/menu-tools/index.mjs:62-69 called chrome.sidePanel.open directly, which throws synchronously in browsers where chrome.sidePanel is not defined (e.g. Firefox). Guard the call with a typeof chrome / chrome.sidePanel check and return a rejected Promise so the caller's .catch() handles it uniformly with API rejections. --- src/background/commands.mjs | 11 ++++++++++- src/content-script/menu-tools/index.mjs | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/background/commands.mjs b/src/background/commands.mjs index 1440d74d..54996b16 100644 --- a/src/background/commands.mjs +++ b/src/background/commands.mjs @@ -12,7 +12,16 @@ export function registerCommands() { if (command in menuConfig) { if (menuConfig[command].action) { - menuConfig[command].action(true, tab) + // The action may return a Promise (e.g. openSidePanel returns the + // chrome.sidePanel.open() Promise). Keep the call synchronous so the + // user-gesture context is preserved, but observe the Promise so a + // rejection does not become an unhandled rejection in the background. + const result = menuConfig[command].action(true, tab) + if (result && typeof result.catch === 'function') { + result.catch((error) => { + console.error(`failed to run command action "${command}"`, error) + }) + } } if (menuConfig[command].genPrompt) { diff --git a/src/content-script/menu-tools/index.mjs b/src/content-script/menu-tools/index.mjs index 77fdc634..21e82eea 100644 --- a/src/content-script/menu-tools/index.mjs +++ b/src/content-script/menu-tools/index.mjs @@ -62,6 +62,11 @@ export const config = { action: (fromBackground, tab) => { console.debug('action is from background', fromBackground) if (fromBackground) { + // eslint-disable-next-line no-undef + if (typeof chrome === 'undefined' || !chrome.sidePanel?.open) { + // sidePanel API is not available in this browser (e.g. Firefox) + return Promise.reject(new Error('chrome.sidePanel API is not available')) + } // eslint-disable-next-line no-undef return chrome.sidePanel.open({ windowId: tab.windowId, tabId: tab.id }) } From 0f82ecd783cfe418bb9b53d5904ff94865f53e8e Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 20 Apr 2026 13:36:26 +0800 Subject: [PATCH 5/6] fix(commands): also catch synchronous throws in command action `Browser.commands.onCommand`'s `tab` parameter is documented as optional, so an action that dereferences `tab.*` (e.g. `openSidePanel` reading `tab.windowId` / `tab.id`) can throw synchronously before returning a Promise. The previous `.catch` only observed Promise rejections, so a synchronous throw would still surface as an uncaught error in the background script. Wrap the action invocation in try/catch and log+return on synchronous failure, mirroring the async rejection handling. Addresses CodeRabbit review on PR #963. --- src/background/commands.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/background/commands.mjs b/src/background/commands.mjs index 54996b16..47577cf6 100644 --- a/src/background/commands.mjs +++ b/src/background/commands.mjs @@ -16,7 +16,16 @@ export function registerCommands() { // chrome.sidePanel.open() Promise). Keep the call synchronous so the // user-gesture context is preserved, but observe the Promise so a // rejection does not become an unhandled rejection in the background. - const result = menuConfig[command].action(true, tab) + // Also wrap in try/catch because Browser.commands.onCommand documents + // `tab` as optional, so an action that dereferences tab.* (e.g. the + // openSidePanel call) can throw synchronously. + let result + try { + result = menuConfig[command].action(true, tab) + } catch (error) { + console.error(`failed to run command action "${command}"`, error) + return + } if (result && typeof result.catch === 'function') { result.catch((error) => { console.error(`failed to run command action "${command}"`, error) From 025cbb81ef8d5ee5c508401ea4e2b3f1667c72a8 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 20 Apr 2026 13:42:47 +0800 Subject: [PATCH 6/6] fix: also wrap menus.mjs openSidePanel call in try/catch contextMenus.onClicked.tab is optional per the Chrome API ('If the click did not take place in a tab, this parameter will be missing'). The openSidePanel action dereferences tab.windowId/tab.id, so it can throw synchronously, which the existing .catch() Promise handler would not catch. Mirror the pattern already used in src/background/commands.mjs by wrapping the call in try/catch. Addresses devin-ai-integration[bot] review on src/background/menus.mjs:23. --- src/background/menus.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/background/menus.mjs b/src/background/menus.mjs index 0565836b..7f086499 100644 --- a/src/background/menus.mjs +++ b/src/background/menus.mjs @@ -15,7 +15,17 @@ const onClickMenu = (info, tab) => { // Keep the call synchronous to preserve the user-gesture requirement, // but observe the returned Promise so a rejected sidePanel.open() does // not become an unhandled rejection in the background script. - const result = menuConfig.openSidePanel.action(true, tab) + // Also wrap in try/catch because contextMenus.onClicked documents `tab` + // as optional ("If the click did not take place in a tab, this parameter + // will be missing"), so the openSidePanel action that dereferences + // tab.windowId/tab.id can throw synchronously. + let result + try { + result = menuConfig.openSidePanel.action(true, tab) + } catch (error) { + console.error('failed to open side panel', error) + return + } if (result && typeof result.catch === 'function') { result.catch((error) => { console.error('failed to open side panel', error)