Skip to content

Commit a79db0f

Browse files
committed
Implement Inline Profile Selector module: add UI, settings management, and integration with global state
1 parent 7b7cbbf commit a79db0f

File tree

9 files changed

+540
-92
lines changed

9 files changed

+540
-92
lines changed

buttons-init.js

Lines changed: 141 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -52,65 +52,87 @@ window.MaxExtensionButtonsInit = {
5252
* @param {HTMLElement} container - The DOM element to which custom buttons will be appended.
5353
* @param {boolean} isPanel - Flag indicating if the container is the floating panel.
5454
*/
55-
generateAndAppendAllButtons: function (container, isPanel) {
56-
// --- Create a unified list of all buttons to be rendered ---
57-
const allButtonDefs = [];
58-
let nonSeparatorCount = 0;
59-
60-
// 1. Add Cross-Chat buttons if they should be placed 'before'
61-
if (window.globalCrossChatConfig?.enabled && window.globalCrossChatConfig.placement === 'before') {
62-
allButtonDefs.push({ type: 'copy' });
63-
allButtonDefs.push({ type: 'paste' });
64-
}
55+
generateAndAppendAllButtons: async function (container, isPanel) {
56+
// --- Create a unified list of all buttons to be rendered ---
57+
const allButtonDefs = [];
58+
let nonSeparatorCount = 0;
6559

66-
// 2. Add standard custom buttons
67-
globalMaxExtensionConfig.customButtons.forEach(config => {
68-
allButtonDefs.push({ type: 'custom', config: config });
69-
});
60+
// 1. Add Cross-Chat buttons if they should be placed 'before'
61+
if (window.globalCrossChatConfig?.enabled && window.globalCrossChatConfig.placement === 'before') {
62+
allButtonDefs.push({ type: 'copy' });
63+
allButtonDefs.push({ type: 'paste' });
64+
}
7065

71-
// 3. Add Cross-Chat buttons if they should be placed 'after'
72-
if (window.globalCrossChatConfig?.enabled && window.globalCrossChatConfig.placement === 'after') {
73-
allButtonDefs.push({ type: 'copy' });
74-
allButtonDefs.push({ type: 'paste' });
75-
}
66+
// 2. Add standard custom buttons
67+
globalMaxExtensionConfig.customButtons.forEach(config => {
68+
allButtonDefs.push({ type: 'custom', config: config });
69+
});
7670

77-
// --- Render all buttons from the unified list ---
71+
// 3. Add Cross-Chat buttons if they should be placed 'after'
72+
if (window.globalCrossChatConfig?.enabled && window.globalCrossChatConfig.placement === 'after') {
73+
allButtonDefs.push({ type: 'copy' });
74+
allButtonDefs.push({ type: 'paste' });
75+
}
7876

79-
// Add floating panel toggle first, if applicable
80-
if (window.MaxExtensionFloatingPanel && !isPanel) {
81-
const floatingPanelToggleButton = window.MaxExtensionFloatingPanel.createPanelToggleButton();
82-
container.appendChild(floatingPanelToggleButton);
83-
logConCgp('[init] Floating panel toggle button has been created and appended for inline container.');
84-
}
77+
// --- Render all buttons from the unified list ---
8578

86-
// Process the unified list to create and append buttons
87-
allButtonDefs.forEach((def, index) => {
88-
// Handle separators from custom buttons
89-
if (def.type === 'custom' && def.config.separator) {
90-
const separatorElement = MaxExtensionUtils.createSeparator();
91-
container.appendChild(separatorElement);
92-
logConCgp('[init] Separator element has been created and appended.');
93-
return; // Skip to next item
94-
}
79+
// Add floating panel toggle first, if applicable
80+
if (window.MaxExtensionFloatingPanel && !isPanel) {
81+
const floatingPanelToggleButton = window.MaxExtensionFloatingPanel.createPanelToggleButton();
82+
container.appendChild(floatingPanelToggleButton);
83+
logConCgp('[init] Floating panel toggle button has been created and appended for inline container.');
84+
}
9585

96-
// Assign a shortcut key if enabled and available
97-
let shortcutKey = null;
98-
if (globalMaxExtensionConfig.enableShortcuts && nonSeparatorCount < 10) {
99-
shortcutKey = nonSeparatorCount + 1;
100-
}
86+
// Inline Profile Selector BEFORE buttons
87+
if (window.globalInlineSelectorConfig?.enabled && window.globalInlineSelectorConfig.placement === 'before' && !isPanel) {
88+
if (typeof this.createInlineProfileSelector === 'function') {
89+
const selectorElBefore = await this.createInlineProfileSelector();
90+
if (selectorElBefore) {
91+
container.appendChild(selectorElBefore);
92+
logConCgp('[init] Inline Profile Selector appended before buttons.');
93+
}
94+
}
95+
}
10196

102-
let buttonElement;
103-
if (def.type === 'copy' || def.type === 'paste') {
104-
buttonElement = MaxExtensionButtons.createCrossChatButton(def.type, shortcutKey);
105-
} else { // 'custom'
106-
buttonElement = MaxExtensionButtons.createCustomSendButton(def.config, index, processCustomSendButtonClick, shortcutKey);
107-
}
97+
// Process the unified list to create and append buttons
98+
allButtonDefs.forEach((def, index) => {
99+
// Handle separators from custom buttons
100+
if (def.type === 'custom' && def.config.separator) {
101+
const separatorElement = MaxExtensionUtils.createSeparator();
102+
container.appendChild(separatorElement);
103+
logConCgp('[init] Separator element has been created and appended.');
104+
return; // Skip to next item
105+
}
108106

109-
container.appendChild(buttonElement);
110-
nonSeparatorCount++;
111-
logConCgp(`[init] Button ${nonSeparatorCount} (${def.type}) has been created and appended.`);
112-
});
113-
},
107+
// Assign a shortcut key if enabled and available
108+
let shortcutKey = null;
109+
if (globalMaxExtensionConfig.enableShortcuts && nonSeparatorCount < 10) {
110+
shortcutKey = nonSeparatorCount + 1;
111+
}
112+
113+
let buttonElement;
114+
if (def.type === 'copy' || def.type === 'paste') {
115+
buttonElement = MaxExtensionButtons.createCrossChatButton(def.type, shortcutKey);
116+
} else { // 'custom'
117+
buttonElement = MaxExtensionButtons.createCustomSendButton(def.config, index, processCustomSendButtonClick, shortcutKey);
118+
}
119+
120+
container.appendChild(buttonElement);
121+
nonSeparatorCount++;
122+
logConCgp(`[init] Button ${nonSeparatorCount} (${def.type}) has been created and appended.`);
123+
});
124+
125+
// Inline Profile Selector AFTER buttons
126+
if (window.globalInlineSelectorConfig?.enabled && window.globalInlineSelectorConfig.placement === 'after' && !isPanel) {
127+
if (typeof this.createInlineProfileSelector === 'function') {
128+
const selectorElAfter = await this.createInlineProfileSelector();
129+
if (selectorElAfter) {
130+
container.appendChild(selectorElAfter);
131+
logConCgp('[init] Inline Profile Selector appended after buttons.');
132+
}
133+
}
134+
}
135+
},
114136

115137
/**
116138
* Creates and inserts custom buttons and toggles into the target container element.
@@ -182,20 +204,75 @@ window.MaxExtensionButtonsInit = {
182204
}
183205
};
184206

185-
// Listen for profile change events
186-
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
187-
if (message.type === 'profileChanged') {
188-
logConCgp('[init] Received profile change notification');
207+
// --- Helper to create Inline Profile Selector element ---
208+
/**
209+
* Creates and returns a DOM element for the inline profile selector.
210+
* @returns {Promise<HTMLElement|null>}
211+
*/
212+
window.MaxExtensionButtonsInit.createInlineProfileSelector = async function () {
213+
try {
214+
const container = document.createElement('div');
215+
container.style.display = 'flex';
216+
container.style.alignItems = 'center';
217+
container.style.gap = '4px';
218+
container.style.marginRight = '8px';
219+
220+
const label = document.createElement('span');
221+
label.textContent = 'Profile:';
222+
label.style.fontSize = '12px';
223+
label.style.color = '#888';
189224

190-
// Update the global config with the new profile data
191-
window.globalMaxExtensionConfig = message.config;
192-
// Note: Cross-chat config is global and does not change with profile.
225+
const select = document.createElement('select');
226+
select.title = 'Switch active profile';
227+
select.style.padding = '2px';
228+
select.style.zIndex = '100000';
229+
select.tabIndex = 0;
193230

194-
// Update the UI components
195-
window.MaxExtensionButtonsInit.updateButtonsForProfileChange();
231+
// Prevent hostile site handlers from closing the dropdown immediately on SPA UIs (e.g. ChatGPT)
232+
const stop = (e) => { e.stopPropagation(); };
233+
['pointerdown','mousedown','mouseup','click','touchstart','touchend','keydown'].forEach(evt => {
234+
select.addEventListener(evt, stop, { capture: true });
235+
});
236+
237+
// Load profiles and current profile
238+
const profilesResponse = await chrome.runtime.sendMessage({ type: 'listProfiles' });
239+
const { currentProfile } = await chrome.storage.local.get('currentProfile');
196240

197-
// Acknowledge the message
198-
sendResponse({ success: true });
241+
const profileNames = Array.isArray(profilesResponse?.profiles) ? profilesResponse.profiles : [];
242+
profileNames.forEach((name) => {
243+
const opt = document.createElement('option');
244+
opt.value = name;
245+
opt.textContent = name;
246+
if (name === currentProfile) opt.selected = true;
247+
select.appendChild(opt);
248+
});
249+
250+
select.addEventListener('change', (e) => {
251+
const selected = e.target.value;
252+
// Request switch and refresh immediately using service worker response for reliability.
253+
chrome.runtime.sendMessage({ type: 'switchProfile', profileName: selected }, (response) => {
254+
if (response && response.config) {
255+
// Immediate local refresh; SW also broadcasts to other tabs
256+
if (typeof window.__OCP_partialRefreshUI === 'function') {
257+
window.__OCP_partialRefreshUI(response.config);
258+
} else if (typeof window.__OCP_nukeAndRefresh === 'function') {
259+
window.__OCP_nukeAndRefresh(response.config);
260+
} else if (window.MaxExtensionButtonsInit && typeof window.MaxExtensionButtonsInit.updateButtonsForProfileChange === 'function') {
261+
// Fallback: partial refresh
262+
window.globalMaxExtensionConfig = response.config;
263+
window.MaxExtensionButtonsInit.updateButtonsForProfileChange();
264+
}
265+
}
266+
});
267+
});
268+
269+
container.appendChild(label);
270+
container.appendChild(select);
271+
return container;
272+
} catch (err) {
273+
logConCgp('[init] Error creating inline profile selector:', err?.message || err);
274+
return null;
199275
}
200-
return true;
201-
});
276+
};
277+
278+
// Profile change messaging is handled centrally in init.js to avoid duplicate listeners.

