Skip to content
Merged
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
48 changes: 48 additions & 0 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { loadConfig, getSettings, testSettings, pingModels, chatCompletions } fr

import { buildInlineAssistMessages, handleSummarize } from './prompts.js';

import { clearMcpRegisteredTab, getMcpRegisteredTab, setMcpRegisteredTab } from './lib/tab-manager.js';

// ============================================================================
// Browser API Shim (cross-browser compatibility)
// ============================================================================
Expand Down Expand Up @@ -144,6 +146,19 @@ chrome.runtime.onStartup.addListener(async () => {
}
});

// ============================================================================
// Tab Lifecycle - MCP Registered Tab Cleanup
// ============================================================================

// Clean up registered tab when it's closed
chrome.tabs.onRemoved.addListener((tabId) => {
const registeredTabId = getMcpRegisteredTab();
if (registeredTabId === tabId) {
clearMcpRegisteredTab();
console.log('[BG] MCP registered tab closed, cleared registration:', tabId);
}
});

// ============================================================================
// Action Click Handler
// ============================================================================
Expand Down Expand Up @@ -534,5 +549,38 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}

// Handle MCP_REGISTER_TAB (register a tab for MCP operations)
if (message?.type === 'MCP_REGISTER_TAB') {
try {
const { tabId } = message.payload || {};
if (tabId) {
setMcpRegisteredTab(tabId);
console.log('[BG] MCP tab registered:', tabId);
sendResponse({ ok: true, tabId });
} else if (tabId === null) {
// Explicitly disconnect/clear the registered tab
setMcpRegisteredTab(null);
console.log('[BG] MCP tab disconnected');
sendResponse({ ok: true, tabId: null });
} else {
sendResponse({ ok: false, error: 'Missing tabId' });
}
} catch (e) {
sendResponse({ ok: false, error: String(e?.message || e) });
}
return true;
}

// Handle MCP_GET_REGISTERED_TAB (get the currently registered MCP tab)
if (message?.type === 'MCP_GET_REGISTERED_TAB') {
try {
const tabId = getMcpRegisteredTab();
sendResponse({ ok: true, tabId });
} catch (e) {
sendResponse({ ok: false, error: String(e?.message || e) });
}
return true;
}

return false;
});
48 changes: 41 additions & 7 deletions src/mcp-tools/navigation.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// navigation.js
// MCP Bridge navigation tools: visit, go_back, go_forward, scroll

import { selectTab, setMcpRegisteredTab } from '../lib/tab-manager.js';
import { selectTab, setMcpRegisteredTab, getMcpRegisteredTab } from '../lib/tab-manager.js';
import { sendMessageWithRetry } from '../lib/fetch-utils.js';
import { CONTENT_LOAD_TIMEOUT, TAB_REGISTRATION_DELAY, VisitOutputModes } from '../constants.js';

