Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"browserOS",
"webNavigation",
"downloads",
"audioCapture"
"audioCapture",
"notifications"
],
"update_url": "https://cdn.browseros.com/extensions/update-manifest.xml",
"host_permissions": [
Expand Down
87 changes: 85 additions & 2 deletions src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,88 @@ async function toggleSidePanel(tabId: number): Promise<void> {
}
}

/**
* Register notification interaction handlers ( notification click , button click etc. )
*/
function registerNotificationListeners() {

// key: notificationId , value: windowId
const windowIds = new Map<string, number>();

// key: windowId , value: notificationId[]
const notificationIds = new Map<number, Array<string>>();

const getNotificationIds = (windowId: number): Array<string> => {

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
*/
Expand Down Expand Up @@ -420,6 +502,8 @@ function initialize(): void {
// Register all handlers
registerHandlers()

registerNotificationListeners();

// Set up port connection listener
chrome.runtime.onConnect.addListener(handlePortConnection)

Expand Down Expand Up @@ -460,5 +544,4 @@ function initialize(): void {
}

// Initialize the extension
initialize()

initialize()
40 changes: 37 additions & 3 deletions src/sidepanel/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down Expand Up @@ -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 (
<ErrorBoundary
Expand Down Expand Up @@ -138,13 +172,13 @@ export function App() {
<div className="border-t border-border bg-background px-2 py-2">
<ModeToggle />
</div>

{humanInputRequest && (
<HumanInputDialog
requestId={humanInputRequest.requestId}
prompt={humanInputRequest.prompt}
onClose={clearHumanInputRequest}
/>
/>
)}
</div>
</ErrorBoundary>
Expand Down
39 changes: 39 additions & 0 deletions src/sidepanel/hooks/usePushNotification.ts
Original file line number Diff line number Diff line change
@@ -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<true>` because `chrome.notifications.create` function expects it like that
*/
const sendNotification = useCallback(async (options: chrome.notifications.NotificationOptions<true>): Promise<string> => {

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,
}
}