#
tokens: 40246/50000 1/574 files (page 56/60)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 56 of 60. Use http://codebase.md/hangwin/mcp-chrome?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   └── workflows
│       └── build-release.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│   └── extensions.json
├── app
│   ├── chrome-extension
│   │   ├── _locales
│   │   │   ├── de
│   │   │   │   └── messages.json
│   │   │   ├── en
│   │   │   │   └── messages.json
│   │   │   ├── ja
│   │   │   │   └── messages.json
│   │   │   ├── ko
│   │   │   │   └── messages.json
│   │   │   ├── zh_CN
│   │   │   │   └── messages.json
│   │   │   └── zh_TW
│   │   │       └── messages.json
│   │   ├── .env.example
│   │   ├── assets
│   │   │   └── vue.svg
│   │   ├── common
│   │   │   ├── agent-models.ts
│   │   │   ├── constants.ts
│   │   │   ├── element-marker-types.ts
│   │   │   ├── message-types.ts
│   │   │   ├── node-types.ts
│   │   │   ├── rr-v3-keepalive-protocol.ts
│   │   │   ├── step-types.ts
│   │   │   ├── tool-handler.ts
│   │   │   └── web-editor-types.ts
│   │   ├── entrypoints
│   │   │   ├── background
│   │   │   │   ├── element-marker
│   │   │   │   │   ├── element-marker-storage.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── keepalive-manager.ts
│   │   │   │   ├── native-host.ts
│   │   │   │   ├── quick-panel
│   │   │   │   │   ├── agent-handler.ts
│   │   │   │   │   ├── commands.ts
│   │   │   │   │   └── tabs-handler.ts
│   │   │   │   ├── record-replay
│   │   │   │   │   ├── actions
│   │   │   │   │   │   ├── adapter.ts
│   │   │   │   │   │   ├── handlers
│   │   │   │   │   │   │   ├── assert.ts
│   │   │   │   │   │   │   ├── click.ts
│   │   │   │   │   │   │   ├── common.ts
│   │   │   │   │   │   │   ├── control-flow.ts
│   │   │   │   │   │   │   ├── delay.ts
│   │   │   │   │   │   │   ├── dom.ts
│   │   │   │   │   │   │   ├── drag.ts
│   │   │   │   │   │   │   ├── extract.ts
│   │   │   │   │   │   │   ├── fill.ts
│   │   │   │   │   │   │   ├── http.ts
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   ├── key.ts
│   │   │   │   │   │   │   ├── navigate.ts
│   │   │   │   │   │   │   ├── screenshot.ts
│   │   │   │   │   │   │   ├── script.ts
│   │   │   │   │   │   │   ├── scroll.ts
│   │   │   │   │   │   │   ├── tabs.ts
│   │   │   │   │   │   │   └── wait.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── registry.ts
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── engine
│   │   │   │   │   │   ├── constants.ts
│   │   │   │   │   │   ├── execution-mode.ts
│   │   │   │   │   │   ├── logging
│   │   │   │   │   │   │   └── run-logger.ts
│   │   │   │   │   │   ├── plugins
│   │   │   │   │   │   │   ├── breakpoint.ts
│   │   │   │   │   │   │   ├── manager.ts
│   │   │   │   │   │   │   └── types.ts
│   │   │   │   │   │   ├── policies
│   │   │   │   │   │   │   ├── retry.ts
│   │   │   │   │   │   │   └── wait.ts
│   │   │   │   │   │   ├── runners
│   │   │   │   │   │   │   ├── after-script-queue.ts
│   │   │   │   │   │   │   ├── control-flow-runner.ts
│   │   │   │   │   │   │   ├── step-executor.ts
│   │   │   │   │   │   │   ├── step-runner.ts
│   │   │   │   │   │   │   └── subflow-runner.ts
│   │   │   │   │   │   ├── scheduler.ts
│   │   │   │   │   │   ├── state-manager.ts
│   │   │   │   │   │   └── utils
│   │   │   │   │   │       └── expression.ts
│   │   │   │   │   ├── flow-runner.ts
│   │   │   │   │   ├── flow-store.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── legacy-types.ts
│   │   │   │   │   ├── nodes
│   │   │   │   │   │   ├── assert.ts
│   │   │   │   │   │   ├── click.ts
│   │   │   │   │   │   ├── conditional.ts
│   │   │   │   │   │   ├── download-screenshot-attr-event-frame-loop.ts
│   │   │   │   │   │   ├── drag.ts
│   │   │   │   │   │   ├── execute-flow.ts
│   │   │   │   │   │   ├── extract.ts
│   │   │   │   │   │   ├── fill.ts
│   │   │   │   │   │   ├── http.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── key.ts
│   │   │   │   │   │   ├── loops.ts
│   │   │   │   │   │   ├── navigate.ts
│   │   │   │   │   │   ├── script.ts
│   │   │   │   │   │   ├── scroll.ts
│   │   │   │   │   │   ├── tabs.ts
│   │   │   │   │   │   ├── types.ts
│   │   │   │   │   │   └── wait.ts
│   │   │   │   │   ├── recording
│   │   │   │   │   │   ├── browser-event-listener.ts
│   │   │   │   │   │   ├── content-injection.ts
│   │   │   │   │   │   ├── content-message-handler.ts
│   │   │   │   │   │   ├── flow-builder.ts
│   │   │   │   │   │   ├── recorder-manager.ts
│   │   │   │   │   │   └── session-manager.ts
│   │   │   │   │   ├── rr-utils.ts
│   │   │   │   │   ├── selector-engine.ts
│   │   │   │   │   ├── storage
│   │   │   │   │   │   └── indexeddb-manager.ts
│   │   │   │   │   ├── trigger-store.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── record-replay-v3
│   │   │   │   │   ├── bootstrap.ts
│   │   │   │   │   ├── domain
│   │   │   │   │   │   ├── debug.ts
│   │   │   │   │   │   ├── errors.ts
│   │   │   │   │   │   ├── events.ts
│   │   │   │   │   │   ├── flow.ts
│   │   │   │   │   │   ├── ids.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── json.ts
│   │   │   │   │   │   ├── policy.ts
│   │   │   │   │   │   ├── triggers.ts
│   │   │   │   │   │   └── variables.ts
│   │   │   │   │   ├── engine
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── keepalive
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   └── offscreen-keepalive.ts
│   │   │   │   │   │   ├── kernel
│   │   │   │   │   │   │   ├── artifacts.ts
│   │   │   │   │   │   │   ├── breakpoints.ts
│   │   │   │   │   │   │   ├── debug-controller.ts
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   ├── kernel.ts
│   │   │   │   │   │   │   ├── recovery-kernel.ts
│   │   │   │   │   │   │   ├── runner.ts
│   │   │   │   │   │   │   └── traversal.ts
│   │   │   │   │   │   ├── plugins
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   ├── register-v2-replay-nodes.ts
│   │   │   │   │   │   │   ├── registry.ts
│   │   │   │   │   │   │   ├── types.ts
│   │   │   │   │   │   │   └── v2-action-adapter.ts
│   │   │   │   │   │   ├── queue
│   │   │   │   │   │   │   ├── enqueue-run.ts
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   ├── leasing.ts
│   │   │   │   │   │   │   ├── queue.ts
│   │   │   │   │   │   │   └── scheduler.ts
│   │   │   │   │   │   ├── recovery
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   └── recovery-coordinator.ts
│   │   │   │   │   │   ├── storage
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   └── storage-port.ts
│   │   │   │   │   │   ├── transport
│   │   │   │   │   │   │   ├── events-bus.ts
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   ├── rpc-server.ts
│   │   │   │   │   │   │   └── rpc.ts
│   │   │   │   │   │   └── triggers
│   │   │   │   │   │       ├── command-trigger.ts
│   │   │   │   │   │       ├── context-menu-trigger.ts
│   │   │   │   │   │       ├── cron-trigger.ts
│   │   │   │   │   │       ├── dom-trigger.ts
│   │   │   │   │   │       ├── index.ts
│   │   │   │   │   │       ├── interval-trigger.ts
│   │   │   │   │   │       ├── manual-trigger.ts
│   │   │   │   │   │       ├── once-trigger.ts
│   │   │   │   │   │       ├── trigger-handler.ts
│   │   │   │   │   │       ├── trigger-manager.ts
│   │   │   │   │   │       └── url-trigger.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── storage
│   │   │   │   │       ├── db.ts
│   │   │   │   │       ├── events.ts
│   │   │   │   │       ├── flows.ts
│   │   │   │   │       ├── import
│   │   │   │   │       │   ├── index.ts
│   │   │   │   │       │   ├── v2-reader.ts
│   │   │   │   │       │   └── v2-to-v3.ts
│   │   │   │   │       ├── index.ts
│   │   │   │   │       ├── persistent-vars.ts
│   │   │   │   │       ├── queue.ts
│   │   │   │   │       ├── runs.ts
│   │   │   │   │       └── triggers.ts
│   │   │   │   ├── semantic-similarity.ts
│   │   │   │   ├── storage-manager.ts
│   │   │   │   ├── tools
│   │   │   │   │   ├── base-browser.ts
│   │   │   │   │   ├── browser
│   │   │   │   │   │   ├── bookmark.ts
│   │   │   │   │   │   ├── common.ts
│   │   │   │   │   │   ├── computer.ts
│   │   │   │   │   │   ├── console-buffer.ts
│   │   │   │   │   │   ├── console.ts
│   │   │   │   │   │   ├── dialog.ts
│   │   │   │   │   │   ├── download.ts
│   │   │   │   │   │   ├── element-picker.ts
│   │   │   │   │   │   ├── file-upload.ts
│   │   │   │   │   │   ├── gif-auto-capture.ts
│   │   │   │   │   │   ├── gif-enhanced-renderer.ts
│   │   │   │   │   │   ├── gif-recorder.ts
│   │   │   │   │   │   ├── history.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── inject-script.ts
│   │   │   │   │   │   ├── interaction.ts
│   │   │   │   │   │   ├── javascript.ts
│   │   │   │   │   │   ├── keyboard.ts
│   │   │   │   │   │   ├── network-capture-debugger.ts
│   │   │   │   │   │   ├── network-capture-web-request.ts
│   │   │   │   │   │   ├── network-capture.ts
│   │   │   │   │   │   ├── network-request.ts
│   │   │   │   │   │   ├── performance.ts
│   │   │   │   │   │   ├── read-page.ts
│   │   │   │   │   │   ├── screenshot.ts
│   │   │   │   │   │   ├── userscript.ts
│   │   │   │   │   │   ├── vector-search.ts
│   │   │   │   │   │   ├── web-fetcher.ts
│   │   │   │   │   │   └── window.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── record-replay.ts
│   │   │   │   ├── utils
│   │   │   │   │   └── sidepanel.ts
│   │   │   │   └── web-editor
│   │   │   │       └── index.ts
│   │   │   ├── builder
│   │   │   │   ├── App.vue
│   │   │   │   ├── index.html
│   │   │   │   └── main.ts
│   │   │   ├── content.ts
│   │   │   ├── element-picker.content.ts
│   │   │   ├── offscreen
│   │   │   │   ├── gif-encoder.ts
│   │   │   │   ├── index.html
│   │   │   │   ├── main.ts
│   │   │   │   └── rr-keepalive.ts
│   │   │   ├── options
│   │   │   │   ├── App.vue
│   │   │   │   ├── index.html
│   │   │   │   └── main.ts
│   │   │   ├── popup
│   │   │   │   ├── App.vue
│   │   │   │   ├── components
│   │   │   │   │   ├── builder
│   │   │   │   │   │   ├── components
│   │   │   │   │   │   │   ├── Canvas.vue
│   │   │   │   │   │   │   ├── EdgePropertyPanel.vue
│   │   │   │   │   │   │   ├── KeyValueEditor.vue
│   │   │   │   │   │   │   ├── nodes
│   │   │   │   │   │   │   │   ├── node-util.ts
│   │   │   │   │   │   │   │   ├── NodeCard.vue
│   │   │   │   │   │   │   │   └── NodeIf.vue
│   │   │   │   │   │   │   ├── properties
│   │   │   │   │   │   │   │   ├── PropertyAssert.vue
│   │   │   │   │   │   │   │   ├── PropertyClick.vue
│   │   │   │   │   │   │   │   ├── PropertyCloseTab.vue
│   │   │   │   │   │   │   │   ├── PropertyDelay.vue
│   │   │   │   │   │   │   │   ├── PropertyDrag.vue
│   │   │   │   │   │   │   │   ├── PropertyExecuteFlow.vue
│   │   │   │   │   │   │   │   ├── PropertyExtract.vue
│   │   │   │   │   │   │   │   ├── PropertyFill.vue
│   │   │   │   │   │   │   │   ├── PropertyForeach.vue
│   │   │   │   │   │   │   │   ├── PropertyFormRenderer.vue
│   │   │   │   │   │   │   │   ├── PropertyFromSpec.vue
│   │   │   │   │   │   │   │   ├── PropertyHandleDownload.vue
│   │   │   │   │   │   │   │   ├── PropertyHttp.vue
│   │   │   │   │   │   │   │   ├── PropertyIf.vue
│   │   │   │   │   │   │   │   ├── PropertyKey.vue
│   │   │   │   │   │   │   │   ├── PropertyLoopElements.vue
│   │   │   │   │   │   │   │   ├── PropertyNavigate.vue
│   │   │   │   │   │   │   │   ├── PropertyOpenTab.vue
│   │   │   │   │   │   │   │   ├── PropertyScreenshot.vue
│   │   │   │   │   │   │   │   ├── PropertyScript.vue
│   │   │   │   │   │   │   │   ├── PropertyScroll.vue
│   │   │   │   │   │   │   │   ├── PropertySetAttribute.vue
│   │   │   │   │   │   │   │   ├── PropertySwitchFrame.vue
│   │   │   │   │   │   │   │   ├── PropertySwitchTab.vue
│   │   │   │   │   │   │   │   ├── PropertyTrigger.vue
│   │   │   │   │   │   │   │   ├── PropertyTriggerEvent.vue
│   │   │   │   │   │   │   │   ├── PropertyWait.vue
│   │   │   │   │   │   │   │   ├── PropertyWhile.vue
│   │   │   │   │   │   │   │   └── SelectorEditor.vue
│   │   │   │   │   │   │   ├── PropertyPanel.vue
│   │   │   │   │   │   │   ├── Sidebar.vue
│   │   │   │   │   │   │   └── TriggerPanel.vue
│   │   │   │   │   │   ├── model
│   │   │   │   │   │   │   ├── form-widget-registry.ts
│   │   │   │   │   │   │   ├── node-spec-registry.ts
│   │   │   │   │   │   │   ├── node-spec.ts
│   │   │   │   │   │   │   ├── node-specs-builtin.ts
│   │   │   │   │   │   │   ├── toast.ts
│   │   │   │   │   │   │   ├── transforms.ts
│   │   │   │   │   │   │   ├── ui-nodes.ts
│   │   │   │   │   │   │   ├── validation.ts
│   │   │   │   │   │   │   └── variables.ts
│   │   │   │   │   │   ├── store
│   │   │   │   │   │   │   └── useBuilderStore.ts
│   │   │   │   │   │   └── widgets
│   │   │   │   │   │       ├── FieldCode.vue
│   │   │   │   │   │       ├── FieldDuration.vue
│   │   │   │   │   │       ├── FieldExpression.vue
│   │   │   │   │   │       ├── FieldKeySequence.vue
│   │   │   │   │   │       ├── FieldSelector.vue
│   │   │   │   │   │       ├── FieldTargetLocator.vue
│   │   │   │   │   │       └── VarInput.vue
│   │   │   │   │   ├── ConfirmDialog.vue
│   │   │   │   │   ├── ElementMarkerManagement.vue
│   │   │   │   │   ├── icons
│   │   │   │   │   │   ├── BoltIcon.vue
│   │   │   │   │   │   ├── CheckIcon.vue
│   │   │   │   │   │   ├── DatabaseIcon.vue
│   │   │   │   │   │   ├── DocumentIcon.vue
│   │   │   │   │   │   ├── EditIcon.vue
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── MarkerIcon.vue
│   │   │   │   │   │   ├── RecordIcon.vue
│   │   │   │   │   │   ├── RefreshIcon.vue
│   │   │   │   │   │   ├── StopIcon.vue
│   │   │   │   │   │   ├── TabIcon.vue
│   │   │   │   │   │   ├── TrashIcon.vue
│   │   │   │   │   │   ├── VectorIcon.vue
│   │   │   │   │   │   └── WorkflowIcon.vue
│   │   │   │   │   ├── LocalModelPage.vue
│   │   │   │   │   ├── ModelCacheManagement.vue
│   │   │   │   │   ├── ProgressIndicator.vue
│   │   │   │   │   └── ScheduleDialog.vue
│   │   │   │   ├── index.html
│   │   │   │   ├── main.ts
│   │   │   │   └── style.css
│   │   │   ├── quick-panel.content.ts
│   │   │   ├── shared
│   │   │   │   ├── composables
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── useRRV3Rpc.ts
│   │   │   │   └── utils
│   │   │   │       ├── index.ts
│   │   │   │       └── rr-flow-convert.ts
│   │   │   ├── sidepanel
│   │   │   │   ├── App.vue
│   │   │   │   ├── components
│   │   │   │   │   ├── agent
│   │   │   │   │   │   ├── AttachmentPreview.vue
│   │   │   │   │   │   ├── ChatInput.vue
│   │   │   │   │   │   ├── CliSettings.vue
│   │   │   │   │   │   ├── ConnectionStatus.vue
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── MessageItem.vue
│   │   │   │   │   │   ├── MessageList.vue
│   │   │   │   │   │   ├── ProjectCreateForm.vue
│   │   │   │   │   │   └── ProjectSelector.vue
│   │   │   │   │   ├── agent-chat
│   │   │   │   │   │   ├── AgentChatShell.vue
│   │   │   │   │   │   ├── AgentComposer.vue
│   │   │   │   │   │   ├── AgentConversation.vue
│   │   │   │   │   │   ├── AgentOpenProjectMenu.vue
│   │   │   │   │   │   ├── AgentProjectMenu.vue
│   │   │   │   │   │   ├── AgentRequestThread.vue
│   │   │   │   │   │   ├── AgentSessionListItem.vue
│   │   │   │   │   │   ├── AgentSessionMenu.vue
│   │   │   │   │   │   ├── AgentSessionSettingsPanel.vue
│   │   │   │   │   │   ├── AgentSessionsView.vue
│   │   │   │   │   │   ├── AgentSettingsMenu.vue
│   │   │   │   │   │   ├── AgentTimeline.vue
│   │   │   │   │   │   ├── AgentTimelineItem.vue
│   │   │   │   │   │   ├── AgentTopBar.vue
│   │   │   │   │   │   ├── ApplyMessageChip.vue
│   │   │   │   │   │   ├── AttachmentCachePanel.vue
│   │   │   │   │   │   ├── ComposerDrawer.vue
│   │   │   │   │   │   ├── ElementChip.vue
│   │   │   │   │   │   ├── FakeCaretOverlay.vue
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── SelectionChip.vue
│   │   │   │   │   │   ├── timeline
│   │   │   │   │   │   │   ├── markstream-thinking.ts
│   │   │   │   │   │   │   ├── ThinkingNode.vue
│   │   │   │   │   │   │   ├── TimelineNarrativeStep.vue
│   │   │   │   │   │   │   ├── TimelineStatusStep.vue
│   │   │   │   │   │   │   ├── TimelineToolCallStep.vue
│   │   │   │   │   │   │   ├── TimelineToolResultCardStep.vue
│   │   │   │   │   │   │   └── TimelineUserPromptStep.vue
│   │   │   │   │   │   └── WebEditorChanges.vue
│   │   │   │   │   ├── AgentChat.vue
│   │   │   │   │   ├── rr-v3
│   │   │   │   │   │   └── DebuggerPanel.vue
│   │   │   │   │   ├── SidepanelNavigator.vue
│   │   │   │   │   └── workflows
│   │   │   │   │       ├── index.ts
│   │   │   │   │       ├── WorkflowListItem.vue
│   │   │   │   │       └── WorkflowsView.vue
│   │   │   │   ├── composables
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── useAgentChat.ts
│   │   │   │   │   ├── useAgentChatViewRoute.ts
│   │   │   │   │   ├── useAgentInputPreferences.ts
│   │   │   │   │   ├── useAgentProjects.ts
│   │   │   │   │   ├── useAgentServer.ts
│   │   │   │   │   ├── useAgentSessions.ts
│   │   │   │   │   ├── useAgentTheme.ts
│   │   │   │   │   ├── useAgentThreads.ts
│   │   │   │   │   ├── useAttachments.ts
│   │   │   │   │   ├── useFakeCaret.ts
│   │   │   │   │   ├── useFloatingDrag.ts
│   │   │   │   │   ├── useOpenProjectPreference.ts
│   │   │   │   │   ├── useRRV3Debugger.ts
│   │   │   │   │   ├── useRRV3Rpc.ts
│   │   │   │   │   ├── useTextareaAutoResize.ts
│   │   │   │   │   ├── useWebEditorTxState.ts
│   │   │   │   │   └── useWorkflowsV3.ts
│   │   │   │   ├── index.html
│   │   │   │   ├── main.ts
│   │   │   │   ├── styles
│   │   │   │   │   └── agent-chat.css
│   │   │   │   └── utils
│   │   │   │       └── loading-texts.ts
│   │   │   ├── styles
│   │   │   │   └── tailwind.css
│   │   │   ├── web-editor-v2
│   │   │   │   ├── attr-ui-refactor.md
│   │   │   │   ├── constants.ts
│   │   │   │   ├── core
│   │   │   │   │   ├── css-compare.ts
│   │   │   │   │   ├── cssom-styles-collector.ts
│   │   │   │   │   ├── debug-source.ts
│   │   │   │   │   ├── design-tokens
│   │   │   │   │   │   ├── design-tokens-service.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── token-detector.ts
│   │   │   │   │   │   ├── token-resolver.ts
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── editor.ts
│   │   │   │   │   ├── element-key.ts
│   │   │   │   │   ├── event-controller.ts
│   │   │   │   │   ├── execution-tracker.ts
│   │   │   │   │   ├── hmr-consistency.ts
│   │   │   │   │   ├── locator.ts
│   │   │   │   │   ├── message-listener.ts
│   │   │   │   │   ├── payload-builder.ts
│   │   │   │   │   ├── perf-monitor.ts
│   │   │   │   │   ├── position-tracker.ts
│   │   │   │   │   ├── props-bridge.ts
│   │   │   │   │   ├── snap-engine.ts
│   │   │   │   │   ├── transaction-aggregator.ts
│   │   │   │   │   └── transaction-manager.ts
│   │   │   │   ├── drag
│   │   │   │   │   └── drag-reorder-controller.ts
│   │   │   │   ├── overlay
│   │   │   │   │   ├── canvas-overlay.ts
│   │   │   │   │   └── handles-controller.ts
│   │   │   │   ├── selection
│   │   │   │   │   └── selection-engine.ts
│   │   │   │   ├── ui
│   │   │   │   │   ├── breadcrumbs.ts
│   │   │   │   │   ├── floating-drag.ts
│   │   │   │   │   ├── icons.ts
│   │   │   │   │   ├── property-panel
│   │   │   │   │   │   ├── class-editor.ts
│   │   │   │   │   │   ├── components
│   │   │   │   │   │   │   ├── alignment-grid.ts
│   │   │   │   │   │   │   ├── icon-button-group.ts
│   │   │   │   │   │   │   ├── input-container.ts
│   │   │   │   │   │   │   ├── slider-input.ts
│   │   │   │   │   │   │   └── token-pill.ts
│   │   │   │   │   │   ├── components-tree.ts
│   │   │   │   │   │   ├── controls
│   │   │   │   │   │   │   ├── appearance-control.ts
│   │   │   │   │   │   │   ├── background-control.ts
│   │   │   │   │   │   │   ├── border-control.ts
│   │   │   │   │   │   │   ├── color-field.ts
│   │   │   │   │   │   │   ├── css-helpers.ts
│   │   │   │   │   │   │   ├── effects-control.ts
│   │   │   │   │   │   │   ├── gradient-control.ts
│   │   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   │   ├── layout-control.ts
│   │   │   │   │   │   │   ├── number-stepping.ts
│   │   │   │   │   │   │   ├── position-control.ts
│   │   │   │   │   │   │   ├── size-control.ts
│   │   │   │   │   │   │   ├── spacing-control.ts
│   │   │   │   │   │   │   ├── token-picker.ts
│   │   │   │   │   │   │   └── typography-control.ts
│   │   │   │   │   │   ├── css-defaults.ts
│   │   │   │   │   │   ├── css-panel.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── property-panel.ts
│   │   │   │   │   │   ├── props-panel.ts
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── shadow-host.ts
│   │   │   │   │   └── toolbar.ts
│   │   │   │   └── utils
│   │   │   │       └── disposables.ts
│   │   │   ├── web-editor-v2.ts
│   │   │   └── welcome
│   │   │       ├── App.vue
│   │   │       ├── index.html
│   │   │       └── main.ts
│   │   ├── env.d.ts
│   │   ├── eslint.config.js
│   │   ├── inject-scripts
│   │   │   ├── accessibility-tree-helper.js
│   │   │   ├── click-helper.js
│   │   │   ├── dom-observer.js
│   │   │   ├── element-marker.js
│   │   │   ├── element-picker.js
│   │   │   ├── fill-helper.js
│   │   │   ├── inject-bridge.js
│   │   │   ├── interactive-elements-helper.js
│   │   │   ├── keyboard-helper.js
│   │   │   ├── network-helper.js
│   │   │   ├── props-agent.js
│   │   │   ├── recorder.js
│   │   │   ├── screenshot-helper.js
│   │   │   ├── wait-helper.js
│   │   │   ├── web-editor.js
│   │   │   └── web-fetcher-helper.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   ├── public
│   │   │   ├── icon
│   │   │   │   ├── 128.png
│   │   │   │   ├── 16.png
│   │   │   │   ├── 32.png
│   │   │   │   ├── 48.png
│   │   │   │   └── 96.png
│   │   │   ├── libs
│   │   │   │   └── ort.min.js
│   │   │   └── wxt.svg
│   │   ├── README.md
│   │   ├── shared
│   │   │   ├── element-picker
│   │   │   │   ├── controller.ts
│   │   │   │   └── index.ts
│   │   │   ├── quick-panel
│   │   │   │   ├── core
│   │   │   │   │   ├── agent-bridge.ts
│   │   │   │   │   ├── search-engine.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── providers
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── tabs-provider.ts
│   │   │   │   └── ui
│   │   │   │       ├── ai-chat-panel.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── markdown-renderer.ts
│   │   │   │       ├── message-renderer.ts
│   │   │   │       ├── panel-shell.ts
│   │   │   │       ├── quick-entries.ts
│   │   │   │       ├── search-input.ts
│   │   │   │       ├── shadow-host.ts
│   │   │   │       └── styles.ts
│   │   │   └── selector
│   │   │       ├── dom-path.ts
│   │   │       ├── fingerprint.ts
│   │   │       ├── generator.ts
│   │   │       ├── index.ts
│   │   │       ├── locator.ts
│   │   │       ├── shadow-dom.ts
│   │   │       ├── stability.ts
│   │   │       ├── strategies
│   │   │       │   ├── anchor-relpath.ts
│   │   │       │   ├── aria.ts
│   │   │       │   ├── css-path.ts
│   │   │       │   ├── css-unique.ts
│   │   │       │   ├── index.ts
│   │   │       │   ├── testid.ts
│   │   │       │   └── text.ts
│   │   │       └── types.ts
│   │   ├── tailwind.config.ts
│   │   ├── tests
│   │   │   ├── __mocks__
│   │   │   │   └── hnswlib-wasm-static.ts
│   │   │   ├── record-replay
│   │   │   │   ├── _test-helpers.ts
│   │   │   │   ├── adapter-policy.contract.test.ts
│   │   │   │   ├── flow-store-strip-steps.contract.test.ts
│   │   │   │   ├── high-risk-actions.integration.test.ts
│   │   │   │   ├── hybrid-actions.integration.test.ts
│   │   │   │   ├── script-control-flow.integration.test.ts
│   │   │   │   ├── session-dag-sync.contract.test.ts
│   │   │   │   ├── step-executor.contract.test.ts
│   │   │   │   └── tab-cursor.integration.test.ts
│   │   │   ├── record-replay-v3
│   │   │   │   ├── command-trigger.test.ts
│   │   │   │   ├── context-menu-trigger.test.ts
│   │   │   │   ├── cron-trigger.test.ts
│   │   │   │   ├── debugger.contract.test.ts
│   │   │   │   ├── dom-trigger.test.ts
│   │   │   │   ├── e2e.integration.test.ts
│   │   │   │   ├── events.contract.test.ts
│   │   │   │   ├── interval-trigger.test.ts
│   │   │   │   ├── manual-trigger.test.ts
│   │   │   │   ├── once-trigger.test.ts
│   │   │   │   ├── queue.contract.test.ts
│   │   │   │   ├── recovery.test.ts
│   │   │   │   ├── rpc-api.test.ts
│   │   │   │   ├── runner.onError.contract.test.ts
│   │   │   │   ├── scheduler-integration.test.ts
│   │   │   │   ├── scheduler.test.ts
│   │   │   │   ├── spec-smoke.test.ts
│   │   │   │   ├── trigger-manager.test.ts
│   │   │   │   ├── triggers.test.ts
│   │   │   │   ├── url-trigger.test.ts
│   │   │   │   ├── v2-action-adapter.test.ts
│   │   │   │   ├── v2-adapter-integration.test.ts
│   │   │   │   ├── v2-to-v3-conversion.test.ts
│   │   │   │   └── v3-e2e-harness.ts
│   │   │   ├── vitest.setup.ts
│   │   │   └── web-editor-v2
│   │   │       ├── design-tokens.test.ts
│   │   │       ├── drag-reorder-controller.test.ts
│   │   │       ├── event-controller.test.ts
│   │   │       ├── locator.test.ts
│   │   │       ├── property-panel-live-sync.test.ts
│   │   │       ├── selection-engine.test.ts
│   │   │       ├── snap-engine.test.ts
│   │   │       └── test-utils
│   │   │           └── dom.ts
│   │   ├── tsconfig.json
│   │   ├── types
│   │   │   ├── gifenc.d.ts
│   │   │   └── icons.d.ts
│   │   ├── utils
│   │   │   ├── cdp-session-manager.ts
│   │   │   ├── content-indexer.ts
│   │   │   ├── i18n.ts
│   │   │   ├── image-utils.ts
│   │   │   ├── indexeddb-client.ts
│   │   │   ├── lru-cache.ts
│   │   │   ├── model-cache-manager.ts
│   │   │   ├── offscreen-manager.ts
│   │   │   ├── output-sanitizer.ts
│   │   │   ├── screenshot-context.ts
│   │   │   ├── semantic-similarity-engine.ts
│   │   │   ├── simd-math-engine.ts
│   │   │   ├── text-chunker.ts
│   │   │   └── vector-database.ts
│   │   ├── vitest.config.ts
│   │   ├── workers
│   │   │   ├── ort-wasm-simd-threaded.jsep.mjs
│   │   │   ├── ort-wasm-simd-threaded.jsep.wasm
│   │   │   ├── ort-wasm-simd-threaded.mjs
│   │   │   ├── ort-wasm-simd-threaded.wasm
│   │   │   ├── simd_math_bg.wasm
│   │   │   ├── simd_math.js
│   │   │   └── similarity.worker.js
│   │   └── wxt.config.ts
│   └── native-server
│       ├── .npmignore
│       ├── debug.sh
│       ├── install.md
│       ├── jest.config.js
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── agent
│       │   │   ├── attachment-service.ts
│       │   │   ├── ccr-detector.ts
│       │   │   ├── chat-service.ts
│       │   │   ├── db
│       │   │   │   ├── client.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── schema.ts
│       │   │   ├── directory-picker.ts
│       │   │   ├── engines
│       │   │   │   ├── claude.ts
│       │   │   │   ├── codex.ts
│       │   │   │   └── types.ts
│       │   │   ├── message-service.ts
│       │   │   ├── open-project.ts
│       │   │   ├── project-service.ts
│       │   │   ├── project-types.ts
│       │   │   ├── session-service.ts
│       │   │   ├── storage.ts
│       │   │   ├── stream-manager.ts
│       │   │   ├── tool-bridge.ts
│       │   │   └── types.ts
│       │   ├── cli.ts
│       │   ├── constant
│       │   │   └── index.ts
│       │   ├── file-handler.ts
│       │   ├── index.ts
│       │   ├── mcp
│       │   │   ├── mcp-server-stdio.ts
│       │   │   ├── mcp-server.ts
│       │   │   ├── register-tools.ts
│       │   │   └── stdio-config.json
│       │   ├── native-messaging-host.ts
│       │   ├── scripts
│       │   │   ├── browser-config.ts
│       │   │   ├── build.ts
│       │   │   ├── constant.ts
│       │   │   ├── doctor.ts
│       │   │   ├── postinstall.ts
│       │   │   ├── register-dev.ts
│       │   │   ├── register.ts
│       │   │   ├── report.ts
│       │   │   ├── run_host.bat
│       │   │   ├── run_host.sh
│       │   │   └── utils.ts
│       │   ├── server
│       │   │   ├── index.ts
│       │   │   ├── routes
│       │   │   │   ├── agent.ts
│       │   │   │   └── index.ts
│       │   │   └── server.test.ts
│       │   ├── shims
│       │   │   └── devtools.d.ts
│       │   ├── trace-analyzer.ts
│       │   ├── types
│       │   │   └── devtools-frontend.d.ts
│       │   └── util
│       │       └── logger.ts
│       └── tsconfig.json
├── commitlint.config.cjs
├── docs
│   ├── ARCHITECTURE_zh.md
│   ├── ARCHITECTURE.md
│   ├── CHANGELOG.md
│   ├── CONTRIBUTING_zh.md
│   ├── CONTRIBUTING.md
│   ├── ISSUE.md
│   ├── mcp-cli-config.md
│   ├── TOOLS_zh.md
│   ├── TOOLS.md
│   ├── TROUBLESHOOTING_zh.md
│   ├── TROUBLESHOOTING.md
│   ├── VisualEditor_zh.md
│   ├── VisualEditor.md
│   └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│   ├── shared
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── agent-types.ts
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── labels.ts
│   │   │   ├── node-spec-registry.ts
│   │   │   ├── node-spec.ts
│   │   │   ├── node-specs-builtin.ts
│   │   │   ├── rr-graph.ts
│   │   │   ├── step-types.ts
│   │   │   ├── tools.ts
│   │   │   └── types.ts
│   │   └── tsconfig.json
│   └── wasm-simd
│       ├── .gitignore
│       ├── BUILD.md
│       ├── Cargo.toml
│       ├── package.json
│       ├── README.md
│       └── src
│           └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│   ├── content-analize.md
│   ├── excalidraw-prompt.md
│   └── modify-web.md
├── README_zh.md
├── README.md
└── releases
    ├── chrome-extension
    │   └── latest
    │       └── chrome-mcp-server-lastest.zip
    └── README.md
