use zustand for background.ts and update icons on state updates only.

less imperative code
This commit is contained in:
Tommy D. Rossi
2025-11-16 09:30:55 +01:00
parent ac3a97dbda
commit c14722abf2
2 changed files with 91 additions and 134 deletions
+89 -132
View File
@@ -5,13 +5,14 @@ import { create } from 'zustand';
// Relay URL - fixed port for MCP bridge // Relay URL - fixed port for MCP bridge
const RELAY_URL = 'ws://localhost:9988/extension'; const RELAY_URL = 'ws://localhost:9988/extension';
type ConnectionState = 'disconnected' | 'reconnecting' | 'connected'; type ConnectionState = 'disconnected' | 'reconnecting' | 'connected' | 'error';
interface ExtensionState { interface ExtensionState {
connection: RelayConnection | undefined; connection: RelayConnection | undefined;
connectedTabs: Map<number, string>; connectedTabs: Map<number, string>;
connectionState: ConnectionState; connectionState: ConnectionState;
currentTabId: number | undefined; currentTabId: number | undefined;
errorText: string | undefined;
} }
const useExtensionStore = create<ExtensionState>(() => ({ const useExtensionStore = create<ExtensionState>(() => ({
@@ -19,110 +20,81 @@ const useExtensionStore = create<ExtensionState>(() => ({
connectedTabs: new Map(), connectedTabs: new Map(),
connectionState: 'disconnected', connectionState: 'disconnected',
currentTabId: undefined, currentTabId: undefined,
errorText: undefined,
})); }));
async function updateIcon(tabId: number, state: 'connected' | 'disconnected' | 'connecting'): Promise<void> { const icons = {
try { connected: {
switch (state) { path: {
case 'connected': '16': '/icons/icon-green-16.png',
await chrome.action.setIcon({ '32': '/icons/icon-green-32.png',
tabId, '48': '/icons/icon-green-48.png',
path: { '128': '/icons/icon-green-128.png'
'16': '/icons/icon-green-16.png', },
'32': '/icons/icon-green-32.png', title: 'Connected - Click to disconnect',
'48': '/icons/icon-green-48.png', badgeText: '',
'128': '/icons/icon-green-128.png' badgeColor: undefined
} },
}); connecting: {
await chrome.action.setBadgeText({ tabId, text: '' }); path: {
await chrome.action.setTitle({ tabId, title: 'Connected - Click to disconnect' }); '16': '/icons/icon-gray-16.png',
break; '32': '/icons/icon-gray-32.png',
'48': '/icons/icon-gray-48.png',
case 'connecting': '128': '/icons/icon-gray-128.png'
await chrome.action.setIcon({ },
tabId, title: 'Connecting...',
path: { badgeText: '...',
'16': '/icons/icon-gray-16.png', badgeColor: '#FF9800'
'32': '/icons/icon-gray-32.png', },
'48': '/icons/icon-gray-48.png', disconnected: {
'128': '/icons/icon-gray-128.png' path: {
} '16': '/icons/icon-gray-16.png',
}); '32': '/icons/icon-gray-32.png',
await chrome.action.setBadgeText({ tabId, text: '...' }); '48': '/icons/icon-gray-48.png',
await chrome.action.setBadgeBackgroundColor({ tabId, color: '#FF9800' }); '128': '/icons/icon-gray-48.png'
await chrome.action.setTitle({ tabId, title: 'Connecting...' }); },
break; title: 'Click to attach debugger',
badgeText: '',
case 'disconnected': badgeColor: undefined
default: },
await chrome.action.setIcon({ error: {
tabId, path: {
path: { '16': '/icons/icon-gray-16.png',
'16': '/icons/icon-gray-16.png', '32': '/icons/icon-gray-32.png',
'32': '/icons/icon-gray-32.png', '48': '/icons/icon-gray-48.png',
'48': '/icons/icon-gray-48.png', '128': '/icons/icon-gray-48.png'
'128': '/icons/icon-gray-128.png' },
} title: 'Error',
}); badgeText: '!',
await chrome.action.setBadgeText({ tabId, text: '' }); badgeColor: '#f44336'
await chrome.action.setTitle({ tabId, title: 'Click to attach debugger' });
break;
}
} catch (error: any) {
debugLog(`Error updating icon: ${error.message}`);
} }
} } as const;
useExtensionStore.subscribe((state, prevState) => { useExtensionStore.subscribe(async (state, prevState) => {
const prevTabs = new Set(prevState.connectedTabs.keys()); console.log(state)
const currentTabs = new Set(state.connectedTabs.keys()); const { connectionState, connectedTabs, errorText } = state;
const prevConnectionState = prevState.connectionState;
const currentConnectionState = state.connectionState;
const prevCurrentTabId = prevState.currentTabId;
const currentTabId = state.currentTabId;
const connectionStateChanged = prevConnectionState !== currentConnectionState; const tabs = await chrome.tabs.query({});
const currentTabChanged = prevCurrentTabId !== currentTabId; const allTabIds = [undefined, ...tabs.map(tab => tab.id).filter((id): id is number => id !== undefined)];
const tabsToUpdate = new Set<number>(); for (const tabId of allTabIds) {
const iconConfig = (() => {
if (connectionStateChanged) { if (connectionState === 'error') {
debugLog('Connection state changed:', prevConnectionState, '->', currentConnectionState); return icons.error;
for (const tabId of currentTabs) { }
tabsToUpdate.add(tabId); if (connectionState === 'reconnecting') {
} return icons.connecting;
} }
if (tabId !== undefined && connectedTabs.has(tabId) && connectionState === 'connected') {
const addedTabs = [...currentTabs].filter(id => !prevTabs.has(id)); return icons.connected;
const removedTabs = [...prevTabs].filter(id => !currentTabs.has(id)); }
return icons.disconnected;
for (const tabId of addedTabs) { })();
tabsToUpdate.add(tabId); const title = connectionState === 'error' && errorText ? errorText : iconConfig.title;
} void chrome.action.setIcon({ tabId, path: iconConfig.path });
void chrome.action.setTitle({ tabId, title });
for (const tabId of removedTabs) { if (iconConfig.badgeColor) void chrome.action.setBadgeBackgroundColor({ tabId, color: iconConfig.badgeColor });
tabsToUpdate.add(tabId); void chrome.action.setBadgeText({ tabId, text: iconConfig.badgeText });
}
if (currentTabChanged && currentTabId !== undefined) {
tabsToUpdate.add(currentTabId);
}
for (const tabId of tabsToUpdate) {
const isTracked = currentTabs.has(tabId);
let iconState: 'connected' | 'disconnected' | 'connecting';
if (currentConnectionState === 'disconnected') {
iconState = 'disconnected';
} else if (!isTracked) {
iconState = 'disconnected';
} else if (currentConnectionState === 'connected') {
iconState = 'connected';
} else {
iconState = 'connecting';
}
void updateIcon(tabId, iconState);
} }
}); });
@@ -133,11 +105,12 @@ async function ensureConnection(): Promise<void> {
return; return;
} }
useExtensionStore.setState({ connectionState: 'reconnecting' });
debugLog('No existing connection, creating new relay connection'); debugLog('No existing connection, creating new relay connection');
debugLog('Waiting for server at http://localhost:9988...'); debugLog('Waiting for server at http://localhost:9988...');
while (useExtensionStore.getState().connectionState !== 'disconnected') { useExtensionStore.setState({ connectionState: 'reconnecting' });
while (true) {
try { try {
await fetch('http://localhost:9988', { method: 'HEAD' }); await fetch('http://localhost:9988', { method: 'HEAD' });
debugLog('Server is available'); debugLog('Server is available');
@@ -148,11 +121,6 @@ async function ensureConnection(): Promise<void> {
} }
} }
if (useExtensionStore.getState().connectionState === 'disconnected') {
debugLog('Connection cancelled by user');
return;
}
debugLog('Server is ready, creating WebSocket connection to:', RELAY_URL); debugLog('Server is ready, creating WebSocket connection to:', RELAY_URL);
const socket = new WebSocket(RELAY_URL); const socket = new WebSocket(RELAY_URL);
debugLog('WebSocket created, initial readyState:', socket.readyState, '(0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)'); debugLog('WebSocket created, initial readyState:', socket.readyState, '(0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)');
@@ -220,15 +188,19 @@ async function ensureConnection(): Promise<void> {
debugLog('No tabs to reconnect'); debugLog('No tabs to reconnect');
} }
}, },
onTabDetached: (tabId) => { onTabDetached: (tabId, reason) => {
debugLog('=== Manual tab detachment detected for tab:', tabId, '==='); debugLog('=== Manual tab detachment detected for tab:', tabId, '===');
debugLog('User closed debugger via Chrome automation bar'); debugLog('User closed debugger via Chrome automation bar');
useExtensionStore.setState((state) => { useExtensionStore.setState((state) => {
const newTabs = new Map(state.connectedTabs); const newTabs = new Map(state.connectedTabs);
newTabs.delete(tabId); newTabs.delete(tabId);
return { connectionState: 'disconnected', connectedTabs: newTabs }; return { connectedTabs: newTabs };
}); });
if (reason=== chrome.debugger.DetachReason.CANCELED_BY_USER) {
// if user cancels debugger. disconnect everything
useExtensionStore.setState({ connectionState: 'disconnected' });
}
debugLog('Removed tab from _connectedTabs map'); debugLog('Removed tab from _connectedTabs map');
} }
}); });
@@ -270,20 +242,8 @@ async function connectTab(tabId: number): Promise<void> {
useExtensionStore.setState((state) => { useExtensionStore.setState((state) => {
const newTabs = new Map(state.connectedTabs); const newTabs = new Map(state.connectedTabs);
newTabs.delete(tabId); newTabs.delete(tabId);
return { connectedTabs: newTabs }; return { connectedTabs: newTabs, connectionState: 'error', errorText: `Error: ${error.message}` };
}); });
chrome.action.setBadgeText({ tabId, text: '!' });
chrome.action.setBadgeBackgroundColor({ tabId, color: '#f44336' });
chrome.action.setTitle({ tabId, title: `Error: ${error.message}` });
setTimeout(() => {
const { connectedTabs } = useExtensionStore.getState();
if (!connectedTabs.has(tabId)) {
chrome.action.setBadgeText({ tabId, text: '' });
chrome.action.setTitle({ tabId, title: 'Click to attach debugger' });
}
}, 3000);
} }
} }
@@ -370,14 +330,11 @@ async function reconnect(): Promise<void> {
} catch (error: any) { } catch (error: any) {
debugLog('=== Reconnection failed ===', error); debugLog('=== Reconnection failed ===', error);
const { connectedTabs: failedTabs } = useExtensionStore.getState(); useExtensionStore.setState({
for (const tabId of failedTabs.keys()) { connectedTabs: new Map(),
chrome.action.setBadgeText({ tabId, text: '!' }); connectionState: 'error',
chrome.action.setBadgeBackgroundColor({ tabId, color: '#f44336' }); errorText: 'Reconnection failed - Click to retry'
chrome.action.setTitle({ tabId, title: 'Reconnection failed - Click to retry' }); });
}
useExtensionStore.setState({ connectedTabs: new Map(), connectionState: 'disconnected' });
} }
} }
@@ -403,8 +360,8 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
const { connectedTabs, connectionState, connection } = useExtensionStore.getState(); const { connectedTabs, connectionState, connection } = useExtensionStore.getState();
if (connectionState === 'reconnecting') { if (connectionState === 'reconnecting' || connectionState === 'error') {
debugLog('User clicked during reconnection, canceling reconnection and disconnecting all tabs'); debugLog('User clicked during reconnection/error, canceling and disconnecting all tabs');
const tabsToDisconnect = Array.from(connectedTabs.keys()); const tabsToDisconnect = Array.from(connectedTabs.keys());
@@ -412,7 +369,7 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
connection?.detachTab(tabId); connection?.detachTab(tabId);
} }
useExtensionStore.setState({ connectionState: 'disconnected', connectedTabs: new Map() }); useExtensionStore.setState({ connectionState: 'disconnected', connectedTabs: new Map(), errorText: undefined });
if (connection) { if (connection) {
connection.close('User cancelled reconnection'); connection.close('User cancelled reconnection');
@@ -28,10 +28,10 @@ Return value:
- generic [ref=e21]: - generic [ref=e21]:
- generic: ⌘ - generic: ⌘
- generic: K - generic: K
- link "98.7k" [ref=e22] [cursor=pointer]: - link "99.8k" [ref=e22] [cursor=pointer]:
- /url: https://github.com/shadcn-ui/ui - /url: https://github.com/shadcn-ui/ui
- img - img
- generic [ref=e23]: 98.7k - generic [ref=e23]: 99.8k
- button "Toggle layout" [ref=e24]: - button "Toggle layout" [ref=e24]:
- generic [ref=e25]: Toggle layout - generic [ref=e25]: Toggle layout
- img - img