Skip to content

Commit ca93ef2

Browse files
fix: Improve notification handling in background script
1 parent 57f6c67 commit ca93ef2

File tree

3 files changed

+112
-61
lines changed

3 files changed

+112
-61
lines changed

src/background/index.ts

Lines changed: 94 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ function registerHandlers(): void {
5555
MessageType.EXECUTE_QUERY,
5656
(msg, port) => executionHandler.handleExecuteQuery(msg, port)
5757
)
58-
58+
5959
messageRouter.registerHandler(
6060
MessageType.CANCEL_TASK,
6161
(msg, port) => executionHandler.handleCancelTask(msg, port)
6262
)
63-
63+
6464
messageRouter.registerHandler(
6565
MessageType.RESET_CONVERSATION,
6666
(msg, port) => executionHandler.handleResetConversation(msg, port)
@@ -87,49 +87,49 @@ function registerHandlers(): void {
8787
MessageType.GET_LLM_PROVIDERS,
8888
(msg, port) => providersHandler.handleGetProviders(msg, port)
8989
)
90-
90+
9191
messageRouter.registerHandler(
9292
MessageType.SAVE_LLM_PROVIDERS,
9393
(msg, port) => providersHandler.handleSaveProviders(msg, port)
9494
)
95-
95+
9696
// MCP handlers
9797
messageRouter.registerHandler(
9898
MessageType.GET_MCP_SERVERS,
9999
(msg, port) => mcpHandler.handleGetMCPServers(msg, port)
100100
)
101-
101+
102102
messageRouter.registerHandler(
103103
MessageType.CONNECT_MCP_SERVER,
104104
(msg, port) => mcpHandler.handleConnectMCPServer(msg, port)
105105
)
106-
106+
107107
messageRouter.registerHandler(
108108
MessageType.DISCONNECT_MCP_SERVER,
109109
(msg, port) => mcpHandler.handleDisconnectMCPServer(msg, port)
110110
)
111-
111+
112112
messageRouter.registerHandler(
113113
MessageType.CALL_MCP_TOOL,
114114
(msg, port) => mcpHandler.handleCallMCPTool(msg, port)
115115
)
116-
116+
117117
messageRouter.registerHandler(
118118
MessageType.MCP_INSTALL_SERVER,
119119
(msg, port) => mcpHandler.handleInstallServer(msg, port)
120120
)
121-
121+
122122
messageRouter.registerHandler(
123123
MessageType.MCP_DELETE_SERVER,
124124
(msg, port) => mcpHandler.handleDeleteServer(msg, port)
125125
)
126-
126+
127127
messageRouter.registerHandler(
128128
MessageType.MCP_GET_INSTALLED_SERVERS,
129129
(msg, port) => mcpHandler.handleGetInstalledServers(msg, port)
130130
)
131-
132-
131+
132+
133133
// Plan generation handlers (for AI plan generation in newtab)
134134
messageRouter.registerHandler(
135135
MessageType.GENERATE_PLAN,
@@ -215,7 +215,7 @@ function registerHandlers(): void {
215215
Logging.log(logMsg.source || 'Unknown', logMsg.message, logMsg.level || 'info')
216216
}
217217
)
218-
218+
219219
// Metrics handler
220220
messageRouter.registerHandler(
221221
MessageType.LOG_METRIC,
@@ -224,7 +224,7 @@ function registerHandlers(): void {
224224
Logging.logMetric(event, properties)
225225
}
226226
)
227-
227+
228228
// Heartbeat handler - acknowledge heartbeats to keep connection alive
229229
messageRouter.registerHandler(
230230
MessageType.HEARTBEAT,
@@ -237,7 +237,7 @@ function registerHandlers(): void {
237237
})
238238
}
239239
)
240-
240+
241241
// Panel close handler
242242
messageRouter.registerHandler(
243243
MessageType.CLOSE_PANEL,
@@ -290,33 +290,33 @@ function registerHandlers(): void {
290290
*/
291291
function handlePortConnection(port: chrome.runtime.Port): void {
292292
const portId = portManager.registerPort(port)
293-
293+
294294
// Handle sidepanel connections
295295
if (port.name === 'sidepanel') {
296296
isPanelOpen = true
297297
Logging.log('Background', `Side panel connected`)
298298
Logging.logMetric('side_panel_opened', { source: 'port_connection' })
299299
}
300-
300+
301301
// Register with logging system
302302
Logging.registerPort(port.name, port)
303-
303+
304304
// Set up message listener
305305
port.onMessage.addListener((message: PortMessage) => {
306306
messageRouter.routeMessage(message, port)
307307
})
308-
308+
309309
// Set up disconnect listener
310310
port.onDisconnect.addListener(() => {
311311
portManager.unregisterPort(port)
312-
312+
313313
// Update panel state if this was the sidepanel
314314
if (port.name === 'sidepanel') {
315315
isPanelOpen = false
316316
Logging.log('Background', `Side panel disconnected`)
317317
Logging.logMetric('side_panel_closed', { source: 'port_disconnection' })
318318
}
319-
319+
320320
// Unregister from logging
321321
Logging.unregisterPort(port.name)
322322
})
@@ -327,9 +327,9 @@ function handlePortConnection(port: chrome.runtime.Port): void {
327327
*/
328328
async function toggleSidePanel(tabId: number): Promise<void> {
329329
if (isPanelToggling) return
330-
330+
331331
isPanelToggling = true
332-
332+
333333
try {
334334
if (isPanelOpen) {
335335
// Signal sidepanel to close itself
@@ -345,7 +345,7 @@ async function toggleSidePanel(tabId: number): Promise<void> {
345345
}
346346
} catch (error) {
347347
Logging.log('Background', `Error toggling side panel: ${error}`, 'error')
348-
348+
349349
// Try fallback with windowId
350350
if (!isPanelOpen) {
351351
try {
@@ -371,36 +371,84 @@ async function toggleSidePanel(tabId: number): Promise<void> {
371371
*/
372372
function registerNotificationListeners() {
373373

374-
// event listener to listen for detecting when browser is opened/resumed
375-
chrome.windows.onFocusChanged.addListener((windowId) => {
374+
// key: notificationId , value: windowId
375+
const windowIds = new Map<string, number>();
376376

377-
// windowId is not none that means window is focues
378-
if( windowId !== chrome.windows.WINDOW_ID_NONE ) {
377+
// key: windowId , value: notificationId[]
378+
const notificationIds = new Map<number, Array<string>>();
379379

380-
//clear all notifications because browser is in focus now
381-
chrome.notifications.getAll((notifications) => {
380+
const getNotificationIds = (windowId: number): Array<string> => {
382381

383-
Object.keys(notifications).forEach(id => {
384-
chrome.notifications.clear(id);
385-
})
382+
if( !notificationIds.has(windowId) ) {
383+
return [];
384+
}
386385

387-
})
386+
return notificationIds.get(windowId)!;
387+
388+
}
389+
390+
// event listener to listen for detecting when browser is opened/resumed
391+
chrome.windows.onFocusChanged.addListener(async (windowId) => {
388392

393+
// windowId is not none when all chrome windows
394+
// are out of focus that means no notification needs to be cleared
395+
if (windowId == chrome.windows.WINDOW_ID_NONE) {
396+
return;
389397
}
390-
398+
399+
const data = getNotificationIds(windowId);
400+
401+
data.forEach( notificationId => {
402+
chrome.notifications.clear(notificationId);
403+
} )
404+
405+
notificationIds.delete(windowId);
406+
391407
});
392408

393409
//handle click of notification
394-
chrome.notifications.onClicked.addListener((noticationId) => {
410+
chrome.notifications.onClicked.addListener((notificationId) => {
395411

396412
// clear notification
397-
chrome.notifications.clear(noticationId);
398413

399-
chrome.windows.getCurrent((window) => {
414+
const windowId = windowIds.get(notificationId);
415+
416+
if( windowId ) {
417+
400418
//open browser window
401-
chrome.windows.update(window.id!, { focused: true });
402-
});
419+
chrome.windows.update( windowId , { focused: true });
420+
windowIds.delete(notificationId);
403421

422+
// Not clearing `notificationId` from `notficationIds` map here becaue
423+
// the above code will open browser window and it will be cleared in the
424+
// `onFocusChange` handler
425+
} else {
426+
console.info("window if not found for notification" , notificationId);
427+
}
428+
429+
chrome.notifications.clear(notificationId);
430+
431+
});
432+
433+
//listener for sending notification
434+
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
435+
if (request.action === "send-notification") {
436+
const windowId = request.windowId;
437+
438+
//setting window if against notification so that it can be used to clear notification when the window opens
439+
chrome.notifications.create(request.options, async (notificationId) => {
440+
if (windowId) {
441+
let existingNotificationIds = getNotificationIds(windowId!);
442+
existingNotificationIds.push(notificationId);
443+
444+
notificationIds.set(windowId!, existingNotificationIds);
445+
windowIds.set(notificationId, windowId!);
446+
console.log("setting window id", notificationId, windowId, windowIds);
447+
}
448+
sendResponse(notificationId);
449+
});
450+
}
451+
return true;
404452
});
405453

406454
}
@@ -463,15 +511,15 @@ function initialize(): void {
463511

464512
// Set up port connection listener
465513
chrome.runtime.onConnect.addListener(handlePortConnection)
466-
514+
467515
// Set up extension icon click handler
468516
chrome.action.onClicked.addListener(async (tab) => {
469517
Logging.log('Background', 'Extension icon clicked')
470518
if (tab.id) {
471519
await toggleSidePanel(tab.id)
472520
}
473521
})
474-
522+
475523
// Set up keyboard shortcut handler
476524
chrome.commands.onCommand.addListener(async (command) => {
477525
if (command === 'toggle-panel') {
@@ -482,24 +530,23 @@ function initialize(): void {
482530
}
483531
}
484532
})
485-
533+
486534
// Clean up on tab removal
487535
chrome.tabs.onRemoved.addListener(async (tabId) => {
488536
// With singleton execution, just log the tab removal
489537
Logging.log('Background', `Tab ${tabId} removed`)
490538
})
491-
539+
492540
// Handle messages from newtab only
493541
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
494542
if (message.type === 'NEWTAB_EXECUTE_QUERY') {
495543
executionHandler.handleNewtabQuery(message, sendResponse)
496544
return true // Keep message channel open for async response
497545
}
498546
})
499-
547+
500548
Logging.log('Background', 'Nxtscape extension initialized successfully')
501549
}
502550

503551
// Initialize the extension
504-
initialize()
505-
552+
initialize()

src/sidepanel/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function App() {
111111
title: "Human input needed",
112112
message: humanInputRequest.prompt,
113113
type: 'basic',
114-
iconUrl: 'assets/icon48.png',
114+
iconUrl: chrome.runtime.getURL('assets/icon48.png'),
115115
isClickable: true,
116116
requireInteraction: true,
117117
});

src/sidepanel/hooks/usePushNotification.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,29 @@ export function usePushNotification() {
77

88
/**
99
* Can be called for sending notifications
10+
*
11+
* options parameter is `chrome.notifications.NotificationOptions<true>` because `chrome.notifications.create` function expects it like that
1012
*/
11-
const sendNotification = useCallback(async (options: chrome.notifications.NotificationOptions<true> ): Promise<string> => {
13+
const sendNotification = useCallback(async (options: chrome.notifications.NotificationOptions<true>): Promise<string> => {
1214

1315
return new Promise(async (resolve, reject) => {
14-
1516
try {
16-
17-
const noticationId = `notification-${Date.now()}`;
18-
19-
chrome.notifications.create( noticationId , options, (notificationId) => {
20-
resolve(notificationId);
17+
const { id: windowId } = await chrome.windows.getCurrent();
18+
19+
chrome.runtime.sendMessage({
20+
action: "send-notification",
21+
options,
22+
windowId
23+
}, (response) => {
24+
if (chrome.runtime.lastError) {
25+
reject(new Error(chrome.runtime.lastError.message));
26+
return;
27+
}
28+
resolve(response);
2129
});
22-
23-
} catch (err: any) {
24-
25-
reject(err);
26-
30+
} catch (err) {
31+
reject(err instanceof Error ? err : new Error(String(err)));
2732
}
28-
2933
})
3034

3135
}, []);

0 commit comments

Comments
 (0)