first commit
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
|
||||
import { BaseBrowserToolExecutor } from '../base-browser';
|
||||
import { TOOL_NAMES } from 'chrome-mcp-shared';
|
||||
|
||||
// Default window dimensions
|
||||
const DEFAULT_WINDOW_WIDTH = 1280;
|
||||
const DEFAULT_WINDOW_HEIGHT = 720;
|
||||
|
||||
interface NavigateToolParams {
|
||||
url?: string;
|
||||
newWindow?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
refresh?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for navigating to URLs in browser tabs or windows
|
||||
*/
|
||||
class NavigateTool extends BaseBrowserToolExecutor {
|
||||
name = TOOL_NAMES.BROWSER.NAVIGATE;
|
||||
|
||||
async execute(args: NavigateToolParams): Promise<ToolResult> {
|
||||
const { newWindow = false, width, height, url, refresh = false } = args;
|
||||
|
||||
console.log(
|
||||
`Attempting to ${refresh ? 'refresh current tab' : `open URL: ${url}`} with options:`,
|
||||
args,
|
||||
);
|
||||
|
||||
try {
|
||||
// Handle refresh option first
|
||||
if (refresh) {
|
||||
console.log('Refreshing current active tab');
|
||||
|
||||
// Get current active tab
|
||||
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (!activeTab || !activeTab.id) {
|
||||
return createErrorResponse('No active tab found to refresh');
|
||||
}
|
||||
|
||||
// Reload the tab
|
||||
await chrome.tabs.reload(activeTab.id);
|
||||
|
||||
console.log(`Refreshed tab ID: ${activeTab.id}`);
|
||||
|
||||
// Get updated tab information
|
||||
const updatedTab = await chrome.tabs.get(activeTab.id);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Successfully refreshed current tab',
|
||||
tabId: updatedTab.id,
|
||||
windowId: updatedTab.windowId,
|
||||
url: updatedTab.url,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate that url is provided when not refreshing
|
||||
if (!url) {
|
||||
return createErrorResponse('URL parameter is required when refresh is not true');
|
||||
}
|
||||
|
||||
// 1. Check if URL is already open
|
||||
// Get all tabs and manually compare URLs
|
||||
console.log(`Checking if URL is already open: ${url}`);
|
||||
// Get all tabs
|
||||
const allTabs = await chrome.tabs.query({});
|
||||
// Manually filter matching tabs
|
||||
const tabs = allTabs.filter((tab) => {
|
||||
// Normalize URLs for comparison (remove trailing slashes)
|
||||
const tabUrl = tab.url?.endsWith('/') ? tab.url.slice(0, -1) : tab.url;
|
||||
const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
return tabUrl === targetUrl;
|
||||
});
|
||||
console.log(`Found ${tabs.length} matching tabs`);
|
||||
|
||||
if (tabs && tabs.length > 0) {
|
||||
const existingTab = tabs[0];
|
||||
console.log(
|
||||
`URL already open in Tab ID: ${existingTab.id}, Window ID: ${existingTab.windowId}`,
|
||||
);
|
||||
|
||||
if (existingTab.id !== undefined) {
|
||||
// Activate the tab
|
||||
await chrome.tabs.update(existingTab.id, { active: true });
|
||||
|
||||
if (existingTab.windowId !== undefined) {
|
||||
// Bring the window containing this tab to the foreground and focus it
|
||||
await chrome.windows.update(existingTab.windowId, { focused: true });
|
||||
}
|
||||
|
||||
console.log(`Activated existing Tab ID: ${existingTab.id}`);
|
||||
// Get updated tab information and return it
|
||||
const updatedTab = await chrome.tabs.get(existingTab.id);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Activated existing tab',
|
||||
tabId: updatedTab.id,
|
||||
windowId: updatedTab.windowId,
|
||||
url: updatedTab.url,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If URL is not already open, decide how to open it based on options
|
||||
const openInNewWindow = newWindow || typeof width === 'number' || typeof height === 'number';
|
||||
|
||||
if (openInNewWindow) {
|
||||
console.log('Opening URL in a new window.');
|
||||
|
||||
// Create new window
|
||||
const newWindow = await chrome.windows.create({
|
||||
url: url,
|
||||
width: typeof width === 'number' ? width : DEFAULT_WINDOW_WIDTH,
|
||||
height: typeof height === 'number' ? height : DEFAULT_WINDOW_HEIGHT,
|
||||
focused: true,
|
||||
});
|
||||
|
||||
if (newWindow && newWindow.id !== undefined) {
|
||||
console.log(`URL opened in new Window ID: ${newWindow.id}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Opened URL in new window',
|
||||
windowId: newWindow.id,
|
||||
tabs: newWindow.tabs
|
||||
? newWindow.tabs.map((tab) => ({
|
||||
tabId: tab.id,
|
||||
url: tab.url,
|
||||
}))
|
||||
: [],
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log('Opening URL in the last active window.');
|
||||
// Try to open a new tab in the most recently active window
|
||||
const lastFocusedWindow = await chrome.windows.getLastFocused({ populate: false });
|
||||
|
||||
if (lastFocusedWindow && lastFocusedWindow.id !== undefined) {
|
||||
console.log(`Found last focused Window ID: ${lastFocusedWindow.id}`);
|
||||
|
||||
const newTab = await chrome.tabs.create({
|
||||
url: url,
|
||||
windowId: lastFocusedWindow.id,
|
||||
active: true,
|
||||
});
|
||||
|
||||
// Ensure the window also gets focus
|
||||
await chrome.windows.update(lastFocusedWindow.id, { focused: true });
|
||||
|
||||
console.log(
|
||||
`URL opened in new Tab ID: ${newTab.id} in existing Window ID: ${lastFocusedWindow.id}`,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Opened URL in new tab in existing window',
|
||||
tabId: newTab.id,
|
||||
windowId: lastFocusedWindow.id,
|
||||
url: newTab.url,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} else {
|
||||
// In rare cases, if there's no recently active window (e.g., browser just started with no windows)
|
||||
// Fall back to opening in a new window
|
||||
console.warn('No last focused window found, falling back to creating a new window.');
|
||||
|
||||
const fallbackWindow = await chrome.windows.create({
|
||||
url: url,
|
||||
width: DEFAULT_WINDOW_WIDTH,
|
||||
height: DEFAULT_WINDOW_HEIGHT,
|
||||
focused: true,
|
||||
});
|
||||
|
||||
if (fallbackWindow && fallbackWindow.id !== undefined) {
|
||||
console.log(`URL opened in fallback new Window ID: ${fallbackWindow.id}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Opened URL in new window',
|
||||
windowId: fallbackWindow.id,
|
||||
tabs: fallbackWindow.tabs
|
||||
? fallbackWindow.tabs.map((tab) => ({
|
||||
tabId: tab.id,
|
||||
url: tab.url,
|
||||
}))
|
||||
: [],
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all attempts fail, return a generic error
|
||||
return createErrorResponse('Failed to open URL: Unknown error occurred');
|
||||
} catch (error) {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
|
||||
return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
|
||||
} else {
|
||||
console.error('Error in navigate:', error);
|
||||
return createErrorResponse(
|
||||
`Error navigating to URL: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export const navigateTool = new NavigateTool();
|
||||
|
||||
interface CloseTabsToolParams {
|
||||
tabIds?: number[];
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for closing browser tabs
|
||||
*/
|
||||
class CloseTabsTool extends BaseBrowserToolExecutor {
|
||||
name = TOOL_NAMES.BROWSER.CLOSE_TABS;
|
||||
|
||||
async execute(args: CloseTabsToolParams): Promise<ToolResult> {
|
||||
const { tabIds, url } = args;
|
||||
let urlPattern = url;
|
||||
console.log(`Attempting to close tabs with options:`, args);
|
||||
|
||||
try {
|
||||
// If URL is provided, close all tabs matching that URL
|
||||
if (urlPattern) {
|
||||
console.log(`Searching for tabs with URL: ${url}`);
|
||||
if (!urlPattern.endsWith('/')) {
|
||||
urlPattern += '/*';
|
||||
}
|
||||
const tabs = await chrome.tabs.query({ url });
|
||||
|
||||
if (!tabs || tabs.length === 0) {
|
||||
console.log(`No tabs found with URL: ${url}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
message: `No tabs found with URL: ${url}`,
|
||||
closedCount: 0,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`Found ${tabs.length} tabs with URL: ${url}`);
|
||||
const tabIdsToClose = tabs
|
||||
.map((tab) => tab.id)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
|
||||
if (tabIdsToClose.length === 0) {
|
||||
return createErrorResponse('Found tabs but could not get their IDs');
|
||||
}
|
||||
|
||||
await chrome.tabs.remove(tabIdsToClose);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: `Closed ${tabIdsToClose.length} tabs with URL: ${url}`,
|
||||
closedCount: tabIdsToClose.length,
|
||||
closedTabIds: tabIdsToClose,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
// If tabIds are provided, close those tabs
|
||||
if (tabIds && tabIds.length > 0) {
|
||||
console.log(`Closing tabs with IDs: ${tabIds.join(', ')}`);
|
||||
|
||||
// Verify that all tabIds exist
|
||||
const existingTabs = await Promise.all(
|
||||
tabIds.map(async (tabId) => {
|
||||
try {
|
||||
return await chrome.tabs.get(tabId);
|
||||
} catch (error) {
|
||||
console.warn(`Tab with ID ${tabId} not found`);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const validTabIds = existingTabs
|
||||
.filter((tab): tab is chrome.tabs.Tab => tab !== null)
|
||||
.map((tab) => tab.id)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
|
||||
if (validTabIds.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
message: 'None of the provided tab IDs exist',
|
||||
closedCount: 0,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
await chrome.tabs.remove(validTabIds);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: `Closed ${validTabIds.length} tabs`,
|
||||
closedCount: validTabIds.length,
|
||||
closedTabIds: validTabIds,
|
||||
invalidTabIds: tabIds.filter((id) => !validTabIds.includes(id)),
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
// If no tabIds or URL provided, close the current active tab
|
||||
console.log('No tabIds or URL provided, closing active tab');
|
||||
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (!activeTab || !activeTab.id) {
|
||||
return createErrorResponse('No active tab found');
|
||||
}
|
||||
|
||||
await chrome.tabs.remove(activeTab.id);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Closed active tab',
|
||||
closedCount: 1,
|
||||
closedTabIds: [activeTab.id],
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in CloseTabsTool.execute:', error);
|
||||
return createErrorResponse(
|
||||
`Error closing tabs: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const closeTabsTool = new CloseTabsTool();
|
||||
|
||||
interface GoBackOrForwardToolParams {
|
||||
isForward?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for navigating back or forward in browser history
|
||||
*/
|
||||
class GoBackOrForwardTool extends BaseBrowserToolExecutor {
|
||||
name = TOOL_NAMES.BROWSER.GO_BACK_OR_FORWARD;
|
||||
|
||||
async execute(args: GoBackOrForwardToolParams): Promise<ToolResult> {
|
||||
const { isForward = false } = args;
|
||||
|
||||
console.log(`Attempting to navigate ${isForward ? 'forward' : 'back'} in browser history`);
|
||||
|
||||
try {
|
||||
// Get current active tab
|
||||
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (!activeTab || !activeTab.id) {
|
||||
return createErrorResponse('No active tab found');
|
||||
}
|
||||
|
||||
// Navigate back or forward based on the isForward parameter
|
||||
if (isForward) {
|
||||
await chrome.tabs.goForward(activeTab.id);
|
||||
console.log(`Navigated forward in tab ID: ${activeTab.id}`);
|
||||
} else {
|
||||
await chrome.tabs.goBack(activeTab.id);
|
||||
console.log(`Navigated back in tab ID: ${activeTab.id}`);
|
||||
}
|
||||
|
||||
// Get updated tab information
|
||||
const updatedTab = await chrome.tabs.get(activeTab.id);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: `Successfully navigated ${isForward ? 'forward' : 'back'} in browser history`,
|
||||
tabId: updatedTab.id,
|
||||
windowId: updatedTab.windowId,
|
||||
url: updatedTab.url,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
|
||||
return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
|
||||
} else {
|
||||
console.error('Error in GoBackOrForwardTool.execute:', error);
|
||||
return createErrorResponse(
|
||||
`Error navigating ${isForward ? 'forward' : 'back'}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const goBackOrForwardTool = new GoBackOrForwardTool();
|
Reference in New Issue
Block a user