buttons-injection.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ function buttonBoxCheckingAndInjection(enableResiliency = true, activeWebsite) {
6767
// Insert custom elements (custom send buttons and toggles) into the target container.
6868
window.MaxExtensionButtonsInit.createAndInsertCustomElements(targetDiv);
6969

70+
// Ensure the inline selector inside does not close due to bubbling site handlers
71+
try {
72+
const selector = targetDiv.querySelector('select');
73+
if (selector) {
74+
const stop = (e) => { e.stopPropagation(); };
75+
['pointerdown','mousedown','mouseup','click','touchstart','touchend','keydown'].forEach(evt => {
76+
selector.addEventListener(evt, stop, { capture: true });
77+
});
78+
}
79+
} catch (e) {}
80+
7081
// Always start resiliency checks when enableResiliency is true.
7182
if (enableResiliency) {
7283
logConCgp('[button-injection] Starting resiliency checks.');

config.js

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,16 @@ async function loadProfileConfig(profileName) {
169169
}
170170

171171
// Function to switch to a different profile
172-
async function switchProfile(profileName) {
172+
async function switchProfile(profileName, excludeTabId) {
173173
logConfigurationRelatedStuff(`Switching to profile: ${profileName}`);
174174
try {
175175
const profile = await loadProfileConfig(profileName);
176176
if (profile) {
177177
await chrome.storage.local.set({ 'currentProfile': profileName });
178178
logConfigurationRelatedStuff(`Switched to profile: ${profileName}`);
179179

180-
// Broadcast profile change to all tabs
181-
broadcastProfileChange(profileName, profile);
180+
// Broadcast profile change to all tabs except the initiator (if provided)
181+
broadcastProfileChange(profileName, profile, excludeTabId);
182182

183183
return profile;
184184
} else {
@@ -192,30 +192,22 @@ async function switchProfile(profileName) {
192192
}
193193

194194
// Function to broadcast profile change to all tabs
195-
async function broadcastProfileChange(profileName, profileData) {
195+
async function broadcastProfileChange(profileName, profileData, excludeTabId) {
196196
try {
197197
const tabs = await chrome.tabs.query({});
198198
tabs.forEach(tab => {
199-
// Only send to URLs that match our content script patterns
200-
if (tab.url && (
201-
tab.url.includes('chat.openai.com') ||
202-
tab.url.includes('grok.x.ai') ||
203-
tab.url.includes('claude.ai') ||
204-
tab.url.includes('o3') ||
205-
tab.url.includes('x.ai')
206-
)) {
207-
chrome.tabs.sendMessage(tab.id, {
208-
type: 'profileChanged',
209-
profileName: profileName,
210-
config: profileData
211-
}).catch(error => {
212-
// Suppress errors when content script is not running on a tab
213-
// This is normal for tabs that don't have our extension active
214-
logConfigurationRelatedStuff(`Could not send message to tab ${tab.id}: ${error.message}`);
215-
});
216-
}
199+
if (excludeTabId && tab.id === excludeTabId) return;
200+
// Send broadly; content scripts will ignore if not present. Errors are expected on non‑matched tabs.
201+
chrome.tabs.sendMessage(tab.id, {
202+
type: 'profileChanged',
203+
profileName: profileName,
204+
config: profileData
205+
}).catch(error => {
206+
// Suppress errors when content script is not running on a tab
207+
logConfigurationRelatedStuff(`Could not send message to tab ${tab.id}: ${error.message}`);
208+
});
217209
});
218-
logConfigurationRelatedStuff(`Broadcasted profile change (${profileName}) to all matching tabs`);
210+
logConfigurationRelatedStuff(`Broadcasted profile change (${profileName}) to all tabs`);
219211
} catch (error) {
220212
handleStorageError(error);
221213
logConfigurationRelatedStuff(`Error broadcasting profile change: ${error.message}`);
@@ -364,7 +356,8 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
364356
});
365357
return true;
366358
case 'switchProfile':
367-
switchProfile(request.profileName).then(config => {
359+
// Identify the sender tab (if any) to avoid echoing a broadcast back immediately.
360+
switchProfile(request.profileName, sender?.tab?.id).then(config => {
368361
sendResponse({ config });
369362
logConfigurationRelatedStuff('Profile switch request processed');
370363
});
@@ -645,6 +638,34 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
645638
return true;
646639
// ===== End Cross-Chat Module Cases =====
647640

641+
// ===== Inline Profile Selector Cases =====
642+
case 'getInlineProfileSelectorSettings':
643+
(async () => {
644+
try {
645+
const settings = await StateStore.getInlineProfileSelectorSettings();
646+
logConfigurationRelatedStuff('Retrieved Inline Profile Selector settings:', settings);
647+
sendResponse({ settings });
648+
} catch (error) {
649+
handleStorageError(error);
650+
sendResponse({ error: error.message });
651+
}
652+
})();
653+
return true;
654+
655+
case 'saveInlineProfileSelectorSettings':
656+
(async () => {
657+
try {
658+
await StateStore.saveInlineProfileSelectorSettings(request.settings);
659+
logConfigurationRelatedStuff('Saved Inline Profile Selector settings:', request.settings);
660+
sendResponse({ success: true });
661+
} catch (error) {
662+
handleStorageError(error);
663+
sendResponse({ error: error.message });
664+
}
665+
})();
666+
return true;
667+
// ===== End Inline Profile Selector Cases =====
668+
648669
default:
649670
logConfigurationRelatedStuff('Unknown message type received:', request.type);
650671
sendResponse({ error: 'Unknown message type' });

floating-panel-settings.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,22 @@ window.MaxExtensionFloatingPanel.switchToProfile = function (profileName) {
230230
{ type: 'switchProfile', profileName: profileName },
231231
(response) => {
232232
if (response.error) {
233-
console.error(`[floating-panel] Error switching to profile ${profileName}:`, response.error);
233+
logConCgp(`[floating-panel] Error switching to profile ${profileName}: ${response.error}`);
234234
return;
235235
}
236236
if (response.config) {
237237
// The service worker broadcasts the change to other tabs. For this tab,
238238
// we update the UI directly for immediate and reliable feedback.
239239
this.currentProfileName = profileName; // Update internal state
240240
window.globalMaxExtensionConfig = response.config; // Update global config
241-
window.MaxExtensionButtonsInit.updateButtonsForProfileChange(); // Trigger UI refresh
241+
// Prefer partial refresh to preserve panel state
242+
if (typeof window.__OCP_partialRefreshUI === 'function') {
243+
window.__OCP_partialRefreshUI(response.config);
244+
} else if (typeof window.__OCP_nukeAndRefresh === 'function') {
245+
window.__OCP_nukeAndRefresh(response.config);
246+
} else {
247+
window.MaxExtensionButtonsInit.updateButtonsForProfileChange();
248+
}
242249
logConCgp(`[floating-panel] Successfully switched to profile: ${profileName}`);
243250
}
244251
}

0 commit comments

Comments
 (0)