diff --git a/manifest.json b/manifest.json index 257f80ac..00ff0f2c 100644 --- a/manifest.json +++ b/manifest.json @@ -18,7 +18,8 @@ "browserOS", "webNavigation", "downloads", - "audioCapture" + "audioCapture", + "notifications" ], "update_url": "https://cdn.browseros.com/extensions/update-manifest.xml", "host_permissions": [ diff --git a/src/background/index.ts b/src/background/index.ts index 85104eea..8c24b5cd 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -366,6 +366,88 @@ async function toggleSidePanel(tabId: number): Promise { } } +/** + * Register notification interaction handlers ( notification click , button click etc. ) + */ +function registerNotificationListeners() { + + // key: notificationId , value: windowId + const windowIds = new Map(); + + // key: windowId , value: notificationId[] + const notificationIds = new Map>(); + + const getNotificationIds = (windowId: number): Array => { + + if( !notificationIds.has(windowId) ) { + return []; + } + + return notificationIds.get(windowId)!; + + } + + // event listener to listen for detecting when browser is opened/resumed + chrome.windows.onFocusChanged.addListener(async (windowId) => { + + // windowId is not none when all chrome windows + // are out of focus that means no notification needs to be cleared + if (windowId == chrome.windows.WINDOW_ID_NONE) { + return; + } + + const data = getNotificationIds(windowId); + + data.forEach(notificationId => chrome.notifications.clear(notificationId)); + + notificationIds.delete(windowId); + + }); + + //handle click of notification + chrome.notifications.onClicked.addListener((notificationId) => { + + + const windowId = windowIds.get(notificationId); + + if( windowId !== undefined ) { + + //open browser window + chrome.windows.update( windowId , { focused: true }); + windowIds.delete(notificationId); + + // Not clearing `notificationId` from `notficationIds` map here becaue + // the above code will open browser window and it will be cleared in the + // `onFocusChange` handler + } + + // clear notification + chrome.notifications.clear(notificationId); + + }); + + //listener for sending notification + chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { + if (request.action === "send-notification") { + const windowId = request.windowId; + + //setting window if against notification so that it can be used to clear notification when the window opens + chrome.notifications.create(request.options, async (notificationId) => { + if (windowId) { + let existingNotificationIds = getNotificationIds(windowId!); + existingNotificationIds.push(notificationId); + + notificationIds.set(windowId!, existingNotificationIds); + windowIds.set(notificationId, windowId!); + } + sendResponse(notificationId); + }); + } + return true; + }); + +} + /** * Handle extension installation */ @@ -420,6 +502,8 @@ function initialize(): void { // Register all handlers registerHandlers() + registerNotificationListeners(); + // Set up port connection listener chrome.runtime.onConnect.addListener(handlePortConnection) @@ -460,5 +544,4 @@ function initialize(): void { } // Initialize the extension -initialize() - +initialize() \ No newline at end of file diff --git a/src/sidepanel/App.tsx b/src/sidepanel/App.tsx index c6fae829..68b0d48d 100644 --- a/src/sidepanel/App.tsx +++ b/src/sidepanel/App.tsx @@ -12,6 +12,7 @@ import { Header } from './components/Header' import { ModeToggle } from './components/ModeToggle' import { useChatStore } from './stores/chatStore' import './styles.css' +import { usePushNotification } from './hooks/usePushNotification' /** * Root component for sidepanel v2 @@ -34,7 +35,10 @@ export function App() { const { teachModeState, abortTeachExecution } = useTeachModeStore(state => ({ teachModeState: state.mode, abortTeachExecution: state.abortExecution - })) + })); + + // Get Push notification function for calling when human-input is needed + const { sendNotification } = usePushNotification(); // Check if any execution is running (chat or teach mode) const isExecuting = isProcessing || teachModeState === 'executing' @@ -96,6 +100,36 @@ export function App() { useEffect(() => { announcer.announce(connected ? 'Extension connected' : 'Extension disconnected') }, [connected, announcer]) + + // show push notification if human input is needed and browser is hidden + useEffect(() => { + + (async () => { + + const { id: windowId } = await chrome.windows.getCurrent(); + + if( windowId === undefined ) { + return; + } + + const window = await chrome.windows.get(windowId); + + if (humanInputRequest && !window.focused) { + + sendNotification({ + title: "Human input needed", + message: humanInputRequest.prompt, + type: 'basic', + iconUrl: chrome.runtime.getURL('assets/icon48.png'), + isClickable: true, + requireInteraction: true, + }); + + } + + })() + + }, [humanInputRequest]); return ( - + {humanInputRequest && ( + /> )} diff --git a/src/sidepanel/hooks/usePushNotification.ts b/src/sidepanel/hooks/usePushNotification.ts new file mode 100644 index 00000000..29be108e --- /dev/null +++ b/src/sidepanel/hooks/usePushNotification.ts @@ -0,0 +1,39 @@ +import { useCallback } from 'react' + +/** +* Custom hook for sending web push notifications + */ +export function usePushNotification() { + + /** + * Can be called for sending notifications + * + * options parameter is `chrome.notifications.NotificationOptions` because `chrome.notifications.create` function expects it like that + */ + const sendNotification = useCallback(async (options: chrome.notifications.NotificationOptions): Promise => { + + return new Promise(async (resolve, reject) => { + try { + const { id: windowId } = await chrome.windows.getCurrent(); + + // Not checking windowId for undefined here because it is already being checked in the background script + // And sending the notification has more priority than saving windowId + + chrome.runtime.sendMessage({ action: "send-notification", options, windowId }, (response) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + resolve(response); + }); + } catch (err) { + reject(err instanceof Error ? err : new Error(String(err))); + } + }) + + }, []); + + return { + sendNotification, + } +}