add right context menu for pinning

This commit is contained in:
Tommy D. Rossi
2025-12-28 23:36:20 +01:00
parent 5c5608cbb7
commit ba84eb5ac1
4 changed files with 117 additions and 1 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Playwriter MCP",
"version": "0.0.55",
"description": "Automate your Browser using Cursor, Claude, VS Code. More capable and context efficient than Playwright MCP.",
"permissions": ["debugger", "tabGroups"],
"permissions": ["debugger", "tabGroups", "contextMenus"],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "lib/background.mjs",
+86
View File
@@ -368,6 +368,17 @@ async function attachTab(tabId: number): Promise<Protocol.Target.TargetInfo> {
await chrome.debugger.sendCommand(debuggee, 'Page.enable')
await chrome.debugger.sendCommand(debuggee, 'Runtime.evaluate', {
expression: `
if (!window.__playwriter_contextmenu_listener) {
window.__playwriter_contextmenu_listener = true;
document.addEventListener('contextmenu', (e) => {
window.__playwriter_lastRightClicked = e.target;
}, true);
}
`,
})
const result = (await chrome.debugger.sendCommand(
debuggee,
'Target.getTargetInfo',
@@ -935,6 +946,19 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
resetDebugger()
chrome.contextMenus.create({
id: 'playwriter-pin-element',
title: 'Pin to Playwriter',
contexts: ['all'],
visible: false,
})
function updateContextMenuVisibility(): void {
const { currentTabId, tabs } = store.getState()
const isConnected = currentTabId !== undefined && tabs.get(currentTabId)?.state === 'connected'
chrome.contextMenus.update('playwriter-pin-element', { visible: isConnected })
}
chrome.runtime.onInstalled.addListener((details) => {
if (import.meta.env.TESTING) return
if (details.reason === 'install') {
@@ -949,6 +973,7 @@ function serializeTabs(tabs: Map<number, TabInfo>): string {
store.subscribe((state, prevState) => {
logger.log(state)
void updateIcons()
updateContextMenuVisibility()
const tabsChanged = serializeTabs(state.tabs) !== serializeTabs(prevState.tabs)
if (tabsChanged) {
syncTabGroupQueue = syncTabGroupQueue.then(syncTabGroup).catch((e) => {
@@ -965,5 +990,66 @@ chrome.tabs.onUpdated.addListener(() => {
void updateIcons()
})
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId !== 'playwriter-pin-element' || !tab?.id) return
const tabInfo = store.getState().tabs.get(tab.id)
if (!tabInfo || tabInfo.state !== 'connected') {
logger.debug('Tab not connected, ignoring')
return
}
const debuggee = { tabId: tab.id }
const count = (tabInfo.pinnedCount || 0) + 1
store.setState((state) => {
const newTabs = new Map(state.tabs)
const existing = newTabs.get(tab.id!)
if (existing) {
newTabs.set(tab.id!, { ...existing, pinnedCount: count })
}
return { tabs: newTabs }
})
const name = `playwriterPinnedElem${count}`
try {
const result = (await chrome.debugger.sendCommand(debuggee, 'Runtime.evaluate', {
expression: `
if (window.__playwriter_lastRightClicked) {
window.${name} = window.__playwriter_lastRightClicked;
'${name}';
} else {
throw new Error('No element was right-clicked');
}
`,
returnByValue: true,
})) as { result?: { value?: string }; exceptionDetails?: { text: string } }
if (result.exceptionDetails) {
logger.error('Failed to pin element:', result.exceptionDetails.text)
return
}
await chrome.debugger.sendCommand(debuggee, 'Runtime.evaluate', {
expression: `
(() => {
const el = window.${name};
if (!el) return;
const orig = el.getAttribute('style') || '';
el.setAttribute('style', orig + '; outline: 3px solid #22c55e !important; outline-offset: 2px !important; box-shadow: 0 0 0 3px #22c55e !important;');
setTimeout(() => el.setAttribute('style', orig), 300);
return navigator.clipboard.writeText('globalThis.${name}');
})()
`,
awaitPromise: true,
})
logger.debug('Pinned element as:', name)
} catch (error: any) {
logger.error('Failed to pin element:', error.message)
}
})
// Sync icons on first load
void updateIcons()
+1
View File
@@ -6,6 +6,7 @@ export interface TabInfo {
targetId?: string
state: TabState
errorText?: string
pinnedCount?: number
}
export interface ExtensionState {