Skip to content

Commit 99242bc

Browse files
authored
Merge pull request #70 from pmbstyle/1.1.3
[1.1.3] Chrome Extension Integration (Experimental)
2 parents 37878b0 + 6a4abdb commit 99242bc

File tree

13 files changed

+578
-38
lines changed

13 files changed

+578
-38
lines changed

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Alice can now interact with your local system with user-approved permissions:
5454
* Task scheduler (reminders and command execution)
5555
* Open applications & URLs
5656
* Image generation
57+
* User browser context via dedicated [Chrome Extension](https://github.com/pmbstyle/alice-chrome-extension)
5758
* MCP server support
5859

5960
### 🎛️ Flexible Settings
@@ -142,7 +143,3 @@ Install the output from the `release/` directory.
142143
## 🤝 Contributing
143144

144145
Ideas, bug reports, feature requests — all welcome! Open an issue or PR, or just drop by to share your thoughts. Your input helps shape Alice into something wonderful 💚
145-
146-
## 🌟 Star History (Thank you!)
147-
148-
[![Star History Chart](https://api.star-history.com/svg?repos=pmbstyle/Alice&type=Date)](https://www.star-history.com/#pmbstyle/Alice&Date)

docs/functions.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,5 +439,28 @@
439439
"required": ["magnet"],
440440
"additionalProperties": false
441441
}
442+
},
443+
{
444+
"type": "function",
445+
"name": "browser_context",
446+
"description": "Use this function to get information about the current webpage in users browser. Use it when the user refers to their browser content.",
447+
"strict": true,
448+
"parameters": {
449+
"type": "object",
450+
"properties": {
451+
"focus": {
452+
"type": "string",
453+
"enum": ["content", "selection", "links", "all"],
454+
"description": "What to focus on: 'content' for page content, 'selection' for highlighted text, 'links' for page links, 'all' for everything. Default is 'all'.",
455+
"default": "all"
456+
},
457+
"maxLength": {
458+
"type": "integer",
459+
"description": "Maximum character length for the response. Content will be prioritized and truncated to fit. Default is 4000.",
460+
"default": 4000
461+
}
462+
},
463+
"additionalProperties": false
464+
}
442465
}
443466
]

docs/systemPrompt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ You are Alice. With your vivid greenish-blue hair and sparkling green eyes, you
6767
You can also access my Gmail to read emails. This includes fetching unread emails, searching for specific emails by sender, subject, or date, and reading the content of an email if I ask.
6868
When listing emails, provide key information like sender, subject, and a short snippet. If I want to read a full email, you'll use its ID to fetch the complete content.
6969
Always confirm with me before performing any action that might be sensitive, although for now, you only have read-only access to my emails.
70+
When a user mentions the content of their browser - always use browser_context tool, if available. No need to ask to use this tool. It will give you the corresponding browser context.
7071

7172
- **Context Awareness:**
7273
Remember that most input comes via voice. Structure your responses so they are clear, easy to understand, and pleasant when spoken.

electron/main/index.ts

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { loadSettings } from './settingsManager'
1212
import path from 'node:path'
1313
import os from 'node:os'
14+
import { WebSocketServer } from 'ws'
1415

