@@ -5,33 +5,107 @@ import { config as menuConfig } from '../content-script/menu-tools/index.mjs'
55
66const menuId = 'ChatGPTBox-Menu'
77const onClickMenu = ( info , tab ) => {
8- Browser . tabs . query ( { active : true , currentWindow : true } ) . then ( ( tabs ) => {
9- const currentTab = tabs [ 0 ]
10- const message = {
11- itemId : info . menuItemId . replace ( menuId , '' ) ,
12- selectionText : info . selectionText ,
13- useMenuPosition : tab . id === currentTab . id ,
14- }
15- console . debug ( 'menu clicked' , message )
8+ const itemId = info . menuItemId . replace ( menuId , '' )
169
17- if ( defaultConfig . selectionTools . includes ( message . itemId ) ) {
18- Browser . tabs . sendMessage ( currentTab . id , {
19- type : 'CREATE_CHAT' ,
20- data : message ,
10+ // sidePanel.open() must be called synchronously within the user gesture handler.
11+ // Calling it inside a Promise callback (e.g. Browser.tabs.query().then()) breaks
12+ // Chrome's user gesture requirement and causes the error:
13+ // "sidePanel.open() may only be called in response to a user gesture."
14+ if ( itemId === 'openSidePanel' && menuConfig . openSidePanel ?. action ) {
15+ // Keep the call synchronous to preserve the user-gesture requirement,
16+ // but observe the returned Promise so a rejected sidePanel.open() does
17+ // not become an unhandled rejection in the background script.
18+ // Also wrap in try/catch because contextMenus.onClicked documents `tab`
19+ // as optional ("If the click did not take place in a tab, this parameter
20+ // will be missing"), so the openSidePanel action that dereferences
21+ // tab.windowId/tab.id can throw synchronously.
22+ let result
23+ try {
24+ result = menuConfig . openSidePanel . action ( true , tab )
25+ } catch ( error ) {
26+ console . error ( 'failed to open side panel' , error )
27+ return
28+ }
29+ if ( result && typeof result . catch === 'function' ) {
30+ result . catch ( ( error ) => {
31+ console . error ( 'failed to open side panel' , error )
2132 } )
22- } else if ( message . itemId in menuConfig ) {
23- if ( menuConfig [ message . itemId ] . action ) {
24- menuConfig [ message . itemId ] . action ( true , tab )
33+ }
34+ return
35+ }
36+
37+ Browser . tabs
38+ . query ( { active : true , currentWindow : true } )
39+ . then ( ( tabs ) => {
40+ const currentTab = tabs && tabs [ 0 ]
41+ if ( ! currentTab ) {
42+ console . debug ( 'menu clicked but no active tab found, skipping' )
43+ return
2544 }
2645
27- if ( menuConfig [ message . itemId ] . genPrompt ) {
28- Browser . tabs . sendMessage ( currentTab . id , {
29- type : 'CREATE_CHAT' ,
30- data : message ,
31- } )
46+ // contextMenus.onClicked documents `tab` as optional ("If the click did
47+ // not take place in a tab, this parameter will be missing"), so guard
48+ // before dereferencing tab.id when computing useMenuPosition.
49+ const message = {
50+ itemId,
51+ selectionText : info . selectionText ,
52+ useMenuPosition : tab ? tab . id === currentTab . id : false ,
3253 }
33- }
34- } )
54+ console . debug ( 'menu clicked' , message )
55+
56+ if ( defaultConfig . selectionTools . includes ( message . itemId ) ) {
57+ // Browser.tabs.sendMessage() (via webextension-polyfill) returns a
58+ // Promise that commonly rejects (no content script listening, restricted
59+ // pages such as chrome://, stale content scripts after extension reload)
60+ // — observe it so we don't leak unhandled rejections in the background.
61+ Browser . tabs
62+ . sendMessage ( currentTab . id , {
63+ type : 'CREATE_CHAT' ,
64+ data : message ,
65+ } )
66+ . catch ( ( error ) => {
67+ console . error ( `failed to send CREATE_CHAT message for "${ message . itemId } "` , error )
68+ } )
69+ } else if ( message . itemId in menuConfig ) {
70+ if ( menuConfig [ message . itemId ] . action ) {
71+ // Several actions in menuConfig are async (e.g. tabs/windows calls)
72+ // and can throw synchronously or return a rejected Promise. Mirror
73+ // the handling already used for openSidePanel above and in
74+ // commands.mjs so neither path leaks an unhandled rejection in the
75+ // background script.
76+ let actionResult
77+ try {
78+ actionResult = menuConfig [ message . itemId ] . action ( true , tab )
79+ } catch ( error ) {
80+ console . error ( `failed to run menu action "${ message . itemId } "` , error )
81+ }
82+ if ( actionResult && typeof actionResult . catch === 'function' ) {
83+ actionResult . catch ( ( error ) => {
84+ console . error ( `failed to run menu action "${ message . itemId } "` , error )
85+ } )
86+ }
87+ }
88+
89+ if ( menuConfig [ message . itemId ] . genPrompt ) {
90+ // Same rationale as the sendMessage call above — observe the Promise
91+ // so a rejected sendMessage (no content script, restricted page, etc.)
92+ // doesn't surface as an unhandled rejection in the background.
93+ Browser . tabs
94+ . sendMessage ( currentTab . id , {
95+ type : 'CREATE_CHAT' ,
96+ data : message ,
97+ } )
98+ . catch ( ( error ) => {
99+ console . error ( `failed to send CREATE_CHAT message for "${ message . itemId } "` , error )
100+ } )
101+ }
102+ }
103+ } )
104+ . catch ( ( error ) => {
105+ // Browser.tabs.query() can reject (e.g. on permission errors); make sure
106+ // it does not become an unhandled promise rejection in the background.
107+ console . error ( 'failed to query active tab for menu click' , error )
108+ } )
35109}
36110export function refreshMenu ( ) {
37111 if ( Browser . contextMenus . onClicked . hasListener ( onClickMenu ) )
0 commit comments