Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions src/background/commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,58 @@ 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.
// 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)
})
}
Comment on lines +29 to +33
}

if (menuConfig[command].genPrompt) {
const currentTab = (await Browser.tabs.query({ active: true, currentWindow: true }))[0]
Browser.tabs.sendMessage(currentTab.id, {
type: 'CREATE_CHAT',
data: message,
})
// Mirror the pattern in menus.mjs so no step here can leak an
// unhandled rejection in the background:
// - Browser.tabs.query() can reject (permission errors, etc.) —
// observe via try/catch.
// - tabs[0] may be undefined when no active tab exists — guard
// before dereferencing currentTab.id.
// - Browser.tabs.sendMessage() (via webextension-polyfill) rejects
// in normal extension usage (no content script listening,
// restricted pages like chrome://, stale content scripts after
// extension reload) — attach a .catch().
let tabs
try {
tabs = await Browser.tabs.query({ active: true, currentWindow: true })
} catch (error) {
console.error(`failed to query active tab for command "${command}"`, error)
return
}
const currentTab = tabs && tabs[0]
if (!currentTab) {
console.debug(`command "${command}" triggered but no active tab found, skipping`)
return
}
Browser.tabs
.sendMessage(currentTab.id, {
type: 'CREATE_CHAT',
data: message,
})
.catch((error) => {
console.error(`failed to send CREATE_CHAT message for command "${command}"`, error)
})
}
}
})
Expand Down
118 changes: 96 additions & 22 deletions src/background/menus.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,107 @@ import { config as menuConfig } from '../content-script/menu-tools/index.mjs'

const menuId = 'ChatGPTBox-Menu'
const onClickMenu = (info, tab) => {
Browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
const currentTab = tabs[0]
const message = {
itemId: info.menuItemId.replace(menuId, ''),
selectionText: info.selectionText,
useMenuPosition: tab.id === currentTab.id,
}
console.debug('menu clicked', message)
const itemId = info.menuItemId.replace(menuId, '')

if (defaultConfig.selectionTools.includes(message.itemId)) {
Browser.tabs.sendMessage(currentTab.id, {
type: 'CREATE_CHAT',
data: message,
// 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) {
// 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.
// 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)
})
} else if (message.itemId in menuConfig) {
if (menuConfig[message.itemId].action) {
menuConfig[message.itemId].action(true, tab)
}
return
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Browser.tabs
.query({ active: true, currentWindow: true })
.then((tabs) => {
const currentTab = tabs && tabs[0]
if (!currentTab) {
console.debug('menu clicked but no active tab found, skipping')
return
}

if (menuConfig[message.itemId].genPrompt) {
Browser.tabs.sendMessage(currentTab.id, {
type: 'CREATE_CHAT',
data: message,
})
// contextMenus.onClicked documents `tab` as optional ("If the click did
// not take place in a tab, this parameter will be missing"), so guard
// before dereferencing tab.id when computing useMenuPosition.
const message = {
itemId,
selectionText: info.selectionText,
useMenuPosition: tab ? tab.id === currentTab.id : false,
}
}
})
console.debug('menu clicked', message)

if (defaultConfig.selectionTools.includes(message.itemId)) {
// Browser.tabs.sendMessage() (via webextension-polyfill) returns a
// Promise that commonly rejects (no content script listening, restricted
// pages such as chrome://, stale content scripts after extension reload)
// — observe it so we don't leak unhandled rejections in the background.
Browser.tabs
.sendMessage(currentTab.id, {
type: 'CREATE_CHAT',
data: message,
})
.catch((error) => {
console.error(`failed to send CREATE_CHAT message for "${message.itemId}"`, error)
})
} else if (message.itemId in menuConfig) {
if (menuConfig[message.itemId].action) {
// Several actions in menuConfig are async (e.g. tabs/windows calls)
// and can throw synchronously or return a rejected Promise. Mirror
// the handling already used for openSidePanel above and in
// commands.mjs so neither path leaks an unhandled rejection in the
// background script.
let actionResult
try {
actionResult = menuConfig[message.itemId].action(true, tab)
} catch (error) {
console.error(`failed to run menu action "${message.itemId}"`, error)
}
if (actionResult && typeof actionResult.catch === 'function') {
actionResult.catch((error) => {
console.error(`failed to run menu action "${message.itemId}"`, error)
})
}
}

if (menuConfig[message.itemId].genPrompt) {
// Same rationale as the sendMessage call above — observe the Promise
// so a rejected sendMessage (no content script, restricted page, etc.)
// doesn't surface as an unhandled rejection in the background.
Browser.tabs
.sendMessage(currentTab.id, {
type: 'CREATE_CHAT',
data: message,
})
.catch((error) => {
console.error(`failed to send CREATE_CHAT message for "${message.itemId}"`, error)
})
}
}
})
.catch((error) => {
// Browser.tabs.query() can reject (e.g. on permission errors); make sure
// it does not become an unhandled promise rejection in the background.
console.error('failed to query active tab for menu click', error)
})
}
export function refreshMenu() {
if (Browser.contextMenus.onClicked.hasListener(onClickMenu))
Expand Down
23 changes: 19 additions & 4 deletions src/content-script/menu-tools/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,29 @@ 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
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'))
}
// contextMenus.onClicked / commands.onCommand document `tab` as
// optional, and even when present the tab may not have an id or
// windowId (e.g. clicks outside a normal browser tab). Guard here so
// callers do not have to wrap every invocation in try/catch just to
// avoid a TypeError from dereferencing tab.windowId / tab.id.
if (!tab || tab.windowId == null || tab.id == null) {
return Promise.reject(
new Error('chrome.sidePanel.open requires a tab with windowId and id'),
)
}
// eslint-disable-next-line no-undef
return chrome.sidePanel.open({ windowId: tab.windowId, tabId: tab.id })
}
// side panel is not supported
return undefined
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
},
closeAllChats: {
Expand Down