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 {
+29
View File
@@ -139,6 +139,25 @@ you have access to some functions in addition to playwright methods:
// search only in CSS files
const cssMatches = await editor.grep({ regex: /background-color/, pattern: /\.css/ });
```
- `getStylesForLocator({ locator, includeUserAgentStyles? })`: gets the CSS styles applied to an element, similar to browser DevTools "Styles" panel.
- `locator`: a Playwright Locator for the element to inspect
- `includeUserAgentStyles`: (optional, default: false) include browser default styles
- Returns: `StylesResult` object with:
- `element`: string description of the element (e.g. `div#main.container`)
- `inlineStyle`: object of `{ property: value }` inline styles, or null
- `rules`: array of CSS rules that apply to this element, each with:
- `selector`: the CSS selector that matched
- `source`: `{ url, line, column }` location in the stylesheet, or null
- `origin`: `"regular"` | `"user-agent"` | `"injected"` | `"inspector"`
- `declarations`: object of `{ property: value }` (values include `!important` if applicable)
- `inheritedFrom`: element description if inherited (e.g. `body`), or null for direct matches
- Example:
```js
const loc = page.locator('.my-button');
const styles = await getStylesForLocator({ locator: loc });
console.log(formatStylesAsText(styles));
```
- `formatStylesAsText(styles)`: formats a `StylesResult` object as human-readable text. Use this to display styles in a readable format.
example:
@@ -184,6 +203,16 @@ const stableLocator = page.getByRole('button', { name: 'Save' })
await stableLocator.click();
```
## pinned elements (user right-click to pin)
Users can right-click an element and select "Pin to Playwriter" to store it in `globalThis.playwriterPinnedElem1` (increments for each pin). The variable name is copied to clipboard.
```js
const el = await page.evaluateHandle(() => globalThis.playwriterPinnedElem1);
await el.click();
const selector = await getLocatorStringForElement(el);
```
## finding specific elements with snapshot
You can use `search` to find specific elements in the snapshot without reading the whole page structure. This is useful for finding forms, textareas, or specific text.