Skip to content

Commit 6b9cab0

Browse files
author
Jayshankar
committed
feat: Show notification when human input is required but browser in not in focus
1 parent ffcbd23 commit 6b9cab0

File tree

4 files changed

+134
-33
lines changed

4 files changed

+134
-33
lines changed

manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"browserOS",
1919
"webNavigation",
2020
"downloads",
21-
"audioCapture"
21+
"audioCapture",
22+
"notifications"
2223
],
2324
"update_url": "https://cdn.browseros.com/extensions/update-manifest.xml",
2425
"host_permissions": [

src/background/index.ts

Lines changed: 70 additions & 29 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 {
@@ -366,6 +366,45 @@ async function toggleSidePanel(tabId: number): Promise<void> {
366366
}
367367
}
368368

369+
/**
370+
* Register notification interaction handlers ( notification click , button click etc. )
371+
*/
372+
function registerNotificationListeners() {
373+
374+
// event listener to listen for detecting when browser is opened/resumed
375+
chrome.windows.onFocusChanged.addListener((windowId) => {
376+
377+
// windowId is not none that means window is focues
378+
if( windowId !== chrome.windows.WINDOW_ID_NONE ) {
379+
380+
//clear all notifications because browser is in focus now
381+
chrome.notifications.getAll((notifications) => {
382+
383+
Object.keys(notifications).forEach(id => {
384+
chrome.notifications.clear(id);
385+
})
386+
387+
})
388+
389+
}
390+
391+
});
392+
393+
//handle click of notification
394+
chrome.notifications.onClicked.addListener((noticationId) => {
395+
396+
// clear notification
397+
chrome.notifications.clear(noticationId);
398+
399+
chrome.windows.getCurrent((window) => {
400+
//open browser window
401+
chrome.windows.update(window.id!, { focused: true });
402+
});
403+
404+
});
405+
406+
}
407+
369408
/**
370409
* Handle extension installation
371410
*/
@@ -420,17 +459,19 @@ function initialize(): void {
420459
// Register all handlers
421460
registerHandlers()
422461

462+
registerNotificationListeners();
463+
423464
// Set up port connection listener
424465
chrome.runtime.onConnect.addListener(handlePortConnection)
425-
466+
426467
// Set up extension icon click handler
427468
chrome.action.onClicked.addListener(async (tab) => {
428469
Logging.log('Background', 'Extension icon clicked')
429470
if (tab.id) {
430471
await toggleSidePanel(tab.id)
431472
}
432473
})
433-
474+
434475
// Set up keyboard shortcut handler
435476
chrome.commands.onCommand.addListener(async (command) => {
436477
if (command === 'toggle-panel') {
@@ -441,21 +482,21 @@ function initialize(): void {
441482
}
442483
}
443484
})
444-
485+
445486
// Clean up on tab removal
446487
chrome.tabs.onRemoved.addListener(async (tabId) => {
447488
// With singleton execution, just log the tab removal
448489
Logging.log('Background', `Tab ${tabId} removed`)
449490
})
450-
491+
451492
// Handle messages from newtab only
452493
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
453494
if (message.type === 'NEWTAB_EXECUTE_QUERY') {
454495
executionHandler.handleNewtabQuery(message, sendResponse)
455496
return true // Keep message channel open for async response
456497
}
457498
})
458-
499+
459500
Logging.log('Background', 'Nxtscape extension initialized successfully')
460501
}
461502

src/sidepanel/App.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Header } from './components/Header'
1212
import { ModeToggle } from './components/ModeToggle'
1313
import { useChatStore } from './stores/chatStore'
1414
import './styles.css'
15+
import { usePushNotification } from './hooks/usePushNotification'
1516

1617
/**
1718
* Root component for sidepanel v2
@@ -34,7 +35,10 @@ export function App() {
3435
const { teachModeState, abortTeachExecution } = useTeachModeStore(state => ({
3536
teachModeState: state.mode,
3637
abortTeachExecution: state.abortExecution
37-
}))
38+
}));
39+
40+
// Get Push notification function for calling when human-input is needed
41+
const { sendNotification } = usePushNotification();
3842

3943
// Check if any execution is running (chat or teach mode)
4044
const isExecuting = isProcessing || teachModeState === 'executing'
@@ -96,6 +100,25 @@ export function App() {
96100
useEffect(() => {
97101
announcer.announce(connected ? 'Extension connected' : 'Extension disconnected')
98102
}, [connected, announcer])
103+
104+
// show push notification if human input is needed and browser is hidden
105+
useEffect(() => {
106+
107+
// document.hidden may incorrect values in some linux distros , but it works in most
108+
if (humanInputRequest && document.hidden) {
109+
110+
sendNotification({
111+
title: "Human input needed",
112+
message: humanInputRequest.prompt,
113+
type: 'basic',
114+
iconUrl: 'assets/icon48.png',
115+
isClickable: true,
116+
requireInteraction: true,
117+
});
118+
119+
}
120+
121+
}, [humanInputRequest]);
99122

100123
return (
101124
<ErrorBoundary
@@ -138,13 +161,13 @@ export function App() {
138161
<div className="border-t border-border bg-background px-2 py-2">
139162
<ModeToggle />
140163
</div>
141-
164+
142165
{humanInputRequest && (
143166
<HumanInputDialog
144167
requestId={humanInputRequest.requestId}
145168
prompt={humanInputRequest.prompt}
146169
onClose={clearHumanInputRequest}
147-
/>
170+
/>
148171
)}
149172
</div>
150173
</ErrorBoundary>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useCallback } from 'react'
2+
3+
/**
4+
* Custom hook for sending web push notifications
5+
*/
6+
export function usePushNotification() {
7+
8+
/**
9+
* Can be called for sending notifications
10+
*/
11+
const sendNotification = useCallback(async (options: chrome.notifications.NotificationOptions<true> ): Promise<string> => {
12+
13+
return new Promise(async (resolve, reject) => {
14+
15+
try {
16+
17+
const noticationId = `notification-${Date.now()}`;
18+
19+
chrome.notifications.create( noticationId , options, (notificationId) => {
20+
resolve(notificationId);
21+
});
22+
23+
} catch (err: any) {
24+
25+
reject(err);
26+
27+
}
28+
29+
})
30+
31+
}, []);
32+
33+
return {
34+
sendNotification,
35+
}
36+
}

0 commit comments

Comments
 (0)