```

# Files

--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/element-marker.js:
--------------------------------------------------------------------------------

```javascript
   1 | /* eslint-disable */
   2 | (function () {
   3 |   if (window.__ELEMENT_MARKER_INSTALLED__) return;
   4 |   window.__ELEMENT_MARKER_INSTALLED__ = true;
   5 | 
   6 |   const IS_MAIN = window === window.top;
   7 | 
   8 |   // ============================================================================
   9 |   // Utility Functions
  10 |   // ============================================================================
  11 | 
  12 |   function sleep(ms) {
  13 |     return new Promise((resolve) => setTimeout(resolve, ms));
  14 |   }
  15 | 
  16 |   // ============================================================================
  17 |   // Constants & Configuration
  18 |   // ============================================================================
  19 | 
  20 |   const CONFIG = {
  21 |     DEFAULTS: {
  22 |       PREFS: {
  23 |         preferId: true,
  24 |         preferStableAttr: true,
  25 |         preferClass: true,
  26 |       },
  27 |       SELECTOR_TYPE: 'css',
  28 |       LIST_MODE: false,
  29 |     },
  30 |     Z_INDEX: {
  31 |       OVERLAY: 2147483646,
  32 |       HIGHLIGHTER: 2147483645,
  33 |       RECTS: 2147483644,
  34 |     },
  35 |     COLORS: {
  36 |       PRIMARY: '#2563eb',
  37 |       SUCCESS: '#10b981',
  38 |       WARNING: '#f59e0b',
  39 |       DANGER: '#ef4444',
  40 |       HOVER: '#10b981',
  41 |       VERIFY: '#3b82f6',
  42 |     },
  43 |   };
  44 | 
  45 |   // ============================================================================
  46 |   // Panel Host Module - Shadow DOM Management
  47 |   // ============================================================================
  48 | 
  49 |   const PanelHost = (() => {
  50 |     let hostElement = null;
  51 |     let shadowRoot = null;
  52 | 
  53 |     const PANEL_STYLES = `
  54 |       * {
  55 |         box-sizing: border-box;
  56 |         margin: 0;
  57 |         padding: 0;
  58 |       }
  59 | 
  60 |       .em-panel {
  61 |         width: 400px;
  62 |         background: #ffffff;
  63 |         border-radius: 12px;
  64 |         box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
  65 |         font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  66 |         padding: 20px;
  67 |         transition: opacity 150ms ease;
  68 |       }
  69 | 
  70 | 
  71 |       /* Header */
  72 |       .em-header {
  73 |         display: flex;
  74 |         align-items: center;
  75 |         justify-content: space-between;
  76 |         margin-bottom: 20px;
  77 |         user-select: none;
  78 |       }
  79 | 
  80 |       .em-title {
  81 |         font-size: 20px;
  82 |         font-weight: 500;
  83 |         color: #262626;
  84 |       }
  85 | 
  86 |       .em-header-actions {
  87 |         display: flex;
  88 |         gap: 4px;
  89 |         align-items: center;
  90 |       }
  91 | 
  92 |       .em-icon-btn {
  93 |         width: 32px;
  94 |         height: 32px;
  95 |         display: flex;
  96 |         align-items: center;
  97 |         justify-content: center;
  98 |         border: none;
  99 |         background: transparent;
 100 |         color: #a3a3a3;
 101 |         cursor: pointer;
 102 |         transition: color 150ms ease;
 103 |         padding: 0;
 104 |       }
 105 | 
 106 |       .em-icon-btn:hover {
 107 |         color: #525252;
 108 |       }
 109 | 
 110 |       .em-icon-btn svg {
 111 |         width: 20px;
 112 |         height: 20px;
 113 |         stroke-width: 2;
 114 |       }
 115 | 
 116 |       /* Controls Row */
 117 |       .em-controls {
 118 |         display: flex;
 119 |         gap: 8px;
 120 |         margin-bottom: 12px;
 121 |       }
 122 | 
 123 |       .em-select-wrapper {
 124 |         flex: 1;
 125 |         position: relative;
 126 |       }
 127 | 
 128 |       .em-select {
 129 |         width: 100%;
 130 |         height: 44px;
 131 |         padding: 0 40px 0 16px;
 132 |         background: #f5f5f5;
 133 |         color: #262626;
 134 |         font-size: 15px;
 135 |         border: none;
 136 |         border-radius: 10px;
 137 |         appearance: none;
 138 |         cursor: pointer;
 139 |         outline: none;
 140 |         font-family: inherit;
 141 |         font-weight: 400;
 142 |       }
 143 | 
 144 |       .em-select-wrapper::after {
 145 |         content: '';
 146 |         position: absolute;
 147 |         right: 16px;
 148 |         top: 50%;
 149 |         transform: translateY(-50%);
 150 |         width: 0;
 151 |         height: 0;
 152 |         border-left: 5px solid transparent;
 153 |         border-right: 5px solid transparent;
 154 |         border-top: 6px solid #737373;
 155 |         pointer-events: none;
 156 |       }
 157 | 
 158 |       .em-square-btn {
 159 |         width: 44px;
 160 |         height: 44px;
 161 |         display: flex;
 162 |         align-items: center;
 163 |         justify-content: center;
 164 |         background: #f5f5f5;
 165 |         border: none;
 166 |         border-radius: 10px;
 167 |         cursor: pointer;
 168 |         transition: background 150ms ease;
 169 |         padding: 0;
 170 |       }
 171 | 
 172 |       .em-square-btn:hover {
 173 |         background: #e5e5e5;
 174 |       }
 175 | 
 176 |       .em-square-btn.active {
 177 |         background: #2563eb;
 178 |       }
 179 | 
 180 |       .em-square-btn.active svg {
 181 |         color: #ffffff;
 182 |       }
 183 | 
 184 |       .em-square-btn svg {
 185 |         width: 18px;
 186 |         height: 18px;
 187 |         color: #525252;
 188 |         stroke-width: 2;
 189 |       }
 190 | 
 191 |       /* Selector Display */
 192 |       .em-selector-display {
 193 |         display: flex;
 194 |         align-items: center;
 195 |         gap: 10px;
 196 |         height: 44px;
 197 |         padding: 0 12px 0 16px;
 198 |         background: #f5f5f5;
 199 |         border-radius: 10px;
 200 |         margin-bottom: 16px;
 201 |       }
 202 | 
 203 |       .em-selector-display svg {
 204 |         width: 18px;
 205 |         height: 18px;
 206 |         color: #a3a3a3;
 207 |         flex-shrink: 0;
 208 |         stroke-width: 2;
 209 |       }
 210 | 
 211 |       .em-selector-text {
 212 |         flex: 1;
 213 |         font-size: 14px;
 214 |         color: #525252;
 215 |         overflow: hidden;
 216 |         text-overflow: ellipsis;
 217 |         white-space: nowrap;
 218 |         user-select: text;
 219 |       }
 220 | 
 221 |       .em-selector-nav {
 222 |         display: flex;
 223 |         gap: 2px;
 224 |       }
 225 | 
 226 |       .em-nav-btn {
 227 |         width: 28px;
 228 |         height: 28px;
 229 |         display: flex;
 230 |         align-items: center;
 231 |         justify-content: center;
 232 |         border: none;
 233 |         background: transparent;
 234 |         cursor: pointer;
 235 |         transition: background 150ms ease;
 236 |         border-radius: 6px;
 237 |         padding: 0;
 238 |       }
 239 | 
 240 |       .em-nav-btn:hover {
 241 |         background: #e5e5e5;
 242 |       }
 243 | 
 244 |       .em-nav-btn svg {
 245 |         width: 16px;
 246 |         height: 16px;
 247 |         color: #525252;
 248 |         stroke-width: 2;
 249 |       }
 250 | 
 251 |       /* Tabs */
 252 |       .em-tabs {
 253 |         display: inline-flex;
 254 |         gap: 2px;
 255 |         padding: 2px;
 256 |         background: #f5f5f5;
 257 |         border-radius: 8px;
 258 |         margin-bottom: 16px;
 259 |       }
 260 | 
 261 |       .em-tab {
 262 |         padding: 6px 16px;
 263 |         font-size: 12px;
 264 |         font-weight: 500;
 265 |         color: #737373;
 266 |         background: transparent;
 267 |         border: none;
 268 |         border-radius: 6px;
 269 |         cursor: pointer;
 270 |         transition: all 150ms ease;
 271 |       }
 272 | 
 273 |       .em-tab:hover {
 274 |         color: #404040;
 275 |       }
 276 | 
 277 |       .em-tab.active {
 278 |         color: #262626;
 279 |         background: #ffffff;
 280 |         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 281 |       }
 282 | 
 283 |       /* Content */
 284 |       .em-content {
 285 |         margin-bottom: 0;
 286 |       }
 287 | 
 288 |       #__em_tab_settings {
 289 |         max-height: min(60vh, 480px);
 290 |         overflow-y: auto;
 291 |         scrollbar-width: none; /* Firefox */
 292 |         -ms-overflow-style: none; /* IE and Edge */
 293 |       }
 294 | 
 295 |       #__em_tab_settings::-webkit-scrollbar {
 296 |         display: none; /* Chrome, Safari, Opera */
 297 |       }
 298 | 
 299 |       .em-section-title {
 300 |         font-size: 13px;
 301 |         color: #737373;
 302 |         margin-bottom: 16px;
 303 |         font-weight: 400;
 304 |       }
 305 | 
 306 |       .em-attributes {
 307 |         display: flex;
 308 |         flex-direction: column;
 309 |         gap: 12px;
 310 |       }
 311 | 
 312 |       .em-attribute {
 313 |         display: flex;
 314 |         flex-direction: column;
 315 |         gap: 6px;
 316 |       }
 317 | 
 318 |       .em-attribute-label {
 319 |         font-size: 12px;
 320 |         color: #a3a3a3;
 321 |         font-weight: 400;
 322 |       }
 323 | 
 324 |       .em-attribute-value {
 325 |         display: flex;
 326 |         align-items: center;
 327 |         gap: 10px;
 328 |         min-height: 44px;
 329 |         padding: 0 12px 0 16px;
 330 |         background: #f5f5f5;
 331 |         border-radius: 10px;
 332 |       }
 333 | 
 334 |       .em-attribute-value.editable {
 335 |         padding: 0 16px;
 336 |       }
 337 | 
 338 |       .em-attribute-value svg {
 339 |         width: 18px;
 340 |         height: 18px;
 341 |         stroke-width: 2;
 342 |         cursor: pointer;
 343 |         transition: color 150ms ease;
 344 |         flex-shrink: 0;
 345 |       }
 346 | 
 347 |       .em-attribute-value svg.copy-icon {
 348 |         color: #a3a3a3;
 349 |       }
 350 | 
 351 |       .em-attribute-value svg.copy-icon:hover {
 352 |         color: #525252;
 353 |       }
 354 | 
 355 |       .em-attribute-value svg.copy-icon.disabled {
 356 |         color: #d4d4d4;
 357 |         cursor: default;
 358 |       }
 359 | 
 360 |       .em-attribute-text {
 361 |         flex: 1;
 362 |         font-size: 14px;
 363 |         color: #404040;
 364 |         user-select: text;
 365 |       }
 366 | 
 367 |       .em-attribute-text.empty {
 368 |         color: #a3a3a3;
 369 |       }
 370 | 
 371 |       .em-input {
 372 |         flex: 1;
 373 |         border: none;
 374 |         background: transparent;
 375 |         font-size: 14px;
 376 |         color: #404040;
 377 |         font-family: inherit;
 378 |         outline: none;
 379 |         padding: 0;
 380 |         height: 44px;
 381 |       }
 382 | 
 383 |       .em-input::placeholder {
 384 |         color: #a3a3a3;
 385 |       }
 386 | 
 387 |       /* Settings Panel */
 388 |       .em-settings {
 389 |         display: flex;
 390 |         flex-direction: column;
 391 |         gap: 16px;
 392 |       }
 393 | 
 394 |       .em-settings-group {
 395 |         display: flex;
 396 |         flex-direction: column;
 397 |         gap: 8px;
 398 |       }
 399 | 
 400 |       .em-settings-label {
 401 |         font-size: 12px;
 402 |         font-weight: 500;
 403 |         color: #737373;
 404 |       }
 405 | 
 406 |       .em-checkbox-group {
 407 |         display: flex;
 408 |         flex-direction: column;
 409 |         gap: 10px;
 410 |       }
 411 | 
 412 |       .em-checkbox-label {
 413 |         display: flex;
 414 |         align-items: center;
 415 |         gap: 8px;
 416 |         font-size: 14px;
 417 |         color: #404040;
 418 |         cursor: pointer;
 419 |       }
 420 | 
 421 |       .em-checkbox-label input[type="checkbox"] {
 422 |         width: 18px;
 423 |         height: 18px;
 424 |         cursor: pointer;
 425 |         margin: 0;
 426 |       }
 427 | 
 428 |       /* Action Buttons */
 429 |       .em-actions {
 430 |         display: flex;
 431 |         gap: 8px;
 432 |         margin-top: 20px;
 433 |       }
 434 | 
 435 |       .em-btn {
 436 |         flex: 1;
 437 |         height: 40px;
 438 |         border: none;
 439 |         border-radius: 8px;
 440 |         font-size: 14px;
 441 |         font-weight: 600;
 442 |         cursor: pointer;
 443 |         transition: all 150ms ease;
 444 |       }
 445 | 
 446 |       .em-btn-primary {
 447 |         background: #2563eb;
 448 |         color: #ffffff;
 449 |       }
 450 | 
 451 |       .em-btn-primary:hover {
 452 |         background: #1d4ed8;
 453 |         transform: translateY(-1px);
 454 |         box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
 455 |       }
 456 | 
 457 |       .em-btn-success {
 458 |         background: #10b981;
 459 |         color: #ffffff;
 460 |       }
 461 | 
 462 |       .em-btn-success:hover {
 463 |         background: #059669;
 464 |         transform: translateY(-1px);
 465 |         box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
 466 |       }
 467 | 
 468 |       .em-btn-ghost {
 469 |         background: #f5f5f5;
 470 |         color: #404040;
 471 |       }
 472 | 
 473 |       .em-btn-ghost:hover {
 474 |         background: #e5e5e5;
 475 |       }
 476 | 
 477 |       /* Footer */
 478 |       .em-footer {
 479 |         font-size: 12px;
 480 |         color: #a3a3a3;
 481 |         text-align: center;
 482 |         margin-top: 16px;
 483 |       }
 484 | 
 485 |       .em-footer kbd {
 486 |         display: inline-block;
 487 |         padding: 2px 6px;
 488 |         background: #f5f5f5;
 489 |         border-radius: 4px;
 490 |         font-family: monospace;
 491 |         font-size: 11px;
 492 |         color: #737373;
 493 |       }
 494 | 
 495 |       /* Status */
 496 |       .em-status {
 497 |         font-size: 13px;
 498 |         padding: 10px 12px;
 499 |         border-radius: 8px;
 500 |         margin-bottom: 12px;
 501 |         display: flex;
 502 |         align-items: center;
 503 |         gap: 6px;
 504 |       }
 505 | 
 506 |       .em-status.idle {
 507 |         display: none;
 508 |       }
 509 | 
 510 |       .em-status.running {
 511 |         background: rgba(37, 99, 235, 0.1);
 512 |         color: #2563eb;
 513 |       }
 514 | 
 515 |       .em-status.success {
 516 |         background: rgba(16, 185, 129, 0.1);
 517 |         color: #10b981;
 518 |       }
 519 | 
 520 |       .em-status.failure {
 521 |         background: rgba(239, 68, 68, 0.1);
 522 |         color: #ef4444;
 523 |       }
 524 | 
 525 |       /* Grid Layout */
 526 |       .em-grid {
 527 |         display: grid;
 528 |         grid-template-columns: repeat(2, 1fr);
 529 |         gap: 12px;
 530 |       }
 531 | 
 532 |       .em-field {
 533 |         display: flex;
 534 |         flex-direction: column;
 535 |         gap: 6px;
 536 |       }
 537 | 
 538 |       .em-field-label {
 539 |         font-size: 12px;
 540 |         color: #a3a3a3;
 541 |       }
 542 | 
 543 |       .em-field-input {
 544 |         height: 40px;
 545 |         padding: 0 12px;
 546 |         background: #f5f5f5;
 547 |         border: none;
 548 |         border-radius: 8px;
 549 |         font-size: 14px;
 550 |         color: #404040;
 551 |         font-family: inherit;
 552 |         outline: none;
 553 |       }
 554 | 
 555 |       .em-field-input:focus {
 556 |         background: #e5e5e5;
 557 |       }
 558 | 
 559 |       /* Details/Accordion */
 560 |       .em-details {
 561 |         margin-top: 12px;
 562 |         padding-top: 12px;
 563 |         border-top: 1px solid #f5f5f5;
 564 |       }
 565 | 
 566 |       .em-details summary {
 567 |         cursor: pointer;
 568 |         font-size: 13px;
 569 |         font-weight: 600;
 570 |         color: #737373;
 571 |         padding: 8px 0;
 572 |         user-select: none;
 573 |         list-style: none;
 574 |       }
 575 | 
 576 |       .em-details summary::-webkit-details-marker {
 577 |         display: none;
 578 |       }
 579 | 
 580 |       .em-details summary:hover {
 581 |         color: #404040;
 582 |       }
 583 | 
 584 |       .em-details[open] summary {
 585 |         margin-bottom: 12px;
 586 |       }
 587 | 
 588 |       /* Dragging state */
 589 |       body[data-em-dragging] {
 590 |         user-select: none !important;
 591 |         cursor: grabbing !important;
 592 |       }
 593 | 
 594 |       body[data-em-dragging] * {
 595 |         cursor: grabbing !important;
 596 |       }
 597 | 
 598 |       /* SVG Icons */
 599 |       svg {
 600 |         fill: none;
 601 |         stroke: currentColor;
 602 |       }
 603 | 
 604 |       .em-drag-handle {
 605 |         cursor: grab;
 606 |       }
 607 | 
 608 |       .em-drag-handle:active {
 609 |         cursor: grabbing;
 610 |       }
 611 |     `;
 612 | 
 613 |     const PANEL_TEMPLATE = `
 614 |       <div class="em-panel" id="em_panel_root">
 615 |         <!-- Header -->
 616 |         <div class="em-header em-drag-handle" id="__em_drag_handle" title="Drag to move">
 617 |           <h2 class="em-title">元素标注</h2>
 618 |           <div class="em-header-actions">
 619 |             <button class="em-icon-btn" id="__em_close" title="Close">
 620 |               <svg viewBox="0 0 24 24">
 621 |                 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
 622 |               </svg>
 623 |             </button>
 624 |           </div>
 625 |         </div>
 626 | 
 627 |         <!-- Controls -->
 628 |         <div class="em-controls">
 629 |           <div class="em-select-wrapper">
 630 |             <select class="em-select" id="__em_selector_type">
 631 |               <option value="css">CSS Selector</option>
 632 |               <option value="xpath">XPath</option>
 633 |             </select>
 634 |           </div>
 635 |           <button class="em-square-btn" id="__em_toggle_list" title="列表模式 - 批量标注相似元素 (仅支持CSS)">
 636 |             <svg viewBox="0 0 24 24">
 637 |               <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
 638 |             </svg>
 639 |           </button>
 640 |           <button class="em-square-btn" id="__em_toggle_tab" title="Toggle Execute tab">
 641 |             <svg viewBox="0 0 24 24">
 642 |               <path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
 643 |               <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
 644 |             </svg>
 645 |           </button>
 646 |         </div>
 647 | 
 648 |         <!-- Selector Display -->
 649 |         <div class="em-selector-display">
 650 |           <svg viewBox="0 0 24 24" id="__em_copy_selector" title="Copy selector">
 651 |             <path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
 652 |           </svg>
 653 |           <span class="em-selector-text" id="__em_selector_text">Click an element to select</span>
 654 |           <div class="em-selector-nav">
 655 |             <button class="em-nav-btn" id="__em_nav_up" title="Select parent">
 656 |               <svg viewBox="0 0 24 24">
 657 |                 <path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7"/>
 658 |               </svg>
 659 |             </button>
 660 |             <button class="em-nav-btn" id="__em_nav_down" title="Select child">
 661 |               <svg viewBox="0 0 24 24">
 662 |                 <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/>
 663 |               </svg>
 664 |             </button>
 665 |           </div>
 666 |         </div>
 667 | 
 668 |         <!-- Tabs -->
 669 |         <div class="em-tabs">
 670 |           <button class="em-tab active" data-tab="attributes">Attributes</button>
 671 |           <button class="em-tab" data-tab="execute">Execute</button>
 672 |         </div>
 673 | 
 674 |         <!-- Status -->
 675 |         <div class="em-status idle" id="__em_status"></div>
 676 | 
 677 |         <!-- Content: Attributes Tab -->
 678 |         <div class="em-content" id="__em_tab_attributes">
 679 |           <h3 class="em-section-title">#1 Element</h3>
 680 |           
 681 |           <div class="em-attributes">
 682 |             <div class="em-attribute">
 683 |               <div class="em-attribute-label">name</div>
 684 |               <div class="em-attribute-value editable">
 685 |                 <input class="em-input" id="__em_name" placeholder="Element name" />
 686 |               </div>
 687 |             </div>
 688 | 
 689 |             <div class="em-attribute">
 690 |               <div class="em-attribute-label">selector</div>
 691 |               <div class="em-attribute-value">
 692 |                 <svg class="copy-icon" viewBox="0 0 24 24" id="__em_copy" title="Copy">
 693 |                   <path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
 694 |                 </svg>
 695 |                 <span class="em-attribute-text" id="__em_selector">-</span>
 696 |               </div>
 697 |             </div>
 698 |           </div>
 699 | 
 700 |           <h3 class="em-section-title">Selector Preferences</h3>
 701 |           <div class="em-settings">
 702 |             <div class="em-checkbox-group">
 703 |               <label class="em-checkbox-label">
 704 |                 <input type="checkbox" id="__em_pref_id" checked />
 705 |                 <span>Prefer ID</span>
 706 |               </label>
 707 |               <label class="em-checkbox-label">
 708 |                 <input type="checkbox" id="__em_pref_attr" checked />
 709 |                 <span>Prefer stable attributes</span>
 710 |               </label>
 711 |               <label class="em-checkbox-label">
 712 |                 <input type="checkbox" id="__em_pref_class" checked />
 713 |                 <span>Prefer class names</span>
 714 |               </label>
 715 |             </div>
 716 |           </div>
 717 | 
 718 |           <div class="em-actions">
 719 |             <button class="em-btn em-btn-primary" id="__em_verify">Verify (Highlight Only)</button>
 720 |           </div>
 721 | 
 722 |           <div class="em-actions">
 723 |             <button class="em-btn em-btn-success" id="__em_save">Save</button>
 724 |             <button class="em-btn em-btn-ghost" id="__em_cancel">Cancel</button>
 725 |           </div>
 726 |         </div>
 727 | 
 728 |         <!-- Content: Execute Tab -->
 729 |         <div class="em-content" id="__em_tab_execute" style="display: none;">
 730 |           <div class="em-settings">
 731 |             <div class="em-settings-group">
 732 |               <div class="em-settings-label">Action</div>
 733 |               <div class="em-select-wrapper">
 734 |                 <select class="em-select" id="__em_action">
 735 |                   <option value="hover">Hover</option>
 736 |                   <option value="left_click">Left click</option>
 737 |                   <option value="double_click">Double click</option>
 738 |                   <option value="right_click">Right click</option>
 739 |                   <option value="scroll">Scroll</option>
 740 |                   <option value="type_text">Type text</option>
 741 |                   <option value="press_keys">Press keys</option>
 742 |                 </select>
 743 |               </div>
 744 |             </div>
 745 | 
 746 |             <!-- Action-specific inputs (dynamically shown/hidden) -->
 747 |             <div class="em-settings-group" id="__em_action_text_group" style="display: none;">
 748 |               <div class="em-settings-label">Text</div>
 749 |               <input class="em-field-input" id="__em_action_text" placeholder="Text to type" />
 750 |             </div>
 751 | 
 752 |             <div class="em-settings-group" id="__em_action_keys_group" style="display: none;">
 753 |               <div class="em-settings-label">Keys</div>
 754 |               <input class="em-field-input" id="__em_action_keys" placeholder="Keys to press (e.g., Enter, Ctrl+C)" />
 755 |             </div>
 756 | 
 757 |             <div class="em-settings-group" id="__em_scroll_options" style="display: none;">
 758 |               <div class="em-settings-label">Scroll Direction</div>
 759 |               <div class="em-select-wrapper">
 760 |                 <select class="em-select" id="__em_scroll_direction">
 761 |                   <option value="down">Down</option>
 762 |                   <option value="up">Up</option>
 763 |                   <option value="left">Left</option>
 764 |                   <option value="right">Right</option>
 765 |                 </select>
 766 |               </div>
 767 |               <div class="em-field" style="margin-top: 8px;">
 768 |                 <div class="em-field-label">Amount (1-10, ~100px each)</div>
 769 |                 <input class="em-field-input" id="__em_scroll_distance" type="number" min="1" max="10" step="1" value="3" />
 770 |               </div>
 771 |             </div>
 772 | 
 773 |             <!-- Click-specific options -->
 774 |             <div id="__em_click_options" style="display: none;">
 775 |               <div class="em-grid">
 776 |                 <div class="em-field">
 777 |                   <div class="em-field-label">Button</div>
 778 |                   <select class="em-select" id="__em_btn">
 779 |                     <option value="left">Left</option>
 780 |                     <option value="middle">Middle</option>
 781 |                     <option value="right">Right</option>
 782 |                   </select>
 783 |                 </div>
 784 |                 <div class="em-field">
 785 |                   <div class="em-field-label">Timeout (ms)</div>
 786 |                   <input class="em-field-input" id="__em_nav_timeout" type="number" value="3000" />
 787 |                 </div>
 788 |               </div>
 789 | 
 790 |               <div class="em-checkbox-group" style="margin-top: 12px;">
 791 |                 <label class="em-checkbox-label">
 792 |                   <input type="checkbox" id="__em_wait_nav" />
 793 |                   <span>Wait for navigation</span>
 794 |                 </label>
 795 |                 <label class="em-checkbox-label">
 796 |                   <input type="checkbox" id="__em_mod_alt" />
 797 |                   <span>Alt key</span>
 798 |                 </label>
 799 |                 <label class="em-checkbox-label">
 800 |                   <input type="checkbox" id="__em_mod_ctrl" />
 801 |                   <span>Ctrl key</span>
 802 |                 </label>
 803 |                 <label class="em-checkbox-label">
 804 |                   <input type="checkbox" id="__em_mod_meta" />
 805 |                   <span>Meta key</span>
 806 |                 </label>
 807 |                 <label class="em-checkbox-label">
 808 |                   <input type="checkbox" id="__em_mod_shift" />
 809 |                   <span>Shift key</span>
 810 |                 </label>
 811 |               </div>
 812 |             </div>
 813 | 
 814 |             <div class="em-actions" style="margin-top: 16px;">
 815 |               <button class="em-btn em-btn-primary" id="__em_execute">Execute</button>
 816 |             </div>
 817 | 
 818 |             <!-- Execution History -->
 819 |             <div id="__em_execution_history" style="margin-top: 16px; display: none;">
 820 |               <div class="em-settings-label">Recent Executions</div>
 821 |               <div id="__em_history_list" style="font-size: 12px; color: #737373; margin-top: 8px;"></div>
 822 |             </div>
 823 |           </div>
 824 |         </div>
 825 | 
 826 |         <!-- Footer -->
 827 |         <div class="em-footer">
 828 |           Click or press <kbd>Space</kbd> to select an element
 829 |         </div>
 830 |       </div>
 831 |     `;
 832 | 
 833 |     function mount() {
 834 |       if (hostElement) return { host: hostElement, shadow: shadowRoot };
 835 | 
 836 |       hostElement = document.createElement('div');
 837 |       hostElement.id = '__element_marker_overlay';
 838 |       Object.assign(hostElement.style, {
 839 |         position: 'fixed',
 840 |         top: '24px',
 841 |         right: '24px',
 842 |         zIndex: String(CONFIG.Z_INDEX.OVERLAY),
 843 |         pointerEvents: 'none',
 844 |       });
 845 | 
 846 |       shadowRoot = hostElement.attachShadow({ mode: 'open' });
 847 |       shadowRoot.innerHTML = `<style>${PANEL_STYLES}</style>${PANEL_TEMPLATE}`;
 848 | 
 849 |       hostElement.querySelector = (...args) => shadowRoot.querySelector(...args);
 850 |       hostElement.querySelectorAll = (...args) => shadowRoot.querySelectorAll(...args);
 851 | 
 852 |       const panel = shadowRoot.querySelector('.em-panel');
 853 |       if (panel) {
 854 |         panel.style.pointerEvents = 'auto';
 855 |       }
 856 | 
 857 |       document.documentElement.appendChild(hostElement);
 858 |       return { host: hostElement, shadow: shadowRoot };
 859 |     }
 860 | 
 861 |     function unmount() {
 862 |       if (hostElement?.parentNode) {
 863 |         hostElement.parentNode.removeChild(hostElement);
 864 |       }
 865 |       hostElement = null;
 866 |       shadowRoot = null;
 867 |     }
 868 | 
 869 |     function getHost() {
 870 |       return hostElement;
 871 |     }
 872 | 
 873 |     function getShadow() {
 874 |       return shadowRoot;
 875 |     }
 876 | 
 877 |     return {
 878 |       mount,
 879 |       unmount,
 880 |       getHost,
 881 |       getShadow,
 882 |     };
 883 |   })();
 884 | 
 885 |   // ============================================================================
 886 |   // State Store Module - Centralized State Management
 887 |   // ============================================================================
 888 | 
 889 |   const StateStore = (() => {
 890 |     const state = {
 891 |       selectorType: CONFIG.DEFAULTS.SELECTOR_TYPE,
 892 |       listMode: CONFIG.DEFAULTS.LIST_MODE,
 893 |       prefs: { ...CONFIG.DEFAULTS.PREFS },
 894 |       activeTab: 'attributes',
 895 |       validation: {
 896 |         status: 'idle',
 897 |         message: '',
 898 |       },
 899 |       validationHistory: [], // Last 5 validation results
 900 |     };
 901 | 
 902 |     const listeners = new Set();
 903 | 
 904 |     function init() {
 905 |       return state;
 906 |     }
 907 | 
 908 |     function get(key) {
 909 |       return key ? state[key] : state;
 910 |     }
 911 | 
 912 |     function set(partial) {
 913 |       const changed = {};
 914 | 
 915 |       Object.keys(partial).forEach((key) => {
 916 |         if (JSON.stringify(state[key]) !== JSON.stringify(partial[key])) {
 917 |           changed[key] = true;
 918 |           state[key] = partial[key];
 919 |         }
 920 |       });
 921 | 
 922 |       if (Object.keys(changed).length === 0) return;
 923 | 
 924 |       if (changed.validation) {
 925 |         updateValidationUI();
 926 |       }
 927 |       if (changed.activeTab) {
 928 |         updateTabUI();
 929 |       }
 930 |       if (changed.listMode) {
 931 |         updateListModeUI();
 932 |       }
 933 |       if (changed.validationHistory) {
 934 |         updateValidationHistoryUI();
 935 |       }
 936 | 
 937 |       notifyListeners();
 938 |     }
 939 | 
 940 |     function subscribe(callback) {
 941 |       listeners.add(callback);
 942 |       return () => listeners.delete(callback);
 943 |     }
 944 | 
 945 |     function notifyListeners() {
 946 |       listeners.forEach((cb) => {
 947 |         try {
 948 |           cb(state);
 949 |         } catch (err) {
 950 |           console.error('[StateStore] Listener error:', err);
 951 |         }
 952 |       });
 953 |     }
 954 | 
 955 |     function updateValidationUI() {
 956 |       const statusEl = PanelHost.getShadow()?.getElementById('__em_status');
 957 |       if (!statusEl) return;
 958 | 
 959 |       const { status, message } = state.validation;
 960 |       statusEl.className = `em-status ${status}`;
 961 |       statusEl.textContent = message;
 962 |     }
 963 | 
 964 |     function updateListModeUI() {
 965 |       const shadow = PanelHost.getShadow();
 966 |       if (!shadow) return;
 967 | 
 968 |       const btn = shadow.getElementById('__em_toggle_list');
 969 |       if (!btn) return;
 970 | 
 971 |       if (state.listMode) {
 972 |         btn.classList.add('active');
 973 |       } else {
 974 |         btn.classList.remove('active');
 975 |       }
 976 |     }
 977 | 
 978 |     function updateTabUI() {
 979 |       const shadow = PanelHost.getShadow();
 980 |       if (!shadow) return;
 981 | 
 982 |       const tabs = shadow.querySelectorAll('.em-tab');
 983 |       tabs.forEach((tab) => {
 984 |         if (tab.dataset.tab === state.activeTab) {
 985 |           tab.classList.add('active');
 986 |         } else {
 987 |           tab.classList.remove('active');
 988 |         }
 989 |       });
 990 | 
 991 |       const attrContent = shadow.getElementById('__em_tab_attributes');
 992 |       const executeContent = shadow.getElementById('__em_tab_execute');
 993 | 
 994 |       if (attrContent)
 995 |         attrContent.style.display = state.activeTab === 'attributes' ? 'block' : 'none';
 996 |       if (executeContent)
 997 |         executeContent.style.display = state.activeTab === 'execute' ? 'block' : 'none';
 998 | 
 999 |       // Sync interaction mode when tab changes
1000 |       syncInteractionMode();
1001 |     }
1002 | 
1003 |     function updateValidationHistoryUI() {
1004 |       const shadow = PanelHost.getShadow();
1005 |       if (!shadow) return;
1006 | 
1007 |       const historyContainer = shadow.getElementById('__em_execution_history');
1008 |       const historyList = shadow.getElementById('__em_history_list');
1009 |       if (!historyContainer || !historyList) return;
1010 | 
1011 |       if (state.validationHistory.length === 0) {
1012 |         historyContainer.style.display = 'none';
1013 |         return;
1014 |       }
1015 | 
1016 |       historyContainer.style.display = 'block';
1017 |       historyList.innerHTML = state.validationHistory
1018 |         .slice(-5)
1019 |         .reverse()
1020 |         .map((entry) => {
1021 |           const icon = entry.success ? '✓' : '✗';
1022 |           const color = entry.success ? '#10b981' : '#ef4444';
1023 |           const timestamp = new Date(entry.timestamp).toLocaleTimeString();
1024 |           return `<div style="padding: 6px 0; border-bottom: 1px solid #f5f5f5;">
1025 |             <span style="color: ${color}; font-weight: 600;">${icon}</span>
1026 |             <span style="margin-left: 6px;">${entry.action}</span>
1027 |             <span style="float: right; color: #a3a3a3; font-size: 11px;">${timestamp}</span>
1028 |           </div>`;
1029 |         })
1030 |         .join('');
1031 |     }
1032 | 
1033 |     return {
1034 |       init,
1035 |       get,
1036 |       set,
1037 |       subscribe,
1038 |     };
1039 |   })();
1040 | 
1041 |   // ============================================================================
1042 |   // Drag Controller Module
1043 |   // ============================================================================
1044 | 
1045 |   const DragController = (() => {
1046 |     let dragging = false;
1047 |     let startPos = { x: 0, y: 0 };
1048 |     let startOffset = { top: 0, right: 0 };
1049 | 
1050 |     function init(handleElement) {
1051 |       if (!handleElement) return;
1052 |       handleElement.addEventListener('mousedown', onDragStart);
1053 |     }
1054 | 
1055 |     function onDragStart(event) {
1056 |       event.preventDefault();
1057 |       dragging = true;
1058 | 
1059 |       const host = PanelHost.getHost();
1060 |       if (!host) return;
1061 | 
1062 |       startPos = { x: event.clientX, y: event.clientY };
1063 |       startOffset = {
1064 |         top: parseInt(host.style.top) || 0,
1065 |         right: parseInt(host.style.right) || 0,
1066 |       };
1067 | 
1068 |       document.addEventListener('mousemove', onDragMove, { capture: true, passive: false });
1069 |       document.addEventListener('mouseup', onDragEnd, { capture: true, passive: false });
1070 |       document.body.setAttribute('data-em-dragging', 'true');
1071 |     }
1072 | 
1073 |     function onDragMove(event) {
1074 |       if (!dragging) return;
1075 |       event.preventDefault();
1076 |       event.stopPropagation();
1077 | 
1078 |       const host = PanelHost.getHost();
1079 |       if (!host) return;
1080 | 
1081 |       const deltaX = event.clientX - startPos.x;
1082 |       const deltaY = event.clientY - startPos.y;
1083 | 
1084 |       const newTop = Math.max(8, startOffset.top + deltaY);
1085 |       const newRight = Math.max(8, startOffset.right - deltaX);
1086 | 
1087 |       host.style.top = `${newTop}px`;
1088 |       host.style.right = `${newRight}px`;
1089 |     }
1090 | 
1091 |     function onDragEnd(event) {
1092 |       if (!dragging) return;
1093 |       event.preventDefault();
1094 |       event.stopPropagation();
1095 | 
1096 |       dragging = false;
1097 |       document.removeEventListener('mousemove', onDragMove, { capture: true });
1098 |       document.removeEventListener('mouseup', onDragEnd, { capture: true });
1099 |       document.body.removeAttribute('data-em-dragging');
1100 |     }
1101 | 
1102 |     function destroy() {
1103 |       if (dragging) {
1104 |         onDragEnd(new MouseEvent('mouseup'));
1105 |       }
1106 |     }
1107 | 
1108 |     return { init, destroy };
1109 |   })();
1110 | 
1111 |   // [继续下一部分...]
1112 |   // ============================================================================
1113 |   // Selector Engine - Heuristic Selector Generation
1114 |   // ============================================================================
1115 | 
1116 |   function generateSelector(el) {
1117 |     if (!(el instanceof Element)) return '';
1118 | 
1119 |     const prefs = StateStore.get('prefs');
1120 | 
1121 |     if (prefs.preferId && el.id) {
1122 |       const idSel = `#${CSS.escape(el.id)}`;
1123 |       if (isDeepSelectorUnique(idSel, el)) return idSel;
1124 |     }
1125 | 
1126 |     if (prefs.preferStableAttr) {
1127 |       const attrNames = [
1128 |         'data-testid',
1129 |         'data-testId',
1130 |         'data-test',
1131 |         'data-qa',
1132 |         'data-cy',
1133 |         'name',
1134 |         'title',
1135 |         'alt',
1136 |         'aria-label',
1137 |       ];
1138 |       const tag = el.tagName.toLowerCase();
1139 | 
1140 |       for (const attr of attrNames) {
1141 |         const v = el.getAttribute(attr);
1142 |         if (!v) continue;
1143 |         const attrSel = `[${attr}="${CSS.escape(v)}"]`;
1144 |         const testSel = /^(input|textarea|select)$/i.test(tag) ? `${tag}${attrSel}` : attrSel;
1145 |         if (isDeepSelectorUnique(testSel, el)) return testSel;
1146 |       }
1147 |     }
1148 | 
1149 |     if (prefs.preferClass) {
1150 |       try {
1151 |         const classes = Array.from(el.classList || []).filter(
1152 |           (c) => c && /^[a-zA-Z0-9_-]+$/.test(c),
1153 |         );
1154 |         const tag = el.tagName.toLowerCase();
1155 | 
1156 |         for (const cls of classes) {
1157 |           const sel = `.${CSS.escape(cls)}`;
1158 |           if (isDeepSelectorUnique(sel, el)) return sel;
1159 |         }
1160 | 
1161 |         for (const cls of classes) {
1162 |           const sel = `${tag}.${CSS.escape(cls)}`;
1163 |           if (isDeepSelectorUnique(sel, el)) return sel;
1164 |         }
1165 | 
1166 |         for (let i = 0; i < Math.min(classes.length, 3); i++) {
1167 |           for (let j = i + 1; j < Math.min(classes.length, 3); j++) {
1168 |             const sel = `.${CSS.escape(classes[i])}.${CSS.escape(classes[j])}`;
1169 |             if (isDeepSelectorUnique(sel, el)) return sel;
1170 |           }
1171 |         }
1172 |       } catch {}
1173 |     }
1174 | 
1175 |     if (prefs.preferStableAttr) {
1176 |       try {
1177 |         let cur = el;
1178 |         const anchorAttrs = [
1179 |           'id',
1180 |           'data-testid',
1181 |           'data-testId',
1182 |           'data-test',
1183 |           'data-qa',
1184 |           'data-cy',
1185 |           'name',
1186 |         ];
1187 | 
1188 |         // Detect shadow DOM boundary
1189 |         const root = el.getRootNode();
1190 |         const isShadowElement = root instanceof ShadowRoot;
1191 |         const boundary = isShadowElement ? root.host : document.body;
1192 | 
1193 |         while (cur && cur !== boundary) {
1194 |           if (cur.id) {
1195 |             const anchor = `#${CSS.escape(cur.id)}`;
1196 |             if (isDeepSelectorUnique(anchor, cur)) {
1197 |               const rel = buildPathFromAncestor(cur, el);
1198 |               const composed = rel ? `${anchor} ${rel}` : anchor;
1199 |               if (isDeepSelectorUnique(composed, el)) return composed;
1200 |             }
1201 |           }
1202 | 
1203 |           for (const attr of anchorAttrs) {
1204 |             const val = cur.getAttribute(attr);
1205 |             if (!val) continue;
1206 |             const aSel = `[${attr}="${CSS.escape(val)}"]`;
1207 |             if (isDeepSelectorUnique(aSel, cur)) {
1208 |               const rel = buildPathFromAncestor(cur, el);
1209 |               const composed = rel ? `${aSel} ${rel}` : aSel;
1210 |               if (isDeepSelectorUnique(composed, el)) return composed;
1211 |             }
1212 |           }
1213 |           cur = cur.parentElement;
1214 |         }
1215 |       } catch {}
1216 |     }
1217 | 
1218 |     return buildFullPath(el);
1219 |   }
1220 | 
1221 |   function buildPathFromAncestor(ancestor, target) {
1222 |     const segs = [];
1223 |     let cur = target;
1224 | 
1225 |     // Detect if we're inside shadow DOM
1226 |     const root = target.getRootNode();
1227 |     const isShadowElement = root instanceof ShadowRoot;
1228 |     const boundary = isShadowElement ? root.host : document.body;
1229 | 
1230 |     while (cur && cur !== ancestor && cur !== boundary) {
1231 |       let seg = cur.tagName.toLowerCase();
1232 |       const parent = cur.parentElement;
1233 | 
1234 |       if (parent) {
1235 |         const siblings = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
1236 |         if (siblings.length > 1) {
1237 |           seg += `:nth-of-type(${siblings.indexOf(cur) + 1})`;
1238 |         }
1239 |       }
1240 | 
1241 |       segs.unshift(seg);
1242 |       cur = parent;
1243 | 
1244 |       // Stop if we've reached the shadow root host
1245 |       if (isShadowElement && cur === boundary) {
1246 |         break;
1247 |       }
1248 |     }
1249 | 
1250 |     return segs.join(' > ');
1251 |   }
1252 | 
1253 |   function buildFullPath(el) {
1254 |     let path = '';
1255 |     let current = el;
1256 | 
1257 |     // Detect if the element is inside a shadow DOM
1258 |     const root = el.getRootNode();
1259 |     const isShadowElement = root instanceof ShadowRoot;
1260 | 
1261 |     // Determine the boundary where we should stop traversing
1262 |     const boundary = isShadowElement ? root.host : document.body;
1263 | 
1264 |     while (current && current.nodeType === Node.ELEMENT_NODE && current !== boundary) {
1265 |       let sel = current.tagName.toLowerCase();
1266 |       const parent = current.parentElement;
1267 | 
1268 |       if (parent) {
1269 |         const siblings = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
1270 |         if (siblings.length > 1) {
1271 |           sel += `:nth-of-type(${siblings.indexOf(current) + 1})`;
1272 |         }
1273 |       }
1274 | 
1275 |       path = path ? `${sel} > ${path}` : sel;
1276 |       current = parent;
1277 | 
1278 |       // Stop if we've reached the shadow root host
1279 |       if (isShadowElement && current === boundary) {
1280 |         break;
1281 |       }
1282 |     }
1283 | 
1284 |     // For shadow DOM elements, don't prepend "body >"
1285 |     // The selector should be relative within the shadow tree
1286 |     if (isShadowElement) {
1287 |       return path || el.tagName.toLowerCase();
1288 |     }
1289 | 
1290 |     // For light DOM elements, keep the original behavior
1291 |     return path ? `body > ${path}` : 'body';
1292 |   }
1293 | 
1294 |   function generateXPath(el) {
1295 |     if (!(el instanceof Element)) return '';
1296 |     if (el.id) return `//*[@id="${el.id}"]`;
1297 | 
1298 |     const segs = [];
1299 |     let cur = el;
1300 | 
1301 |     while (cur && cur.nodeType === 1 && cur !== document.documentElement) {
1302 |       const tag = cur.tagName.toLowerCase();
1303 | 
1304 |       if (cur.id) {
1305 |         segs.unshift(`//*[@id="${cur.id}"]`);
1306 |         break;
1307 |       }
1308 | 
1309 |       let i = 1;
1310 |       let sib = cur;
1311 |       while ((sib = sib.previousElementSibling)) {
1312 |         if (sib.tagName.toLowerCase() === tag) i++;
1313 |       }
1314 | 
1315 |       segs.unshift(`${tag}[${i}]`);
1316 |       cur = cur.parentElement;
1317 |     }
1318 | 
1319 |     return segs[0]?.startsWith('//*') ? segs.join('/') : '//' + segs.join('/');
1320 |   }
1321 | 
1322 |   function generateListSelector(target) {
1323 |     const list = computeElementList(target);
1324 |     const selected = list?.[0] || target;
1325 |     const parent = selected.parentElement;
1326 | 
1327 |     if (!parent) return generateSelector(target);
1328 | 
1329 |     const parentSel = generateSelector(parent);
1330 |     const childRel = generateSelectorWithinRoot(selected, parent);
1331 | 
1332 |     return parentSel && childRel ? `${parentSel} ${childRel}` : generateSelector(target);
1333 |   }
1334 | 
1335 |   function generateSelectorWithinRoot(el, root) {
1336 |     if (!(el instanceof Element)) return '';
1337 | 
1338 |     const tag = el.tagName.toLowerCase();
1339 | 
1340 |     // Use isDeepSelectorUnique for ID to support shadow DOM elements
1341 |     if (el.id) {
1342 |       const idSel = `#${CSS.escape(el.id)}`;
1343 |       if (isDeepSelectorUnique(idSel, el)) return idSel;
1344 |     }
1345 | 
1346 |     const attrNames = [
1347 |       'data-testid',
1348 |       'data-testId',
1349 |       'data-test',
1350 |       'data-qa',
1351 |       'data-cy',
1352 |       'name',
1353 |       'title',
1354 |       'alt',
1355 |       'aria-label',
1356 |     ];
1357 | 
1358 |     // Use isDeepSelectorUnique for attributes to support shadow DOM elements
1359 |     for (const attr of attrNames) {
1360 |       const v = el.getAttribute(attr);
1361 |       if (!v) continue;
1362 |       const aSel = `[${attr}="${CSS.escape(v)}"]`;
1363 |       const testSel = /^(input|textarea|select)$/i.test(tag) ? `${tag}${aSel}` : aSel;
1364 |       if (isDeepSelectorUnique(testSel, el)) return testSel;
1365 |     }
1366 | 
1367 |     try {
1368 |       const classes = Array.from(el.classList || []).filter((c) => c && /^[a-zA-Z0-9_-]+$/.test(c));
1369 | 
1370 |       // Use isDeepSelectorUnique for classes to support shadow DOM elements
1371 |       for (const cls of classes) {
1372 |         const sel = `.${CSS.escape(cls)}`;
1373 |         if (isDeepSelectorUnique(sel, el)) return sel;
1374 |       }
1375 | 
1376 |       for (const cls of classes) {
1377 |         const sel = `${tag}.${CSS.escape(cls)}`;
1378 |         if (isDeepSelectorUnique(sel, el)) return sel;
1379 |       }
1380 |     } catch {}
1381 | 
1382 |     return buildPathFromAncestor(root, el);
1383 |   }
1384 | 
1385 |   function getAccessibleName(el) {
1386 |     try {
1387 |       const labelledby = el.getAttribute('aria-labelledby');
1388 |       if (labelledby) {
1389 |         const labelEl = document.getElementById(labelledby);
1390 |         if (labelEl) return (labelEl.textContent || '').trim();
1391 |       }
1392 | 
1393 |       const ariaLabel = el.getAttribute('aria-label');
1394 |       if (ariaLabel) return ariaLabel.trim();
1395 | 
1396 |       if (el.id) {
1397 |         const label = document.querySelector(`label[for="${el.id}"]`);
1398 |         if (label) return (label.textContent || '').trim();
1399 |       }
1400 | 
1401 |       const parentLabel = el.closest('label');
1402 |       if (parentLabel) return (parentLabel.textContent || '').trim();
1403 | 
1404 |       return (
1405 |         el.getAttribute('placeholder') ||
1406 |         el.getAttribute('value') ||
1407 |         el.textContent ||
1408 |         ''
1409 |       ).trim();
1410 |     } catch {
1411 |       return '';
1412 |     }
1413 |   }
1414 | 
1415 |   // ============================================================================
1416 |   // List Mode Utilities
1417 |   // ============================================================================
1418 | 
1419 |   function getAllSiblings(el, selector) {
1420 |     const siblings = [el];
1421 |     const validate = (element) => {
1422 |       const isSameTag = el.tagName === element.tagName;
1423 |       let ok = isSameTag;
1424 |       if (selector) {
1425 |         try {
1426 |           ok = ok && !!element.querySelector(selector);
1427 |         } catch {}
1428 |       }
1429 |       return ok;
1430 |     };
1431 | 
1432 |     let next = el;
1433 |     let prev = el;
1434 |     let elementIndex = 1;
1435 | 
1436 |     while ((prev = prev?.previousElementSibling)) {
1437 |       if (validate(prev)) {
1438 |         elementIndex += 1;
1439 |         siblings.unshift(prev);
1440 |       }
1441 |     }
1442 | 
1443 |     while ((next = next?.nextElementSibling)) {
1444 |       if (validate(next)) siblings.push(next);
1445 |     }
1446 | 
1447 |     return { elements: siblings, index: elementIndex };
1448 |   }
1449 | 
1450 |   function getElementList(el, maxDepth = 50, paths = []) {
1451 |     if (maxDepth === 0 || !el || el.tagName === 'BODY') return null;
1452 | 
1453 |     let selector = el.tagName.toLowerCase();
1454 |     const { elements, index } = getAllSiblings(el, paths.join(' > '));
1455 |     let siblings = elements;
1456 | 
1457 |     if (index !== 1) selector += `:nth-of-type(${index})`;
1458 |     paths.unshift(selector);
1459 | 
1460 |     if (siblings.length === 1) {
1461 |       siblings = getElementList(el.parentElement, maxDepth - 1, paths);
1462 |     }
1463 | 
1464 |     return siblings;
1465 |   }
1466 | 
1467 |   function computeElementList(target) {
1468 |     try {
1469 |       return getElementList(target) || [target];
1470 |     } catch {
1471 |       return [target];
1472 |     }
1473 |   }
1474 | 
1475 |   // ============================================================================
1476 |   // Deep Query (Shadow DOM Support)
1477 |   // ============================================================================
1478 | 
1479 |   function* walkAllNodesDeep(root) {
1480 |     const stack = [root];
1481 |     let count = 0;
1482 |     const MAX = 10000;
1483 | 
1484 |     while (stack.length) {
1485 |       const node = stack.pop();
1486 |       if (!node || ++count > MAX) continue;
1487 | 
1488 |       // Skip overlay elements to prevent panel self-highlighting
1489 |       if (isOverlayElement(node)) {
1490 |         continue;
1491 |       }
1492 | 
1493 |       yield node;
1494 | 
1495 |       try {
1496 |         if (node.children) {
1497 |           const children = Array.from(node.children);
1498 |           for (let i = children.length - 1; i >= 0; i--) {
1499 |             stack.push(children[i]);
1500 |           }
1501 |         }
1502 | 
1503 |         if (node.shadowRoot?.children) {
1504 |           const srChildren = Array.from(node.shadowRoot.children);
1505 |           for (let i = srChildren.length - 1; i >= 0; i--) {
1506 |             stack.push(srChildren[i]);
1507 |           }
1508 |         }
1509 |       } catch {}
1510 |     }
1511 |   }
1512 | 
1513 |   function queryAllDeep(selector) {
1514 |     const results = [];
1515 |     for (const node of walkAllNodesDeep(document)) {
1516 |       if (!(node instanceof Element)) continue;
1517 |       try {
1518 |         if (node.matches(selector)) results.push(node);
1519 |       } catch {}
1520 |     }
1521 |     return results;
1522 |   }
1523 | 
1524 |   /**
1525 |    * Check if a selector uniquely identifies the target element across the entire DOM tree,
1526 |    * including shadow DOM boundaries.
1527 |    *
1528 |    * This function uses queryAllDeep to traverse both light DOM and shadow DOM,
1529 |    * ensuring that selectors work correctly for elements inside shadow roots.
1530 |    *
1531 |    * @param {string} selector - The CSS selector to test
1532 |    * @param {Element} target - The target element that should be uniquely identified
1533 |    * @returns {boolean} True if the selector matches exactly one element and it's the target
1534 |    */
1535 |   function isDeepSelectorUnique(selector, target) {
1536 |     if (!selector || !(target instanceof Element)) return false;
1537 |     try {
1538 |       const matches = queryAllDeep(selector);
1539 |       return matches.length === 1 && matches[0] === target;
1540 |     } catch (error) {
1541 |       return false;
1542 |     }
1543 |   }
1544 | 
1545 |   function evaluateXPathAll(xpath) {
1546 |     try {
1547 |       const arr = [];
1548 |       const res = document.evaluate(
1549 |         xpath,
1550 |         document,
1551 |         null,
1552 |         XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1553 |         null,
1554 |       );
1555 | 
1556 |       for (let i = 0; i < res.snapshotLength; i++) {
1557 |         const n = res.snapshotItem(i);
1558 |         // Filter out overlay elements to prevent panel self-highlighting
1559 |         if (n?.nodeType === 1 && !isOverlayElement(n)) {
1560 |           arr.push(n);
1561 |         }
1562 |       }
1563 |       return arr;
1564 |     } catch {
1565 |       return [];
1566 |     }
1567 |   }
1568 | 
1569 |   // ============================================================================
1570 |   // Highlighter & Rects Management
1571 |   // ============================================================================
1572 | 
1573 |   const STATE = {
1574 |     active: false,
1575 |     hoverEl: null,
1576 |     selectedEl: null,
1577 |     box: null,
1578 |     highlighter: null,
1579 |     listenersAttached: false,
1580 |     rectsHost: null,
1581 |     hoveredList: [],
1582 |     verifyRectsActive: false, // Track if verify rects are showing
1583 |     // Performance optimization: rAF throttling for hover
1584 |     hoverRafId: null,
1585 |     lastHoverTarget: null,
1586 |     // DOM pooling for rect elements
1587 |     rectPool: [],
1588 |     rectPoolUsed: 0,
1589 |   };
1590 | 
1591 |   function ensureHighlighter() {
1592 |     if (STATE.highlighter) return STATE.highlighter;
1593 | 
1594 |     const hl = document.createElement('div');
1595 |     hl.id = '__element_marker_highlight';
1596 |     Object.assign(hl.style, {
1597 |       position: 'fixed',
1598 |       zIndex: String(CONFIG.Z_INDEX.HIGHLIGHTER),
1599 |       pointerEvents: 'none',
1600 |       border: `2px solid ${CONFIG.COLORS.HOVER}`,
1601 |       borderRadius: '4px',
1602 |       boxShadow: `0 0 0 2px ${CONFIG.COLORS.HOVER}33`,
1603 |       transition: 'all 100ms ease-out',
1604 |     });
1605 | 
1606 |     document.documentElement.appendChild(hl);
1607 |     STATE.highlighter = hl;
1608 |     return hl;
1609 |   }
1610 | 
1611 |   function ensureRectsHost() {
1612 |     if (STATE.rectsHost) return STATE.rectsHost;
1613 | 
1614 |     const host = document.createElement('div');
1615 |     host.id = '__element_marker_rects';
1616 |     Object.assign(host.style, {
1617 |       position: 'fixed',
1618 |       zIndex: String(CONFIG.Z_INDEX.RECTS),
1619 |       pointerEvents: 'none',
1620 |       inset: '0',
1621 |     });
1622 | 
1623 |     document.documentElement.appendChild(host);
1624 |     STATE.rectsHost = host;
1625 |     return host;
1626 |   }
1627 | 
1628 |   function moveHighlighterTo(el) {
1629 |     const hl = ensureHighlighter();
1630 |     const r = el.getBoundingClientRect();
1631 |     hl.style.left = `${r.left}px`;
1632 |     hl.style.top = `${r.top}px`;
1633 |     hl.style.width = `${r.width}px`;
1634 |     hl.style.height = `${r.height}px`;
1635 |     hl.style.display = 'block';
1636 |   }
1637 | 
1638 |   function clearHighlighter() {
1639 |     if (STATE.highlighter) STATE.highlighter.style.display = 'none';
1640 |     // Only clear hover rects, not verify rects
1641 |     if (!STATE.verifyRectsActive) {
1642 |       clearRects();
1643 |     }
1644 |   }
1645 | 
1646 |   function clearRects() {
1647 |     // Hide all pooled rect boxes instead of destroying them
1648 |     const used = STATE.rectPoolUsed || 0;
1649 |     for (let i = 0; i < used; i++) {
1650 |       const box = STATE.rectPool[i];
1651 |       if (box) box.style.display = 'none';
1652 |     }
1653 |     STATE.rectPoolUsed = 0;
1654 |     STATE.verifyRectsActive = false;
1655 |     // Invalidate lastHoverTarget so next hover will redraw even on same element
1656 |     STATE.lastHoverTarget = null;
1657 |   }
1658 | 
1659 |   /**
1660 |    * Get or create a rect box from the pool
1661 |    * @param {HTMLElement} host - The container element
1662 |    * @param {number} index - The pool index
1663 |    * @returns {HTMLDivElement} The rect box element
1664 |    */
1665 |   function getOrCreateRectBox(host, index) {
1666 |     let box = STATE.rectPool[index];
1667 |     if (!box) {
1668 |       box = document.createElement('div');
1669 |       Object.assign(box.style, {
1670 |         position: 'fixed',
1671 |         pointerEvents: 'none',
1672 |         borderRadius: '4px',
1673 |         transition: 'all 100ms ease-out',
1674 |         display: 'none',
1675 |       });
1676 |       STATE.rectPool[index] = box;
1677 |     }
1678 |     // Ensure the box is attached to the host
1679 |     if (!box.isConnected) {
1680 |       host.appendChild(box);
1681 |     }
1682 |     return box;
1683 |   }
1684 | 
1685 |   // Maximum rect pool size to prevent memory bloat
1686 |   const MAX_RECT_POOL_SIZE = 100;
1687 | 
1688 |   /**
1689 |    * Draw rect boxes with pooling optimization
1690 |    * @param {Array<{x: number, y: number, width: number, height: number}>} rects - Rect data
1691 |    * @param {Object} options - Drawing options
1692 |    * @param {boolean} options.isVerify - Whether this is a verify highlight (affects verifyRectsActive)
1693 |    */
1694 |   function drawRectBoxes(
1695 |     rects,
1696 |     { color = CONFIG.COLORS.HOVER, dashed = true, offsetX = 0, offsetY = 0, isVerify = false } = {},
1697 |   ) {
1698 |     const host = ensureRectsHost();
1699 |     const prevUsed = STATE.rectPoolUsed || 0;
1700 |     // Limit rect count to prevent memory bloat
1701 |     const count = Math.min(Array.isArray(rects) ? rects.length : 0, MAX_RECT_POOL_SIZE);
1702 | 
1703 |     // Update or show rect boxes
1704 |     for (let i = 0; i < count; i++) {
1705 |       const r = rects[i];
1706 |       if (!r) continue;
1707 | 
1708 |       const x = Number.isFinite(r.left) ? r.left : Number.isFinite(r.x) ? r.x : 0;
1709 |       const y = Number.isFinite(r.top) ? r.top : Number.isFinite(r.y) ? r.y : 0;
1710 |       const w = Number.isFinite(r.width) ? r.width : 0;
1711 |       const h = Number.isFinite(r.height) ? r.height : 0;
1712 | 
1713 |       const box = getOrCreateRectBox(host, i);
1714 |       Object.assign(box.style, {
1715 |         left: `${offsetX + x}px`,
1716 |         top: `${offsetY + y}px`,
1717 |         width: `${w}px`,
1718 |         height: `${h}px`,
1719 |         border: `2px ${dashed ? 'dashed' : 'solid'} ${color}`,
1720 |         boxShadow: `0 0 0 2px ${color}22`,
1721 |         display: 'block',
1722 |       });
1723 |     }
1724 | 
1725 |     // Hide excess boxes from previous render
1726 |     for (let i = count; i < prevUsed; i++) {
1727 |       const box = STATE.rectPool[i];
1728 |       if (box) box.style.display = 'none';
1729 |     }
1730 | 
1731 |     STATE.rectPoolUsed = count;
1732 |     // Reset verifyRectsActive for hover operations (so clearHighlighter works correctly)
1733 |     // Only set to true when isVerify is explicitly true
1734 |     STATE.verifyRectsActive = isVerify;
1735 |   }
1736 | 
1737 |   function drawRects(elements, color = CONFIG.COLORS.HOVER, dashed = true, isVerify = false) {
1738 |     const rects = elements.map((el) => {
1739 |       const r = el.getBoundingClientRect();
1740 |       return { x: r.left, y: r.top, width: r.width, height: r.height };
1741 |     });
1742 |     drawRectBoxes(rects, { color, dashed, isVerify });
1743 |   }
1744 | 
1745 |   // ============================================================================
1746 |   // Interaction Logic
1747 |   // ============================================================================
1748 | 
1749 |   function isInsidePanel(target) {
1750 |     const shadow = PanelHost.getShadow();
1751 |     return !!shadow && target instanceof Node && shadow.contains(target);
1752 |   }
1753 | 
1754 |   /**
1755 |    * Check if a node belongs to the element marker overlay (panel host or its shadow DOM)
1756 |    * This is used to filter out overlay elements from query results to prevent self-highlighting
1757 |    *
1758 |    * @param {Node} node - The node to check
1759 |    * @returns {boolean} True if the node is part of the overlay
1760 |    */
1761 |   function isOverlayElement(node) {
1762 |     if (!(node instanceof Node)) return false;
1763 | 
1764 |     const host = PanelHost.getHost();
1765 |     if (!host) return false;
1766 | 
1767 |     // Check if node is the panel host itself
1768 |     if (node === host) return true;
1769 | 
1770 |     // Check if node is within the shadow DOM of the panel host
1771 |     const root = typeof node.getRootNode === 'function' ? node.getRootNode() : null;
1772 |     return root instanceof ShadowRoot && root.host === host;
1773 |   }
1774 | 
1775 |   /**
1776 |    * Filter out overlay elements from an array of elements
1777 |    * This ensures that panel components are never included in highlight/verification results
1778 |    *
1779 |    * @param {Array} elements - Array of elements to filter
1780 |    * @returns {Array} Filtered array without overlay elements
1781 |    */
1782 |   function filterOverlayElements(elements) {
1783 |     if (!Array.isArray(elements)) return [];
1784 |     return elements.filter((node) => !isOverlayElement(node));
1785 |   }
1786 | 
1787 |   /**
1788 |    * Get the effective event target for page element selection, considering shadow DOM boundaries.
1789 |    *
1790 |    * This function resolves the real target element from a pointer event by walking the
1791 |    * composed path (if available) to find the innermost page element, skipping overlay elements.
1792 |    *
1793 |    * Background:
1794 |    * - When events bubble up from inside shadow DOM, they get "retargeted" at shadow boundaries
1795 |    * - By the time a window-level listener receives the event, ev.target points to the shadow host
1796 |    * - composedPath() exposes the original event path before retargeting
1797 |    * - This allows us to select elements inside shadow DOM (e.g., <td-header> internals)
1798 |    *
1799 |    * IMPORTANT: This function should only be called AFTER verifying the event is not from
1800 |    * overlay UI (panel buttons, etc). Otherwise it will filter out overlay elements and break
1801 |    * panel interactions.
1802 |    *
1803 |    * @param {Event} ev - The pointer event (mousemove, click, etc.)
1804 |    * @returns {Element|null} The innermost non-overlay page element, or null if none found
1805 |    */
1806 |   function getDeepPageTarget(ev) {
1807 |     if (!ev) return null;
1808 | 
1809 |     // Try to walk the composed path to find the innermost non-overlay element
1810 |     try {
1811 |       const path = typeof ev.composedPath === 'function' ? ev.composedPath() : null;
1812 |       if (Array.isArray(path) && path.length > 0) {
1813 |         // Walk from innermost to outermost, find the first real page element
1814 |         for (const node of path) {
1815 |           if (node instanceof Element && !isOverlayElement(node)) {
1816 |             return node;
1817 |           }
1818 |         }
1819 |       }
1820 |     } catch (error) {
1821 |       // composedPath() may throw in some edge cases (e.g., detached nodes)
1822 |       // Fall through to use ev.target
1823 |     }
1824 | 
1825 |     // Fallback: use ev.target if composedPath is unavailable or all nodes were filtered
1826 |     const fallback = ev.target instanceof Element ? ev.target : null;
1827 |     // If fallback is overlay, return null (caller should handle this case)
1828 |     if (fallback && !isOverlayElement(fallback)) {
1829 |       return fallback;
1830 |     }
1831 |     return null;
1832 |   }
1833 | 
1834 |   // Store pending hover event for rAF processing
1835 |   let pendingHoverEvent = null;
1836 | 
1837 |   /**
1838 |    * Process mouse move event - the actual hover update logic
1839 |    * Separated from onMouseMove for rAF throttling
1840 |    */
1841 |   function processMouseMove(ev) {
1842 |     if (!STATE.active) return;
1843 | 
1844 |     const rawTarget = ev?.target;
1845 |     if (!(rawTarget instanceof Element)) {
1846 |       STATE.hoverEl = null;
1847 |       STATE.lastHoverTarget = null;
1848 |       clearHighlighter();
1849 |       return;
1850 |     }
1851 | 
1852 |     const host = PanelHost.getHost();
1853 |     if ((host && rawTarget === host) || isInsidePanel(rawTarget)) {
1854 |       STATE.hoverEl = null;
1855 |       STATE.lastHoverTarget = null;
1856 |       clearHighlighter();
1857 |       return;
1858 |     }
1859 | 
1860 |     const target = getDeepPageTarget(ev) || rawTarget;
1861 |     STATE.hoverEl = target;
1862 | 
1863 |     // Get current listMode
1864 |     let listMode = false;
1865 |     try {
1866 |       listMode = !!StateStore.get('listMode');
1867 |     } catch {}
1868 | 
1869 |     // Skip update if target and mode haven't changed
1870 |     const last = STATE.lastHoverTarget;
1871 |     if (last && last.element === target && last.listMode === listMode) {
1872 |       return;
1873 |     }
1874 |     STATE.lastHoverTarget = { element: target, listMode };
1875 | 
1876 |     if (!IS_MAIN) {
1877 |       try {
1878 |         const list = listMode ? computeElementList(target) || [target] : [target];
1879 |         const rects = list.map((el) => {
1880 |           const r = el.getBoundingClientRect();
1881 |           return { x: r.left, y: r.top, width: r.width, height: r.height };
1882 |         });
1883 | 
1884 |         // Performance: Don't generate selector on hover (defer to click)
1885 |         window.top.postMessage({ type: 'em_hover', rects }, '*');
1886 |       } catch {}
1887 |       return;
1888 |     }
1889 | 
1890 |     if (listMode) {
1891 |       STATE.hoveredList = computeElementList(target) || [target];
1892 |       drawRects(STATE.hoveredList);
1893 |     } else {
1894 |       moveHighlighterTo(target);
1895 |     }
1896 |   }
1897 | 
1898 |   /**
1899 |    * Mouse move handler with rAF throttling
1900 |    * Ensures hover updates are batched to animation frame rate
1901 |    */
1902 |   function onMouseMove(ev) {
1903 |     if (!STATE.active) return;
1904 | 
1905 |     // Store the latest event
1906 |     pendingHoverEvent = ev;
1907 | 
1908 |     // Skip if already scheduled
1909 |     if (STATE.hoverRafId != null) return;
1910 | 
1911 |     // Schedule processing on next animation frame
1912 |     STATE.hoverRafId = requestAnimationFrame(() => {
1913 |       STATE.hoverRafId = null;
1914 |       const latest = pendingHoverEvent;
1915 |       pendingHoverEvent = null;
1916 |       if (!latest) return;
1917 |       processMouseMove(latest);
1918 |     });
1919 |   }
1920 | 
1921 |   // ============================================================================
1922 |   // Event Listeners Management
1923 |   // ============================================================================
1924 | 
1925 |   function attachPointerListeners() {
1926 |     if (STATE.listenersAttached) return;
1927 |     window.addEventListener('mousemove', onMouseMove, true);
1928 |     window.addEventListener('click', onClick, true);
1929 |     STATE.listenersAttached = true;
1930 |   }
1931 | 
1932 |   function detachPointerListeners() {
1933 |     if (!STATE.listenersAttached) return;
1934 |     window.removeEventListener('mousemove', onMouseMove, true);
1935 |     window.removeEventListener('click', onClick, true);
1936 |     STATE.listenersAttached = false;
1937 |   }
1938 | 
1939 |   function attachKeyboardListener() {
1940 |     window.addEventListener('keydown', onKeyDown, true);
1941 |   }
1942 | 
1943 |   function detachKeyboardListener() {
1944 |     window.removeEventListener('keydown', onKeyDown, true);
1945 |   }
1946 | 
1947 |   function syncInteractionMode() {
1948 |     if (!STATE.active) return;
1949 |     const activeTab = StateStore.get('activeTab');
1950 |     if (activeTab === 'execute') {
1951 |       // In execute mode, detach pointer listeners to allow real interactions
1952 |       // but keep keyboard listener for Esc key
1953 |       detachPointerListeners();
1954 |       // Only clear the hover highlighter, not the verification rects
1955 |       if (STATE.highlighter) STATE.highlighter.style.display = 'none';
1956 |     } else {
1957 |       // In attributes mode, attach all listeners for element selection
1958 |       attachPointerListeners();
1959 |     }
1960 |   }
1961 | 
1962 |   // ============================================================================
1963 |   // Event Handlers
1964 |   // ============================================================================
1965 | 
1966 |   function onClick(ev) {
1967 |     if (!STATE.active) return;
1968 | 
1969 |     // First, use the raw ev.target to check for overlay UI
1970 |     // This ensures panel buttons and other UI elements remain interactive
1971 |     const rawTarget = ev.target;
1972 |     const host = PanelHost.getHost();
1973 | 
1974 |     // Check if raw target is the panel host itself or inside the shadow DOM
1975 |     // IMPORTANT: Return early WITHOUT preventDefault to allow overlay button clicks
1976 |     if ((host && rawTarget === host) || isInsidePanel(rawTarget)) {
1977 |       return;
1978 |     }
1979 | 
1980 |     // Now we know it's a page element, prevent default and get deep target
1981 |     ev.preventDefault();
1982 |     ev.stopPropagation();
1983 | 
1984 |     if (!(rawTarget instanceof Element)) return;
1985 | 
1986 |     // Get the deep target (considering shadow DOM) after confirming it's not overlay
1987 |     const target = getDeepPageTarget(ev) || rawTarget;
1988 | 
1989 |     if (!IS_MAIN) {
1990 |       try {
1991 |         const selectorType = StateStore.get('selectorType');
1992 |         const listMode = StateStore.get('listMode');
1993 | 
1994 |         const sel =
1995 |           selectorType === 'xpath'
1996 |             ? generateXPath(target)
1997 |             : listMode
1998 |               ? generateListSelector(target)
1999 |               : generateSelector(target);
2000 | 
2001 |         window.top.postMessage({ type: 'em_click', innerSel: sel }, '*');
2002 |       } catch {}
2003 |       return;
2004 |     }
2005 | 
2006 |     setSelection(target);
2007 |   }
2008 | 
2009 |   function onKeyDown(e) {
2010 |     if (!STATE.active) return;
2011 | 
2012 |     // Check if the focused element is inside the panel - if so, don't handle selection keys
2013 |     if (isInsidePanel(e.target)) {
2014 |       // Key event is from panel, don't interfere
2015 |       if (e.key !== 'Escape') return; // Still allow Escape to close
2016 |     }
2017 | 
2018 |     // In execute mode, only handle Escape to close - don't intercept other keys
2019 |     // This allows real page interactions (typing, scrolling, etc.)
2020 |     const activeTab = StateStore.get('activeTab');
2021 |     if (activeTab === 'execute') {
2022 |       if (e.key === 'Escape') {
2023 |         e.preventDefault();
2024 |         stop();
2025 |       }
2026 |       return; // Don't intercept Space/Arrow keys in execute mode
2027 |     }
2028 | 
2029 |     if (e.key === 'Escape') {
2030 |       e.preventDefault();
2031 |       stop();
2032 |     } else if (e.key === ' ' || e.code === 'Space') {
2033 |       e.preventDefault();
2034 |       const t = STATE.hoverEl || STATE.selectedEl;
2035 |       if (t) setSelection(t);
2036 |     } else if (e.key === 'ArrowUp') {
2037 |       e.preventDefault();
2038 |       const base = STATE.selectedEl || STATE.hoverEl;
2039 |       if (base?.parentElement) setSelection(base.parentElement);
2040 |     } else if (e.key === 'ArrowDown') {
2041 |       e.preventDefault();
2042 |       const base = STATE.selectedEl || STATE.hoverEl;
2043 |       if (base?.firstElementChild) setSelection(base.firstElementChild);
2044 |     }
2045 |   }
2046 | 
2047 |   function setSelection(el) {
2048 |     if (!(el instanceof Element)) return;
2049 | 
2050 |     STATE.selectedEl = el;
2051 | 
2052 |     const selectorType = StateStore.get('selectorType');
2053 |     const listMode = StateStore.get('listMode');
2054 | 
2055 |     const sel =
2056 |       selectorType === 'xpath'
2057 |         ? generateXPath(el)
2058 |         : listMode
2059 |           ? generateListSelector(el)
2060 |           : generateSelector(el);
2061 | 
2062 |     const name = getAccessibleName(el) || el.tagName.toLowerCase();
2063 | 
2064 |     const selectorText = STATE.box?.querySelector('#__em_selector');
2065 |     const inputName = STATE.box?.querySelector('#__em_name');
2066 |     const selectorDisplay = STATE.box?.querySelector('#__em_selector_text');
2067 | 
2068 |     if (selectorText) selectorText.textContent = sel;
2069 |     if (selectorDisplay) selectorDisplay.textContent = sel;
2070 |     if (inputName && !inputName.value) inputName.value = name;
2071 | 
2072 |     moveHighlighterTo(el);
2073 |   }
2074 | 
2075 |   // ============================================================================
2076 |   // Validation Logic
2077 |   // ============================================================================
2078 | 
2079 |   /**
2080 |    * Verify selector by highlighting only (non-destructive)
2081 |    */
2082 |   async function verifyHighlightOnly() {
2083 |     try {
2084 |       const selector = STATE.box?.querySelector('#__em_selector')?.textContent?.trim();
2085 |       if (!selector) return;
2086 | 
2087 |       StateStore.set({
2088 |         validation: { status: 'running', message: 'Verifying selector...' },
2089 |       });
2090 | 
2091 |       const selectorType = StateStore.get('selectorType');
2092 |       const listMode = StateStore.get('listMode');
2093 |       const effectiveType = listMode ? 'css' : selectorType;
2094 | 
2095 |       // Query for matches
2096 |       const matches =
2097 |         effectiveType === 'xpath' ? evaluateXPathAll(selector) : queryAllDeep(selector);
2098 | 
2099 |       // Additional defense: filter out any overlay elements that might have slipped through
2100 |       const filteredMatches = filterOverlayElements(matches);
2101 | 
2102 |       if (!filteredMatches || filteredMatches.length === 0) {
2103 |         StateStore.set({
2104 |           validation: { status: 'failure', message: 'No elements found' },
2105 |         });
2106 |         return;
2107 |       }
2108 | 
2109 |       // Scroll first match into view
2110 |       const primaryMatch = filteredMatches[0];
2111 |       if (primaryMatch) {
2112 |         primaryMatch.scrollIntoView({
2113 |           block: 'center',
2114 |           inline: 'center',
2115 |           behavior: 'smooth',
2116 |         });
2117 |       }
2118 | 
2119 |       await sleep(200);
2120 | 
2121 |       // Highlight matches with isVerify=true to prevent clearing on hover
2122 |       drawRects(filteredMatches, CONFIG.COLORS.VERIFY, false, true);
2123 | 
2124 |       StateStore.set({
2125 |         validation: {
2126 |           status: 'success',
2127 |           message: `Found ${filteredMatches.length} element${filteredMatches.length > 1 ? 's' : ''}`,
2128 |         },
2129 |       });
2130 | 
2131 |       // Auto-clear highlight after 2 seconds
2132 |       setTimeout(() => {
2133 |         clearRects();
2134 |         StateStore.set({
2135 |           validation: { status: 'idle', message: '' },
2136 |         });
2137 |       }, 2000);
2138 |     } catch (error) {
2139 |       console.error('[verifyHighlightOnly] error:', error);
2140 |       StateStore.set({
2141 |         validation: { status: 'failure', message: error.message || 'Verification failed' },
2142 |       });
2143 |     }
2144 |   }
2145 | 
2146 |   /**
2147 |    * Execute action on selector (destructive)
2148 |    */
2149 |   async function verifySelectorNow() {
2150 |     try {
2151 |       const selector = STATE.box?.querySelector('#__em_selector')?.textContent?.trim();
2152 |       if (!selector) return;
2153 | 
2154 |       StateStore.set({
2155 |         validation: { status: 'running', message: 'Executing action...' },
2156 |       });
2157 | 
2158 |       const selectorType = StateStore.get('selectorType');
2159 |       const listMode = StateStore.get('listMode');
2160 | 
2161 |       const effectiveType = listMode ? 'css' : selectorType;
2162 | 
2163 |       const matches =
2164 |         effectiveType === 'xpath' ? evaluateXPathAll(selector) : queryAllDeep(selector);
2165 | 
2166 |       // Additional defense: filter out any overlay elements that might have slipped through
2167 |       const filteredMatches = filterOverlayElements(matches);
2168 | 
2169 |       if (!filteredMatches || filteredMatches.length === 0) {
2170 |         StateStore.set({
2171 |           validation: { status: 'failure', message: 'No elements found' },
2172 |         });
2173 |         return;
2174 |       }
2175 | 
2176 |       drawRects(filteredMatches, CONFIG.COLORS.VERIFY, false);
2177 | 
2178 |       const action = STATE.box?.querySelector('#__em_action')?.value || 'hover';
2179 | 
2180 |       const payload = {
2181 |         type: 'element_marker_validate',
2182 |         selector,
2183 |         selectorType: effectiveType,
2184 |         action,
2185 |         listMode,
2186 |       };
2187 | 
2188 |       // Action-specific parameters with validation
2189 |       if (action === 'type_text') {
2190 |         const actionText = String(
2191 |           STATE.box?.querySelector('#__em_action_text')?.value || '',
2192 |         ).trim();
2193 |         if (!actionText) {
2194 |           StateStore.set({
2195 |             validation: { status: 'failure', message: 'Text is required for type_text' },
2196 |           });
2197 |           return;
2198 |         }
2199 |         payload.text = actionText;
2200 |       }
2201 | 
2202 |       if (action === 'press_keys') {
2203 |         const actionKeys = String(
2204 |           STATE.box?.querySelector('#__em_action_keys')?.value || '',
2205 |         ).trim();
2206 |         if (!actionKeys) {
2207 |           StateStore.set({
2208 |             validation: { status: 'failure', message: 'Keys are required for press_keys' },
2209 |           });
2210 |           return;
2211 |         }
2212 |         payload.keys = actionKeys;
2213 |       }
2214 | 
2215 |       if (action === 'scroll') {
2216 |         const direction = STATE.box?.querySelector('#__em_scroll_direction')?.value || 'down';
2217 |         const rawAmount = Number(STATE.box?.querySelector('#__em_scroll_distance')?.value);
2218 |         // Clamp to 1-10 range (backend expects ticks, not pixels)
2219 |         const amount = Math.max(
2220 |           1,
2221 |           Math.min(Math.round(Number.isFinite(rawAmount) ? rawAmount : 3), 10),
2222 |         );
2223 |         payload.scrollDirection = direction;
2224 |         payload.scrollAmount = amount;
2225 |       }
2226 | 
2227 |       if (['left_click', 'double_click', 'right_click'].includes(action)) {
2228 |         payload.modifiers = {
2229 |           altKey: !!STATE.box?.querySelector('#__em_mod_alt')?.checked,
2230 |           ctrlKey: !!STATE.box?.querySelector('#__em_mod_ctrl')?.checked,
2231 |           metaKey: !!STATE.box?.querySelector('#__em_mod_meta')?.checked,
2232 |           shiftKey: !!STATE.box?.querySelector('#__em_mod_shift')?.checked,
2233 |         };
2234 |         payload.button = STATE.box?.querySelector('#__em_btn')?.value || 'left';
2235 |         payload.waitForNavigation = !!STATE.box?.querySelector('#__em_wait_nav')?.checked;
2236 |         payload.timeoutMs = Number(STATE.box?.querySelector('#__em_nav_timeout')?.value) || 3000;
2237 |       }
2238 | 
2239 |       const res = await chrome.runtime.sendMessage(payload);
2240 | 
2241 |       const success = !!res?.tool?.ok;
2242 |       const newEntry = {
2243 |         action,
2244 |         success,
2245 |         timestamp: Date.now(),
2246 |         matchCount: filteredMatches.length,
2247 |       };
2248 |       const history = [...(StateStore.get('validationHistory') || []), newEntry].slice(-5);
2249 | 
2250 |       if (res?.tool?.ok) {
2251 |         StateStore.set({
2252 |           validation: {
2253 |             status: 'success',
2254 |             message: `✓ 验证成功 (匹配 ${filteredMatches.length} 个元素)`,
2255 |           },
2256 |           validationHistory: history,
2257 |         });
2258 |       } else {
2259 |         StateStore.set({
2260 |           validation: {
2261 |             status: 'failure',
2262 |             message: res?.tool?.error || '验证失败',
2263 |           },
2264 |           validationHistory: history,
2265 |         });
2266 |       }
2267 |     } catch (err) {
2268 |       const newEntry = {
2269 |         action: STATE.box?.querySelector('#__em_action')?.value || 'hover',
2270 |         success: false,
2271 |         timestamp: Date.now(),
2272 |         matchCount: 0,
2273 |       };
2274 |       const history = [...(StateStore.get('validationHistory') || []), newEntry].slice(-5);
2275 | 
2276 |       StateStore.set({
2277 |         validation: {
2278 |           status: 'failure',
2279 |           message: `错误: ${err.message}`,
2280 |         },
2281 |         validationHistory: history,
2282 |       });
2283 |     }
2284 |   }
2285 | 
2286 |   /**
2287 |    * Highlight selector from external request (popup/background)
2288 |    * Supports composite iframe selectors: "frameSelector |> innerSelector"
2289 |    */
2290 |   async function highlightSelectorExternal({ selector, selectorType = 'css', listMode = false }) {
2291 |     const normalized = String(selector || '').trim();
2292 |     if (!normalized) {
2293 |       return { success: false, error: 'selector is required' };
2294 |     }
2295 | 
2296 |     try {
2297 |       // Handle composite iframe selector
2298 |       if (normalized.includes('|>')) {
2299 |         const parts = normalized
2300 |           .split('|>')
2301 |           .map((s) => s.trim())
2302 |           .filter(Boolean);
2303 | 
2304 |         if (parts.length >= 2) {
2305 |           const frameSel = parts[0];
2306 |           const innerSel = parts.slice(1).join(' |> ');
2307 | 
2308 |           // Find frame element
2309 |           let frameEl = null;
2310 |           try {
2311 |             frameEl = querySelectorDeepFirst(frameSel) || document.querySelector(frameSel);
2312 |           } catch {}
2313 | 
2314 |           if (
2315 |             !frameEl ||
2316 |             !(frameEl instanceof HTMLIFrameElement || frameEl instanceof HTMLFrameElement)
2317 |           ) {
2318 |             return { success: false, error: `Frame element not found: ${frameSel}` };
2319 |           }
2320 | 
2321 |           const cw = frameEl.contentWindow;
2322 |           if (!cw) {
2323 |             return { success: false, error: 'Unable to access frame contentWindow' };
2324 |           }
2325 | 
2326 |           // Forward highlight request to iframe
2327 |           return new Promise((resolve) => {
2328 |             const reqId = `em_highlight_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2329 |             const listener = (ev) => {
2330 |               try {
2331 |                 const data = ev?.data;
2332 |                 if (!data || data.type !== 'em-highlight-result' || data.reqId !== reqId) return;
2333 |                 window.removeEventListener('message', listener, true);
2334 |                 resolve(data.result);
2335 |               } catch {}
2336 |             };
2337 | 
2338 |             window.addEventListener('message', listener, true);
2339 |             setTimeout(() => {
2340 |               window.removeEventListener('message', listener, true);
2341 |               resolve({ success: false, error: 'Frame highlight timeout' });
2342 |             }, 3000);
2343 | 
2344 |             cw.postMessage(
2345 |               {
2346 |                 type: 'em-highlight-request',
2347 |                 reqId,
2348 |                 selector: innerSel,
2349 |                 selectorType,
2350 |                 listMode,
2351 |               },
2352 |               '*',
2353 |             );
2354 |           });
2355 |         }
2356 |       }
2357 | 
2358 |       // Handle normal selector (non-iframe)
2359 |       const effectiveType = listMode ? 'css' : selectorType;
2360 |       const matches =
2361 |         effectiveType === 'xpath' ? evaluateXPathAll(normalized) : queryAllDeep(normalized);
2362 | 
2363 |       // Additional defense: filter out any overlay elements that might have slipped through
2364 |       const filteredMatches = filterOverlayElements(matches);
2365 | 
2366 |       if (!filteredMatches || filteredMatches.length === 0) {
2367 |         return { success: false, error: 'No elements found for selector' };
2368 |       }
2369 | 
2370 |       // Scroll first match into view
2371 |       const primaryMatch = filteredMatches[0];
2372 |       if (primaryMatch) {
2373 |         primaryMatch.scrollIntoView({
2374 |           block: 'center',
2375 |           inline: 'center',
2376 |           behavior: 'smooth',
2377 |         });
2378 |       }
2379 | 
2380 |       await sleep(150);
2381 | 
2382 |       // Draw highlight rectangles
2383 |       drawRects(filteredMatches, CONFIG.COLORS.VERIFY, false);
2384 | 
2385 |       // Auto-clear after 2 seconds
2386 |       setTimeout(() => {
2387 |         clearRects();
2388 |       }, 2000);
2389 | 
2390 |       return { success: true, count: filteredMatches.length };
2391 |     } catch (error) {
2392 |       return { success: false, error: error.message || String(error) };
2393 |     }
2394 |   }
2395 | 
2396 |   function copySelectorNow() {
2397 |     try {
2398 |       const sel = STATE.box?.querySelector('#__em_selector')?.textContent?.trim();
2399 |       if (!sel) return;
2400 |       navigator.clipboard?.writeText(sel).catch(() => {});
2401 | 
2402 |       StateStore.set({
2403 |         validation: { status: 'success', message: '✓ 已复制到剪贴板' },
2404 |       });
2405 | 
2406 |       setTimeout(() => {
2407 |         StateStore.set({ validation: { status: 'idle', message: '' } });
2408 |       }, 2000);
2409 |     } catch {}
2410 |   }
2411 | 
2412 |   async function save() {
2413 |     try {
2414 |       const name = STATE.box?.querySelector('#__em_name')?.value?.trim();
2415 |       const selector = STATE.box?.querySelector('#__em_selector')?.textContent?.trim();
2416 | 
2417 |       if (!selector) return;
2418 | 
2419 |       const url = location.href;
2420 |       let selectorType = StateStore.get('selectorType');
2421 |       const listMode = StateStore.get('listMode');
2422 | 
2423 |       if (listMode && selectorType === 'xpath') {
2424 |         selectorType = 'css';
2425 |       }
2426 | 
2427 |       await chrome.runtime.sendMessage({
2428 |         type: 'element_marker_save',
2429 |         marker: {
2430 |           url,
2431 |           name: name || selector,
2432 |           selector,
2433 |           selectorType,
2434 |           listMode,
2435 |         },
2436 |       });
2437 |     } catch {}
2438 | 
2439 |     stop();
2440 |   }
2441 | 
2442 |   // ============================================================================
2443 |   // Lifecycle Management
2444 |   // ============================================================================
2445 | 
2446 |   function start() {
2447 |     if (STATE.active) return;
2448 |     STATE.active = true;
2449 | 
2450 |     if (IS_MAIN) {
2451 |       const { host } = PanelHost.mount();
2452 |       STATE.box = host;
2453 |       StateStore.init();
2454 |       bindControls();
2455 |     }
2456 | 
2457 |     ensureHighlighter();
2458 |     ensureRectsHost();
2459 | 
2460 |     attachPointerListeners();
2461 |     attachKeyboardListener();
2462 |     syncInteractionMode();
2463 |   }
2464 | 
2465 |   function stop() {
2466 |     STATE.active = false;
2467 | 
2468 |     detachPointerListeners();
2469 |     detachKeyboardListener();
2470 | 
2471 |     // Cancel pending rAF
2472 |     if (STATE.hoverRafId != null) {
2473 |       cancelAnimationFrame(STATE.hoverRafId);
2474 |       STATE.hoverRafId = null;
2475 |     }
2476 |     pendingHoverEvent = null;
2477 | 
2478 |     try {
2479 |       STATE.highlighter?.remove();
2480 |       STATE.rectsHost?.remove();
2481 |       PanelHost.unmount();
2482 |       DragController.destroy();
2483 |     } catch {}
2484 | 
2485 |     STATE.highlighter = null;
2486 |     STATE.rectsHost = null;
2487 |     STATE.box = null;
2488 |     STATE.hoveredList = [];
2489 |     STATE.hoverEl = null;
2490 |     STATE.selectedEl = null;
2491 |     STATE.lastHoverTarget = null;
2492 |     STATE.verifyRectsActive = false;
2493 | 
2494 |     // Clear rect pool to release DOM references
2495 |     STATE.rectPool.length = 0;
2496 |     STATE.rectPoolUsed = 0;
2497 |   }
2498 | 
2499 |   // ============================================================================
2500 |   // Controls Binding
2501 |   // ============================================================================
2502 | 
2503 |   function bindControls() {
2504 |     const host = STATE.box;
2505 |     if (!host) return;
2506 | 
2507 |     // Close/Cancel
2508 |     host.querySelector('#__em_close')?.addEventListener('click', stop);
2509 |     host.querySelector('#__em_cancel')?.addEventListener('click', stop);
2510 | 
2511 |     // Save
2512 |     host.querySelector('#__em_save')?.addEventListener('click', save);
2513 | 
2514 |     // Verify (highlight only) & Execute (real action)
2515 |     host.querySelector('#__em_verify')?.addEventListener('click', verifyHighlightOnly);
2516 |     host.querySelector('#__em_execute')?.addEventListener('click', verifySelectorNow);
2517 | 
2518 |     // Copy
2519 |     host.querySelector('#__em_copy')?.addEventListener('click', copySelectorNow);
2520 |     host.querySelector('#__em_copy_selector')?.addEventListener('click', copySelectorNow);
2521 | 
2522 |     // Action change handler - show/hide action-specific options
2523 |     host.querySelector('#__em_action')?.addEventListener('change', (e) => {
2524 |       updateActionSpecificUI(e.target.value);
2525 |     });
2526 | 
2527 |     // Selector type
2528 |     host.querySelector('#__em_selector_type')?.addEventListener('change', (e) => {
2529 |       const newType = e.target.value;
2530 |       const listMode = StateStore.get('listMode');
2531 | 
2532 |       // If switching to XPath while in list mode, disable list mode
2533 |       if (newType === 'xpath' && listMode) {
2534 |         StateStore.set({ selectorType: newType, listMode: false });
2535 |       } else {
2536 |         StateStore.set({ selectorType: newType });
2537 |       }
2538 | 
2539 |       // Regenerate selector for the currently selected element
2540 |       if (STATE.selectedEl) {
2541 |         setSelection(STATE.selectedEl);
2542 |       }
2543 |       // Note: If no selectedEl (e.g., iframe selections or manual input),
2544 |       // preserve existing selector text instead of clearing it
2545 |     });
2546 | 
2547 |     // List mode toggle
2548 |     host.querySelector('#__em_toggle_list')?.addEventListener('click', (e) => {
2549 |       const listMode = StateStore.get('listMode');
2550 |       const newListMode = !listMode;
2551 | 
2552 |       // If enabling list mode, force CSS selector type
2553 |       if (newListMode) {
2554 |         StateStore.set({ listMode: true, selectorType: 'css' });
2555 |         const selectorTypeSelect = host.querySelector('#__em_selector_type');
2556 |         if (selectorTypeSelect) selectorTypeSelect.value = 'css';
2557 |       } else {
2558 |         StateStore.set({ listMode: false });
2559 |       }
2560 | 
2561 |       // Update button active state
2562 |       const btn = e.currentTarget;
2563 |       if (btn) {
2564 |         if (newListMode) {
2565 |           btn.classList.add('active');
2566 |         } else {
2567 |           btn.classList.remove('active');
2568 |         }
2569 |       }
2570 | 
2571 |       // Regenerate selector for the currently selected element
2572 |       if (STATE.selectedEl) {
2573 |         setSelection(STATE.selectedEl);
2574 |       }
2575 | 
2576 |       clearHighlighter();
2577 |     });
2578 | 
2579 |     // Tab toggle (switch between Attributes and Execute)
2580 |     host.querySelector('#__em_toggle_tab')?.addEventListener('click', () => {
2581 |       const currentTab = StateStore.get('activeTab');
2582 |       StateStore.set({ activeTab: currentTab === 'attributes' ? 'execute' : 'attributes' });
2583 |     });
2584 | 
2585 |     // Tab switching
2586 |     const tabs = host.querySelectorAll('.em-tab');
2587 |     tabs.forEach((tab) => {
2588 |       tab.addEventListener('click', () => {
2589 |         StateStore.set({ activeTab: tab.dataset.tab });
2590 |       });
2591 |     });
2592 | 
2593 |     // Navigation buttons
2594 |     host.querySelector('#__em_nav_up')?.addEventListener('click', () => {
2595 |       const base = STATE.selectedEl || STATE.hoverEl;
2596 |       if (base?.parentElement) setSelection(base.parentElement);
2597 |     });
2598 | 
2599 |     host.querySelector('#__em_nav_down')?.addEventListener('click', () => {
2600 |       const base = STATE.selectedEl || STATE.hoverEl;
2601 |       if (base?.firstElementChild) setSelection(base.firstElementChild);
2602 |     });
2603 | 
2604 |     // Preferences
2605 |     host.querySelector('#__em_pref_id')?.addEventListener('change', (e) => {
2606 |       const prefs = { ...StateStore.get('prefs'), preferId: !!e.target.checked };
2607 |       StateStore.set({ prefs });
2608 |     });
2609 |     host.querySelector('#__em_pref_attr')?.addEventListener('change', (e) => {
2610 |       const prefs = { ...StateStore.get('prefs'), preferStableAttr: !!e.target.checked };
2611 |       StateStore.set({ prefs });
2612 |     });
2613 |     host.querySelector('#__em_pref_class')?.addEventListener('change', (e) => {
2614 |       const prefs = { ...StateStore.get('prefs'), preferClass: !!e.target.checked };
2615 |       StateStore.set({ prefs });
2616 |     });
2617 | 
2618 |     // Drag - use entire header as drag handle
2619 |     const dragHandle = host.querySelector('#__em_drag_handle');
2620 |     if (dragHandle) {
2621 |       DragController.init(dragHandle);
2622 |     }
2623 | 
2624 |     syncUIWithState();
2625 |   }
2626 | 
2627 |   function updateActionSpecificUI(action) {
2628 |     const host = STATE.box;
2629 |     if (!host) return;
2630 | 
2631 |     // Hide all action-specific groups
2632 |     const textGroup = host.querySelector('#__em_action_text_group');
2633 |     const keysGroup = host.querySelector('#__em_action_keys_group');
2634 |     const scrollOptions = host.querySelector('#__em_scroll_options');
2635 |     const clickOptions = host.querySelector('#__em_click_options');
2636 | 
2637 |     if (textGroup) textGroup.style.display = 'none';
2638 |     if (keysGroup) keysGroup.style.display = 'none';
2639 |     if (scrollOptions) scrollOptions.style.display = 'none';
2640 |     if (clickOptions) clickOptions.style.display = 'none';
2641 | 
2642 |     // Show relevant options based on action
2643 |     if (action === 'type_text') {
2644 |       if (textGroup) textGroup.style.display = 'block';
2645 |     } else if (action === 'press_keys') {
2646 |       if (keysGroup) keysGroup.style.display = 'block';
2647 |     } else if (action === 'scroll') {
2648 |       if (scrollOptions) scrollOptions.style.display = 'block';
2649 |     } else if (['left_click', 'double_click', 'right_click'].includes(action)) {
2650 |       if (clickOptions) clickOptions.style.display = 'block';
2651 | 
2652 |       // For right_click, button selector is not relevant (always 'right')
2653 |       // Hide the button field for right_click
2654 |       const buttonField = host.querySelector('#__em_btn')?.closest('.em-field');
2655 |       if (buttonField) {
2656 |         buttonField.style.display = action === 'right_click' ? 'none' : 'block';
2657 |       }
2658 |     }
2659 |     // hover: no extra options needed
2660 |   }
2661 | 
2662 |   function syncUIWithState() {
2663 |     const host = STATE.box;
2664 |     if (!host) return;
2665 | 
2666 |     const state = StateStore.get();
2667 | 
2668 |     const typeSelect = host.querySelector('#__em_selector_type');
2669 |     if (typeSelect) typeSelect.value = state.selectorType;
2670 | 
2671 |     // Initialize list mode button state
2672 |     const listModeBtn = host.querySelector('#__em_toggle_list');
2673 |     if (listModeBtn) {
2674 |       if (state.listMode) {
2675 |         listModeBtn.classList.add('active');
2676 |       } else {
2677 |         listModeBtn.classList.remove('active');
2678 |       }
2679 |     }
2680 | 
2681 |     const prefId = host.querySelector('#__em_pref_id');
2682 |     const prefAttr = host.querySelector('#__em_pref_attr');
2683 |     const prefClass = host.querySelector('#__em_pref_class');
2684 |     if (prefId) prefId.checked = state.prefs.preferId;
2685 |     if (prefAttr) prefAttr.checked = state.prefs.preferStableAttr;
2686 |     if (prefClass) prefClass.checked = state.prefs.preferClass;
2687 | 
2688 |     // Initialize action-specific UI
2689 |     const actionSelect = host.querySelector('#__em_action');
2690 |     if (actionSelect) {
2691 |       updateActionSpecificUI(actionSelect.value);
2692 |     }
2693 |   }
2694 | 
2695 |   // ============================================================================
2696 |   // Cross-Frame Bridge
2697 |   // ============================================================================
2698 | 
2699 |   // Register window message listener in all frames (not just main)
2700 |   // to support cross-frame highlighting from popup validation
2701 |   window.addEventListener(
2702 |     'message',
2703 |     (ev) => {
2704 |       try {
2705 |         const data = ev?.data;
2706 |         if (!data) return;
2707 | 
2708 |         // Handle iframe highlight request (works even when overlay is inactive)
2709 |         if (data.type === 'em-highlight-request') {
2710 |           highlightSelectorExternal({
2711 |             selector: data.selector,
2712 |             selectorType: data.selectorType || 'css',
2713 |             listMode: !!data.listMode,
2714 |           })
2715 |             .then((result) => {
2716 |               window.parent.postMessage(
2717 |                 {
2718 |                   type: 'em-highlight-result',
2719 |                   reqId: data.reqId,
2720 |                   result,
2721 |                 },
2722 |                 '*',
2723 |               );
2724 |             })
2725 |             .catch((error) => {
2726 |               window.parent.postMessage(
2727 |                 {
2728 |                   type: 'em-highlight-result',
2729 |                   reqId: data.reqId,
2730 |                   result: { success: false, error: error?.message || String(error) },
2731 |                 },
2732 |                 '*',
2733 |               );
2734 |             });
2735 |           return;
2736 |         }
2737 | 
2738 |         // Following messages only relevant when overlay is active
2739 |         if (!STATE.active) return;
2740 | 
2741 |         // Only main frame handles these overlay-related messages
2742 |         if (!IS_MAIN) return;
2743 | 
2744 |         const iframes = Array.from(document.querySelectorAll('iframe'));
2745 |         const host = iframes.find((f) => {
2746 |           try {
2747 |             return f.contentWindow === ev.source;
2748 |           } catch {
2749 |             return false;
2750 |           }
2751 |         });
2752 | 
2753 |         if (!host) return;
2754 | 
2755 |         const base = host.getBoundingClientRect();
2756 | 
2757 |         if (data.type === 'em_hover' && Array.isArray(data.rects)) {
2758 |           // Use pooled rect boxes for better performance
2759 |           drawRectBoxes(data.rects, {
2760 |             offsetX: base.left,
2761 |             offsetY: base.top,
2762 |             color: CONFIG.COLORS.HOVER,
2763 |             dashed: true,
2764 |           });
2765 |         } else if (data.type === 'em_click' && data.innerSel) {
2766 |           const frameSel = generateSelector(host);
2767 |           const composite = frameSel ? `${frameSel} |> ${data.innerSel}` : data.innerSel;
2768 |           const selectorText = STATE.box?.querySelector('#__em_selector');
2769 |           const selectorDisplay = STATE.box?.querySelector('#__em_selector_text');
2770 |           if (selectorText) selectorText.textContent = composite;
2771 |           if (selectorDisplay) selectorDisplay.textContent = composite;
2772 |         }
2773 |       } catch {}
2774 |     },
2775 |     true,
2776 |   );
2777 | 
2778 |   // ============================================================================
2779 |   // Message Handlers
2780 |   // ============================================================================
2781 | 
2782 |   chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
2783 |     if (request?.action === 'element_marker_start') {
2784 |       start();
2785 |       sendResponse({ ok: true });
2786 |       return true;
2787 |     } else if (request?.action === 'element_marker_ping') {
2788 |       sendResponse({ status: 'pong' });
2789 |       return false;
2790 |     } else if (request?.action === 'element_marker_highlight') {
2791 |       highlightSelectorExternal({
2792 |         selector: request.selector,
2793 |         selectorType: request.selectorType,
2794 |         listMode: !!request.listMode,
2795 |       })
2796 |         .then((result) => sendResponse(result))
2797 |         .catch((error) => sendResponse({ success: false, error: error?.message || String(error) }));
2798 |       return true;
2799 |     }
2800 |     return false;
2801 |   });
2802 | })();
2803 | 
```
Page 56/60FirstPrevNextLast