/**
* Visits a URL and extracts page content
* If no tab is registered, creates and registers a new tab
* If a tab is already registered, navigates that tab to the new URL
*/
export async function handleVisit(params) {
const url = String(params?.url || '').trim();
Expand All @@ -28,9 +30,35 @@ export async function handleVisit(params) {
console.log('[MCP Tools] visit', { url, mode, maxContentLength, closeTab });

try {
// Create a new tab to visit the URL
const tab = await chrome.tabs.create({ url, active: false });
const tabId = tab.id;
let tabId;
let isNewTab = false;

// Check if we have a registered tab
const registeredTabId = getMcpRegisteredTab();

if (registeredTabId) {
// Try to use the existing registered tab
try {
await chrome.tabs.get(registeredTabId);
console.log('[MCP Tools] Using existing registered tab:', registeredTabId);

// Navigate the existing tab to the new URL
await chrome.tabs.update(registeredTabId, { url, active: false });
tabId = registeredTabId;
} catch (e) {
// Registered tab no longer exists, create a new one
console.log('[MCP Tools] Registered tab no longer exists, creating new tab');
const tab = await chrome.tabs.create({ url, active: false });
tabId = tab.id;
isNewTab = true;
}
} else {
// No registered tab, create a new one
console.log('[MCP Tools] No registered tab, creating new tab');
const tab = await chrome.tabs.create({ url, active: false });
tabId = tab.id;
isNewTab = true;
}

// Wait for the page to load
await new Promise((resolve, reject) => {
Expand Down Expand Up @@ -89,13 +117,19 @@ export async function handleVisit(params) {
await chrome.tabs.remove(tabId);
console.log('[MCP Tools] Tab closed as requested');
} else {
// Register tab for agentic workflows and make it active (visible)
setMcpRegisteredTab(tabId);
// Register tab if it's a new tab (only register once, not on every visit)
if (isNewTab) {
setMcpRegisteredTab(tabId);
console.log('[MCP Tools] Registered new tab:', tabId);
} else {
console.log('[MCP Tools] Navigated existing registered tab:', tabId);
}

// Focus the tab's window first, then activate the tab
const currentTab = await chrome.tabs.get(tabId);
await chrome.windows.update(currentTab.windowId, { focused: true });
await chrome.tabs.update(tabId, { active: true });
console.log('[MCP Tools] Registered tab:', tabId, '(now active and visible in focused window)');
console.log('[MCP Tools] Tab is now active and visible in focused window');
}

console.log('[MCP Tools] visit result', {
Expand Down
124 changes: 124 additions & 0 deletions ui/sidepanel/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ export default function App() {
const [tabSessionMap, setTabSessionMap] = useState({}) // { [tabId]: sessionId }
const [selectionText, setSelectionText] = useState('')
const [bridgeStatus, setBridgeStatus] = useState({ connected: false, usingToken: false, url: '' })
const [mcpConnectedTabId, setMcpConnectedTabId] = useState(null) // MCP registered tab for agentic workflows
const [currentActiveTabId, setCurrentActiveTabId] = useState(null) // Track the active browser tab
// Debug preview state
const [debugOpen, setDebugOpen] = useState(false)
const [debugInfo, setDebugInfo] = useState(null)
Expand Down Expand Up @@ -750,6 +752,123 @@ export default function App() {
setAutoFollowActiveTab(true)
}, [sessions, saveSessions])

// MCP Tab Connection Handlers
const handleConnectTab = useCallback(async (tabId) => {
try {
console.log('[Side Panel] 🔗 Registering MCP tab:', tabId)

// Get tab info for logging
let tabInfo = null
try {
const tab = await chrome.tabs.get(tabId)
tabInfo = { title: tab.title, url: tab.url }
console.log('[Side Panel] Tab details:', tabInfo)
} catch (_) {}

// Send message to background to register this tab for MCP operations
const response = await chrome.runtime.sendMessage({
type: 'MCP_REGISTER_TAB',
payload: { tabId }
})

if (response?.ok) {
setMcpConnectedTabId(tabId)
console.log('[Side Panel] ✅ MCP tab connected successfully:', tabId)
console.log('[Side Panel] 📍 MCP tools (visit, click, screenshot, etc.) will now operate on this tab')
if (tabInfo) {
console.log('[Side Panel] 📄 Connected tab:', tabInfo.title || '(no title)')
console.log('[Side Panel] 🔗 URL:', tabInfo.url || '(no url)')
}
} else {
console.error('[Side Panel] ❌ Failed to connect tab:', response?.error)
}
} catch (e) {
console.error('[Side Panel] ❌ Error connecting tab:', e)
}
}, [])

const handleFocusToConnectedTab = useCallback(async (tabId) => {
try {
if (!tabId) {
console.warn('[Side Panel] ⚠️ Cannot focus - no tab ID provided')
return
}

console.log('[Side Panel] 👁️ Focusing to connected tab:', tabId)

// Get the tab and focus it
const tab = await chrome.tabs.get(tabId)
console.log('[Side Panel] 📄 Focusing tab:', tab.title || '(no title)')

await chrome.windows.update(tab.windowId, { focused: true })
await chrome.tabs.update(tabId, { active: true })

console.log('[Side Panel] ✅ Successfully focused to tab:', tabId)
} catch (e) {
console.error('[Side Panel] ❌ Failed to focus tab:', e)
console.log('[Side Panel] Tab may have been closed, clearing connection')
// Tab might have been closed, clear the connection
setMcpConnectedTabId(null)
}
}, [])

const handleDisconnectTab = useCallback(async () => {
try {
console.log('[Side Panel] 🔓 Disconnecting MCP tab:', mcpConnectedTabId)

// Send message to background to clear the registered tab
await chrome.runtime.sendMessage({
type: 'MCP_REGISTER_TAB',
payload: { tabId: null }
})

setMcpConnectedTabId(null)
console.log('[Side Panel] ✅ MCP tab disconnected successfully')
} catch (e) {
console.error('[Side Panel] ❌ Error disconnecting tab:', e)
}
}, [mcpConnectedTabId])

// Poll for connected tab status from background
useEffect(() => {
const checkConnectedTab = async () => {
try {
const response = await chrome.runtime.sendMessage({
type: 'MCP_GET_REGISTERED_TAB'
})
if (response?.tabId !== undefined) {
setMcpConnectedTabId(response.tabId)
}
} catch (e) {
// Ignore errors (background might not be ready)
}
}

checkConnectedTab()
const interval = setInterval(checkConnectedTab, 2000) // Poll every 2 seconds

return () => clearInterval(interval)
}, [])

// Track the active browser tab
useEffect(() => {
const updateActiveTab = async () => {
try {
const activeTab = await getActiveTab()
if (activeTab?.id) {
setCurrentActiveTabId(activeTab.id)
}
} catch (e) {
// Ignore errors
}
}

updateActiveTab()
const interval = setInterval(updateActiveTab, 1000) // Update every second

return () => clearInterval(interval)
}, [getActiveTab])

// Auto-scroll to bottom during streaming if user hasn't scrolled up
const scrollToBottom = useCallback(() => {
if (listRef.current && !userHasScrolledUp) {
Expand Down Expand Up @@ -1701,6 +1820,11 @@ export default function App() {
setSidebarOpen={setSidebarOpen}
createNewChat={createNewChat}
SettingsTrigger={() => <SettingsTrigger onSettingsOpen={() => { setSidebarOpen(true); setSettingsOpen(true); }} />}
connectedTabId={mcpConnectedTabId}
currentTabId={currentActiveTabId}
onConnectTab={handleConnectTab}
onFocusToConnectedTab={handleFocusToConnectedTab}
onDisconnectTab={handleDisconnectTab}
/>

<ScrollArea.Root className="flex-1 relative min-h-0">
Expand Down
Loading
Loading