Skip to content

Commit 1f065d2

Browse files
Fixed alot of stuff and added the github actions
1 parent be7b509 commit 1f065d2

10 files changed

Lines changed: 468 additions & 93 deletions

File tree

.github/workflows/build.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
workflow_dispatch:
9+
10+
jobs:
11+
build:
12+
name: Build on ${{ matrix.os }}
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
matrix:
16+
os: [windows-latest, ubuntu-latest, macos-latest]
17+
18+
steps:
19+
- name: Check out Git repository
20+
uses: actions/checkout@v4
21+
22+
- name: Install Node.js
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: 20
26+
cache: 'npm'
27+
28+
- name: Install Dependencies
29+
run: npm install
30+
31+
- name: Build TypeScript
32+
run: npm run build
33+
34+
- name: Build & Release (Windows)
35+
if: runner.os == 'Windows'
36+
run: npm run package:win -- -p always
37+
env:
38+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
40+
- name: Build & Release (macOS)
41+
if: runner.os == 'macOS'
42+
run: npm run package:mac -- -p always
43+
env:
44+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45+
46+
- name: Build & Release (Linux)
47+
if: runner.os == 'Linux'
48+
run: npm run package:linux -- -p always
49+
env:
50+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51+
52+
- name: Upload Artifacts
53+
uses: actions/upload-artifact@v4
54+
with:
55+
name: OctoBrowser-${{ runner.os }}
56+
path: |
57+
release/*.exe
58+
release/*.dmg
59+
release/*.AppImage

assets/github-copilot-icon.svg

Lines changed: 1 addition & 0 deletions
Loading

src/main/copilot-service.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface BrowserToolCallbacks {
3131
searchInPage: (text: string) => Promise<{ found: boolean; matches: string[] }>;
3232
getVisualDescription: () => Promise<string>;
3333
saveScreenshotToFile: () => Promise<string | null>;
34+
pressKey: (key: string) => Promise<boolean>;
3435
}
3536

3637
// Wrap tool handlers with timeout
@@ -76,6 +77,7 @@ export class CopilotService {
7677
private onToolResult: ((toolName: string, result: string) => void) | null = null;
7778
private sessionErrorCount: number = 0;
7879
private readonly MAX_SESSION_ERRORS = 3;
80+
private activeStreamCleanup: (() => void) | null = null;
7981

8082
async initialize(): Promise<boolean> {
8183
try {
@@ -199,6 +201,10 @@ If you've opened multiple tabs trying to find something please close the old unu
199201
- **YouTube Videos**: YouTube videos automatically play when opened, theres no need to click play.
200202
- **Tab management**: If you have more than 2 tabs open, close any that are not needed using browser_close_tabs to keep your workspace tidy. Make sure to not continue opening new tabs without closing old ones. and dont close the active tab unless instructed. and dont close tabs opened by the user. Make sure after completing a task to list open tabs using browser_get_open_tabs and close any unneeded ones.
201203
</best_practices>
204+
205+
<never_do>
206+
Never use any tools or take any actions outside of the provided browser tools. You ARE a BROWSER AUTOMATION AGENT.
207+
</never_do>
202208
`,
203209
},
204210
});
@@ -474,6 +480,32 @@ If you've opened multiple tabs trying to find something please close the old unu
474480
}
475481
},
476482
}),
483+
defineToolFn('browser_press_key', {
484+
description: 'Press a specific key or key combination (e.g., "Enter", "Tab", "ArrowDown", "Control+C"). Use this for navigation, shortcuts, or submitting forms without a submit button.',
485+
parameters: {
486+
type: 'object',
487+
properties: {
488+
key: {
489+
type: 'string',
490+
description: 'The key or combination to press (e.g. "Enter", "a", "Control+a")',
491+
},
492+
},
493+
required: ['key'],
494+
},
495+
handler: async (args: { key: string }) => {
496+
if (!callbacks || !callbacks.pressKey) return 'Press key capability not available';
497+
try {
498+
const success = await withTimeout(callbacks.pressKey(args.key), 5000);
499+
const msg = success ? `Pressed key: "${args.key}"` : `Failed to press key: "${args.key}"`;
500+
reportResult('browser_press_key', msg);
501+
return msg;
502+
} catch (error: any) {
503+
const msg = `Failed to press key: ${error.message || 'unknown error'}`;
504+
reportResult('browser_press_key', msg);
505+
return msg;
506+
}
507+
},
508+
}),
477509
defineToolFn('browser_find_in_page', {
478510
description: 'Find and highlight text on the current page.',
479511
parameters: {
@@ -885,6 +917,12 @@ If you've opened multiple tabs trying to find something please close the old unu
885917
throw new Error('Session not created');
886918
}
887919

920+
// Ensure any previous stream is cleaned up to prevent duplicate listeners
921+
if (this.activeStreamCleanup) {
922+
console.log('Cleaning up previous active stream before starting new one');
923+
this.activeStreamCleanup();
924+
}
925+
888926
this.conversationHistory.push({ role: 'user', content: message });
889927

890928
return new Promise((resolve, reject) => {
@@ -936,6 +974,14 @@ If you've opened multiple tabs trying to find something please close the old unu
936974
idleTimeout = null;
937975
}
938976
this.onToolResult = null;
977+
this.activeStreamCleanup = null;
978+
};
979+
980+
// Register global cleanup for abort/new stream
981+
this.activeStreamCleanup = () => {
982+
cleanup();
983+
// Resolve with partial content if aborted/interrupted
984+
resolve(fullContent);
939985
};
940986

941987
// Set up tool result listener to capture exact output from handlers
@@ -1109,7 +1155,32 @@ If you've opened multiple tabs trying to find something please close the old unu
11091155
this.conversationHistory = [];
11101156
}
11111157

1158+
async resetSession(): Promise<void> {
1159+
// Clear conversation history
1160+
this.conversationHistory = [];
1161+
1162+
// Destroy existing session and create a fresh one
1163+
if (this.session) {
1164+
try {
1165+
await this.session.destroy();
1166+
} catch (e) {
1167+
// Ignore session cleanup errors
1168+
}
1169+
this.session = null;
1170+
}
1171+
1172+
// Create a fresh session
1173+
await this.createSession();
1174+
console.log('Copilot session reset - context cleared');
1175+
}
1176+
11121177
async abort(): Promise<void> {
1178+
// First cleanup any active stream listeners
1179+
if (this.activeStreamCleanup) {
1180+
console.log('Aborting active stream listener...');
1181+
this.activeStreamCleanup();
1182+
}
1183+
11131184
if (this.session) {
11141185
try {
11151186
await this.session.abort();

src/main/main.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ function setupIpcHandlers() {
125125
takeScreenshot: async () => {
126126
return tabManager?.takeScreenshot() || null;
127127
},
128+
pressKey: async (key) => {
129+
return tabManager?.pressKey(key) || false;
130+
},
128131
});
129132
}
130133
return copilotService.initialize();
@@ -227,7 +230,19 @@ function createMenu() {
227230
{ type: 'separator' },
228231
{ role: 'togglefullscreen' },
229232
{ type: 'separator' },
230-
{ role: 'toggleDevTools' }
233+
{
234+
label: 'Toggle Developer Tools',
235+
accelerator: 'F12',
236+
click: () => mainWindow?.webContents.toggleDevTools()
237+
},
238+
{ type: 'separator' },
239+
{
240+
label: 'Toggle Sidebar',
241+
accelerator: 'Ctrl+Shift+I',
242+
click: () => {
243+
mainWindow?.webContents.send('sidebar:toggle');
244+
}
245+
}
231246
]
232247
},
233248
{

src/main/main.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ function createMainWindow(): BrowserWindow {
5858
// Dev tools disabled by default - use Ctrl+Shift+I or View menu
5959
});
6060

61+
// Listen for window state changes
62+
mainWindow.on('maximize', () => {
63+
mainWindow?.webContents.send('window:state-changed', { isMaximized: true });
64+
});
65+
mainWindow.on('unmaximize', () => {
66+
mainWindow?.webContents.send('window:state-changed', { isMaximized: false });
67+
});
68+
6169
// Save window bounds on close
6270
mainWindow.on('close', () => {
6371
if (mainWindow) {
@@ -223,6 +231,9 @@ function setupIpcHandlers(): void {
223231
saveScreenshotToFile: async () => {
224232
return tabManager?.saveScreenshotToFile() || null;
225233
},
234+
pressKey: async (key: string) => {
235+
return tabManager?.pressKey(key) || false;
236+
},
226237
});
227238
}
228239
return copilotService.initialize();
@@ -275,6 +286,11 @@ function setupIpcHandlers(): void {
275286
copilotService?.abort();
276287
});
277288

289+
ipcMain.handle('copilot:resetSession', async () => {
290+
await copilotService?.resetSession();
291+
return true;
292+
});
293+
278294
// Settings
279295
ipcMain.handle('settings:get', (_event, key: string) => settingsStore.get(key));
280296
ipcMain.handle('settings:set', (_event, key: string, value: unknown) => {
@@ -405,7 +421,19 @@ function createMenu(): void {
405421
{ type: 'separator' },
406422
{ role: 'togglefullscreen' },
407423
{ type: 'separator' },
408-
{ role: 'toggleDevTools' }
424+
{
425+
label: 'Toggle Developer Tools',
426+
accelerator: 'F12',
427+
click: () => mainWindow?.webContents.toggleDevTools()
428+
},
429+
{ type: 'separator' },
430+
{
431+
label: 'Toggle Sidebar',
432+
accelerator: 'Ctrl+Shift+I',
433+
click: () => {
434+
mainWindow?.webContents.send('sidebar:toggle');
435+
}
436+
}
409437
]
410438
},
411439
{

src/main/tab-manager.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,4 +1119,66 @@ export class TabManager {
11191119
return null;
11201120
}
11211121
}
1122+
1123+
async pressKey(key: string): Promise<boolean> {
1124+
if (!this.activeTabId) return false;
1125+
1126+
const view = this.tabs.get(this.activeTabId);
1127+
if (!view) return false;
1128+
1129+
try {
1130+
// Parse modifiers (e.g. "Control+Shift+A")
1131+
const parts = key.split('+');
1132+
let inputKey = parts.pop() || ''; // The last part is the key
1133+
const modifiers: string[] = [];
1134+
1135+
parts.forEach(part => {
1136+
const p = part.toLowerCase().trim();
1137+
if (['ctrl', 'control'].includes(p)) modifiers.push('control');
1138+
if (['shift'].includes(p)) modifiers.push('shift');
1139+
if (['alt', 'option'].includes(p)) modifiers.push('alt');
1140+
if (['meta', 'cmd', 'command', 'super'].includes(p)) modifiers.push('meta'); // 'meta' is Command on Mac
1141+
});
1142+
1143+
// Map common keys to Electron accelerator format
1144+
const keyMap: {[key: string]: string} = {
1145+
'arrowdown': 'Down', 'down': 'Down',
1146+
'arrowup': 'Up', 'up': 'Up',
1147+
'arrowleft': 'Left', 'left': 'Left',
1148+
'arrowright': 'Right', 'right': 'Right',
1149+
'enter': 'Enter', 'return': 'Enter',
1150+
'tab': 'Tab',
1151+
'space': 'Space',
1152+
'backspace': 'Backspace',
1153+
'delete': 'Delete',
1154+
'escape': 'Escape', 'esc': 'Escape',
1155+
'pagedown': 'PageDown', 'pgdn': 'PageDown',
1156+
'pageup': 'PageUp', 'pgup': 'PageUp',
1157+
'home': 'Home',
1158+
'end': 'End',
1159+
'insert': 'Insert'
1160+
};
1161+
1162+
const lowerKey = inputKey.toLowerCase();
1163+
const finalKeyCode = keyMap[lowerKey] || inputKey;
1164+
1165+
// Send keyDown then keyUp with modifiers
1166+
view.webContents.sendInputEvent({
1167+
type: 'keyDown',
1168+
keyCode: finalKeyCode,
1169+
modifiers: modifiers as any
1170+
});
1171+
1172+
view.webContents.sendInputEvent({
1173+
type: 'keyUp',
1174+
keyCode: finalKeyCode,
1175+
modifiers: modifiers as any
1176+
});
1177+
1178+
return true;
1179+
} catch (error) {
1180+
console.error('Failed to press key:', error);
1181+
return false;
1182+
}
1183+
}
11221184
}

src/preload/preload.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
5353
startStream: (message: string, model?: string) =>
5454
ipcRenderer.send('copilot:stream-start', message, model),
5555
abortStream: () => ipcRenderer.send('copilot:abort'),
56+
resetSession: () => ipcRenderer.invoke('copilot:resetSession'),
5657
sendAnswer: (answers: any) => ipcRenderer.invoke('copilot:answer-user', answers),
5758

5859
// Settings
@@ -110,6 +111,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
110111
ipcRenderer.on('show:about', () => callback());
111112
},
112113

114+
// Sidebar Toggle Event
115+
onSidebarToggle: (callback: () => void) => {
116+
ipcRenderer.on('sidebar:toggle', () => callback());
117+
},
118+
119+
// Window State Event
120+
onWindowStateChanged: (callback: (state: { isMaximized: boolean }) => void) => {
121+
ipcRenderer.on('window:state-changed', (_event, state) => callback(state));
122+
},
123+
113124
// Zero State Event
114125
onZeroStateChanged: (callback: (isZeroState: boolean) => void) => {
115126
ipcRenderer.on('tab:zero-state', (_event, isZeroState) => callback(isZeroState));
@@ -147,6 +158,7 @@ declare global {
147158
searchWeb: (query: string) => Promise<{ success: boolean; url: string }>;
148159
startStream: (message: string, model?: string) => void;
149160
abortStream: () => void;
161+
resetSession: () => Promise<boolean>;
150162
sendAnswer: (answers: any) => Promise<boolean>;
151163
getSetting: (key: string) => Promise<unknown>;
152164
setSetting: (key: string, value: unknown) => Promise<boolean>;

0 commit comments

Comments
 (0)