Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<p align="center">
<a href="https://github.com/compnew2006/browser-controller/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/compnew2006/browser-controller/ci.yml?branch=main&label=CI&style=flat-square" alt="CI" /></a>
<a href="https://github.com/compnew2006/browser-controller/releases"><img src="https://img.shields.io/badge/version-2.2.0-blue?style=flat-square" alt="v2.2.0" /></a>
<a href="https://github.com/compnew2006/browser-controller/releases"><img src="https://img.shields.io/badge/version-2.2.1-blue?style=flat-square" alt="v2.2.1" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow?style=flat-square" alt="License: MIT" /></a>
<img src="https://img.shields.io/badge/node-%E2%89%A520-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node >= 20" />
<img src="https://img.shields.io/badge/TypeScript-strict-blue?style=flat-square" alt="TypeScript strict" />
Expand Down
17 changes: 14 additions & 3 deletions extension/handlers/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,24 @@ export async function getActiveTab() {
}

export async function handleNavigate(params, _sessionId, _agentName, signal) {
const { url, waitUntil = 'load', tabId, snapshot: wantSnapshot = true } = params;
let { url } = params;
const { waitUntil = 'load', tabId, snapshot: wantSnapshot = true } = params;
// navigate is the one page tool allowed to omit tabId → active tab fallback.
const tab = tabId != null ? await resolveTab(tabId) : await getActiveTab();

// Fix #2 (hash-aware): a hash-only navigation does NOT reload the document,
// so `chrome.tabs.onUpdated` never fires `status === 'complete'` and the wait
// below would hang for the full 55s timeout. Detect this case and skip the
// wait entirely — the SPA router updates client-side near-instantly.
// "back"/"forward" walk the tab's history instead of being treated as a
// relative URL (which used to land on chrome-extension://<id>/back).
const historyStep = url === 'back' || url === 'forward' ? url : null;
const go = () => historyStep === 'back' ? chrome.tabs.goBack(tab.id)
: historyStep === 'forward' ? chrome.tabs.goForward(tab.id)
: chrome.tabs.update(tab.id, { url });

const currentTab = await chrome.tabs.get(tab.id);
const hashOnly = isHashOnlyChange(currentTab.url, url);
const hashOnly = !historyStep && isHashOnlyChange(currentTab.url, url);

// A promise that rejects when this call is cancelled (client gone / timeout
// forwarded from the bridge). Handlers that await long-running operations
Expand Down Expand Up @@ -84,7 +92,7 @@ export async function handleNavigate(params, _sessionId, _agentName, signal) {
};
chrome.tabs.onUpdated.addListener(listener);
if (wantDcl) pollTimer = setInterval(probeReady, 150);
chrome.tabs.update(tab.id, { url }).catch((err) => {
go().catch((err) => {
chrome.tabs.onUpdated.removeListener(listener);
if (pollTimer) clearInterval(pollTimer);
reject(err);
Expand All @@ -111,6 +119,9 @@ export async function handleNavigate(params, _sessionId, _agentName, signal) {
// Return the snapshot inline ONLY if the caller asked for it (default true).
// Skipping it (snapshot:false) saves a large chunk of tokens when the agent
// intends to call browser_snapshot itself or doesn't need the tree yet.
// For back/forward report where we actually landed, not the keyword.
if (historyStep) url = (await chrome.tabs.get(tab.id)).url;

if (!wantSnapshot) {
return { url, status: 'navigated', tabId: tab.id };
}
Expand Down
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Browser Controller",
"version": "2.2.0",
"version": "2.2.1",
"description": "Let AI agents control your real browser - your tabs, your sessions, your logins",
"icons": {
"16": "icons/icon16.png",
Expand Down
2 changes: 1 addition & 1 deletion mcp-server/src/tools/navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const navigateTool: ToolDefinition = {
name: 'browser_navigate',
summary: 'Navigate a tab to a URL (+ optional inline snapshot)', description: 'Navigate to a URL in a browser tab. If tabId is omitted, navigates the active tab (or use browser_tabs create to open a new one). By default also returns a compact snapshot of the new page so you can act immediately; set snapshot:false to skip it and save tokens.',
inputSchema: z.object({
url: z.string().describe('The URL to navigate to'),
url: z.string().describe('The URL to navigate to, or "back" / "forward" to move through the tab history'),
tabId: optionalTabId(),
waitUntil: z
.enum(['load', 'domcontentloaded'])
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "browser-controller",
"mcpName": "io.github.noiemany/browser-controller",
"version": "2.2.0",
"version": "2.2.1",
"description": "MCP server + Chrome extension that gives AI agents control of your real browser with existing sessions and logins",
"type": "module",
"bin": {
Expand Down
5 changes: 4 additions & 1 deletion scripts/drive.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import path from 'node:path';

const STATE = path.join(os.homedir(), '.browser-controller');
const token = JSON.parse(fs.readFileSync(path.join(STATE, 'token.json'), 'utf8')).token;
const sockPath = path.join(STATE, 'daemon.sock');
// Must match daemon-config.ts: Windows uses a named pipe, not a socket file.
const sockPath = process.platform === 'win32'
? '\\\\.\\pipe\\browser-controller'
: path.join(STATE, 'daemon.sock');

const tool = process.argv[2];
if (!tool) { console.error('usage: drive.mjs <tool> [json params]'); process.exit(1); }
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
"url": "https://github.com/compnew2006/browser-controller",
"source": "github"
},
"version": "2.2.0",
"version": "2.2.1",
"packages": [
{
"registryType": "npm",
"identifier": "browser-controller",
"version": "2.2.0",
"version": "2.2.1",
"transport": {
"type": "stdio"
}
Expand Down
Loading