Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
99 changes: 70 additions & 29 deletions src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ function registerHandlers(): void {
MessageType.EXECUTE_QUERY,
(msg, port) => executionHandler.handleExecuteQuery(msg, port)
)

messageRouter.registerHandler(
MessageType.CANCEL_TASK,
(msg, port) => executionHandler.handleCancelTask(msg, port)
)

messageRouter.registerHandler(
MessageType.RESET_CONVERSATION,
(msg, port) => executionHandler.handleResetConversation(msg, port)
Expand All @@ -87,49 +87,49 @@ function registerHandlers(): void {
MessageType.GET_LLM_PROVIDERS,
(msg, port) => providersHandler.handleGetProviders(msg, port)
)

messageRouter.registerHandler(
MessageType.SAVE_LLM_PROVIDERS,
(msg, port) => providersHandler.handleSaveProviders(msg, port)
)

// MCP handlers
messageRouter.registerHandler(
MessageType.GET_MCP_SERVERS,
(msg, port) => mcpHandler.handleGetMCPServers(msg, port)
)

messageRouter.registerHandler(
MessageType.CONNECT_MCP_SERVER,
(msg, port) => mcpHandler.handleConnectMCPServer(msg, port)
)

messageRouter.registerHandler(
MessageType.DISCONNECT_MCP_SERVER,
(msg, port) => mcpHandler.handleDisconnectMCPServer(msg, port)
)

messageRouter.registerHandler(
MessageType.CALL_MCP_TOOL,
(msg, port) => mcpHandler.handleCallMCPTool(msg, port)
)

messageRouter.registerHandler(
MessageType.MCP_INSTALL_SERVER,
(msg, port) => mcpHandler.handleInstallServer(msg, port)
)

messageRouter.registerHandler(
MessageType.MCP_DELETE_SERVER,
(msg, port) => mcpHandler.handleDeleteServer(msg, port)
)

messageRouter.registerHandler(
MessageType.MCP_GET_INSTALLED_SERVERS,
(msg, port) => mcpHandler.handleGetInstalledServers(msg, port)
)


// Plan generation handlers (for AI plan generation in newtab)
messageRouter.registerHandler(
MessageType.GENERATE_PLAN,
Expand Down Expand Up @@ -215,7 +215,7 @@ function registerHandlers(): void {
Logging.log(logMsg.source || 'Unknown', logMsg.message, logMsg.level || 'info')
}
)

// Metrics handler
messageRouter.registerHandler(
MessageType.LOG_METRIC,
Expand All @@ -224,7 +224,7 @@ function registerHandlers(): void {
Logging.logMetric(event, properties)
}
)

// Heartbeat handler - acknowledge heartbeats to keep connection alive
messageRouter.registerHandler(
MessageType.HEARTBEAT,
Expand All @@ -237,7 +237,7 @@ function registerHandlers(): void {
})
}
)

// Panel close handler
messageRouter.registerHandler(
MessageType.CLOSE_PANEL,
Expand Down Expand Up @@ -290,33 +290,33 @@ function registerHandlers(): void {
*/
function handlePortConnection(port: chrome.runtime.Port): void {
const portId = portManager.registerPort(port)

// Handle sidepanel connections
if (port.name === 'sidepanel') {
isPanelOpen = true
Logging.log('Background', `Side panel connected`)
Logging.logMetric('side_panel_opened', { source: 'port_connection' })
}

// Register with logging system
Logging.registerPort(port.name, port)

// Set up message listener
port.onMessage.addListener((message: PortMessage) => {
messageRouter.routeMessage(message, port)
})

// Set up disconnect listener
port.onDisconnect.addListener(() => {
portManager.unregisterPort(port)

// Update panel state if this was the sidepanel
if (port.name === 'sidepanel') {
isPanelOpen = false
Logging.log('Background', `Side panel disconnected`)
Logging.logMetric('side_panel_closed', { source: 'port_disconnection' })
}

// Unregister from logging
Logging.unregisterPort(port.name)
})
Expand All @@ -327,9 +327,9 @@ function handlePortConnection(port: chrome.runtime.Port): void {
*/
async function toggleSidePanel(tabId: number): Promise<void> {
if (isPanelToggling) return

isPanelToggling = true

try {
if (isPanelOpen) {
// Signal sidepanel to close itself
Expand All @@ -345,7 +345,7 @@ async function toggleSidePanel(tabId: number): Promise<void> {
}
} catch (error) {
Logging.log('Background', `Error toggling side panel: ${error}`, 'error')

// Try fallback with windowId
if (!isPanelOpen) {
try {
Expand All @@ -366,6 +366,45 @@ async function toggleSidePanel(tabId: number): Promise<void> {
}
}

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

// event listener to listen for detecting when browser is opened/resumed
chrome.windows.onFocusChanged.addListener((windowId) => {

// windowId is not none that means window is focues
if( windowId !== chrome.windows.WINDOW_ID_NONE ) {

//clear all notifications because browser is in focus now
chrome.notifications.getAll((notifications) => {

Object.keys(notifications).forEach(id => {
chrome.notifications.clear(id);
})

})

}

});

//handle click of notification
chrome.notifications.onClicked.addListener((noticationId) => {

// clear notification
chrome.notifications.clear(noticationId);

chrome.windows.getCurrent((window) => {
//open browser window
chrome.windows.update(window.id!, { focused: true });
});

});

}

/**
* Handle extension installation
*/
Expand Down Expand Up @@ -420,17 +459,19 @@ function initialize(): void {
// Register all handlers
registerHandlers()

registerNotificationListeners();

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

// Set up extension icon click handler
chrome.action.onClicked.addListener(async (tab) => {
Logging.log('Background', 'Extension icon clicked')
if (tab.id) {
await toggleSidePanel(tab.id)
}
})

// Set up keyboard shortcut handler
chrome.commands.onCommand.addListener(async (command) => {
if (command === 'toggle-panel') {
Expand All @@ -441,21 +482,21 @@ function initialize(): void {
}
}
})

// Clean up on tab removal
chrome.tabs.onRemoved.addListener(async (tabId) => {
// With singleton execution, just log the tab removal
Logging.log('Background', `Tab ${tabId} removed`)
})

// Handle messages from newtab only
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'NEWTAB_EXECUTE_QUERY') {
executionHandler.handleNewtabQuery(message, sendResponse)
return true // Keep message channel open for async response
}
})

Logging.log('Background', 'Nxtscape extension initialized successfully')
}

Expand Down
29 changes: 26 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,25 @@ 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(() => {

// document.hidden may incorrect values in some linux distros , but it works in most
if (humanInputRequest && document.hidden) {

sendNotification({
title: "Human input needed",
message: humanInputRequest.prompt,
type: 'basic',
iconUrl: 'assets/icon48.png',
isClickable: true,
requireInteraction: true,
});

}

}, [humanInputRequest]);

return (
<ErrorBoundary
Expand Down Expand Up @@ -138,13 +161,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
36 changes: 36 additions & 0 deletions src/sidepanel/hooks/usePushNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useCallback } from 'react'

/**
* Custom hook for sending web push notifications
*/
export function usePushNotification() {

/**
* Can be called for sending notifications
*/
const sendNotification = useCallback(async (options: chrome.notifications.NotificationOptions<true> ): Promise<string> => {

return new Promise(async (resolve, reject) => {

try {

const noticationId = `notification-${Date.now()}`;

chrome.notifications.create( noticationId , options, (notificationId) => {
resolve(notificationId);
});

} catch (err: any) {

reject(err);

}

})

}, []);

return {
sendNotification,
}
}