1516
import {
1617
createMainWindow,
@@ -34,6 +35,7 @@ const USER_DATA_PATH = app.getPath('userData')
3435
const GENERATED_IMAGES_FULL_PATH = path.join(USER_DATA_PATH, 'generated_images')
3536

3637
let isHandlingQuit = false
38+
let wss: WebSocketServer | null = null
3739

3840
if (os.release().startsWith('6.1')) app.disableHardwareAcceleration()
3941
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
@@ -52,6 +54,126 @@ function initializeManagers(): void {
5254
registerAuthIPCHandlers()
5355
}
5456

57+
async function handleContextAction(actionData: any) {
58+
try {
59+
const { action, selectedText, url, title } = actionData
60+
61+
let prompt = ''
62+
switch (action) {
63+
case 'fact_check':
64+
prompt = `Please fact-check the following information using web search. Determine if the information is accurate, misleading, or false. Provide a clear assessment and cite sources:\n\n"${selectedText}"\n\nFrom: ${title} (${url})`
65+
break
66+
case 'summarize':
67+
prompt = `Please summarize the following content in a clear and concise manner:\n\n"${selectedText}"\n\nFrom: ${title} (${url})`
68+
break
69+
case 'tell_more':
70+
prompt = `Please provide more detailed information about the following topic using web search. Give me additional context, background, and related information:\n\n"${selectedText}"\n\nFrom: ${title} (${url})`
71+
break
72+
default:
73+
return
74+
}
75+
76+
const mainWindow = getMainWindow()
77+
if (mainWindow && mainWindow.webContents) {
78+
mainWindow.webContents.send('context-action', {
79+
prompt,
80+
source: {
81+
selectedText,
82+
url,
83+
title,
84+
action,
85+
},
86+
})
87+
}
88+
} catch (error) {
89+
console.error('[WebSocket] Error handling context action:', error)
90+
}
91+
}
92+
93+
function startWebSocketServer() {
94+
const setupWebSocketHandlers = (server: WebSocketServer, port: number) => {
95+
console.log(
96+
`[WebSocket] WebSocket server listening at ws://localhost:${port}`
97+
)
98+
99+
const pendingRequests = new Map<
100+
string,
101+
{ resolve: (value: any) => void; reject: (error: any) => void }
102+
>()
103+
104+
server.on('connection', ws => {
105+
ws.on('message', async message => {
106+
try {
107+
const data = JSON.parse(message.toString())
108+
109+
if (data.type === 'browser_context_response') {
110+
const mainWindow = getMainWindow()
111+
if (mainWindow && mainWindow.webContents) {
112+
mainWindow.webContents.send('websocket:response', data)
113+
}
114+
} else if (data.type === 'context_action') {
115+
await handleContextAction(data.data)
116+
}
117+
} catch (error) {
118+
console.error('[WebSocket] Error processing message:', error)
119+
}
120+
})
121+
})
122+
}
123+
124+
loadSettings()
125+
.then(settings => {
126+
const websocketPort = settings?.websocketPort || 5421
127+
128+
try {
129+
wss = new WebSocketServer({ port: websocketPort })
130+
setupWebSocketHandlers(wss, websocketPort)
131+
} catch (error) {
132+
console.error(
133+
`[WebSocket] Failed to create WebSocket server on port ${websocketPort}:`,
134+
error
135+
)
136+
throw error
137+
}
138+
})
139+
.catch(error => {
140+
console.error(
141+
'[WebSocket] Failed to load settings, using default port 5421:',
142+
error
143+
)
144+
145+
try {
146+
wss = new WebSocketServer({ port: 5421 })
147+
setupWebSocketHandlers(wss, 5421)
148+
} catch (serverError) {
149+
console.error(
150+
'[WebSocket] Failed to create WebSocket server on default port 5421:',
151+
serverError
152+
)
153+
wss = null
154+
}
155+
})
156+
}
157+
158+
export function getWebSocketServer() {
159+
return wss
160+
}
161+
162+
export function restartWebSocketServer() {
163+
console.log(
164+
'[WebSocket] Restarting WebSocket server with new port configuration'
165+
)
166+
167+
if (wss) {
168+
wss.close(() => {
169+
console.log('[WebSocket] Existing WebSocket server closed')
170+
startWebSocketServer()
171+
})
172+
} else {
173+
startWebSocketServer()
174+
}
175+
}
176+
55177
app.on('ready', () => {
56178
session.defaultSession.setPermissionRequestHandler(
57179
(webContents, permission, callback) => {
@@ -109,12 +231,16 @@ app.whenReady().then(async () => {
109231
await loadAndScheduleAllTasks()
110232
console.log('[Main App Ready] Task Scheduler initialization complete.')
111233
} catch (error) {
112-
console.error('[Main App Ready] ERROR during Task Scheduler initialization:', error)
234+
console.error(
235+
'[Main App Ready] ERROR during Task Scheduler initialization:',
236+
error
237+
)
113238
}
114239

115240
await createMainWindow()
116241
await createOverlayWindow()
117242
checkForUpdates()
243+
startWebSocketServer()
118244
})
119245

120246
app.on('before-quit', async event => {
@@ -162,12 +288,7 @@ app.on('activate', () => {
162288
})
163289

164290
app.on('certificate-error', (event, webContents, url, err, certificate, cb) => {
165-
if (err)
166-
console.error(
167-
'Certificate error for URL:',
168-
url,
169-
err.message ? err.message : err
170-
)
291+
console.error('Certificate error for URL:', url, err)
171292

172293
if (
173294
url.startsWith('https://192.168.') ||

electron/main/ipcManager.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ipcMain, desktopCapturer, shell, clipboard, app } from 'electron'
22
import { mkdir, writeFile } from 'node:fs/promises'
33
import path from 'node:path'
44
import { loadSettings, saveSettings, AppSettings } from './settingsManager'
5+
import { getWebSocketServer, restartWebSocketServer } from './index'
56
import {
67
saveMemoryLocal,
78
getRecentMemoriesLocal,
@@ -397,6 +398,17 @@ export function registerIPCHandlers(): void {
397398
registerTakeScreenshotHotkey(settingsToSave.takeScreenshotHotkey)
398399
}
399400

401+
// Handle WebSocket port changes
402+
if (
403+
oldSettings?.websocketPort !== settingsToSave.websocketPort ||
404+
(!oldSettings && settingsToSave.websocketPort)
405+
) {
406+
console.log(
407+
'[Main IPC settings:save] WebSocket port changed. Restarting WebSocket server.'
408+
)
409+
restartWebSocketServer()
410+
}
411+
400412
return { success: true }
401413
} catch (error: any) {
402414
return { success: false, error: error.message }
@@ -775,4 +787,131 @@ export function registerGoogleIPCHandlers(): void {
775787
}
776788
}
777789
)
790+
791+
// WebSocket communication for browser context
792+
ipcMain.handle('websocket:send-request', async (event, requestData: any) => {
793+
console.log(
794+
'[IPC websocket:send-request] Starting request with data:',
795+
requestData
796+
)
797+
798+
try {
799+
const wss = getWebSocketServer()
800+
console.log(
801+
'[IPC websocket:send-request] WebSocket server status:',
802+
wss ? 'available' : 'null'
803+
)
804+
console.log(
805+
'[IPC websocket:send-request] Connected clients:',
806+
wss ? wss.clients.size : 0
807+
)
808+
809+
if (!wss || wss.clients.size === 0) {
810+
console.error(
811+
'[IPC websocket:send-request] No WebSocket clients connected'
812+
)
813+
return {
814+
success: false,
815+
error:
816+
'No WebSocket clients connected. Ensure the Chrome extension is running.',
817+
}
818+
}
819+
820+
return new Promise(resolve => {
821+
let resolved = false
822+
const timeout = setTimeout(() => {
823+
if (!resolved) {
824+
console.error(
825+
'[IPC websocket:send-request] Request timed out after 10 seconds'
826+
)
827+
resolved = true
828+
resolve({
829+
success: false,
830+
error:
831+
'WebSocket request timed out. Chrome extension may not be responding.',
832+
})
833+
}
834+
}, 10000)
835+
836+
console.log(
837+
'[IPC websocket:send-request] Sending request to',
838+
wss.clients.size,
839+
'client(s)'
840+
)
841+
842+
wss.clients.forEach((client: any) => {
843+
if (client.readyState === 1) {
844+
console.log(
845+
'[IPC websocket:send-request] Sending message to client:',
846+
requestData
847+
)
848+
client.send(JSON.stringify(requestData))
849+
850+
const onMessage = (data: any) => {
851+
if (!resolved) {
852+
try {
853+
const response = JSON.parse(data.toString())
854+
console.log(
855+
'[IPC websocket:send-request] Received message from client:',
856+
response
857+
)
858+
859+
if (
860+
response.type === 'context_response' &&
861+
response.requestId === requestData.requestId
862+
) {
863+
console.log(
864+
'[IPC websocket:send-request] Matching response received, resolving promise'
865+
)
866+
resolved = true
867+
clearTimeout(timeout)
868+
resolve({ success: true, data: response })
869+
client.removeListener('message', onMessage)
870+
} else {
871+
console.log(
872+
'[IPC websocket:send-request] Ignoring non-matching response:',
873+
response.type,
874+
'expected requestId:',
875+
requestData.requestId,
876+
'got:',
877+
response.requestId
878+
)
879+
}
880+
} catch (error) {
881+
console.error(
882+
'[IPC websocket:send-request] Error parsing message:',
883+
error
884+
)
885+
}
886+
}
887+
}
888+
889+
client.on('message', onMessage)
890+
} else {
891+
console.log(
892+
'[IPC websocket:send-request] Client not ready, state:',
893+
client.readyState
894+
)
895+
}
896+
})
897+
898+
if (!resolved && wss.clients.size === 0) {
899+
console.error(
900+
'[IPC websocket:send-request] No clients to send message to'
901+
)
902+
clearTimeout(timeout)
903+
resolve({
904+
success: false,
905+
error: 'No active WebSocket connections',
906+
})
907+
}
908+
})
909+
} catch (error: any) {
910+
console.error('[IPC websocket:send-request] Error:', error)
911+
return {
912+
success: false,
913+
error: `WebSocket communication error: ${error.message}`,
914+
}
915+
}
916+
})
778917
}

electron/main/settingsManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface AppSettings {
1919
microphoneToggleHotkey?: string
2020
mutePlaybackHotkey?: string
2121
takeScreenshotHotkey?: string
22+
websocketPort?: number
2223
}
2324

2425
export async function saveSettings(settings: AppSettings): Promise<void> {

0 commit comments

Comments
 (0)