@@ -3,14 +3,102 @@ import {
33 createStore ,
44 findStorageModule ,
55} from '@lace-contract/module' ;
6+ import {
7+ applySidePanelBehavior ,
8+ DEFAULT_OPEN_MODE ,
9+ isSidePanelApiAvailable ,
10+ } from '@lace-contract/views' ;
611import { devToolsEnhancer } from '@redux-devtools/remote' ;
712
8- import { createExtensionModuleLoader , ENV , logger } from '../util' ;
13+ import {
14+ createExtensionModuleLoader ,
15+ ENV ,
16+ logger ,
17+ readDefaultOpenMode ,
18+ } from '../util' ;
919
20+ import { handleActionClick } from './action-click-handler' ;
1021import { createRemoteStore } from './create-remote-store' ;
1122import { exposeAPIs } from './expose-apis' ;
1223
1324import type { FeatureFlagApi } from '../util' ;
25+ import type { DefaultOpenMode } from '@lace-contract/views' ;
26+
27+ // MV3 only dispatches events to listeners registered before the SW script
28+ // hits its first await. If `chrome.action.onClicked.addListener` is registered
29+ // after an await, the click that woke the SW is dropped — the user has to
30+ // click again once boot finishes. Everything in this top section therefore
31+ // runs synchronously, before the awaits below.
32+
33+ // In-memory mirror of the persisted preference, refreshed from
34+ // chrome.storage.local on every click and at SW boot. Initialised to the
35+ // contract's default so the first click after boot has *some* value to use
36+ // if the storage read somehow fails. See ADR 26.
37+ //
38+ // We deliberately do NOT subscribe to chrome.storage.onChanged here: that
39+ // listener would fire on every chrome.storage.local write (including
40+ // redux-persist's), resetting the SW idle timer and preventing it from going
41+ // dormant. The popup updates Chrome's setPanelBehavior itself after writing,
42+ // so cross-context propagation does not require an SW listener.
43+ const cachedDefaultOpenMode : { current : DefaultOpenMode } = {
44+ current : DEFAULT_OPEN_MODE ,
45+ } ;
46+
47+ const openLaceTab = async ( ) => {
48+ try {
49+ const indexUrl = chrome . runtime . getURL ( 'expo/index.html' ) ;
50+ const existing = await chrome . tabs . query ( { url : `${ indexUrl } *` } ) ;
51+ if ( existing [ 0 ] ?. id !== undefined ) {
52+ await chrome . tabs . update ( existing [ 0 ] . id , { active : true } ) ;
53+ if ( existing [ 0 ] . windowId !== undefined ) {
54+ await chrome . windows . update ( existing [ 0 ] . windowId , { focused : true } ) ;
55+ }
56+ return ;
57+ }
58+ await chrome . tabs . create ( { url : 'expo/index.html' , active : true } ) ;
59+ } catch ( error ) {
60+ logger . error ( 'Failed to open Lace tab' , error ) ;
61+ }
62+ } ;
63+
64+ const refreshFromStorage = async ( ) : Promise < DefaultOpenMode > => {
65+ const mode = await readDefaultOpenMode ( ) ;
66+ cachedDefaultOpenMode . current = mode ;
67+ // Fire-and-forget: handleActionClick decides what to open by reading
68+ // cachedDefaultOpenMode (just assigned above), not Chrome's setPanelBehavior
69+ // state, so the click can be handled before this Promise settles.
70+ applySidePanelBehavior ( mode ) ?. catch ( ( error : unknown ) => {
71+ logger . error ( 'Failed to update side panel behavior' , error ) ;
72+ } ) ;
73+ return mode ;
74+ } ;
75+
76+ // Kick off the initial read synchronously so Chrome's setPanelBehavior is
77+ // reconciled at boot (in case the popup was unable to update it the last
78+ // time the user changed mode).
79+ void refreshFromStorage ( ) . catch ( ( error : unknown ) => {
80+ logger . error ( 'Failed to read defaultOpenMode from storage' , error ) ;
81+ } ) ;
82+
83+ chrome . action . onClicked . addListener ( tab => {
84+ void ( async ( ) => {
85+ // Re-read on every click so popup-side changes propagate without a
86+ // chrome.storage.onChanged subscription (which would keep the SW alive).
87+ // The await preserves the user-gesture window — chrome.storage reads
88+ // typically resolve in well under a millisecond.
89+ await refreshFromStorage ( ) . catch ( ( ) => cachedDefaultOpenMode . current ) ;
90+ await handleActionClick (
91+ {
92+ isSidePanelApiAvailable,
93+ getStoredDefaultOpenMode : ( ) => cachedDefaultOpenMode . current ,
94+ openLaceTab,
95+ openSidePanel : async windowId => chrome . sidePanel . open ( { windowId } ) ,
96+ logger,
97+ } ,
98+ tab ,
99+ ) ;
100+ } ) ( ) ;
101+ } ) ;
14102
15103const {
16104 loadModules,
@@ -45,8 +133,6 @@ const { store } = await createStore(
45133) ;
46134const remoteStore = createRemoteStore ( store ) ;
47135
48- void chrome . sidePanel . setPanelBehavior ( { openPanelOnActionClick : true } ) ;
49-
50136const featureFlags : FeatureFlagApi = {
51137 getFeatureFlags : async ( ) => loadedFeatureFlags ,
52138} ;
0 commit comments