hangwin--mcp-chrome
206 行
6.5 KiB
TypeScript
206 行
6.5 KiB
TypeScript
/**
|
|
* Element Picker Content Script
|
|
*
|
|
* Renders the Element Picker Panel UI (Quick Panel style) and forwards UI events
|
|
* to background while a chrome_request_element_selection session is active.
|
|
*
|
|
* This script only runs in the top frame and handles:
|
|
* - Displaying the element picker panel UI
|
|
* - Forwarding user actions (cancel, confirm, etc.) to background
|
|
* - Receiving state updates from background
|
|
*/
|
|
|
|
import {
|
|
createElementPickerController,
|
|
type ElementPickerController,
|
|
type ElementPickerUiState,
|
|
} from '@/shared/element-picker';
|
|
import { BACKGROUND_MESSAGE_TYPES, TOOL_MESSAGE_TYPES } from '@/common/message-types';
|
|
import type { PickedElement } from 'chrome-mcp-shared';
|
|
|
|
// ============================================================
|
|
// Message Types
|
|
// ============================================================
|
|
|
|
interface UiShowMessage {
|
|
action: typeof TOOL_MESSAGE_TYPES.ELEMENT_PICKER_UI_SHOW;
|
|
sessionId: string;
|
|
requests: Array<{ id: string; name: string; description?: string }>;
|
|
activeRequestId: string | null;
|
|
deadlineTs: number;
|
|
}
|
|
|
|
interface UiUpdateMessage {
|
|
action: typeof TOOL_MESSAGE_TYPES.ELEMENT_PICKER_UI_UPDATE;
|
|
sessionId: string;
|
|
activeRequestId: string | null;
|
|
selections: Record<string, PickedElement | null>;
|
|
deadlineTs: number;
|
|
errorMessage: string | null;
|
|
}
|
|
|
|
interface UiHideMessage {
|
|
action: typeof TOOL_MESSAGE_TYPES.ELEMENT_PICKER_UI_HIDE;
|
|
sessionId: string;
|
|
}
|
|
|
|
interface UiPingMessage {
|
|
action: typeof TOOL_MESSAGE_TYPES.ELEMENT_PICKER_UI_PING;
|
|
}
|
|
|
|
type PickerMessage = UiPingMessage | UiShowMessage | UiUpdateMessage | UiHideMessage;
|
|
|
|
// ============================================================
|
|
// Content Script Definition
|
|
// ============================================================
|
|
|
|
export default defineContentScript({
|
|
matches: ['<all_urls>'],
|
|
runAt: 'document_idle',
|
|
|
|
main() {
|
|
// Only mount UI in the top frame
|
|
if (window.top !== window) return;
|
|
|
|
let controller: ElementPickerController | null = null;
|
|
let currentSessionId: string | null = null;
|
|
|
|
/**
|
|
* Ensure the controller is created and configured.
|
|
*/
|
|
function ensureController(): ElementPickerController {
|
|
if (controller) return controller;
|
|
|
|
controller = createElementPickerController({
|
|
onCancel: () => {
|
|
if (!currentSessionId) return;
|
|
void chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.ELEMENT_PICKER_UI_EVENT,
|
|
sessionId: currentSessionId,
|
|
event: 'cancel',
|
|
});
|
|
},
|
|
onConfirm: () => {
|
|
if (!currentSessionId) return;
|
|
void chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.ELEMENT_PICKER_UI_EVENT,
|
|
sessionId: currentSessionId,
|
|
event: 'confirm',
|
|
});
|
|
},
|
|
onSetActiveRequest: (requestId: string) => {
|
|
if (!currentSessionId) return;
|
|
void chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.ELEMENT_PICKER_UI_EVENT,
|
|
sessionId: currentSessionId,
|
|
event: 'set_active_request',
|
|
requestId,
|
|
});
|
|
},
|
|
onClearSelection: (requestId: string) => {
|
|
if (!currentSessionId) return;
|
|
void chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.ELEMENT_PICKER_UI_EVENT,
|
|
sessionId: currentSessionId,
|
|
event: 'clear_selection',
|
|
requestId,
|
|
});
|
|
},
|
|
});
|
|
|
|
return controller;
|
|
}
|
|
|
|
/**
|
|
* Handle incoming messages from background.
|
|
*/
|
|
function handleMessage(
|
|
message: unknown,
|
|
_sender: chrome.runtime.MessageSender,
|
|
sendResponse: (response?: unknown) => void,
|
|
): boolean | void {
|
|
const msg = message as PickerMessage | undefined;
|
|
if (!msg?.action) return false;
|
|
|
|
// Respond to ping (used by background to check if UI script is ready)
|
|
if (msg.action === TOOL_MESSAGE_TYPES.ELEMENT_PICKER_UI_PING) {
|
|
sendResponse({ success: true });
|
|
return true;
|
|
}
|
|
|
|
// Show the picker panel
|
|
if (msg.action === TOOL_MESSAGE_TYPES.ELEMENT_PICKER_UI_SHOW) {
|
|
const showMsg = msg as UiShowMessage;
|
|
currentSessionId = typeof showMsg.sessionId === 'string' ? showMsg.sessionId : null;
|
|
|
|
if (!currentSessionId) {
|
|
sendResponse({ success: false, error: 'Missing sessionId' });
|
|
return true;
|
|
}
|
|
|
|
const ctrl = ensureController();
|
|
const initialState: ElementPickerUiState = {
|
|
sessionId: currentSessionId,
|
|
requests: Array.isArray(showMsg.requests) ? showMsg.requests : [],
|
|
activeRequestId: showMsg.activeRequestId ?? null,
|
|
selections: {},
|
|
deadlineTs: typeof showMsg.deadlineTs === 'number' ? showMsg.deadlineTs : Date.now(),
|
|
errorMessage: null,
|
|
};
|
|
ctrl.show(initialState);
|
|
sendResponse({ success: true });
|
|
return true;
|
|
}
|
|
|
|
// Update the picker panel state
|
|
if (msg.action === TOOL_MESSAGE_TYPES.ELEMENT_PICKER_UI_UPDATE) {
|
|
const updateMsg = msg as UiUpdateMessage;
|
|
|
|
if (!currentSessionId || updateMsg.sessionId !== currentSessionId) {
|
|
sendResponse({ success: false, error: 'Session mismatch' });
|
|
return true;
|
|
}
|
|
|
|
controller?.update({
|
|
sessionId: currentSessionId,
|
|
activeRequestId: updateMsg.activeRequestId ?? null,
|
|
selections: updateMsg.selections || {},
|
|
deadlineTs: updateMsg.deadlineTs,
|
|
errorMessage: updateMsg.errorMessage ?? null,
|
|
});
|
|
sendResponse({ success: true });
|
|
return true;
|
|
}
|
|
|
|
// Hide the picker panel
|
|
if (msg.action === TOOL_MESSAGE_TYPES.ELEMENT_PICKER_UI_HIDE) {
|
|
const hideMsg = msg as UiHideMessage;
|
|
|
|
// Best-effort hide even if session mismatches
|
|
if (currentSessionId && hideMsg.sessionId !== currentSessionId) {
|
|
// Log but don't fail
|
|
console.warn('[ElementPicker] Session mismatch on hide, hiding anyway');
|
|
}
|
|
|
|
controller?.hide();
|
|
currentSessionId = null;
|
|
sendResponse({ success: true });
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Register message listener
|
|
chrome.runtime.onMessage.addListener(handleMessage);
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('unload', () => {
|
|
chrome.runtime.onMessage.removeListener(handleMessage);
|
|
controller?.dispose();
|
|
controller = null;
|
|
currentSessionId = null;
|
|
});
|
|
},
|
|
});
|