#
tokens: 48418/50000 29/574 files (page 4/43)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 of 43. Use http://codebase.md/hangwin/mcp-chrome?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/entrypoints/web-editor-v2/ui/icons.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Shared SVG Icons for Web Editor UI
 *
 * All icons are created as inline SVG elements to:
 * - Avoid external asset dependencies
 * - Support theming via `currentColor`
 * - Enable direct DOM manipulation
 *
 * Design standards:
 * - ViewBox: 20x20 (default) or 24x24 (for specific icons)
 * - Stroke width: 2px
 * - Line caps/joins: round
 */

// =============================================================================
// Icon Factory Helpers
// =============================================================================

function createSvgElement(): SVGElement {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.setAttribute('viewBox', '0 0 20 20');
  svg.setAttribute('fill', 'none');
  svg.setAttribute('aria-hidden', 'true');
  return svg;
}

function createSvgElement24(): SVGElement {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.setAttribute('viewBox', '0 0 24 24');
  svg.setAttribute('fill', 'none');
  svg.setAttribute('aria-hidden', 'true');
  return svg;
}

function createStrokePath(d: string): SVGPathElement {
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  path.setAttribute('d', d);
  path.setAttribute('stroke', 'currentColor');
  path.setAttribute('stroke-width', '2');
  path.setAttribute('stroke-linecap', 'round');
  path.setAttribute('stroke-linejoin', 'round');
  return path;
}

// =============================================================================
// Icon Creators
// =============================================================================

/**
 * Minus icon (—) for minimize button
 */
export function createMinusIcon(): SVGElement {
  const svg = createSvgElement();
  svg.append(createStrokePath('M5 10h10'));
  return svg;
}

/**
 * Plus icon (+) for restore/expand button
 */
export function createPlusIcon(): SVGElement {
  const svg = createSvgElement();
  svg.append(createStrokePath('M10 5v10M5 10h10'));
  return svg;
}

/**
 * Close icon (×) for close button
 */
export function createCloseIcon(): SVGElement {
  const svg = createSvgElement();
  svg.append(createStrokePath('M6 6l8 8M14 6l-8 8'));
  return svg;
}

/**
 * Grip icon (6 dots) for drag handle
 */
export function createGripIcon(): SVGElement {
  const svg = createSvgElement();

  const DOT_POSITIONS: ReadonlyArray<readonly [number, number]> = [
    [7, 6],
    [13, 6],
    [7, 10],
    [13, 10],
    [7, 14],
    [13, 14],
  ];

  for (const [cx, cy] of DOT_POSITIONS) {
    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    circle.setAttribute('cx', String(cx));
    circle.setAttribute('cy', String(cy));
    circle.setAttribute('r', '1.4');
    circle.setAttribute('fill', 'currentColor');
    svg.append(circle);
  }

  return svg;
}

/**
 * Chevron icon (▼) for collapse/expand indicator
 */
export function createChevronIcon(): SVGElement {
  const svg = createSvgElement();
  svg.classList.add('we-chevron');
  svg.append(createStrokePath('M7 8l3 3 3-3'));
  return svg;
}

/**
 * Undo icon (↶) for undo button
 * Uses 24x24 viewBox matching toolbar-ui.html design spec
 */
export function createUndoIcon(): SVGElement {
  const svg = createSvgElement24();
  svg.append(createStrokePath('M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6'));
  return svg;
}

/**
 * Redo icon (↷) for redo button
 * Uses 24x24 viewBox matching toolbar-ui.html design spec
 */
export function createRedoIcon(): SVGElement {
  const svg = createSvgElement24();
  svg.append(createStrokePath('M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6'));
  return svg;
}

/**
 * Chevron Up icon (^) for minimize/restore button
 * Rotates 180deg when minimized to point down
 */
export function createChevronUpIcon(): SVGElement {
  const svg = createSvgElement();
  svg.append(createStrokePath('M6 12l4-4 4 4'));
  return svg;
}

/**
 * Chevron Down icon (small, 24x24 viewBox) for dropdown buttons
 * Matches toolbar-ui.html design spec
 */
export function createChevronDownSmallIcon(): SVGElement {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.setAttribute('viewBox', '0 0 24 24');
  svg.setAttribute('fill', 'none');
  svg.setAttribute('aria-hidden', 'true');

  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  path.setAttribute('d', 'M19 9l-7 7-7-7');
  path.setAttribute('stroke', 'currentColor');
  path.setAttribute('stroke-width', '2');
  path.setAttribute('stroke-linecap', 'round');
  path.setAttribute('stroke-linejoin', 'round');
  svg.append(path);

  return svg;
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/AgentTimelineItem.vue:
--------------------------------------------------------------------------------

```vue
<template>
  <div class="relative group/step">
    <!-- Timeline Node: Loading icon for running status, colored dot otherwise -->
    <template v-if="showLoadingIcon">
      <!-- Loading scribble icon for running/starting status -->
      <svg
        class="absolute loading-scribble flex-shrink-0"
        :style="{
          left: '-24px',
          top: nodeTopOffset,
          width: '14px',
          height: '14px',
        }"
        viewBox="0 0 100 100"
        fill="none"
      >
        <path
          d="M50 50 C50 48, 52 46, 54 46 C58 46, 60 50, 60 54 C60 60, 54 64, 48 64 C40 64, 36 56, 36 48 C36 38, 44 32, 54 32 C66 32, 74 42, 74 54 C74 68, 62 78, 48 78 C32 78, 22 64, 22 48 C22 30, 36 18, 54 18 C74 18, 88 34, 88 54 C88 76, 72 92, 50 92"
          stroke="var(--ac-accent, #D97757)"
          stroke-width="8"
          stroke-linecap="round"
        />
      </svg>
    </template>
    <template v-else>
      <!-- Colored dot -->
      <span
        class="absolute w-2 h-2 rounded-full transition-colors"
        :style="{
          left: '-20px',
          top: nodeTopOffset,
          backgroundColor: nodeColor,
          boxShadow: isStreaming ? 'var(--ac-timeline-node-pulse-shadow)' : 'none',
        }"
        :class="{ 'ac-pulse': isStreaming }"
      />
    </template>

    <!-- Content based on item kind -->
    <TimelineUserPromptStep v-if="item.kind === 'user_prompt'" :item="item" />
    <TimelineNarrativeStep v-else-if="item.kind === 'assistant_text'" :item="item" />
    <TimelineToolCallStep v-else-if="item.kind === 'tool_use'" :item="item" />
    <TimelineToolResultCardStep v-else-if="item.kind === 'tool_result'" :item="item" />
    <TimelineStatusStep
      v-else-if="item.kind === 'status'"
      :item="item"
      :hide-icon="showLoadingIcon"
    />
  </div>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import type { TimelineItem } from '../../composables/useAgentThreads';
import TimelineUserPromptStep from './timeline/TimelineUserPromptStep.vue';
import TimelineNarrativeStep from './timeline/TimelineNarrativeStep.vue';
import TimelineToolCallStep from './timeline/TimelineToolCallStep.vue';
import TimelineToolResultCardStep from './timeline/TimelineToolResultCardStep.vue';
import TimelineStatusStep from './timeline/TimelineStatusStep.vue';

const props = defineProps<{
  item: TimelineItem;
  /** Whether this is the last item in the timeline */
  isLast?: boolean;
}>();

const isStreaming = computed(() => {
  if (props.item.kind === 'assistant_text' || props.item.kind === 'tool_use') {
    return props.item.isStreaming;
  }
  if (props.item.kind === 'status') {
    return props.item.status === 'running' || props.item.status === 'starting';
  }
  return false;
});

// Show loading icon for status items that are running/starting
const showLoadingIcon = computed(() => {
  if (props.item.kind === 'status') {
    return props.item.status === 'running' || props.item.status === 'starting';
  }
  return false;
});

// Calculate top offset based on item type to align with first line of text
const nodeTopOffset = computed(() => {
  // user_prompt and assistant_text have py-1 (4px) + text-sm leading-relaxed
  if (props.item.kind === 'user_prompt' || props.item.kind === 'assistant_text') {
    return '12px';
  }
  // tool_use/tool_result have items-baseline with text-[11px]
  if (props.item.kind === 'tool_use' || props.item.kind === 'tool_result') {
    return '6px';
  }
  // status has flex items-center with text-xs (12px line-height ~18px)
  // For loading icon (14px), center it: (18-14)/2 = 2px
  if (props.item.kind === 'status') {
    return '2px';
  }
  return '7px';
});

const nodeColor = computed(() => {
  // Active/streaming node
  if (isStreaming.value) {
    return 'var(--ac-timeline-node-active)';
  }

  // Tool result nodes - success/error colors
  if (props.item.kind === 'tool_result') {
    if (props.item.isError) {
      return 'var(--ac-danger)';
    }
    return 'var(--ac-success)';
  }

  // Tool use nodes - use tool color
  if (props.item.kind === 'tool_use') {
    return 'var(--ac-timeline-node-tool)';
  }

  // Assistant text - use accent color
  if (props.item.kind === 'assistant_text') {
    return 'var(--ac-timeline-node-active)';
  }

  // User prompt - slightly stronger than default node for visual distinction
  if (props.item.kind === 'user_prompt') {
    return 'var(--ac-timeline-node-hover)';
  }

  // Status nodes (completed/error/cancelled) - use muted color
  if (props.item.kind === 'status') {
    return 'var(--ac-timeline-node)';
  }

  // Default node color
  return 'var(--ac-timeline-node)';
});
</script>

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/fill.ts:
--------------------------------------------------------------------------------

```typescript
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { handleCallTool } from '@/entrypoints/background/tools';
import type { StepFill } from '../types';
import { locateElement } from '../selector-engine';
import { expandTemplatesDeep } from '../rr-utils';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';

export const fillNode: NodeRuntime<StepFill> = {
  validate: (step) => {
    const ok = !!(step as any).target?.candidates?.length && 'value' in (step as any);
    return ok ? { ok } : { ok, errors: ['缺少目标选择器候选或输入值'] };
  },
  run: async (ctx: ExecCtx, step: StepFill) => {
    const s: any = step;
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const firstTab = tabs && tabs[0];
    const tabId = firstTab && typeof firstTab.id === 'number' ? firstTab.id : undefined;
    if (!tabId) throw new Error('Active tab not found');
    await handleCallTool({ name: TOOL_NAMES.BROWSER.READ_PAGE, args: {} });
    const located = await locateElement(tabId, s.target, ctx.frameId);
    const frameId = (located as any)?.frameId ?? ctx.frameId;
    const first = s.target?.candidates?.[0]?.type;
    const resolvedBy = (located as any)?.resolvedBy || ((located as any)?.ref ? 'ref' : '');
    const fallbackUsed = resolvedBy && first && resolvedBy !== 'ref' && resolvedBy !== first;
    const interpolate = (v: any) =>
      typeof v === 'string'
        ? v.replace(/\{([^}]+)\}/g, (_m, k) => (ctx.vars[k] ?? '').toString())
        : v;
    const value = interpolate(s.value);
    if ((located as any)?.ref) {
      const resolved: any = (await chrome.tabs.sendMessage(
        tabId,
        { action: 'resolveRef', ref: (located as any).ref } as any,
        { frameId } as any,
      )) as any;
      const rect = resolved?.rect;
      if (!rect || rect.width <= 0 || rect.height <= 0) throw new Error('element not visible');
    }
    const cssSelector = !(located as any)?.ref
      ? s.target.candidates?.find((c: any) => c.type === 'css' || c.type === 'attr')?.value
      : undefined;
    if (cssSelector) {
      try {
        const attr: any = (await chrome.tabs.sendMessage(
          tabId,
          { action: 'getAttributeForSelector', selector: cssSelector, name: 'type' } as any,
          { frameId } as any,
        )) as any;
        const typeName = (attr && attr.value ? String(attr.value) : '').toLowerCase();
        if (typeName === 'file') {
          const uploadRes = await handleCallTool({
            name: TOOL_NAMES.BROWSER.FILE_UPLOAD,
            args: { selector: cssSelector, filePath: String(value ?? '') },
          });
          if ((uploadRes as any).isError) throw new Error('file upload failed');
          if (fallbackUsed)
            ctx.logger({
              stepId: (step as any).id,
              status: 'success',
              message: `Selector fallback used (${String(first)} -> ${String(resolvedBy)})`,
              fallbackUsed: true,
              fallbackFrom: String(first),
              fallbackTo: String(resolvedBy),
            } as any);
          return {} as ExecResult;
        }
      } catch {}
    }
    try {
      if (cssSelector)
        await handleCallTool({
          name: TOOL_NAMES.BROWSER.INJECT_SCRIPT,
          args: {
            type: 'MAIN',
            jsScript: `try{var el=document.querySelector(${JSON.stringify(cssSelector)});if(el){el.scrollIntoView({behavior:'instant',block:'center',inline:'nearest'});} }catch(e){}`,
          },
        });
    } catch {}
    try {
      if ((located as any)?.ref)
        await chrome.tabs.sendMessage(
          tabId,
          { action: 'focusByRef', ref: (located as any).ref } as any,
          { frameId } as any,
        );
      else if (cssSelector)
        await handleCallTool({
          name: TOOL_NAMES.BROWSER.INJECT_SCRIPT,
          args: {
            type: 'MAIN',
            jsScript: `try{var el=document.querySelector(${JSON.stringify(cssSelector)});if(el&&el.focus){el.focus();}}catch(e){}`,
          },
        });
    } catch {}
    const res = await handleCallTool({
      name: TOOL_NAMES.BROWSER.FILL,
      args: {
        ref: (located as any)?.ref || (s as any).target?.ref,
        selector: cssSelector,
        value,
        frameId,
      },
    });
    if ((res as any).isError) throw new Error('fill failed');
    if (fallbackUsed)
      ctx.logger({
        stepId: (step as any).id,
        status: 'success',
        message: `Selector fallback used (${String(first)} -> ${String(resolvedBy)})`,
        fallbackUsed: true,
        fallbackFrom: String(first),
        fallbackTo: String(resolvedBy),
      } as any);
    return {} as ExecResult;
  },
};

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/click.ts:
--------------------------------------------------------------------------------

```typescript
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { handleCallTool } from '@/entrypoints/background/tools';
import type { Step } from '../types';
import { locateElement } from '../selector-engine';
import { expandTemplatesDeep } from '../rr-utils';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';

export const clickNode: NodeRuntime<any> = {
  validate: (step) => {
    const ok = !!(step as any).target?.candidates?.length;
    return ok ? { ok } : { ok, errors: ['缺少目标选择器候选'] };
  },
  run: async (ctx: ExecCtx, step: Step) => {
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const firstTab = tabs && tabs[0];
    const tabId = firstTab && typeof firstTab.id === 'number' ? firstTab.id : undefined;
    if (!tabId) throw new Error('Active tab not found');
    await handleCallTool({ name: TOOL_NAMES.BROWSER.READ_PAGE, args: {} });
    const s: any = expandTemplatesDeep(step as any, ctx.vars);
    const located = await locateElement(tabId, s.target, ctx.frameId);
    const frameId = (located as any)?.frameId ?? ctx.frameId;
    const first = s.target?.candidates?.[0]?.type;
    const resolvedBy = (located as any)?.resolvedBy || ((located as any)?.ref ? 'ref' : '');
    const fallbackUsed = resolvedBy && first && resolvedBy !== 'ref' && resolvedBy !== first;
    if ((located as any)?.ref) {
      const resolved: any = (await chrome.tabs.sendMessage(
        tabId,
        { action: 'resolveRef', ref: (located as any).ref } as any,
        { frameId } as any,
      )) as any;
      const rect = resolved?.rect;
      if (!rect || rect.width <= 0 || rect.height <= 0) throw new Error('element not visible');
    }
    const res = await handleCallTool({
      name: TOOL_NAMES.BROWSER.CLICK,
      args: {
        ref: (located as any)?.ref || (step as any).target?.ref,
        selector: !(located as any)?.ref
          ? s.target?.candidates?.find((c: any) => c.type === 'css' || c.type === 'attr')?.value
          : undefined,
        waitForNavigation: false,
        timeout: Math.max(1000, Math.min(s.timeoutMs || 10000, 30000)),
        frameId,
      },
    });
    if ((res as any).isError) throw new Error('click failed');
    if (fallbackUsed)
      ctx.logger({
        stepId: step.id,
        status: 'success',
        message: `Selector fallback used (${String(first)} -> ${String(resolvedBy)})`,
        fallbackUsed: true,
        fallbackFrom: String(first),
        fallbackTo: String(resolvedBy),
      } as any);
    return {} as ExecResult;
  },
};

export const dblclickNode: NodeRuntime<any> = {
  validate: clickNode.validate,
  run: async (ctx: ExecCtx, step: Step) => {
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const firstTab = tabs && tabs[0];
    const tabId = firstTab && typeof firstTab.id === 'number' ? firstTab.id : undefined;
    if (!tabId) throw new Error('Active tab not found');
    await handleCallTool({ name: TOOL_NAMES.BROWSER.READ_PAGE, args: {} });
    const s: any = expandTemplatesDeep(step as any, ctx.vars);
    const located = await locateElement(tabId, s.target, ctx.frameId);
    const frameId = (located as any)?.frameId ?? ctx.frameId;
    const first = s.target?.candidates?.[0]?.type;
    const resolvedBy = (located as any)?.resolvedBy || ((located as any)?.ref ? 'ref' : '');
    const fallbackUsed = resolvedBy && first && resolvedBy !== 'ref' && resolvedBy !== first;
    if ((located as any)?.ref) {
      const resolved: any = (await chrome.tabs.sendMessage(
        tabId,
        { action: 'resolveRef', ref: (located as any).ref } as any,
        { frameId } as any,
      )) as any;
      const rect = resolved?.rect;
      if (!rect || rect.width <= 0 || rect.height <= 0) throw new Error('element not visible');
    }
    const res = await handleCallTool({
      name: TOOL_NAMES.BROWSER.CLICK,
      args: {
        ref: (located as any)?.ref || (step as any).target?.ref,
        selector: !(located as any)?.ref
          ? s.target?.candidates?.find((c: any) => c.type === 'css' || c.type === 'attr')?.value
          : undefined,
        waitForNavigation: false,
        timeout: Math.max(1000, Math.min(s.timeoutMs || 10000, 30000)),
        frameId,
        double: true,
      },
    });
    if ((res as any).isError) throw new Error('dblclick failed');
    if (fallbackUsed)
      ctx.logger({
        stepId: step.id,
        status: 'success',
        message: `Selector fallback used (${String(first)} -> ${String(resolvedBy)})`,
        fallbackUsed: true,
        fallbackFrom: String(first),
        fallbackTo: String(resolvedBy),
      } as any);
    return {} as ExecResult;
  },
};

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2/ui/property-panel/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Property Panel Types
 *
 * Type definitions for the property panel component.
 * The panel displays Design controls and DOM tree for the selected element.
 */

import type { TransactionManager } from '../../core/transaction-manager';
import type { PropsBridge } from '../../core/props-bridge';
import type { DesignTokensService } from '../../core/design-tokens';
import type { FloatingPosition } from '../floating-drag';

// =============================================================================
// Tab Types
// =============================================================================

/** Property panel tab identifiers */
export type PropertyPanelTab = 'design' | 'css' | 'props' | 'dom';

// =============================================================================
// Options Types
// =============================================================================

/** Options for creating the property panel */
export interface PropertyPanelOptions {
  /** Shadow UI container element (elements.uiRoot from shadow-host) */
  container: HTMLElement;

  /** Transaction manager for applying style changes with undo/redo support */
  transactionManager: TransactionManager;

  /** Bridge to the MAIN-world props agent (Phase 7) */
  propsBridge: PropsBridge;

  /**
   * Callback when user selects an element from the Components tree (DOM tab).
   * Used to update the editor's selection state.
   */
  onSelectElement: (element: Element) => void;

  /**
   * Optional callback to close the editor.
   * If provided, a close button will be shown in the header.
   */
  onRequestClose?: () => void;

  /**
   * Initial floating position (viewport coordinates).
   * When provided, the panel uses left/top positioning and becomes draggable.
   */
  initialPosition?: FloatingPosition | null;

  /**
   * Called whenever the floating position changes.
   * Use null to indicate the panel is in its default anchored position.
   */
  onPositionChange?: (position: FloatingPosition | null) => void;

  /** Initial tab to display (default: 'design') */
  defaultTab?: PropertyPanelTab;

  /** Optional: Design tokens service for TokenPill/TokenPicker integration (Phase 5.3) */
  tokensService?: DesignTokensService;
}

// =============================================================================
// Panel Interface
// =============================================================================

/** Property panel public interface */
export interface PropertyPanel {
  /**
   * Update the panel to display properties for the given element.
   * Pass null to show empty state.
   */
  setTarget(element: Element | null): void;

  /** Switch to a specific tab */
  setTab(tab: PropertyPanelTab): void;

  /** Get the currently active tab */
  getTab(): PropertyPanelTab;

  /** Force refresh the current controls (e.g., after external style change) */
  refresh(): void;

  /** Get current floating position (viewport coordinates), null when anchored */
  getPosition(): FloatingPosition | null;

  /** Set floating position (viewport coordinates), pass null to reset to anchored */
  setPosition(position: FloatingPosition | null): void;

  /** Cleanup and remove the panel */
  dispose(): void;
}

// =============================================================================
// Control Types
// =============================================================================

/** Common interface for design controls (Size, Spacing, Position, etc.) */
export interface DesignControl {
  /** Update the control to display values for the given element */
  setTarget(element: Element | null): void;

  /** Refresh control values from current element styles */
  refresh(): void;

  /** Cleanup the control */
  dispose(): void;
}

/** Factory function type for creating design controls */
export type DesignControlFactory = (options: {
  container: HTMLElement;
  transactionManager: TransactionManager;
}) => DesignControl;

// =============================================================================
// Group Types
// =============================================================================

/** State for a collapsible control group */
export interface ControlGroupState {
  /** Whether the group is collapsed */
  collapsed: boolean;
}

/** Collapsible control group interface */
export interface ControlGroup {
  /** The root element of the group */
  root: HTMLElement;

  /** The body container where controls are mounted */
  body: HTMLElement;

  /** Optional: Container for header action buttons (e.g., add button) */
  headerActions?: HTMLElement;

  /** Set collapsed state */
  setCollapsed(collapsed: boolean): void;

  /** Get current collapsed state */
  isCollapsed(): boolean;

  /** Toggle collapsed state */
  toggle(): void;
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2/ui/property-panel/css-defaults.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * CSS Defaults Provider
 *
 * Computes baseline (browser-default) computed style values for element tag names.
 * Used by the CSS panel to hide active declarations that match defaults.
 *
 * Isolation strategy:
 * - Mount a hidden host element with `all: initial` into the document.
 * - Attach an isolated ShadowRoot and insert probe elements (one per tag name).
 * - Page author styles do not cross the shadow boundary, so probe values reflect UA defaults.
 */

export interface CssDefaultsProvider {
  /** Precompute/cache baseline values for a tag + set of properties. */
  ensureBaselineValues(tagName: string, properties: readonly string[]): void;
  /** Get baseline computed value for a tag + property (cached). */
  getBaselineValue(tagName: string, property: string): string;
  /** Cleanup DOM and caches. */
  dispose(): void;
}

interface ProbeRoot {
  host: HTMLDivElement;
  shadow: ShadowRoot;
  container: HTMLDivElement;
}

function normalizeTagName(tagName: string): string {
  return String(tagName ?? '')
    .trim()
    .toLowerCase();
}

function normalizePropertyName(property: string): string {
  return String(property ?? '').trim();
}

export function createCssDefaultsProvider(): CssDefaultsProvider {
  let disposed = false;
  let probeRoot: ProbeRoot | null = null;

  const probeByTag = new Map<string, Element>();
  const cacheByTag = new Map<string, Map<string, string>>();

  function ensureProbeRoot(): ProbeRoot | null {
    if (disposed) return null;
    if (typeof document === 'undefined') return null;

    if (probeRoot?.host?.isConnected) return probeRoot;

    const mountPoint = document.documentElement ?? document.body;
    if (!mountPoint) return null;

    const host = document.createElement('div');
    host.setAttribute('aria-hidden', 'true');
    // Use fixed size to avoid layout-dependent property issues
    // all: initial resets inherited styles, fixed positioning takes out of flow
    host.style.cssText =
      'all: initial;' +
      'display: block;' +
      'position: fixed;' +
      'left: -100000px;' +
      'top: 0;' +
      'width: 100px;' +
      'height: 100px;' +
      'overflow: hidden;' +
      'pointer-events: none;' +
      'contain: layout style paint;' +
      'z-index: -1;' +
      'visibility: hidden;';

    const shadow = host.attachShadow({ mode: 'open' });

    const container = document.createElement('div');
    container.style.cssText = 'all: initial; display: block;';
    shadow.append(container);

    mountPoint.append(host);
    probeRoot = { host, shadow, container };
    return probeRoot;
  }

  function ensureProbeElement(tagName: string): Element | null {
    const tag = normalizeTagName(tagName);
    if (!tag) return null;

    const existing = probeByTag.get(tag);
    if (existing?.isConnected) return existing;

    const root = ensureProbeRoot();
    if (!root) return null;

    let probe: Element;
    try {
      probe = document.createElement(tag);
    } catch {
      probe = document.createElement('div');
    }

    root.container.append(probe);
    probeByTag.set(tag, probe);
    return probe;
  }

  function ensureBaselineValues(tagName: string, properties: readonly string[]): void {
    const tag = normalizeTagName(tagName);
    if (!tag) return;

    const list = (properties ?? []).map((p) => normalizePropertyName(p)).filter(Boolean);
    if (list.length === 0) return;

    const perTag = cacheByTag.get(tag) ?? new Map<string, string>();
    if (!cacheByTag.has(tag)) cacheByTag.set(tag, perTag);

    const missing: string[] = [];
    for (const prop of list) {
      if (!perTag.has(prop)) missing.push(prop);
    }
    if (missing.length === 0) return;

    const probe = ensureProbeElement(tag);
    if (!probe) return;

    let computed: CSSStyleDeclaration | null = null;
    try {
      computed = window.getComputedStyle(probe);
    } catch {
      computed = null;
    }

    if (!computed) {
      for (const prop of missing) perTag.set(prop, '');
      return;
    }

    for (const prop of missing) {
      let value = '';
      try {
        value = String(computed.getPropertyValue(prop) ?? '').trim();
      } catch {
        value = '';
      }
      perTag.set(prop, value);
    }
  }

  function getBaselineValue(tagName: string, property: string): string {
    const tag = normalizeTagName(tagName);
    const prop = normalizePropertyName(property);
    if (!tag || !prop) return '';

    ensureBaselineValues(tag, [prop]);
    return cacheByTag.get(tag)?.get(prop) ?? '';
  }

  function dispose(): void {
    disposed = true;

    try {
      probeRoot?.host?.remove();
    } catch {
      // Best-effort
    }

    probeRoot = null;
    probeByTag.clear();
    cacheByTag.clear();
  }

  return {
    ensureBaselineValues,
    getBaselineValue,
    dispose,
  };
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/tests/record-replay/adapter-policy.contract.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Adapter Policy Contract Tests
 *
 * Verifies that skipRetry and skipNavWait flags correctly modify
 * action execution behavior:
 * - skipRetry: removes action.policy.retry before execution
 * - skipNavWait: sets ctx.execution.skipNavWait for handlers
 */

import { describe, expect, it, vi, beforeEach } from 'vitest';
import { createStepExecutor } from '@/entrypoints/background/record-replay/actions/adapter';
import { createMockExecCtx, createMockStep } from './_test-helpers';

describe('adapter policy flags contract', () => {
  let registryExecute: ReturnType<typeof vi.fn>;
  let mockRegistry: any;

  beforeEach(() => {
    registryExecute = vi.fn(async () => ({ status: 'success' }));
    mockRegistry = {
      get: vi.fn(() => ({ type: 'fill' })), // Returns truthy = handler exists
      execute: registryExecute,
    };
  });

  describe('skipRetry flag', () => {
    it('removes action.policy.retry when skipRetry is true', async () => {
      const executor = createStepExecutor(mockRegistry);

      await executor(
        createMockExecCtx(),
        createMockStep('fill', {
          retry: { count: 3, intervalMs: 100, backoff: 'exp' },
          target: { candidates: [{ type: 'css', value: '#input' }] },
          value: 'test',
        }),
        1, // tabId
        { skipRetry: true },
      );

      expect(registryExecute).toHaveBeenCalledTimes(1);
      const [, action] = registryExecute.mock.calls[0];
      expect(action.policy?.retry).toBeUndefined();
    });

    it('preserves action.policy.retry when skipRetry is false', async () => {
      const executor = createStepExecutor(mockRegistry);

      await executor(
        createMockExecCtx(),
        createMockStep('fill', {
          retry: { count: 3, intervalMs: 100, backoff: 'exp' },
          target: { candidates: [{ type: 'css', value: '#input' }] },
          value: 'test',
        }),
        1,
        { skipRetry: false },
      );

      expect(registryExecute).toHaveBeenCalledTimes(1);
      const [, action] = registryExecute.mock.calls[0];
      expect(action.policy?.retry).toBeDefined();
      expect(action.policy.retry.retries).toBe(3);
    });

    it('preserves action.policy.retry when skipRetry is not specified', async () => {
      const executor = createStepExecutor(mockRegistry);

      await executor(
        createMockExecCtx(),
        createMockStep('fill', {
          retry: { count: 2, intervalMs: 50 },
          target: { candidates: [{ type: 'css', value: '#input' }] },
          value: 'test',
        }),
        1,
        {}, // No skipRetry
      );

      const [, action] = registryExecute.mock.calls[0];
      expect(action.policy?.retry).toBeDefined();
    });
  });

  describe('skipNavWait flag', () => {
    it('sets ctx.execution.skipNavWait when skipNavWait is true', async () => {
      const executor = createStepExecutor(mockRegistry);

      await executor(
        createMockExecCtx(),
        createMockStep('click', {
          target: { candidates: [{ type: 'css', value: '#btn' }] },
        }),
        1,
        { skipNavWait: true },
      );

      expect(registryExecute).toHaveBeenCalledTimes(1);
      const [actionCtx] = registryExecute.mock.calls[0];
      expect(actionCtx.execution?.skipNavWait).toBe(true);
    });

    it('does not set ctx.execution when skipNavWait is false', async () => {
      const executor = createStepExecutor(mockRegistry);

      await executor(
        createMockExecCtx(),
        createMockStep('click', {
          target: { candidates: [{ type: 'css', value: '#btn' }] },
        }),
        1,
        { skipNavWait: false },
      );

      const [actionCtx] = registryExecute.mock.calls[0];
      expect(actionCtx.execution).toBeUndefined();
    });

    it('does not set ctx.execution when skipNavWait is not specified', async () => {
      const executor = createStepExecutor(mockRegistry);

      await executor(
        createMockExecCtx(),
        createMockStep('navigate', {
          url: 'https://example.com',
        }),
        1,
        {}, // No skipNavWait
      );

      const [actionCtx] = registryExecute.mock.calls[0];
      expect(actionCtx.execution).toBeUndefined();
    });
  });

  describe('combined flags', () => {
    it('applies both skipRetry and skipNavWait together', async () => {
      const executor = createStepExecutor(mockRegistry);

      await executor(
        createMockExecCtx(),
        createMockStep('click', {
          retry: { count: 5, intervalMs: 200 },
          target: { candidates: [{ type: 'css', value: '#btn' }] },
        }),
        1,
        { skipRetry: true, skipNavWait: true },
      );

      const [actionCtx, action] = registryExecute.mock.calls[0];
      expect(action.policy?.retry).toBeUndefined();
      expect(actionCtx.execution?.skipNavWait).toBe(true);
    });
  });
});

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2/constants.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Web Editor V2 Constants
 *
 * Centralized configuration values for the visual editor.
 * All magic strings/numbers should be defined here.
 */

/** Editor version number */
export const WEB_EDITOR_V2_VERSION = 2 as const;

/** Log prefix for console messages */
export const WEB_EDITOR_V2_LOG_PREFIX = '[WebEditorV2]' as const;

// =============================================================================
// DOM Element IDs
// =============================================================================

/** Shadow host element ID */
export const WEB_EDITOR_V2_HOST_ID = '__mcp_web_editor_v2_host__';

/** Overlay container ID (for Canvas and visual feedback) */
export const WEB_EDITOR_V2_OVERLAY_ID = '__mcp_web_editor_v2_overlay__';

/** UI container ID (for panels and controls) */
export const WEB_EDITOR_V2_UI_ID = '__mcp_web_editor_v2_ui__';

// =============================================================================
// Styling
// =============================================================================

/** Maximum z-index to ensure editor is always on top */
export const WEB_EDITOR_V2_Z_INDEX = 2147483647;

/** Default panel width */
export const WEB_EDITOR_V2_PANEL_WIDTH = 320;

// =============================================================================
// Colors (Design System)
// =============================================================================

export const WEB_EDITOR_V2_COLORS = {
  /** Hover highlight color */
  hover: '#3b82f6', // blue-500
  /** Selected element color */
  selected: '#22c55e', // green-500
  /** Selection box border */
  selectionBorder: '#6366f1', // indigo-500
  /** Drag ghost color */
  dragGhost: 'rgba(99, 102, 241, 0.3)',
  /** Insertion line color */
  insertionLine: '#f59e0b', // amber-500
  /** Alignment guide line color (snap guides) */
  guideLine: '#ec4899', // pink-500
  /** Distance label background (Phase 4.3) */
  distanceLabelBg: 'rgba(15, 23, 42, 0.92)', // slate-900 @ 92%
  /** Distance label border (Phase 4.3) */
  distanceLabelBorder: 'rgba(51, 65, 85, 0.5)', // slate-600 @ 50%
  /** Distance label text (Phase 4.3) */
  distanceLabelText: 'rgba(255, 255, 255, 0.98)',
} as const;

// =============================================================================
// Drag Reorder (Phase 2.4-2.6)
// =============================================================================

/** Minimum pointer movement (px) to start dragging */
export const WEB_EDITOR_V2_DRAG_THRESHOLD_PX = 5;

/** Hysteresis (px) for stable before/after decision to avoid flip-flop */
export const WEB_EDITOR_V2_DRAG_HYSTERESIS_PX = 6;

/** Max elements to inspect per hit-test (elementsFromPoint) */
export const WEB_EDITOR_V2_DRAG_MAX_HIT_ELEMENTS = 8;

/** Insertion indicator line width in CSS pixels */
export const WEB_EDITOR_V2_INSERTION_LINE_WIDTH = 3;

// =============================================================================
// Snapping & Alignment Guides (Phase 4.2)
// =============================================================================

/** Snap threshold in CSS pixels - distance at which snapping activates */
export const WEB_EDITOR_V2_SNAP_THRESHOLD_PX = 6;

/** Hysteresis in CSS pixels - keeps snap stable near boundary to prevent flicker */
export const WEB_EDITOR_V2_SNAP_HYSTERESIS_PX = 2;

/** Maximum sibling elements to consider for snapping (nearest first) */
export const WEB_EDITOR_V2_SNAP_MAX_ANCHOR_ELEMENTS = 30;

/** Maximum siblings to scan before applying distance filter */
export const WEB_EDITOR_V2_SNAP_MAX_SIBLINGS_SCAN = 300;

/** Alignment guide line width in CSS pixels */
export const WEB_EDITOR_V2_GUIDE_LINE_WIDTH = 1;

// =============================================================================
// Distance Labels (Phase 4.3)
// =============================================================================

/** Minimum distance (px) to display a label - hides 0 and sub-pixel gaps */
export const WEB_EDITOR_V2_DISTANCE_LABEL_MIN_PX = 1;

/** Measurement line width in CSS pixels */
export const WEB_EDITOR_V2_DISTANCE_LINE_WIDTH = 1;

/** Tick size at the ends of measurement lines (CSS pixels) */
export const WEB_EDITOR_V2_DISTANCE_TICK_SIZE = 4;

/** Font used for distance label pills */
export const WEB_EDITOR_V2_DISTANCE_LABEL_FONT =
  '600 11px system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';

/** Horizontal padding inside distance label pill (CSS pixels) */
export const WEB_EDITOR_V2_DISTANCE_LABEL_PADDING_X = 6;

/** Vertical padding inside distance label pill (CSS pixels) */
export const WEB_EDITOR_V2_DISTANCE_LABEL_PADDING_Y = 3;

/** Border radius for distance label pill (CSS pixels) */
export const WEB_EDITOR_V2_DISTANCE_LABEL_RADIUS = 4;

/** Offset from the measurement line to place the pill (CSS pixels) */
export const WEB_EDITOR_V2_DISTANCE_LABEL_OFFSET = 8;

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/composables/useFloatingDrag.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Vue composable for floating drag functionality.
 * Wraps the installFloatingDrag utility for use in Vue components.
 */

import { ref, onMounted, onUnmounted, type Ref } from 'vue';
import {
  installFloatingDrag,
  type FloatingPosition,
} from '@/entrypoints/web-editor-v2/ui/floating-drag';

const STORAGE_KEY = 'sidepanel_navigator_position';

export interface UseFloatingDragOptions {
  /** Storage key for position persistence */
  storageKey?: string;
  /** Margin from viewport edges in pixels */
  clampMargin?: number;
  /** Threshold for distinguishing click vs drag (ms) */
  clickThresholdMs?: number;
  /** Movement threshold for drag activation (px) */
  moveThresholdPx?: number;
  /** Default position calculator (called when no saved position exists) */
  getDefaultPosition?: () => FloatingPosition;
}

export interface UseFloatingDragReturn {
  /** Current position (reactive) */
  position: Ref<FloatingPosition>;
  /** Whether dragging is in progress */
  isDragging: Ref<boolean>;
  /** Reset position to default */
  resetToDefault: () => void;
  /** Computed style object for binding */
  positionStyle: Ref<{ left: string; top: string }>;
}

/**
 * Calculate default position (bottom-right corner with margin)
 */
function getDefaultBottomRightPosition(
  buttonSize: number = 40,
  margin: number = 12,
): FloatingPosition {
  return {
    left: window.innerWidth - buttonSize - margin,
    top: window.innerHeight - buttonSize - margin,
  };
}

/**
 * Load position from chrome.storage.local
 */
async function loadPosition(storageKey: string): Promise<FloatingPosition | null> {
  try {
    const result = await chrome.storage.local.get(storageKey);
    const saved = result[storageKey];
    if (
      saved &&
      typeof saved.left === 'number' &&
      typeof saved.top === 'number' &&
      Number.isFinite(saved.left) &&
      Number.isFinite(saved.top)
    ) {
      return saved as FloatingPosition;
    }
  } catch (e) {
    console.warn('Failed to load navigator position:', e);
  }
  return null;
}

/**
 * Save position to chrome.storage.local
 */
async function savePosition(storageKey: string, position: FloatingPosition): Promise<void> {
  try {
    await chrome.storage.local.set({ [storageKey]: position });
  } catch (e) {
    console.warn('Failed to save navigator position:', e);
  }
}

/**
 * Vue composable for making an element draggable with position persistence.
 */
export function useFloatingDrag(
  handleRef: Ref<HTMLElement | null>,
  targetRef: Ref<HTMLElement | null>,
  options: UseFloatingDragOptions = {},
): UseFloatingDragReturn {
  const {
    storageKey = STORAGE_KEY,
    clampMargin = 12,
    clickThresholdMs = 150,
    moveThresholdPx = 5,
    getDefaultPosition = () => getDefaultBottomRightPosition(40, clampMargin),
  } = options;

  const position = ref<FloatingPosition>(getDefaultPosition());
  const isDragging = ref(false);
  const positionStyle = ref({ left: `${position.value.left}px`, top: `${position.value.top}px` });

  let cleanup: (() => void) | null = null;

  function updatePositionStyle(): void {
    positionStyle.value = {
      left: `${position.value.left}px`,
      top: `${position.value.top}px`,
    };
  }

  function resetToDefault(): void {
    position.value = getDefaultPosition();
    updatePositionStyle();
    savePosition(storageKey, position.value);
  }

  async function initPosition(): Promise<void> {
    const saved = await loadPosition(storageKey);
    if (saved) {
      // Validate position is within current viewport
      const maxLeft = window.innerWidth - 40 - clampMargin;
      const maxTop = window.innerHeight - 40 - clampMargin;
      position.value = {
        left: Math.min(Math.max(clampMargin, saved.left), maxLeft),
        top: Math.min(Math.max(clampMargin, saved.top), maxTop),
      };
    } else {
      position.value = getDefaultPosition();
    }
    updatePositionStyle();
  }

  onMounted(async () => {
    await initPosition();

    // Wait for refs to be available
    await new Promise((resolve) => setTimeout(resolve, 0));

    if (!handleRef.value || !targetRef.value) {
      console.warn('useFloatingDrag: handleRef or targetRef is null');
      return;
    }

    cleanup = installFloatingDrag({
      handleEl: handleRef.value,
      targetEl: targetRef.value,
      onPositionChange: (pos) => {
        position.value = pos;
        updatePositionStyle();
        savePosition(storageKey, pos);
      },
      clampMargin,
      clickThresholdMs,
      moveThresholdPx,
    });

    // Monitor dragging state via data attribute
    const observer = new MutationObserver(() => {
      isDragging.value = handleRef.value?.dataset.dragging === 'true';
    });
    if (handleRef.value) {
      observer.observe(handleRef.value, { attributes: true, attributeFilter: ['data-dragging'] });
    }
  });

  onUnmounted(() => {
    cleanup?.();
  });

  return {
    position,
    isDragging,
    resetToDefault,
    positionStyle,
  };
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/screenshot-helper.js:
--------------------------------------------------------------------------------

```javascript
/* eslint-disable */
/**
 * Screenshot helper content script
 * Handles page preparation, scrolling, element positioning, etc.
 */

if (window.__SCREENSHOT_HELPER_INITIALIZED__) {
  // Already initialized, skip
} else {
  window.__SCREENSHOT_HELPER_INITIALIZED__ = true;

  // Save original styles
  let originalOverflowStyle = '';
  let hiddenFixedElements = [];

  /**
   * Get fixed/sticky positioned elements
   * @returns Array of fixed/sticky elements
   */
  function getFixedElements() {
    const fixed = [];

    document.querySelectorAll('*').forEach((el) => {
      const htmlEl = el;
      const style = window.getComputedStyle(htmlEl);
      if (style.position === 'fixed' || style.position === 'sticky') {
        // Filter out tiny or invisible elements, and elements that are part of the extension UI
        if (
          htmlEl.offsetWidth > 1 &&
          htmlEl.offsetHeight > 1 &&
          !htmlEl.id.startsWith('chrome-mcp-')
        ) {
          fixed.push({
            element: htmlEl,
            originalDisplay: htmlEl.style.display,
            originalVisibility: htmlEl.style.visibility,
          });
        }
      }
    });
    return fixed;
  }

  /**
   * Hide fixed/sticky elements
   */
  function hideFixedElements() {
    hiddenFixedElements = getFixedElements();
    hiddenFixedElements.forEach((item) => {
      item.element.style.display = 'none';
    });
  }

  /**
   * Restore fixed/sticky elements
   */
  function showFixedElements() {
    hiddenFixedElements.forEach((item) => {
      item.element.style.display = item.originalDisplay || '';
    });
    hiddenFixedElements = [];
  }

  // Listen for messages from the extension
  chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
    // Respond to ping message
    if (request.action === 'chrome_screenshot_ping') {
      sendResponse({ status: 'pong' });
      return false; // Synchronous response
    }

    // Prepare page for capture
    else if (request.action === 'preparePageForCapture') {
      originalOverflowStyle = document.documentElement.style.overflow;
      document.documentElement.style.overflow = 'hidden'; // Hide main scrollbar
      if (request.options?.fullPage) {
        // Only hide fixed elements for full page to avoid flicker
        hideFixedElements();
      }
      // Give styles a moment to apply
      setTimeout(() => {
        sendResponse({ success: true });
      }, 50);
      return true; // Async response
    }

    // Get page details
    else if (request.action === 'getPageDetails') {
      const body = document.body;
      const html = document.documentElement;
      sendResponse({
        totalWidth: Math.max(
          body.scrollWidth,
          body.offsetWidth,
          html.clientWidth,
          html.scrollWidth,
          html.offsetWidth,
        ),
        totalHeight: Math.max(
          body.scrollHeight,
          body.offsetHeight,
          html.clientHeight,
          html.scrollHeight,
          html.offsetHeight,
        ),
        viewportWidth: window.innerWidth,
        viewportHeight: window.innerHeight,
        devicePixelRatio: window.devicePixelRatio || 1,
        currentScrollX: window.scrollX,
        currentScrollY: window.scrollY,
      });
    }

    // Get element details
    else if (request.action === 'getElementDetails') {
      const element = document.querySelector(request.selector);
      if (element) {
        element.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' });
        setTimeout(() => {
          // Wait for scroll
          const rect = element.getBoundingClientRect();
          sendResponse({
            rect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
            devicePixelRatio: window.devicePixelRatio || 1,
          });
        }, 200); // Increased delay for scrollIntoView
        return true; // Async response
      } else {
        sendResponse({ error: `Element with selector "${request.selector}" not found.` });
      }
      return true; // Async response
    }

    // Scroll page
    else if (request.action === 'scrollPage') {
      window.scrollTo({ left: request.x, top: request.y, behavior: 'instant' });
      // Wait for scroll and potential reflows/lazy-loading
      setTimeout(() => {
        sendResponse({
          success: true,
          newScrollX: window.scrollX,
          newScrollY: window.scrollY,
        });
      }, request.scrollDelay || 300); // Configurable delay
      return true; // Async response
    }

    // Reset page
    else if (request.action === 'resetPageAfterCapture') {
      document.documentElement.style.overflow = originalOverflowStyle;
      showFixedElements();
      if (typeof request.scrollX !== 'undefined' && typeof request.scrollY !== 'undefined') {
        window.scrollTo({ left: request.scrollX, top: request.scrollY, behavior: 'instant' });
      }
      sendResponse({ success: true });
    }

    return false; // Synchronous response
  });
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/engine/kernel/artifacts.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview 工件(Artifacts)接口
 * @description 定义截图等工件的获取和存储接口
 */

import type { NodeId, RunId } from '../../domain/ids';
import type { RRError } from '../../domain/errors';
import { RR_ERROR_CODES, createRRError } from '../../domain/errors';

/**
 * 截图结果
 */
export type ScreenshotResult = { ok: true; base64: string } | { ok: false; error: RRError };

/**
 * 工件服务接口
 * @description 提供工件获取和存储功能
 */
export interface ArtifactService {
  /**
   * 截取页面截图
   * @param tabId Tab ID
   * @param options 截图选项
   */
  screenshot(
    tabId: number,
    options?: {
      format?: 'png' | 'jpeg';
      quality?: number;
    },
  ): Promise<ScreenshotResult>;

  /**
   * 保存截图
   * @param runId Run ID
   * @param nodeId Node ID
   * @param base64 截图数据
   * @param filename 文件名(可选)
   */
  saveScreenshot(
    runId: RunId,
    nodeId: NodeId,
    base64: string,
    filename?: string,
  ): Promise<{ savedAs: string } | { error: RRError }>;
}

/**
 * 创建 NotImplemented 的 ArtifactService
 * @description Phase 0-1 占位实现
 */
export function createNotImplementedArtifactService(): ArtifactService {
  return {
    screenshot: async () => ({
      ok: false,
      error: createRRError(RR_ERROR_CODES.INTERNAL, 'ArtifactService.screenshot not implemented'),
    }),
    saveScreenshot: async () => ({
      error: createRRError(
        RR_ERROR_CODES.INTERNAL,
        'ArtifactService.saveScreenshot not implemented',
      ),
    }),
  };
}

/**
 * 创建基于 chrome.tabs.captureVisibleTab 的 ArtifactService
 * @description 使用 Chrome API 截取可见标签页
 */
export function createChromeArtifactService(): ArtifactService {
  // In-memory storage for screenshots (could be replaced with IndexedDB)
  const screenshotStore = new Map<string, string>();

  return {
    screenshot: async (tabId, options) => {
      try {
        // Get the window ID for the tab
        const tab = await chrome.tabs.get(tabId);
        if (!tab.windowId) {
          return {
            ok: false,
            error: createRRError(RR_ERROR_CODES.INTERNAL, `Tab ${tabId} has no window`),
          };
        }

        // Capture the visible tab
        const format = options?.format ?? 'png';
        const quality = options?.quality ?? 100;

        const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
          format,
          quality: format === 'jpeg' ? quality : undefined,
        });

        // Extract base64 from data URL
        const base64Match = dataUrl.match(/^data:image\/\w+;base64,(.+)$/);
        if (!base64Match) {
          return {
            ok: false,
            error: createRRError(RR_ERROR_CODES.INTERNAL, 'Invalid screenshot data URL'),
          };
        }

        return { ok: true, base64: base64Match[1] };
      } catch (e) {
        const message = e instanceof Error ? e.message : String(e);
        return {
          ok: false,
          error: createRRError(RR_ERROR_CODES.INTERNAL, `Screenshot failed: ${message}`),
        };
      }
    },

    saveScreenshot: async (runId, nodeId, base64, filename) => {
      try {
        // Generate filename if not provided
        const savedAs = filename ?? `${runId}_${nodeId}_${Date.now()}.png`;
        const key = `${runId}/${savedAs}`;

        // Store in memory (in production, this would go to IndexedDB or cloud storage)
        screenshotStore.set(key, base64);

        return { savedAs };
      } catch (e) {
        const message = e instanceof Error ? e.message : String(e);
        return {
          error: createRRError(RR_ERROR_CODES.INTERNAL, `Save screenshot failed: ${message}`),
        };
      }
    },
  };
}

/**
 * 工件策略执行器
 * @description 根据策略配置决定是否获取工件
 */
export interface ArtifactPolicyExecutor {
  /**
   * 执行截图策略
   * @param policy 截图策略
   * @param context 上下文
   */
  executeScreenshotPolicy(
    policy: 'never' | 'onFailure' | 'always',
    context: {
      tabId: number;
      runId: RunId;
      nodeId: NodeId;
      failed: boolean;
      saveAs?: string;
    },
  ): Promise<{ captured: boolean; savedAs?: string; error?: RRError }>;
}

/**
 * 创建默认的工件策略执行器
 */
export function createArtifactPolicyExecutor(service: ArtifactService): ArtifactPolicyExecutor {
  return {
    executeScreenshotPolicy: async (policy, context) => {
      // 根据策略决定是否截图
      const shouldCapture = policy === 'always' || (policy === 'onFailure' && context.failed);

      if (!shouldCapture) {
        return { captured: false };
      }

      // 截图
      const result = await service.screenshot(context.tabId);
      if (!result.ok) {
        return { captured: false, error: result.error };
      }

      // 保存(如果指定了文件名)
      if (context.saveAs) {
        const saveResult = await service.saveScreenshot(
          context.runId,
          context.nodeId,
          result.base64,
          context.saveAs,
        );
        if ('error' in saveResult) {
          return { captured: true, error: saveResult.error };
        }
        return { captured: true, savedAs: saveResult.savedAs };
      }

      return { captured: true };
    },
  };
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/shared/quick-panel/ui/quick-entries.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Quick Panel Quick Entries
 *
 * Four-grid shortcuts for quickly switching scopes:
 * - Tabs / Bookmarks / History / Commands
 *
 * Following PRD spec for Quick Panel entry UI.
 */

import { Disposer } from '@/entrypoints/web-editor-v2/utils/disposables';
import { QUICK_PANEL_SCOPES, normalizeQuickPanelScope, type QuickPanelScope } from '../core/types';

// ============================================================
// Types
// ============================================================

export interface QuickEntriesOptions {
  /** Container to mount quick entries */
  container: HTMLElement;
  /**
   * Scopes to render as quick entries.
   * Default: tabs/bookmarks/history/commands
   */
  scopes?: readonly QuickPanelScope[];
  /** Called when an entry is selected */
  onSelect: (scope: QuickPanelScope) => void;
}

export interface QuickEntriesManager {
  /** Root DOM element */
  root: HTMLDivElement;
  /** Set the active (highlighted) scope */
  setActiveScope: (scope: QuickPanelScope | null) => void;
  /** Enable/disable a specific entry */
  setDisabled: (scope: QuickPanelScope, disabled: boolean) => void;
  /** Show/hide the quick entries grid */
  setVisible: (visible: boolean) => void;
  /** Clean up resources */
  dispose: () => void;
}

// ============================================================
// Constants
// ============================================================

const DEFAULT_SCOPES: QuickPanelScope[] = ['tabs', 'bookmarks', 'history', 'commands'];

// ============================================================
// Main Factory
// ============================================================

/**
 * Create Quick Panel quick entries component.
 *
 * @example
 * ```typescript
 * const quickEntries = createQuickEntries({
 *   container: contentSearchMount,
 *   onSelect: (scope) => {
 *     searchInput.setScope(scope);
 *     controller.search(scope, '');
 *   },
 * });
 *
 * // Highlight active scope
 * quickEntries.setActiveScope('tabs');
 *
 * // Cleanup
 * quickEntries.dispose();
 * ```
 */
export function createQuickEntries(options: QuickEntriesOptions): QuickEntriesManager {
  const disposer = new Disposer();
  let disposed = false;

  const scopes = (options.scopes?.length ? [...options.scopes] : DEFAULT_SCOPES).map((s) =>
    normalizeQuickPanelScope(s),
  );

  // --------------------------------------------------------
  // DOM Construction
  // --------------------------------------------------------

  const root = document.createElement('div');
  root.className = 'qp-entries';
  options.container.append(root);
  disposer.add(() => root.remove());

  const buttonsByScope = new Map<QuickPanelScope, HTMLButtonElement>();

  function createEntry(scope: QuickPanelScope): HTMLButtonElement {
    const def = QUICK_PANEL_SCOPES[scope];

    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'qp-entry ac-btn ac-focus-ring';
    btn.dataset.scope = scope;
    btn.dataset.active = 'false';
    btn.setAttribute('aria-label', `Switch scope to ${def.label}`);

    const icon = document.createElement('div');
    icon.className = 'qp-entry__icon';
    icon.textContent = def.icon;

    const label = document.createElement('div');
    label.className = 'qp-entry__label';
    label.textContent = def.label;

    const prefix = document.createElement('div');
    prefix.className = 'qp-entry__prefix';
    prefix.textContent = def.prefix ? def.prefix.trim() : '';
    prefix.hidden = !def.prefix;

    btn.append(icon, label, prefix);

    disposer.listen(btn, 'click', () => {
      if (disposed) return;
      options.onSelect(scope);
    });

    return btn;
  }

  // Build entries
  for (const scope of scopes) {
    // Only render known scopes and avoid 'all' in quick entries
    if (!(scope in QUICK_PANEL_SCOPES) || scope === 'all') continue;

    const btn = createEntry(scope);
    buttonsByScope.set(scope, btn);
    root.append(btn);
  }

  // --------------------------------------------------------
  // State Management
  // --------------------------------------------------------

  function setActiveScope(scope: QuickPanelScope | null): void {
    if (disposed) return;

    const active = scope ? normalizeQuickPanelScope(scope) : null;
    for (const [id, btn] of buttonsByScope) {
      btn.dataset.active = active === id ? 'true' : 'false';
    }
  }

  function setDisabled(scope: QuickPanelScope, disabled: boolean): void {
    if (disposed) return;

    const id = normalizeQuickPanelScope(scope);
    const btn = buttonsByScope.get(id);
    if (!btn) return;

    btn.disabled = disabled;
  }

  function setVisible(visible: boolean): void {
    if (disposed) return;
    root.hidden = !visible;
  }

  // --------------------------------------------------------
  // Public API
  // --------------------------------------------------------

  return {
    root,
    setActiveScope,
    setDisabled,
    setVisible,
    dispose: () => {
      if (disposed) return;
      disposed = true;
      buttonsByScope.clear();
      disposer.dispose();
    },
  };
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/keyboard.ts:
--------------------------------------------------------------------------------

```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';

interface KeyboardToolParams {
  keys: string; // Required: string representing keys or key combinations to simulate (e.g., "Enter", "Ctrl+C")
  selector?: string; // Optional: CSS selector or XPath for target element to send keyboard events to
  selectorType?: 'css' | 'xpath'; // Type of selector (default: 'css')
  delay?: number; // Optional: delay between keystrokes in milliseconds
  tabId?: number; // target existing tab id
  windowId?: number; // when no tabId, pick active tab from this window
  frameId?: number; // target frame id for iframe support
}

/**
 * Tool for simulating keyboard input on web pages
 */
class KeyboardTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.KEYBOARD;

  /**
   * Execute keyboard operation
   */
  async execute(args: KeyboardToolParams): Promise<ToolResult> {
    const { keys, selector, selectorType = 'css', delay = TIMEOUTS.KEYBOARD_DELAY } = args;

    console.log(`Starting keyboard operation with options:`, args);

    if (!keys) {
      return createErrorResponse(
        ERROR_MESSAGES.INVALID_PARAMETERS + ': Keys parameter must be provided',
      );
    }

    try {
      const explicit = await this.tryGetTab(args.tabId);
      const tab = explicit || (await this.getActiveTabOrThrowInWindow(args.windowId));
      if (!tab.id) {
        return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID');
      }

      let finalSelector = selector;
      let refForFocus: string | undefined = undefined;

      // Ensure helper is loaded for XPath or potential focus operations
      await this.injectContentScript(tab.id, ['inject-scripts/accessibility-tree-helper.js']);

      // If selector is XPath, convert to ref then try to get CSS selector
      if (selector && selectorType === 'xpath') {
        try {
          // First convert XPath to ref
          const ensured = await this.sendMessageToTab(tab.id, {
            action: TOOL_MESSAGE_TYPES.ENSURE_REF_FOR_SELECTOR,
            selector,
            isXPath: true,
          });
          if (!ensured || !ensured.success || !ensured.ref) {
            return createErrorResponse(
              `Failed to resolve XPath selector: ${ensured?.error || 'unknown error'}`,
            );
          }
          refForFocus = ensured.ref;
          // Try to resolve ref to CSS selector
          const resolved = await this.sendMessageToTab(tab.id, {
            action: TOOL_MESSAGE_TYPES.RESOLVE_REF,
            ref: ensured.ref,
          });
          if (resolved && resolved.success && resolved.selector) {
            finalSelector = resolved.selector;
            refForFocus = undefined; // Prefer CSS selector if available
          }
          // If no CSS selector available, we'll use ref to focus below
        } catch (error) {
          return createErrorResponse(
            `Error resolving XPath: ${error instanceof Error ? error.message : String(error)}`,
          );
        }
      }

      // If we have a ref but no CSS selector, focus the element via helper
      if (refForFocus) {
        const focusResult = await this.sendMessageToTab(tab.id, {
          action: 'focusByRef',
          ref: refForFocus,
        });
        if (focusResult && !focusResult.success) {
          return createErrorResponse(
            `Failed to focus element by ref: ${focusResult.error || 'unknown error'}`,
          );
        }
        // Clear selector so keyboard events go to the focused element
        finalSelector = undefined;
      }

      const frameIds = typeof args.frameId === 'number' ? [args.frameId] : undefined;
      await this.injectContentScript(
        tab.id,
        ['inject-scripts/keyboard-helper.js'],
        false,
        'ISOLATED',
        false,
        frameIds,
      );

      // Send keyboard simulation message to content script
      const result = await this.sendMessageToTab(
        tab.id,
        {
          action: TOOL_MESSAGE_TYPES.SIMULATE_KEYBOARD,
          keys,
          selector: finalSelector,
          delay,
        },
        args.frameId,
      );

      if (result.error) {
        return createErrorResponse(result.error);
      }

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              success: true,
              message: result.message || 'Keyboard operation successful',
              targetElement: result.targetElement,
              results: result.results,
            }),
          },
        ],
        isError: false,
      };
    } catch (error) {
      console.error('Error in keyboard operation:', error);
      return createErrorResponse(
        `Error simulating keyboard events: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }
}

export const keyboardTool = new KeyboardTool();

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Record & Replay Core Types
 *
 * This file contains the core type definitions for the record-replay system.
 * Legacy Step types have been moved to ./legacy-types.ts and are re-exported
 * here for backward compatibility.
 *
 * Type system architecture:
 * - Legacy types (./legacy-types.ts): Step-based execution model (being phased out)
 * - Action types (./actions/types.ts): DAG-based execution model (new standard)
 * - Core types (this file): Flow, Node, Edge, Run records (shared by both)
 */

import { NODE_TYPES } from '@/common/node-types';

// =============================================================================
// Re-export Legacy Types for Backward Compatibility
// =============================================================================

export type {
  // Selector types
  SelectorType,
  SelectorCandidate,
  TargetLocator,
  // Step types
  StepType,
  StepBase,
  StepClick,
  StepFill,
  StepTriggerEvent,
  StepSetAttribute,
  StepScreenshot,
  StepSwitchFrame,
  StepLoopElements,
  StepKey,
  StepScroll,
  StepDrag,
  StepWait,
  StepAssert,
  StepScript,
  StepIf,
  StepForeach,
  StepWhile,
  StepHttp,
  StepExtract,
  StepOpenTab,
  StepSwitchTab,
  StepCloseTab,
  StepNavigate,
  StepHandleDownload,
  StepExecuteFlow,
  Step,
} from './legacy-types';

// Import Step type for use in Flow interface
import type { Step } from './legacy-types';

// =============================================================================
// Variable Definitions
// =============================================================================

export type VariableType = 'string' | 'number' | 'boolean' | 'enum' | 'array';

export interface VariableDef {
  key: string;
  label?: string;
  sensitive?: boolean;
  // default value can be string/number/boolean/array depending on type
  default?: any; // keep broad for backward compatibility
  type?: VariableType; // default to 'string' when omitted
  rules?: { required?: boolean; pattern?: string; enum?: string[] };
}

// =============================================================================
// DAG Node and Edge Types (Flow V2)
// =============================================================================

export type NodeType = (typeof NODE_TYPES)[keyof typeof NODE_TYPES];

export interface NodeBase {
  id: string;
  type: NodeType;
  name?: string;
  disabled?: boolean;
  config?: any;
  ui?: { x: number; y: number };
}

export interface Edge {
  id: string;
  from: string;
  to: string;
  // label identifies the logical branch. Keep 'default' for linear/main path.
  // For conditionals, use arbitrary strings like 'case:<id>' or 'else'.
  label?: string;
}

// =============================================================================
// Flow Definition
// =============================================================================

export interface Flow {
  id: string;
  name: string;
  description?: string;
  version: number;
  meta?: {
    createdAt: string;
    updatedAt: string;
    domain?: string;
    tags?: string[];
    bindings?: Array<{ type: 'domain' | 'path' | 'url'; value: string }>;
    tool?: { category?: string; description?: string };
    exposedOutputs?: Array<{ nodeId: string; as: string }>;
    /** Recording stop barrier status (used during recording stop) */
    stopBarrier?: {
      ok: boolean;
      sessionId?: string;
      stoppedAt?: string;
      failed?: Array<{
        tabId: number;
        skipped?: boolean;
        reason?: string;
        topTimedOut?: boolean;
        topError?: string;
        subframesFailed?: number;
      }>;
    };
  };
  variables?: VariableDef[];
  /**
   * @deprecated Use nodes/edges instead. This field is no longer written to storage.
   * Kept as optional for backward compatibility with existing flows and imports.
   */
  steps?: Step[];
  // Flow V2: DAG-based execution model
  nodes?: NodeBase[];
  edges?: Edge[];
  subflows?: Record<string, { nodes: NodeBase[]; edges: Edge[] }>;
}

// =============================================================================
// Run Records and Results
// =============================================================================

export interface RunLogEntry {
  stepId: string;
  status: 'success' | 'failed' | 'retrying' | 'warning';
  message?: string;
  tookMs?: number;
  screenshotBase64?: string; // small thumbnail (optional)
  consoleSnippets?: string[]; // critical lines
  networkSnippets?: Array<{ method: string; url: string; status?: number; ms?: number }>;
  // selector fallback info
  fallbackUsed?: boolean;
  fallbackFrom?: string;
  fallbackTo?: string;
}

export interface RunRecord {
  id: string;
  flowId: string;
  startedAt: string;
  finishedAt?: string;
  success?: boolean;
  entries: RunLogEntry[];
}

export interface RunResult {
  runId: string;
  success: boolean;
  summary: { total: number; success: number; failed: number; tookMs: number };
  url?: string | null;
  outputs?: Record<string, any> | null;
  logs?: RunLogEntry[];
  screenshots?: { onFailure?: string | null };
  paused?: boolean; // when true, the run was intentionally paused (e.g., breakpoint)
}

```

--------------------------------------------------------------------------------
/app/native-server/src/agent/db/schema.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Drizzle ORM Schema for Agent Storage.
 *
 * Design principles:
 * - Type-safe database access
 * - Consistent with shared types (AgentProject, AgentStoredMessage)
 * - Proper indexes for common query patterns
 * - Foreign key constraints with cascade delete
 */
import { sqliteTable, text, index } from 'drizzle-orm/sqlite-core';

// ============================================================
// Projects Table
// ============================================================

export const projects = sqliteTable(
  'projects',
  {
    id: text().primaryKey(),
    name: text().notNull(),
    description: text(),
    rootPath: text('root_path').notNull(),
    preferredCli: text('preferred_cli'),
    selectedModel: text('selected_model'),
    /**
     * Active Claude session ID (UUID format) for session resumption.
     * Captured from SDK's system/init message.
     */
    activeClaudeSessionId: text('active_claude_session_id'),
    /**
     * Whether to use Claude Code Router (CCR) for this project.
     * Stored as '1' (true) or '0'/null (false).
     */
    useCcr: text('use_ccr'),
    /**
     * Whether to enable the local Chrome MCP server integration for this project.
     * Stored as '1' (true) or '0' (false). Default: '1' (enabled).
     */
    enableChromeMcp: text('enable_chrome_mcp').notNull().default('1'),
    createdAt: text('created_at').notNull(),
    updatedAt: text('updated_at').notNull(),
    lastActiveAt: text('last_active_at'),
  },
  (table) => ({
    lastActiveIdx: index('projects_last_active_idx').on(table.lastActiveAt),
  }),
);

// ============================================================
// Sessions Table
// ============================================================

export const sessions = sqliteTable(
  'sessions',
  {
    id: text().primaryKey(),
    projectId: text('project_id')
      .notNull()
      .references(() => projects.id, { onDelete: 'cascade' }),
    /**
     * Engine name: claude, codex, cursor, qwen, glm, etc.
     */
    engineName: text('engine_name').notNull(),
    /**
     * Engine-specific session ID for resumption.
     * For Claude: SDK's session_id from system:init message.
     */
    engineSessionId: text('engine_session_id'),
    /**
     * User-defined session name for display.
     */
    name: text(),
    /**
     * Model override for this session.
     */
    model: text(),
    /**
     * Permission mode: default, acceptEdits, bypassPermissions, plan, dontAsk.
     */
    permissionMode: text('permission_mode').notNull().default('bypassPermissions'),
    /**
     * Whether to allow bypassing interactive permission prompts.
     * Stored as '1' (true) or null (false).
     */
    allowDangerouslySkipPermissions: text('allow_dangerously_skip_permissions'),
    /**
     * JSON: System prompt configuration.
     * Format: { type: 'custom', text: string } | { type: 'preset', preset: 'claude_code', append?: string }
     */
    systemPromptConfig: text('system_prompt_config'),
    /**
     * JSON: Engine/session option overrides (settingSources, tools, betas, etc.).
     */
    optionsConfig: text('options_config'),
    /**
     * JSON: Cached management info (supported models, commands, account, MCP servers, etc.).
     */
    managementInfo: text('management_info'),
    createdAt: text('created_at').notNull(),
    updatedAt: text('updated_at').notNull(),
  },
  (table) => ({
    projectIdIdx: index('sessions_project_id_idx').on(table.projectId),
    engineNameIdx: index('sessions_engine_name_idx').on(table.engineName),
  }),
);

// ============================================================
// Messages Table
// ============================================================

export const messages = sqliteTable(
  'messages',
  {
    id: text().primaryKey(),
    projectId: text('project_id')
      .notNull()
      .references(() => projects.id, { onDelete: 'cascade' }),
    sessionId: text('session_id').notNull(),
    conversationId: text('conversation_id'),
    role: text().notNull(), // 'user' | 'assistant' | 'tool' | 'system'
    content: text().notNull(),
    messageType: text('message_type').notNull(), // 'chat' | 'tool_use' | 'tool_result' | 'status'
    metadata: text(), // JSON string
    cliSource: text('cli_source'),
    requestId: text('request_id'),
    createdAt: text('created_at').notNull(),
  },
  (table) => ({
    projectIdIdx: index('messages_project_id_idx').on(table.projectId),
    sessionIdIdx: index('messages_session_id_idx').on(table.sessionId),
    createdAtIdx: index('messages_created_at_idx').on(table.createdAt),
    requestIdIdx: index('messages_request_id_idx').on(table.requestId),
  }),
);

// ============================================================
// Type Inference Helpers
// ============================================================

export type ProjectRow = typeof projects.$inferSelect;
export type ProjectInsert = typeof projects.$inferInsert;
export type SessionRow = typeof sessions.$inferSelect;
export type SessionInsert = typeof sessions.$inferInsert;
export type MessageRow = typeof messages.$inferSelect;
export type MessageInsert = typeof messages.$inferInsert;

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/network-capture.ts:
--------------------------------------------------------------------------------

```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { networkCaptureStartTool, networkCaptureStopTool } from './network-capture-web-request';
import { networkDebuggerStartTool, networkDebuggerStopTool } from './network-capture-debugger';

type NetworkCaptureBackend = 'webRequest' | 'debugger';

interface NetworkCaptureToolParams {
  action: 'start' | 'stop';
  needResponseBody?: boolean;
  url?: string;
  maxCaptureTime?: number;
  inactivityTimeout?: number;
  includeStatic?: boolean;
}

/**
 * Extract text content from ToolResult
 */
function getFirstText(result: ToolResult): string | undefined {
  const first = result.content?.[0];
  return first && first.type === 'text' ? first.text : undefined;
}

/**
 * Decorate JSON result with additional fields
 */
function decorateJsonResult(result: ToolResult, extra: Record<string, unknown>): ToolResult {
  const text = getFirstText(result);
  if (typeof text !== 'string') return result;

  try {
    const parsed = JSON.parse(text);
    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
      return {
        ...result,
        content: [{ type: 'text', text: JSON.stringify({ ...parsed, ...extra }) }],
      };
    }
  } catch {
    // If the underlying tool didn't return JSON, keep it as-is
  }
  return result;
}

/**
 * Check if debugger-based capture is active
 */
function isDebuggerCaptureActive(): boolean {
  const captureData = (
    networkDebuggerStartTool as unknown as { captureData?: Map<number, unknown> }
  ).captureData;
  return captureData instanceof Map && captureData.size > 0;
}

/**
 * Check if webRequest-based capture is active
 */
function isWebRequestCaptureActive(): boolean {
  return networkCaptureStartTool.captureData.size > 0;
}

/**
 * Unified Network Capture Tool
 *
 * Provides a single entry point for network capture, automatically selecting
 * the appropriate backend based on the `needResponseBody` parameter:
 * - needResponseBody=false (default): uses webRequest API (lightweight, no debugger conflict)
 * - needResponseBody=true: uses Debugger API (captures response body, may conflict with DevTools)
 */
class NetworkCaptureTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.NETWORK_CAPTURE;

  async execute(args: NetworkCaptureToolParams): Promise<ToolResult> {
    const action = args?.action;
    if (action !== 'start' && action !== 'stop') {
      return createErrorResponse('Parameter [action] is required and must be one of: start, stop');
    }

    const wantBody = args?.needResponseBody === true;
    const debuggerActive = isDebuggerCaptureActive();
    const webActive = isWebRequestCaptureActive();

    if (action === 'start') {
      return this.handleStart(args, wantBody, debuggerActive, webActive);
    }

    return this.handleStop(args, debuggerActive, webActive);
  }

  private async handleStart(
    args: NetworkCaptureToolParams,
    wantBody: boolean,
    debuggerActive: boolean,
    webActive: boolean,
  ): Promise<ToolResult> {
    // Prevent any capture conflict (cross-mode or same-mode)
    if (debuggerActive || webActive) {
      const activeMode = debuggerActive ? 'debugger' : 'webRequest';
      return createErrorResponse(
        `Network capture is already active in ${activeMode} mode. Stop it before starting a new capture.`,
      );
    }

    const delegate = wantBody ? networkDebuggerStartTool : networkCaptureStartTool;
    const backend: NetworkCaptureBackend = wantBody ? 'debugger' : 'webRequest';

    const result = await delegate.execute({
      url: args.url,
      maxCaptureTime: args.maxCaptureTime,
      inactivityTimeout: args.inactivityTimeout,
      includeStatic: args.includeStatic,
    });

    return decorateJsonResult(result, { backend, needResponseBody: wantBody });
  }

  private async handleStop(
    args: NetworkCaptureToolParams,
    debuggerActive: boolean,
    webActive: boolean,
  ): Promise<ToolResult> {
    // Determine which backend to stop
    let backendToStop: NetworkCaptureBackend | null = null;

    // If user explicitly specified needResponseBody, try to stop that specific backend
    if (args?.needResponseBody === true) {
      backendToStop = debuggerActive ? 'debugger' : null;
    } else if (args?.needResponseBody === false) {
      backendToStop = webActive ? 'webRequest' : null;
    }

    // If no explicit preference or the specified backend isn't active, auto-detect
    if (!backendToStop) {
      if (debuggerActive) {
        backendToStop = 'debugger';
      } else if (webActive) {
        backendToStop = 'webRequest';
      }
    }

    if (!backendToStop) {
      return createErrorResponse('No active network captures found in any tab.');
    }

    const delegateStop =
      backendToStop === 'debugger' ? networkDebuggerStopTool : networkCaptureStopTool;
    const result = await delegateStop.execute();

    return decorateJsonResult(result, {
      backend: backendToStop,
      needResponseBody: backendToStop === 'debugger',
    });
  }
}

export const networkCaptureTool = new NetworkCaptureTool();

```

--------------------------------------------------------------------------------
/app/native-server/src/mcp/register-tools.ts:
--------------------------------------------------------------------------------

```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
  CallToolRequestSchema,
  CallToolResult,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import nativeMessagingHostInstance from '../native-messaging-host';
import { NativeMessageType, TOOL_SCHEMAS } from 'chrome-mcp-shared';
import type { Tool } from '@modelcontextprotocol/sdk/types.js';

async function listDynamicFlowTools(): Promise<Tool[]> {
  try {
    const response = await nativeMessagingHostInstance.sendRequestToExtensionAndWait(
      {},
      'rr_list_published_flows',
      20000,
    );
    if (response && response.status === 'success' && Array.isArray(response.items)) {
      const tools: Tool[] = [];
      for (const item of response.items) {
        const name = `flow.${item.slug}`;
        const description =
          (item.meta && item.meta.tool && item.meta.tool.description) ||
          item.description ||
          'Recorded flow';
        const properties: Record<string, any> = {};
        const required: string[] = [];
        for (const v of item.variables || []) {
          const desc = v.label || v.key;
          const typ = (v.type || 'string').toLowerCase();
          const prop: any = { description: desc };
          if (typ === 'boolean') prop.type = 'boolean';
          else if (typ === 'number') prop.type = 'number';
          else if (typ === 'enum') {
            prop.type = 'string';
            if (v.rules && Array.isArray(v.rules.enum)) prop.enum = v.rules.enum;
          } else if (typ === 'array') {
            // default array of strings; can extend with itemType later
            prop.type = 'array';
            prop.items = { type: 'string' };
          } else {
            prop.type = 'string';
          }
          if (v.default !== undefined) prop.default = v.default;
          if (v.rules && v.rules.required) required.push(v.key);
          properties[v.key] = prop;
        }
        // Run options
        properties['tabTarget'] = { type: 'string', enum: ['current', 'new'], default: 'current' };
        properties['refresh'] = { type: 'boolean', default: false };
        properties['captureNetwork'] = { type: 'boolean', default: false };
        properties['returnLogs'] = { type: 'boolean', default: false };
        properties['timeoutMs'] = { type: 'number', minimum: 0 };
        const tool: Tool = {
          name,
          description,
          inputSchema: { type: 'object', properties, required },
        };
        tools.push(tool);
      }
      return tools;
    }
    return [];
  } catch (e) {
    return [];
  }
}

export const setupTools = (server: Server) => {
  // List tools handler
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    const dynamicTools = await listDynamicFlowTools();
    return { tools: [...TOOL_SCHEMAS, ...dynamicTools] };
  });

  // Call tool handler
  server.setRequestHandler(CallToolRequestSchema, async (request) =>
    handleToolCall(request.params.name, request.params.arguments || {}),
  );
};

const handleToolCall = async (name: string, args: any): Promise<CallToolResult> => {
  try {
    // If calling a dynamic flow tool (name starts with flow.), proxy to common flow-run tool
    if (name && name.startsWith('flow.')) {
      // We need to resolve flow by slug to ID
      try {
        const resp = await nativeMessagingHostInstance.sendRequestToExtensionAndWait(
          {},
          'rr_list_published_flows',
          20000,
        );
        const items = (resp && resp.items) || [];
        const slug = name.slice('flow.'.length);
        const match = items.find((it: any) => it.slug === slug);
        if (!match) throw new Error(`Flow not found for tool ${name}`);
        const flowArgs = { flowId: match.id, args };
        const proxyRes = await nativeMessagingHostInstance.sendRequestToExtensionAndWait(
          { name: 'record_replay_flow_run', args: flowArgs },
          NativeMessageType.CALL_TOOL,
          120000,
        );
        if (proxyRes.status === 'success') return proxyRes.data;
        return {
          content: [{ type: 'text', text: `Error calling dynamic flow tool: ${proxyRes.error}` }],
          isError: true,
        };
      } catch (err: any) {
        return {
          content: [
            {
              type: 'text',
              text: `Error resolving dynamic flow tool: ${err?.message || String(err)}`,
            },
          ],
          isError: true,
        };
      }
    }
    // 发送请求到Chrome扩展并等待响应
    const response = await nativeMessagingHostInstance.sendRequestToExtensionAndWait(
      {
        name,
        args,
      },
      NativeMessageType.CALL_TOOL,
      120000, // 延长到 120 秒,避免性能分析等长任务超时
    );
    if (response.status === 'success') {
      return response.data;
    } else {
      return {
        content: [
          {
            type: 'text',
            text: `Error calling tool: ${response.error}`,
          },
        ],
        isError: true,
      };
    }
  } catch (error: any) {
    return {
      content: [
        {
          type: 'text',
          text: `Error calling tool: ${error.message}`,
        },
      ],
      isError: true,
    };
  }
};

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/offscreen/gif-encoder.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * GIF Encoder Module for Offscreen Document
 *
 * Handles GIF encoding using the gifenc library in the offscreen document context.
 * This module provides frame-by-frame GIF encoding with palette quantization.
 */

import { GIFEncoder, quantize, applyPalette } from 'gifenc';
import { MessageTarget, OFFSCREEN_MESSAGE_TYPES } from '@/common/message-types';

// ============================================================================
// Types
// ============================================================================

interface GifEncoderState {
  encoder: ReturnType<typeof GIFEncoder> | null;
  width: number;
  height: number;
  frameCount: number;
  isInitialized: boolean;
}

interface GifAddFrameMessage {
  target: MessageTarget;
  type: typeof OFFSCREEN_MESSAGE_TYPES.GIF_ADD_FRAME;
  imageData: number[];
  width: number;
  height: number;
  delay: number;
  maxColors?: number;
}

interface GifFinishMessage {
  target: MessageTarget;
  type: typeof OFFSCREEN_MESSAGE_TYPES.GIF_FINISH;
}

interface GifResetMessage {
  target: MessageTarget;
  type: typeof OFFSCREEN_MESSAGE_TYPES.GIF_RESET;
}

type GifMessage = GifAddFrameMessage | GifFinishMessage | GifResetMessage;

interface GifMessageResponse {
  success: boolean;
  error?: string;
  frameCount?: number;
  gifData?: number[];
  byteLength?: number;
}

// ============================================================================
// State
// ============================================================================

const state: GifEncoderState = {
  encoder: null,
  width: 0,
  height: 0,
  frameCount: 0,
  isInitialized: false,
};

// ============================================================================
// Handlers
// ============================================================================

function initializeEncoder(width: number, height: number): void {
  state.encoder = GIFEncoder();
  state.width = width;
  state.height = height;
  state.frameCount = 0;
  state.isInitialized = true;
}

function addFrame(
  imageData: Uint8ClampedArray,
  width: number,
  height: number,
  delay: number,
  maxColors: number = 256,
): void {
  // Initialize encoder on first frame
  if (!state.isInitialized || state.width !== width || state.height !== height) {
    initializeEncoder(width, height);
  }

  if (!state.encoder) {
    throw new Error('GIF encoder not initialized');
  }

  // Quantize colors to create palette
  const palette = quantize(imageData, maxColors, { format: 'rgb444' });

  // Map pixels to palette indices
  const indexedPixels = applyPalette(imageData, palette, 'rgb444');

  // Write frame to encoder
  state.encoder.writeFrame(indexedPixels, width, height, {
    palette,
    delay,
    dispose: 2, // Restore to background color
  });

  state.frameCount++;
}

function finishEncoding(): Uint8Array {
  if (!state.encoder) {
    throw new Error('GIF encoder not initialized');
  }

  state.encoder.finish();
  const bytes = state.encoder.bytes();

  // Reset state after finishing
  resetEncoder();

  return bytes;
}

function resetEncoder(): void {
  if (state.encoder) {
    state.encoder.reset();
  }
  state.encoder = null;
  state.width = 0;
  state.height = 0;
  state.frameCount = 0;
  state.isInitialized = false;
}

// ============================================================================
// Message Handler
// ============================================================================

function isGifMessage(message: unknown): message is GifMessage {
  if (!message || typeof message !== 'object') return false;
  const msg = message as Record<string, unknown>;
  if (msg.target !== MessageTarget.Offscreen) return false;

  const gifTypes = [
    OFFSCREEN_MESSAGE_TYPES.GIF_ADD_FRAME,
    OFFSCREEN_MESSAGE_TYPES.GIF_FINISH,
    OFFSCREEN_MESSAGE_TYPES.GIF_RESET,
  ];

  return gifTypes.includes(msg.type as string);
}

export function handleGifMessage(
  message: unknown,
  sendResponse: (response: GifMessageResponse) => void,
): boolean {
  if (!isGifMessage(message)) {
    return false;
  }

  try {
    switch (message.type) {
      case OFFSCREEN_MESSAGE_TYPES.GIF_ADD_FRAME: {
        const { imageData, width, height, delay, maxColors } = message;
        const clampedData = new Uint8ClampedArray(imageData);
        addFrame(clampedData, width, height, delay, maxColors);
        sendResponse({
          success: true,
          frameCount: state.frameCount,
        });
        break;
      }

      case OFFSCREEN_MESSAGE_TYPES.GIF_FINISH: {
        const gifBytes = finishEncoding();
        sendResponse({
          success: true,
          gifData: Array.from(gifBytes),
          byteLength: gifBytes.byteLength,
        });
        break;
      }

      case OFFSCREEN_MESSAGE_TYPES.GIF_RESET: {
        resetEncoder();
        sendResponse({ success: true });
        break;
      }

      default:
        sendResponse({ success: false, error: `Unknown GIF message type` });
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    console.error('GIF encoder error:', errorMessage);
    sendResponse({ success: false, error: errorMessage });
  }

  return true;
}

console.log('GIF encoder module loaded');

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/ScheduleDialog.vue:
--------------------------------------------------------------------------------

```vue
<template>
  <div v-if="visible" class="rr-modal">
    <div class="rr-dialog">
      <div class="rr-header">
        <div class="title">定时执行</div>
        <button class="close" @click="$emit('close')">✕</button>
      </div>
      <div class="rr-body">
        <div class="row">
          <label>启用</label>
          <label class="chk"><input type="checkbox" v-model="enabled" />启用定时</label>
        </div>
        <div class="row">
          <label>类型</label>
          <select v-model="type">
            <option value="interval">每隔 N 分钟</option>
            <option value="daily">每天固定时间</option>
            <option value="once">只执行一次</option>
          </select>
        </div>
        <div class="row" v-if="type === 'interval'">
          <label>间隔(分钟)</label>
          <input type="number" v-model.number="intervalMinutes" />
        </div>
        <div class="row" v-if="type === 'daily'">
          <label>时间(HH:mm)</label>
          <input v-model="dailyTime" placeholder="例如 09:30" />
        </div>
        <div class="row" v-if="type === 'once'">
          <label>时间(ISO)</label>
          <input v-model="onceAt" placeholder="例如 2025-10-05T10:00:00" />
        </div>
        <div class="row">
          <label>参数(JSON)</label>
          <textarea v-model="argsJson" placeholder='{ "username": "xx" }'></textarea>
        </div>
        <div class="section">
          <div class="section-title">已有计划</div>
          <div class="sched-list">
            <div class="sched-row" v-for="s in schedules" :key="s.id">
              <div class="meta">
                <span class="badge" :class="{ on: s.enabled, off: !s.enabled }">{{ s.type }}</span>
                <span class="desc">{{ describe(s) }}</span>
              </div>
              <div class="actions">
                <button class="small danger" @click="$emit('remove', s.id)">删除</button>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="rr-footer">
        <button class="primary" @click="save">保存</button>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref, watch } from 'vue';

const props = defineProps<{ visible: boolean; flowId: string | null; schedules: any[] }>();
const emit = defineEmits(['close', 'save', 'remove']);

const enabled = ref(true);
const type = ref<'interval' | 'daily' | 'once'>('interval');
const intervalMinutes = ref(30);
const dailyTime = ref('09:00');
const onceAt = ref('');
const argsJson = ref('');

watch(
  () => props.visible,
  (v) => {
    if (v) {
      enabled.value = true;
      type.value = 'interval';
      intervalMinutes.value = 30;
      dailyTime.value = '09:00';
      onceAt.value = '';
      argsJson.value = '';
    }
  },
);

function save() {
  if (!props.flowId) return;
  const schedule = {
    id: `sch_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
    flowId: props.flowId,
    type: type.value,
    enabled: enabled.value,
    when:
      type.value === 'interval'
        ? String(intervalMinutes.value)
        : type.value === 'daily'
          ? dailyTime.value
          : onceAt.value,
    args: safeParse(argsJson.value),
  } as any;
  emit('save', schedule);
}

function safeParse(s: string) {
  if (!s || !s.trim()) return {};
  try {
    return JSON.parse(s);
  } catch {
    return {};
  }
}

function describe(s: any) {
  if (s.type === 'interval') return `每 ${s.when} 分钟`;
  if (s.type === 'daily') return `每天 ${s.when}`;
  if (s.type === 'once') return `一次 ${s.when}`;
  return '';
}
</script>

<style scoped>
.rr-modal {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
  z-index: 2147483646;
  display: flex;
  align-items: center;
  justify-content: center;
}
.rr-dialog {
  background: #fff;
  border-radius: 8px;
  max-width: 720px;
  width: 96vw;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
}
.rr-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid #e5e7eb;
}
.rr-header .title {
  font-weight: 600;
}
.rr-header .close {
  border: none;
  background: #f3f4f6;
  border-radius: 6px;
  padding: 4px 8px;
  cursor: pointer;
}
.rr-body {
  padding: 12px 16px;
  overflow: auto;
}
.row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin: 6px 0;
}
.row > label {
  width: 120px;
  color: #374151;
}
.row > input,
.row > textarea,
.row > select {
  flex: 1;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  padding: 6px 8px;
}
.row > textarea {
  min-height: 64px;
}
.chk {
  display: inline-flex;
  gap: 6px;
  align-items: center;
}
.sched-list .sched-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 8px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  margin: 4px 0;
}
.badge {
  padding: 2px 6px;
  border-radius: 6px;
  background: #e5e7eb;
}
.badge.on {
  background: #dcfce7;
}
.badge.off {
  background: #fee2e2;
}
.small {
  font-size: 12px;
  padding: 4px 8px;
  border: 1px solid #d1d5db;
  background: #fff;
  border-radius: 6px;
  cursor: pointer;
}
.danger {
  background: #fee2e2;
  border-color: #fecaca;
}
.primary {
  background: #111;
  color: #fff;
  border: none;
  border-radius: 6px;
  padding: 8px 16px;
  cursor: pointer;
}
.rr-footer {
  padding: 12px 16px;
  border-top: 1px solid #e5e7eb;
  display: flex;
  justify-content: flex-end;
}
</style>

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/ConfirmDialog.vue:
--------------------------------------------------------------------------------

```vue
<template>
  <div v-if="visible" class="confirmation-dialog" @click.self="$emit('cancel')">
    <div class="dialog-content">
      <div class="dialog-header">
        <span class="dialog-icon">{{ icon }}</span>
        <h3 class="dialog-title">{{ title }}</h3>
      </div>

      <div class="dialog-body">
        <p class="dialog-message">{{ message }}</p>

        <ul v-if="items && items.length > 0" class="dialog-list">
          <li v-for="item in items" :key="item">{{ item }}</li>
        </ul>

        <div v-if="warning" class="dialog-warning">
          <strong>{{ warning }}</strong>
        </div>
      </div>

      <div class="dialog-actions">
        <button class="dialog-button cancel-button" @click="$emit('cancel')">
          {{ cancelText }}
        </button>
        <button
          class="dialog-button confirm-button"
          :disabled="isConfirming"
          @click="$emit('confirm')"
        >
          {{ isConfirming ? confirmingText : confirmText }}
        </button>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { getMessage } from '@/utils/i18n';
interface Props {
  visible: boolean;
  title: string;
  message: string;
  items?: string[];
  warning?: string;
  icon?: string;
  confirmText?: string;
  cancelText?: string;
  confirmingText?: string;
  isConfirming?: boolean;
}

interface Emits {
  (e: 'confirm'): void;
  (e: 'cancel'): void;
}

withDefaults(defineProps<Props>(), {
  icon: '⚠️',
  confirmText: getMessage('confirmButton'),
  cancelText: getMessage('cancelButton'),
  confirmingText: getMessage('processingStatus'),
  isConfirming: false,
});

defineEmits<Emits>();
</script>

<style scoped>
.confirmation-dialog {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  backdrop-filter: blur(8px);
  animation: dialogFadeIn 0.3s ease-out;
}

@keyframes dialogFadeIn {
  from {
    opacity: 0;
    backdrop-filter: blur(0px);
  }
  to {
    opacity: 1;
    backdrop-filter: blur(8px);
  }
}

.dialog-content {
  background: white;
  border-radius: 12px;
  padding: 24px;
  max-width: 360px;
  width: 90%;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  animation: dialogSlideIn 0.3s ease-out;
  border: 1px solid rgba(255, 255, 255, 0.2);
}

@keyframes dialogSlideIn {
  from {
    opacity: 0;
    transform: translateY(-30px) scale(0.9);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.dialog-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 20px;
}

.dialog-icon {
  font-size: 24px;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}

.dialog-title {
  font-size: 18px;
  font-weight: 600;
  color: #2d3748;
  margin: 0;
}

.dialog-body {
  margin-bottom: 24px;
}

.dialog-message {
  font-size: 14px;
  color: #4a5568;
  margin: 0 0 16px 0;
  line-height: 1.6;
}

.dialog-list {
  margin: 16px 0;
  padding-left: 24px;
  background: linear-gradient(135deg, #f7fafc, #edf2f7);
  border-radius: 6px;
  padding: 12px 12px 12px 32px;
  border-left: 3px solid #667eea;
}

.dialog-list li {
  font-size: 13px;
  color: #718096;
  margin-bottom: 6px;
  line-height: 1.4;
}

.dialog-list li:last-child {
  margin-bottom: 0;
}

.dialog-warning {
  font-size: 13px;
  color: #e53e3e;
  margin: 16px 0 0 0;
  padding: 12px;
  background: linear-gradient(135deg, rgba(245, 101, 101, 0.1), rgba(229, 62, 62, 0.05));
  border-radius: 6px;
  border-left: 3px solid #e53e3e;
  border: 1px solid rgba(245, 101, 101, 0.2);
}

.dialog-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
}

.dialog-button {
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s ease;
  min-width: 80px;
}

.cancel-button {
  background: linear-gradient(135deg, #e2e8f0, #cbd5e0);
  color: #4a5568;
  border: 1px solid #cbd5e0;
}

.cancel-button:hover {
  background: linear-gradient(135deg, #cbd5e0, #a0aec0);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(160, 174, 192, 0.3);
}

.confirm-button {
  background: linear-gradient(135deg, #f56565, #e53e3e);
  color: white;
  border: 1px solid #e53e3e;
}

.confirm-button:hover:not(:disabled) {
  background: linear-gradient(135deg, #e53e3e, #c53030);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
}

.confirm-button:disabled {
  opacity: 0.7;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

/* 响应式设计 */
@media (max-width: 420px) {
  .dialog-content {
    padding: 20px;
    max-width: 320px;
  }

  .dialog-header {
    gap: 10px;
    margin-bottom: 16px;
  }

  .dialog-icon {
    font-size: 20px;
  }

  .dialog-title {
    font-size: 16px;
  }

  .dialog-message {
    font-size: 13px;
  }

  .dialog-list {
    padding: 10px 10px 10px 28px;
  }

  .dialog-list li {
    font-size: 12px;
  }

  .dialog-warning {
    font-size: 12px;
    padding: 10px;
  }

  .dialog-actions {
    gap: 8px;
    flex-direction: column-reverse;
  }

  .dialog-button {
    width: 100%;
    padding: 12px 16px;
  }
}

/* 焦点样式 */
.dialog-button:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3);
}

.cancel-button:focus {
  box-shadow: 0 0 0 3px rgba(160, 174, 192, 0.3);
}

.confirm-button:focus {
  box-shadow: 0 0 0 3px rgba(245, 101, 101, 0.3);
}
</style>

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/actions/handlers/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Action Handlers Registry
 *
 * Central registration point for all action handlers.
 * Provides factory function to create a fully-configured ActionRegistry
 * with all replay handlers registered.
 */

import { ActionRegistry, createActionRegistry } from '../registry';
import { assertHandler } from './assert';
import { clickHandler, dblclickHandler } from './click';
import { foreachHandler, ifHandler, switchFrameHandler, whileHandler } from './control-flow';
import { delayHandler } from './delay';
import { setAttributeHandler, triggerEventHandler } from './dom';
import { dragHandler } from './drag';
import { extractHandler } from './extract';
import { fillHandler } from './fill';
import { httpHandler } from './http';
import { keyHandler } from './key';
import { navigateHandler } from './navigate';
import { screenshotHandler } from './screenshot';
import { scriptHandler } from './script';
import { scrollHandler } from './scroll';
import { closeTabHandler, handleDownloadHandler, openTabHandler, switchTabHandler } from './tabs';
import { waitHandler } from './wait';

// Re-export individual handlers for direct access
export { assertHandler } from './assert';
export { clickHandler, dblclickHandler } from './click';
export { foreachHandler, ifHandler, switchFrameHandler, whileHandler } from './control-flow';
export { delayHandler } from './delay';
export { setAttributeHandler, triggerEventHandler } from './dom';
export { dragHandler } from './drag';
export { extractHandler } from './extract';
export { fillHandler } from './fill';
export { httpHandler } from './http';
export { keyHandler } from './key';
export { navigateHandler } from './navigate';
export { screenshotHandler } from './screenshot';
export { scriptHandler } from './script';
export { scrollHandler } from './scroll';
export { closeTabHandler, handleDownloadHandler, openTabHandler, switchTabHandler } from './tabs';
export { waitHandler } from './wait';

// Re-export common utilities
export * from './common';

/**
 * All available action handlers for replay
 *
 * Organized by category:
 * - Navigation: navigate
 * - Interaction: click, dblclick, fill, key, scroll, drag
 * - Timing: wait, delay
 * - Validation: assert
 * - Data: extract, script, http, screenshot
 * - DOM Tools: triggerEvent, setAttribute
 * - Tabs: openTab, switchTab, closeTab, handleDownload
 * - Control Flow: if, foreach, while, switchFrame
 *
 * TODO: Add remaining handlers:
 * - loopElements, executeFlow (advanced control flow)
 */
const ALL_HANDLERS = [
  // Navigation
  navigateHandler,
  // Interaction
  clickHandler,
  dblclickHandler,
  fillHandler,
  keyHandler,
  scrollHandler,
  dragHandler,
  // Timing
  waitHandler,
  delayHandler,
  // Validation
  assertHandler,
  // Data
  extractHandler,
  scriptHandler,
  httpHandler,
  screenshotHandler,
  // DOM Tools
  triggerEventHandler,
  setAttributeHandler,
  // Tabs
  openTabHandler,
  switchTabHandler,
  closeTabHandler,
  handleDownloadHandler,
  // Control Flow
  ifHandler,
  foreachHandler,
  whileHandler,
  switchFrameHandler,
] as const;

/**
 * Register all replay handlers to an ActionRegistry instance
 */
export function registerReplayHandlers(registry: ActionRegistry): void {
  // Register each handler individually to satisfy TypeScript's type checker
  registry.register(navigateHandler, { override: true });
  registry.register(clickHandler, { override: true });
  registry.register(dblclickHandler, { override: true });
  registry.register(fillHandler, { override: true });
  registry.register(keyHandler, { override: true });
  registry.register(scrollHandler, { override: true });
  registry.register(dragHandler, { override: true });
  registry.register(waitHandler, { override: true });
  registry.register(delayHandler, { override: true });
  registry.register(assertHandler, { override: true });
  registry.register(extractHandler, { override: true });
  registry.register(scriptHandler, { override: true });
  registry.register(httpHandler, { override: true });
  registry.register(screenshotHandler, { override: true });
  registry.register(triggerEventHandler, { override: true });
  registry.register(setAttributeHandler, { override: true });
  registry.register(openTabHandler, { override: true });
  registry.register(switchTabHandler, { override: true });
  registry.register(closeTabHandler, { override: true });
  registry.register(handleDownloadHandler, { override: true });
  registry.register(ifHandler, { override: true });
  registry.register(foreachHandler, { override: true });
  registry.register(whileHandler, { override: true });
  registry.register(switchFrameHandler, { override: true });
}

/**
 * Create a new ActionRegistry with all replay handlers registered
 *
 * This is the primary entry point for creating an action execution context.
 *
 * @example
 * ```ts
 * const registry = createReplayActionRegistry();
 *
 * const result = await registry.execute(ctx, {
 *   id: 'action-1',
 *   type: 'click',
 *   params: { target: { candidates: [...] } },
 * });
 * ```
 */
export function createReplayActionRegistry(): ActionRegistry {
  const registry = createActionRegistry();
  registerReplayHandlers(registry);
  return registry;
}

/**
 * Get list of supported action types
 */
export function getSupportedActionTypes(): ReadonlyArray<string> {
  return ALL_HANDLERS.map((h) => h.type);
}

/**
 * Check if an action type is supported
 */
export function isActionTypeSupported(type: string): boolean {
  return ALL_HANDLERS.some((h) => h.type === type);
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/EdgePropertyPanel.vue:
--------------------------------------------------------------------------------

```vue
<template>
  <aside class="property-panel">
    <div v-if="edge" class="panel-content">
      <div class="panel-header">
        <div>
          <div class="header-title">Edge</div>
          <div class="header-id">{{ edge.id }}</div>
        </div>
        <button class="btn-delete" type="button" title="删除边" @click.stop="onRemove">
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
            <path
              d="m4 4 8 8M12 4 4 12"
              stroke="currentColor"
              stroke-width="1.8"
              stroke-linecap="round"
            />
          </svg>
        </button>
      </div>

      <div class="form-section">
        <div class="form-group">
          <label class="form-label">Source</label>
          <div class="text">{{ srcName }}</div>
        </div>
        <div class="form-group">
          <label class="form-label">Target</label>
          <div class="text">{{ dstName }}</div>
        </div>
        <div class="form-group">
          <label class="form-label">Connection status</label>
          <div class="status" :class="{ ok: isValid, bad: !isValid }">
            {{ isValid ? 'Valid' : 'Invalid' }}
          </div>
        </div>
        <div class="form-group">
          <label class="form-label">Branch</label>
          <div class="text">{{ labelPretty }}</div>
        </div>
      </div>
      <div class="divider"></div>

      <div class="form-section">
        <div class="text-xs text-slate-500" style="padding: 0 20px">
          Inspect connection only. Editing of branch/handles will be supported in a later pass.
        </div>
      </div>
    </div>
    <div v-else class="panel-empty">
      <div class="empty-text">未选择边</div>
    </div>
  </aside>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import type { Edge as EdgeV2, NodeBase } from '@/entrypoints/background/record-replay/types';

const props = defineProps<{ edge: EdgeV2 | null; nodes: NodeBase[] }>();
const emit = defineEmits<{ (e: 'remove-edge', id: string): void }>();

const src = computed(() => props.nodes?.find?.((n) => n.id === (props.edge as any)?.from) || null);
const dst = computed(() => props.nodes?.find?.((n) => n.id === (props.edge as any)?.to) || null);
const srcName = computed(() =>
  src.value ? src.value.name || `${src.value.type} (${src.value.id})` : 'Unknown',
);
const dstName = computed(() =>
  dst.value ? dst.value.name || `${dst.value.type} (${dst.value.id})` : 'Unknown',
);
const isValid = computed(() => !!(src.value && dst.value && src.value.id !== dst.value.id));
const labelPretty = computed(() => {
  const raw = String((props.edge as any)?.label || 'default');
  if (raw === 'default') return 'default';
  if (raw === 'true') return 'true ✓';
  if (raw === 'false') return 'false ✗';
  if (raw === 'onError') return 'onError !';
  if (raw === 'else') return 'else';
  if (raw.startsWith('case:')) {
    const id = raw.slice('case:'.length);
    const ifNode = src.value && (src.value as any).type === 'if' ? (src.value as any) : null;
    const found = ifNode?.config?.branches?.find?.((b: any) => String(b.id) === id);
    if (found) return `case: ${found.name || found.expr || id}`;
    return `case: ${id}`;
  }
  return raw;
});

function onRemove() {
  if (!props.edge) return;
  emit('remove-edge', props.edge.id);
}
</script>

<style scoped>
.property-panel {
  background: var(--rr-card);
  border: 1px solid var(--rr-border);
  border-radius: 16px;
  margin: 16px;
  padding: 0;
  width: 380px;
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 72px);
  overflow-y: auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  flex-shrink: 0;
  scrollbar-width: none;
  scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
}
.panel-content {
  display: flex;
  flex-direction: column;
}
.panel-header {
  padding: 12px 12px 12px 20px;
  border-bottom: 1px solid var(--rr-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.header-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--rr-text);
  margin-bottom: 4px;
}
.header-id {
  font-size: 11px;
  color: var(--rr-text-weak);
  font-family: 'Monaco', monospace;
  opacity: 0.7;
}
.btn-delete {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--rr-border);
  background: var(--rr-card);
  color: var(--rr-danger);
  border-radius: 6px;
  cursor: pointer;
}
.btn-delete:hover {
  background: rgba(239, 68, 68, 0.08);
  border-color: rgba(239, 68, 68, 0.3);
}
.form-section {
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.form-group {
  display: grid;
  grid-template-columns: 110px 1fr;
  align-items: center;
  gap: 8px;
}
.form-label {
  color: var(--rr-text-secondary);
  font-size: 13px;
  font-weight: 500;
}
.text {
  font-size: 13px;
}
.status.ok {
  color: #059669;
  font-weight: 600;
}
.status.bad {
  color: #ef4444;
  font-weight: 600;
}
.divider {
  height: 1px;
  background: var(--rr-border);
}
.panel-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
}
.empty-text {
  color: var(--rr-text-secondary);
}

/* Hide scrollbars in WebKit while keeping scrollability */
.property-panel :deep(::-webkit-scrollbar) {
  width: 0;
  height: 0;
}
.property-panel :deep(::-webkit-scrollbar-thumb) {
  background-color: rgba(0, 0, 0, 0.25);
  border-radius: 6px;
}
.property-panel :deep(::-webkit-scrollbar-track) {
  background: transparent !important;
}
</style>

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/runners/subflow-runner.ts:
--------------------------------------------------------------------------------

```typescript
// subflow-runner.ts — execute a subflow (nodes/edges) using DAG traversal with branch support

import { STEP_TYPES } from 'chrome-mcp-shared';
import type { ExecCtx } from '../../nodes';
import { RunLogger } from '../logging/run-logger';
import { PluginManager } from '../plugins/manager';
import { mapDagNodeToStep } from '../../rr-utils';
import type { Edge, NodeBase, Step } from '../../types';
import { StepRunner } from './step-runner';
import { ENGINE_CONSTANTS } from '../constants';

export interface SubflowEnv {
  runId: string;
  flow: any;
  vars: Record<string, any>;
  logger: RunLogger;
  pluginManager: PluginManager;
  stepRunner: StepRunner;
}

export class SubflowRunner {
  constructor(private env: SubflowEnv) {}

  async runSubflowById(subflowId: string, ctx: ExecCtx, pausedRef: () => boolean): Promise<void> {
    const sub = (this.env.flow.subflows || {})[subflowId];
    if (!sub || !Array.isArray(sub.nodes) || sub.nodes.length === 0) return;

    try {
      await this.env.pluginManager.subflowStart({
        runId: this.env.runId,
        flow: this.env.flow,
        vars: this.env.vars,
        subflowId,
      });
    } catch (e: any) {
      this.env.logger.push({
        stepId: `subflow:${subflowId}`,
        status: 'warning',
        message: `plugin.subflowStart error: ${e?.message || String(e)}`,
      });
    }

    const sNodes: NodeBase[] = sub.nodes;
    const sEdges: Edge[] = sub.edges || [];

    // Build lookup maps
    const id2node = new Map(sNodes.map((n) => [n.id, n] as const));
    const outEdges = new Map<string, Edge[]>();
    for (const e of sEdges) {
      if (!outEdges.has(e.from)) outEdges.set(e.from, []);
      outEdges.get(e.from)!.push(e);
    }

    // Calculate in-degrees to find root nodes
    const indeg = new Map<string, number>(sNodes.map((n) => [n.id, 0] as const));
    for (const e of sEdges) {
      indeg.set(e.to, (indeg.get(e.to) || 0) + 1);
    }

    // Find start node: prefer non-trigger nodes with indeg=0
    const findFirstExecutableRoot = (): string | undefined => {
      const executableRoot = sNodes.find(
        (n) => (indeg.get(n.id) || 0) === 0 && n.type !== STEP_TYPES.TRIGGER,
      );
      if (executableRoot) return executableRoot.id;

      // If all roots are triggers, follow default edge to first executable
      const triggerRoot = sNodes.find((n) => (indeg.get(n.id) || 0) === 0);
      if (triggerRoot) {
        const defaultEdge = (outEdges.get(triggerRoot.id) || []).find(
          (e) => !e.label || e.label === ENGINE_CONSTANTS.EDGE_LABELS.DEFAULT,
        );
        if (defaultEdge) return defaultEdge.to;
      }

      return sNodes[0]?.id;
    };

    let currentId: string | undefined = findFirstExecutableRoot();
    let guard = 0;
    const maxIterations = ENGINE_CONSTANTS.MAX_ITERATIONS;

    const ok = (s: Step) => this.env.logger.overlayAppend(`✔ ${s.type} (${s.id})`);
    const fail = (s: Step, e: any) =>
      this.env.logger.overlayAppend(`✘ ${s.type} (${s.id}) -> ${e?.message || String(e)}`);

    while (currentId) {
      if (pausedRef()) break;
      if (guard++ >= maxIterations) {
        this.env.logger.push({
          stepId: `subflow:${subflowId}`,
          status: 'warning',
          message: `Subflow exceeded ${maxIterations} iterations - possible cycle`,
        });
        break;
      }

      const node = id2node.get(currentId);
      if (!node) break;

      // Skip trigger nodes
      if (node.type === STEP_TYPES.TRIGGER) {
        const defaultEdge = (outEdges.get(currentId) || []).find(
          (e) => !e.label || e.label === ENGINE_CONSTANTS.EDGE_LABELS.DEFAULT,
        );
        if (defaultEdge) {
          currentId = defaultEdge.to;
          continue;
        }
        break;
      }

      const step: Step = mapDagNodeToStep(node);
      const r = await this.env.stepRunner.run(ctx, step, ok, fail);

      if (r.status === 'paused' || pausedRef()) break;

      if (r.status === 'failed') {
        // Try to find on_error edge
        const errEdge = (outEdges.get(currentId) || []).find(
          (e) => e.label === ENGINE_CONSTANTS.EDGE_LABELS.ON_ERROR,
        );
        if (errEdge) {
          currentId = errEdge.to;
          continue;
        }
        break;
      }

      // Determine next edge by label
      const suggestedLabel = r.nextLabel
        ? String(r.nextLabel)
        : ENGINE_CONSTANTS.EDGE_LABELS.DEFAULT;
      const oes = outEdges.get(currentId) || [];
      const nextEdge =
        oes.find((e) => (e.label || ENGINE_CONSTANTS.EDGE_LABELS.DEFAULT) === suggestedLabel) ||
        oes.find((e) => !e.label || e.label === ENGINE_CONSTANTS.EDGE_LABELS.DEFAULT);

      if (!nextEdge) {
        // Log warning if we expected a labeled edge but couldn't find it
        if (r.nextLabel && oes.length > 0) {
          const availableLabels = oes.map((e) => e.label || ENGINE_CONSTANTS.EDGE_LABELS.DEFAULT);
          this.env.logger.push({
            stepId: step.id,
            status: 'warning',
            message: `No edge for label '${suggestedLabel}'. Available: [${availableLabels.join(', ')}]`,
          });
        }
        break;
      }
      currentId = nextEdge.to;
    }

    try {
      await this.env.pluginManager.subflowEnd({
        runId: this.env.runId,
        flow: this.env.flow,
        vars: this.env.vars,
        subflowId,
      });
    } catch (e: any) {
      this.env.logger.push({
        stepId: `subflow:${subflowId}`,
        status: 'warning',
        message: `plugin.subflowEnd error: ${e?.message || String(e)}`,
      });
    }
  }
}

```

--------------------------------------------------------------------------------
/docs/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.0.5]

### Improved

- **Image Compression**: Compress base64 images when using screenshot tool
- **Interactive Elements Detection Optimization**: Enhanced interactive elements detection tool with expanded search scope, now supports finding interactive div elements

## [v0.0.4]

### Added

- **STDIO Connection Support**: Added support for connecting to the MCP server via standard input/output (stdio) method
- **Console Output Capture Tool**: New `chrome_console` tool for capturing browser console output

## [v0.0.3]

### Added

- **Inject script tool**: For injecting content scripts into web page
- **Send command to inject script tool**: For sending commands to the injected script

## [v0.0.2]

### Added

- **Conditional Semantic Engine Initialization**: Smart cache-based initialization that only loads models when cached versions are available
- **Enhanced Model Cache Management**: Comprehensive cache management system with automatic cleanup and size limits
- **Windows Platform Compatibility**: Full support for Windows Chrome Native Messaging with registry-based manifest detection
- **Cache Statistics and Manual Management**: User interface for viewing cache stats and manual cache cleanup
- **Concurrent Initialization Protection**: Prevents duplicate initialization attempts across components

### Improved

- **Startup Performance**: Dramatically reduced startup time when no model cache exists (from ~3s to ~0.5s)
- **Memory Usage**: Optimized memory consumption through on-demand model loading
- **Cache Expiration Logic**: Intelligent cache expiration (14 days) with automatic cleanup
- **Error Handling**: Enhanced error handling for model initialization failures
- **Component Coordination**: Simplified initialization flow between semantic engine and content indexer

### Fixed

- **Windows Native Host Issues**: Resolved Node.js environment conflicts with multiple NVM installations
- **Race Condition Prevention**: Eliminated concurrent initialization attempts that could cause conflicts
- **Cache Size Management**: Automatic cleanup when cache exceeds 500MB limit
- **Model Download Optimization**: Prevents unnecessary model downloads during plugin startup

### Technical Improvements

- **ModelCacheManager**: Added `isModelCached()` and `hasAnyValidCache()` methods for cache detection
- **SemanticSimilarityEngine**: Added cache checking functions and conditional initialization logic
- **Background Script**: Implemented smart initialization based on cache availability
- **VectorSearchTool**: Simplified to passive initialization model
- **ContentIndexer**: Enhanced with semantic engine readiness checks

### Documentation

- Added comprehensive conditional initialization documentation
- Updated cache management system documentation
- Created troubleshooting guides for Windows platform issues

## [v0.0.1]

### Added

- **Core Browser Tools**: Complete set of browser automation tools for web interaction

  - **Click Tool**: Intelligent element clicking with coordinate and selector support
  - **Fill Tool**: Form filling with text input and selection capabilities
  - **Screenshot Tool**: Full page and element-specific screenshot capture
  - **Navigation Tools**: URL navigation and page interaction utilities
  - **Keyboard Tool**: Keyboard input simulation and hotkey support

- **Vector Search Engine**: Advanced semantic search capabilities

  - **Content Indexing**: Automatic indexing of browser tab content
  - **Semantic Similarity**: AI-powered text similarity matching
  - **Vector Database**: Efficient storage and retrieval of embeddings
  - **Multi-language Support**: Comprehensive multilingual text processing

- **Native Host Integration**: Seamless communication with external applications

  - **Chrome Native Messaging**: Bidirectional communication channel
  - **Cross-platform Support**: Windows, macOS, and Linux compatibility
  - **Message Protocol**: Structured messaging system for tool execution

- **AI Model Integration**: State-of-the-art language models for semantic processing

  - **Transformer Models**: Support for multiple pre-trained models
  - **ONNX Runtime**: Optimized model inference with WebAssembly
  - **Model Management**: Dynamic model loading and switching
  - **Performance Optimization**: SIMD acceleration and memory pooling

- **User Interface**: Intuitive popup interface for extension management
  - **Model Selection**: Easy switching between different AI models
  - **Status Monitoring**: Real-time initialization and download progress
  - **Settings Management**: User preferences and configuration options
  - **Cache Management**: Visual cache statistics and cleanup controls

### Technical Foundation

- **Extension Architecture**: Robust Chrome extension with background scripts and content injection
- **Worker-based Processing**: Offscreen document for heavy computational tasks
- **Memory Management**: LRU caching and efficient resource utilization
- **Error Handling**: Comprehensive error reporting and recovery mechanisms
- **TypeScript Implementation**: Full type safety and modern JavaScript features

### Initial Features

- Multi-tab content analysis and search
- Real-time semantic similarity computation
- Automated web page interaction
- Cross-platform native messaging
- Extensible tool framework for future enhancements

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/engine/transport/events-bus.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview EventsBus Interface and Implementation
 * @description Event subscription, publishing, and persistence
 */

import type { RunId } from '../../domain/ids';
import type { RunEvent, RunEventInput, Unsubscribe } from '../../domain/events';
import type { EventsStore } from '../storage/storage-port';

/**
 * Event query parameters
 */
export interface EventsQuery {
  /** Run ID */
  runId: RunId;
  /** Starting sequence number (inclusive) */
  fromSeq?: number;
  /** Maximum number of results */
  limit?: number;
}

/**
 * Subscription filter
 */
export interface EventsFilter {
  /** Only receive events for this Run */
  runId?: RunId;
}

/**
 * EventsBus Interface
 * @description Responsible for event subscription, publishing, and persistence
 */
export interface EventsBus {
  /**
   * Subscribe to events
   * @param listener Event listener
   * @param filter Optional filter
   * @returns Unsubscribe function
   */
  subscribe(listener: (event: RunEvent) => void, filter?: EventsFilter): Unsubscribe;

  /**
   * Append event
   * @description Delegates to EventsStore for atomic seq allocation, then broadcasts
   * @param event Event input (without seq)
   * @returns Complete event (with seq and ts)
   */
  append(event: RunEventInput): Promise<RunEvent>;

  /**
   * Query historical events
   * @param query Query parameters
   * @returns Events sorted by seq ascending
   */
  list(query: EventsQuery): Promise<RunEvent[]>;
}

/**
 * Create NotImplemented EventsBus
 * @description Phase 0 placeholder
 */
export function createNotImplementedEventsBus(): EventsBus {
  const notImplemented = () => {
    throw new Error('EventsBus not implemented');
  };

  return {
    subscribe: () => {
      notImplemented();
      return () => {};
    },
    append: async () => notImplemented(),
    list: async () => notImplemented(),
  };
}

/**
 * Listener entry for subscription management
 */
interface ListenerEntry {
  listener: (event: RunEvent) => void;
  filter?: EventsFilter;
}

/**
 * Storage-backed EventsBus Implementation
 * @description
 * - seq allocation is done by EventsStore.append() (atomic with RunRecordV3.nextSeq)
 * - broadcast happens only after append resolves (i.e. after commit)
 */
export class StorageBackedEventsBus implements EventsBus {
  private listeners = new Set<ListenerEntry>();

  constructor(private readonly store: EventsStore) {}

  subscribe(listener: (event: RunEvent) => void, filter?: EventsFilter): Unsubscribe {
    const entry: ListenerEntry = { listener, filter };
    this.listeners.add(entry);
    return () => {
      this.listeners.delete(entry);
    };
  }

  async append(input: RunEventInput): Promise<RunEvent> {
    // Delegate to storage for atomic seq allocation
    const event = await this.store.append(input);

    // Broadcast after successful commit
    this.broadcast(event);

    return event;
  }

  async list(query: EventsQuery): Promise<RunEvent[]> {
    return this.store.list(query.runId, {
      fromSeq: query.fromSeq,
      limit: query.limit,
    });
  }

  /**
   * Broadcast event to all matching listeners
   */
  private broadcast(event: RunEvent): void {
    const { runId } = event;
    for (const { listener, filter } of this.listeners) {
      if (!filter || !filter.runId || filter.runId === runId) {
        try {
          listener(event);
        } catch (error) {
          console.error('[StorageBackedEventsBus] Listener error:', error);
        }
      }
    }
  }
}

/**
 * In-memory EventsBus for testing
 * @description Uses internal seq counter, NOT suitable for production
 * @deprecated Use StorageBackedEventsBus with mock EventsStore for testing
 */
export class InMemoryEventsBus implements EventsBus {
  private events = new Map<RunId, RunEvent[]>();
  private seqCounters = new Map<RunId, number>();
  private listeners = new Set<ListenerEntry>();

  subscribe(listener: (event: RunEvent) => void, filter?: EventsFilter): Unsubscribe {
    const entry: ListenerEntry = { listener, filter };
    this.listeners.add(entry);
    return () => {
      this.listeners.delete(entry);
    };
  }

  async append(input: RunEventInput): Promise<RunEvent> {
    const { runId } = input;

    // Allocate seq (NOT atomic, for testing only)
    const currentSeq = this.seqCounters.get(runId) ?? 0;
    const seq = currentSeq + 1;
    this.seqCounters.set(runId, seq);

    // Create complete event
    const event: RunEvent = {
      ...input,
      seq,
      ts: input.ts ?? Date.now(),
    } as RunEvent;

    // Store
    const runEvents = this.events.get(runId) ?? [];
    runEvents.push(event);
    this.events.set(runId, runEvents);

    // Broadcast
    for (const { listener, filter } of this.listeners) {
      if (!filter || !filter.runId || filter.runId === runId) {
        try {
          listener(event);
        } catch (error) {
          console.error('[InMemoryEventsBus] Listener error:', error);
        }
      }
    }

    return event;
  }

  async list(query: EventsQuery): Promise<RunEvent[]> {
    const runEvents = this.events.get(query.runId) ?? [];

    let result = runEvents;

    if (query.fromSeq !== undefined) {
      result = result.filter((e) => e.seq >= query.fromSeq!);
    }

    if (query.limit !== undefined) {
      result = result.slice(0, query.limit);
    }

    return result;
  }

  /**
   * Clear all data (for testing)
   */
  clear(): void {
    this.events.clear();
    this.seqCounters.clear();
    this.listeners.clear();
  }

  /**
   * Get current seq for a run (for testing)
   */
  getSeq(runId: RunId): number {
    return this.seqCounters.get(runId) ?? 0;
  }
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/base-browser.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolExecutor } from '@/common/tool-handler';
import type { ToolResult } from '@/common/tool-handler';
import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';

const PING_TIMEOUT_MS = 300;

/**
 * Base class for browser tool executors
 */
export abstract class BaseBrowserToolExecutor implements ToolExecutor {
  abstract name: string;
  abstract execute(args: any): Promise<ToolResult>;

  /**
   * Inject content script into tab
   */
  protected async injectContentScript(
    tabId: number,
    files: string[],
    injectImmediately = false,
    world: 'MAIN' | 'ISOLATED' = 'ISOLATED',
    allFrames: boolean = false,
    frameIds?: number[],
  ): Promise<void> {
    console.log(`Injecting ${files.join(', ')} into tab ${tabId}`);

    // check if script is already injected
    try {
      const pingFrameId = frameIds?.[0];
      const response = await Promise.race([
        typeof pingFrameId === 'number'
          ? chrome.tabs.sendMessage(
              tabId,
              { action: `${this.name}_ping` },
              { frameId: pingFrameId },
            )
          : chrome.tabs.sendMessage(tabId, { action: `${this.name}_ping` }),
        new Promise((_, reject) =>
          setTimeout(
            () => reject(new Error(`${this.name} Ping action to tab ${tabId} timed out`)),
            PING_TIMEOUT_MS,
          ),
        ),
      ]);

      if (response && response.status === 'pong') {
        console.log(
          `pong received for action '${this.name}' in tab ${tabId}. Assuming script is active.`,
        );
        return;
      } else {
        console.warn(`Unexpected ping response in tab ${tabId}:`, response);
      }
    } catch (error) {
      console.error(
        `ping content script failed: ${error instanceof Error ? error.message : String(error)}`,
      );
    }

    try {
      const target: { tabId: number; allFrames?: boolean; frameIds?: number[] } = { tabId };
      if (frameIds && frameIds.length > 0) {
        target.frameIds = frameIds;
      } else if (allFrames) {
        target.allFrames = true;
      }
      await chrome.scripting.executeScript({
        target,
        files,
        injectImmediately,
        world,
      } as any);
      console.log(`'${files.join(', ')}' injection successful for tab ${tabId}`);
    } catch (injectionError) {
      const errorMessage =
        injectionError instanceof Error ? injectionError.message : String(injectionError);
      console.error(
        `Content script '${files.join(', ')}' injection failed for tab ${tabId}: ${errorMessage}`,
      );
      throw new Error(
        `${ERROR_MESSAGES.TOOL_EXECUTION_FAILED}: Failed to inject content script in tab ${tabId}: ${errorMessage}`,
      );
    }
  }

  /**
   * Send message to tab
   */
  protected async sendMessageToTab(tabId: number, message: any, frameId?: number): Promise<any> {
    try {
      const response =
        typeof frameId === 'number'
          ? await chrome.tabs.sendMessage(tabId, message, { frameId })
          : await chrome.tabs.sendMessage(tabId, message);

      if (response && response.error) {
        throw new Error(String(response.error));
      }

      return response;
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      console.error(
        `Error sending message to tab ${tabId} for action ${message?.action || 'unknown'}: ${errorMessage}`,
      );

      if (error instanceof Error) {
        throw error;
      }
      throw new Error(errorMessage);
    }
  }

  /**
   * Try to get an existing tab by id. Returns null when not found.
   */
  protected async tryGetTab(tabId?: number): Promise<chrome.tabs.Tab | null> {
    if (typeof tabId !== 'number') return null;
    try {
      return await chrome.tabs.get(tabId);
    } catch {
      return null;
    }
  }

  /**
   * Get the active tab in the current window. Throws when not found.
   */
  protected async getActiveTabOrThrow(): Promise<chrome.tabs.Tab> {
    const [active] = await chrome.tabs.query({ active: true, currentWindow: true });
    if (!active || !active.id) throw new Error('Active tab not found');
    return active;
  }

  /**
   * Optionally focus window and/or activate tab. Defaults preserve current behavior
   * when caller sets activate/focus flags explicitly.
   */
  protected async ensureFocus(
    tab: chrome.tabs.Tab,
    options: { activate?: boolean; focusWindow?: boolean } = {},
  ): Promise<void> {
    const activate = options.activate === true;
    const focusWindow = options.focusWindow === true;
    if (focusWindow && typeof tab.windowId === 'number') {
      await chrome.windows.update(tab.windowId, { focused: true });
    }
    if (activate && typeof tab.id === 'number') {
      await chrome.tabs.update(tab.id, { active: true });
    }
  }

  /**
   * Get the active tab. When windowId provided, search within that window; otherwise currentWindow.
   */
  protected async getActiveTabInWindow(windowId?: number): Promise<chrome.tabs.Tab | null> {
    if (typeof windowId === 'number') {
      const tabs = await chrome.tabs.query({ active: true, windowId });
      return tabs && tabs[0] ? tabs[0] : null;
    }
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    return tabs && tabs[0] ? tabs[0] : null;
  }

  /**
   * Same as getActiveTabInWindow, but throws if not found.
   */
  protected async getActiveTabOrThrowInWindow(windowId?: number): Promise<chrome.tabs.Tab> {
    const tab = await this.getActiveTabInWindow(windowId);
    if (!tab || !tab.id) throw new Error('Active tab not found');
    return tab;
  }
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/storage/indexeddb-manager.ts:
--------------------------------------------------------------------------------

```typescript
// indexeddb-manager.ts
// IndexedDB storage manager for Record & Replay data.
// Stores: flows, runs, published, schedules, triggers.

import type { Flow, RunRecord } from '../types';
import type { FlowSchedule } from '../flow-store';
import type { PublishedFlowInfo } from '../flow-store';
import type { FlowTrigger } from '../trigger-store';
import { IndexedDbClient } from '@/utils/indexeddb-client';

type StoreName = 'flows' | 'runs' | 'published' | 'schedules' | 'triggers';

const DB_NAME = 'rr_storage';
// Version history:
// v1: Initial schema with flows, runs, published, schedules, triggers stores
// v2: (Previous iteration - no schema change, version was bumped during development)
// v3: Current - ensure all stores exist, support upgrade from any previous version
const DB_VERSION = 3;

const REQUIRED_STORES = ['flows', 'runs', 'published', 'schedules', 'triggers'] as const;

const idb = new IndexedDbClient(DB_NAME, DB_VERSION, (db, oldVersion) => {
  // Idempotent upgrade: ensure all required stores exist regardless of oldVersion
  // This handles both fresh installs (oldVersion=0) and upgrades from any version
  for (const storeName of REQUIRED_STORES) {
    if (!db.objectStoreNames.contains(storeName)) {
      db.createObjectStore(storeName, { keyPath: 'id' });
    }
  }
});

const tx = <T>(
  store: StoreName,
  mode: IDBTransactionMode,
  op: (s: IDBObjectStore, t: IDBTransaction) => T | Promise<T>,
) => idb.tx<T>(store, mode, op);

async function getAll<T>(store: StoreName): Promise<T[]> {
  return idb.getAll<T>(store);
}

async function getOne<T>(store: StoreName, key: string): Promise<T | undefined> {
  return idb.get<T>(store, key);
}

async function putOne<T>(store: StoreName, value: T): Promise<void> {
  return idb.put(store, value);
}

async function deleteOne(store: StoreName, key: string): Promise<void> {
  return idb.delete(store, key);
}

async function clearStore(store: StoreName): Promise<void> {
  return idb.clear(store);
}

async function putMany<T>(storeName: StoreName, values: T[]): Promise<void> {
  return idb.putMany(storeName, values);
}

export const IndexedDbStorage = {
  flows: {
    async list(): Promise<Flow[]> {
      return getAll<Flow>('flows');
    },
    async get(id: string): Promise<Flow | undefined> {
      return getOne<Flow>('flows', id);
    },
    async save(flow: Flow): Promise<void> {
      return putOne<Flow>('flows', flow);
    },
    async delete(id: string): Promise<void> {
      return deleteOne('flows', id);
    },
  },
  runs: {
    async list(): Promise<RunRecord[]> {
      return getAll<RunRecord>('runs');
    },
    async save(record: RunRecord): Promise<void> {
      return putOne<RunRecord>('runs', record);
    },
    async replaceAll(records: RunRecord[]): Promise<void> {
      return tx<void>('runs', 'readwrite', async (st) => {
        st.clear();
        for (const r of records) st.put(r);
        return;
      });
    },
  },
  published: {
    async list(): Promise<PublishedFlowInfo[]> {
      return getAll<PublishedFlowInfo>('published');
    },
    async save(info: PublishedFlowInfo): Promise<void> {
      return putOne<PublishedFlowInfo>('published', info);
    },
    async delete(id: string): Promise<void> {
      return deleteOne('published', id);
    },
  },
  schedules: {
    async list(): Promise<FlowSchedule[]> {
      return getAll<FlowSchedule>('schedules');
    },
    async save(s: FlowSchedule): Promise<void> {
      return putOne<FlowSchedule>('schedules', s);
    },
    async delete(id: string): Promise<void> {
      return deleteOne('schedules', id);
    },
  },
  triggers: {
    async list(): Promise<FlowTrigger[]> {
      return getAll<FlowTrigger>('triggers');
    },
    async save(t: FlowTrigger): Promise<void> {
      return putOne<FlowTrigger>('triggers', t);
    },
    async delete(id: string): Promise<void> {
      return deleteOne('triggers', id);
    },
  },
};

// One-time migration from chrome.storage.local to IndexedDB
let migrationPromise: Promise<void> | null = null;
let migrationFailed = false;

export async function ensureMigratedFromLocal(): Promise<void> {
  // If previous migration failed, allow retry
  if (migrationFailed) {
    migrationPromise = null;
    migrationFailed = false;
  }
  if (migrationPromise) return migrationPromise;

  migrationPromise = (async () => {
    try {
      const flag = await chrome.storage.local.get(['rr_idb_migrated']);
      if (flag && flag['rr_idb_migrated']) return;

      // Read existing data from chrome.storage.local
      const res = await chrome.storage.local.get([
        'rr_flows',
        'rr_runs',
        'rr_published_flows',
        'rr_schedules',
        'rr_triggers',
      ]);
      const flows = (res['rr_flows'] as Flow[]) || [];
      const runs = (res['rr_runs'] as RunRecord[]) || [];
      const published = (res['rr_published_flows'] as PublishedFlowInfo[]) || [];
      const schedules = (res['rr_schedules'] as FlowSchedule[]) || [];
      const triggers = (res['rr_triggers'] as FlowTrigger[]) || [];

      // Write into IDB
      if (flows.length) await putMany('flows', flows);
      if (runs.length) await putMany('runs', runs);
      if (published.length) await putMany('published', published);
      if (schedules.length) await putMany('schedules', schedules);
      if (triggers.length) await putMany('triggers', triggers);

      await chrome.storage.local.set({ rr_idb_migrated: true });
    } catch (e) {
      migrationFailed = true;
      console.error('IndexedDbStorage migration failed:', e);
      // Re-throw to let callers know migration failed
      throw e;
    }
  })();
  return migrationPromise;
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/wxt.config.ts:
--------------------------------------------------------------------------------

```typescript
import { defineConfig } from 'wxt';
import tailwindcss from '@tailwindcss/vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { config } from 'dotenv';
import { resolve } from 'path';
import Icons from 'unplugin-icons/vite';
import Components from 'unplugin-vue-components/vite';
import IconsResolver from 'unplugin-icons/resolver';

config({ path: resolve(process.cwd(), '.env') });
config({ path: resolve(process.cwd(), '.env.local') });

const CHROME_EXTENSION_KEY = process.env.CHROME_EXTENSION_KEY;
// Detect dev mode early for manifest-level switches
const IS_DEV = process.env.NODE_ENV !== 'production' && process.env.MODE !== 'production';

// See https://wxt.dev/api/config.html
export default defineConfig({
  modules: ['@wxt-dev/module-vue'],
  runner: {
    // 方案1: 禁用自动启动(推荐)
    disabled: true,

    // 方案2: 如果要启用自动启动并使用现有配置,取消注释下面的配置
    // chromiumArgs: [
    //   '--user-data-dir=' + homedir() + (process.platform === 'darwin'
    //     ? '/Library/Application Support/Google/Chrome'
    //     : process.platform === 'win32'
    //     ? '/AppData/Local/Google/Chrome/User Data'
    //     : '/.config/google-chrome'),
    //   '--remote-debugging-port=9222',
    // ],
  },
  manifest: {
    // Use environment variable for the key, fallback to undefined if not set
    key: CHROME_EXTENSION_KEY,
    default_locale: 'zh_CN',
    name: '__MSG_extensionName__',
    description: '__MSG_extensionDescription__',
    permissions: [
      'nativeMessaging',
      'tabs',
      'activeTab',
      'scripting',
      'contextMenus',
      'downloads',
      'webRequest',
      'webNavigation',
      'debugger',
      'history',
      'bookmarks',
      'offscreen',
      'storage',
      'declarativeNetRequest',
      'alarms',
      // Allow programmatic control of Chrome Side Panel
      'sidePanel',
    ],
    host_permissions: ['<all_urls>'],
    options_ui: {
      page: 'options.html',
      open_in_tab: true,
    },
    action: {
      default_popup: 'popup.html',
      default_title: 'Chrome MCP Server',
    },
    // Chrome Side Panel entry for workflow management
    // Ref: https://developer.chrome.com/docs/extensions/reference/api/sidePanel
    side_panel: {
      default_path: 'sidepanel.html',
    },
    // Keyboard shortcuts for quick triggers
    commands: {
      // run_quick_trigger_1: {
      //   suggested_key: { default: 'Ctrl+Shift+1' },
      //   description: 'Run quick trigger 1',
      // },
      // run_quick_trigger_2: {
      //   suggested_key: { default: 'Ctrl+Shift+2' },
      //   description: 'Run quick trigger 2',
      // },
      // run_quick_trigger_3: {
      //   suggested_key: { default: 'Ctrl+Shift+3' },
      //   description: 'Run quick trigger 3',
      // },
      // open_workflow_sidepanel: {
      //   suggested_key: { default: 'Ctrl+Shift+O' },
      //   description: 'Open workflow sidepanel',
      // },
      toggle_web_editor: {
        suggested_key: { default: 'Ctrl+Shift+O', mac: 'Command+Shift+O' },
        description: 'Toggle Web Editor mode',
      },
      toggle_quick_panel: {
        suggested_key: { default: 'Ctrl+Shift+U', mac: 'Command+Shift+U' },
        description: 'Toggle Quick Panel AI Chat',
      },
    },
    web_accessible_resources: [
      {
        resources: [
          '/models/*', // 允许访问 public/models/ 下的所有文件
          '/workers/*', // 允许访问 workers 文件
          '/inject-scripts/*', // 允许内容脚本注入的助手文件
        ],
        matches: ['<all_urls>'],
      },
    ],
    // 注意:以下安全策略在开发环境会阻断 dev server 的资源加载,
    // 只在生产环境启用,开发环境交由 WXT 默认策略处理。
    ...(IS_DEV
      ? {}
      : {
          cross_origin_embedder_policy: { value: 'require-corp' as const },
          cross_origin_opener_policy: { value: 'same-origin' as const },
          content_security_policy: {
            // Allow inline styles injected by Vite (compiled CSS) and data images used in UI thumbnails
            extension_pages:
              "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;",
          },
        }),
  },
  vite: (env) => ({
    plugins: [
      // TailwindCSS v4 Vite plugin – no PostCSS config required
      tailwindcss(),
      // Auto-register SVG icons as Vue components; all icons are bundled locally
      Components({
        dts: false,
        resolvers: [IconsResolver({ prefix: 'i', enabledCollections: ['lucide', 'mdi', 'ri'] })],
      }) as any,
      Icons({ compiler: 'vue3', autoInstall: false }) as any,
      // Ensure static assets are available as early as possible to avoid race conditions in dev
      // Copy workers/_locales/inject-scripts into the build output before other steps
      viteStaticCopy({
        targets: [
          {
            src: 'inject-scripts/*.js',
            dest: 'inject-scripts',
          },
          {
            src: ['workers/*'],
            dest: 'workers',
          },
          {
            src: '_locales/**/*',
            dest: '_locales',
          },
        ],
        // Use writeBundle so outDir exists for dev and prod
        hook: 'writeBundle',
        // Enable watch so changes to these files are reflected during dev
        watch: {
          // Use default patterns inferred from targets; explicit true enables watching
          // Vite plugin will watch src patterns and re-copy on change
        } as any,
      }) as any,
    ],
    build: {
      // 我们的构建产物需要兼容到es6
      target: 'es2015',
      // 非生产环境下生成sourcemap
      sourcemap: env.mode !== 'production',
      // 禁用gzip 压缩大小报告,因为压缩大型文件可能会很慢
      reportCompressedSize: false,
      // chunk大小超过1500kb是触发警告
      chunkSizeWarningLimit: 1500,
      minify: false,
    },
  }),
});

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/actions/handlers/click.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Click and Double-Click Action Handlers
 *
 * Handles click interactions:
 * - Single click
 * - Double click
 * - Post-click navigation/network wait
 * - Selector fallback with logging
 */

import { handleCallTool } from '@/entrypoints/background/tools';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { ENGINE_CONSTANTS } from '../../engine/constants';
import {
  maybeQuickWaitForNav,
  waitForNavigationDone,
  waitForNetworkIdle,
} from '../../engine/policies/wait';
import { failed, invalid, ok } from '../registry';
import type {
  Action,
  ActionExecutionContext,
  ActionExecutionResult,
  ActionHandler,
} from '../types';
import {
  clampInt,
  ensureElementVisible,
  logSelectorFallback,
  readTabUrl,
  selectorLocator,
  toSelectorTarget,
} from './common';

/**
 * Shared click execution logic for both click and dblclick
 */
async function executeClick<T extends 'click' | 'dblclick'>(
  ctx: ActionExecutionContext,
  action: Action<T>,
): Promise<ActionExecutionResult<T>> {
  const vars = ctx.vars;
  const tabId = ctx.tabId;
  // Check if StepRunner owns nav-wait (skip internal nav-wait logic)
  const skipNavWait = ctx.execution?.skipNavWait === true;

  if (typeof tabId !== 'number') {
    return failed('TAB_NOT_FOUND', 'No active tab found');
  }

  // Ensure page is read before locating element
  await handleCallTool({ name: TOOL_NAMES.BROWSER.READ_PAGE, args: {} });

  // Only read beforeUrl if we need to do nav-wait
  const beforeUrl = skipNavWait ? '' : await readTabUrl(tabId);
  const { selectorTarget, firstCandidateType, firstCssOrAttr } = toSelectorTarget(
    action.params.target,
    vars,
  );

  // Locate element using shared selector locator
  const located = await selectorLocator.locate(tabId, selectorTarget, {
    frameId: ctx.frameId,
    preferRef: false,
  });

  const frameId = located?.frameId ?? ctx.frameId;
  const refToUse = located?.ref ?? selectorTarget.ref;
  const selectorToUse = !located?.ref ? firstCssOrAttr : undefined;

  if (!refToUse && !selectorToUse) {
    return failed('TARGET_NOT_FOUND', 'Could not locate target element');
  }

  // Verify element visibility if we have a ref
  if (located?.ref) {
    const isVisible = await ensureElementVisible(tabId, located.ref, frameId);
    if (!isVisible) {
      return failed('ELEMENT_NOT_VISIBLE', 'Target element is not visible');
    }
  }

  // Execute click with tool timeout
  const toolTimeout = clampInt(action.policy?.timeout?.ms ?? 10000, 1000, 30000);

  const clickResult = await handleCallTool({
    name: TOOL_NAMES.BROWSER.CLICK,
    args: {
      ref: refToUse,
      selector: selectorToUse,
      waitForNavigation: false,
      timeout: toolTimeout,
      frameId,
      tabId,
      double: action.type === 'dblclick',
    },
  });

  if ((clickResult as { isError?: boolean })?.isError) {
    const errorContent = (clickResult as { content?: Array<{ text?: string }> })?.content;
    const errorMsg = errorContent?.[0]?.text || `${action.type} action failed`;
    return failed('UNKNOWN', errorMsg);
  }

  // Log selector fallback if used
  const resolvedBy = located?.resolvedBy || (located?.ref ? 'ref' : '');
  const fallbackUsed =
    resolvedBy && firstCandidateType && resolvedBy !== 'ref' && resolvedBy !== firstCandidateType;

  if (fallbackUsed) {
    logSelectorFallback(ctx, action.id, String(firstCandidateType), String(resolvedBy));
  }

  // Skip post-click wait if StepRunner handles it
  if (skipNavWait) {
    return { status: 'success' };
  }

  // Post-click wait handling (only when handler owns nav-wait)
  const waitMs = clampInt(
    action.policy?.timeout?.ms ?? ENGINE_CONSTANTS.DEFAULT_WAIT_MS,
    0,
    ENGINE_CONSTANTS.MAX_WAIT_MS,
  );
  const after = action.params.after ?? {};

  if (after.waitForNavigation) {
    await waitForNavigationDone(beforeUrl, waitMs);
  } else if (after.waitForNetworkIdle) {
    const totalMs = clampInt(waitMs, 1000, ENGINE_CONSTANTS.MAX_WAIT_MS);
    const idleMs = Math.min(1500, Math.max(500, Math.floor(totalMs / 3)));
    await waitForNetworkIdle(totalMs, idleMs);
  } else {
    // Quick sniff for navigation that might have been triggered
    await maybeQuickWaitForNav(beforeUrl, waitMs);
  }

  return { status: 'success' };
}

/**
 * Validate click target configuration
 */
function validateClickTarget(target: {
  ref?: string;
  candidates?: unknown[];
}): { ok: true } | { ok: false; errors: [string, ...string[]] } {
  const hasRef = typeof target?.ref === 'string' && target.ref.trim().length > 0;
  const hasCandidates = Array.isArray(target?.candidates) && target.candidates.length > 0;

  if (hasRef || hasCandidates) {
    return ok();
  }
  return invalid('Missing target selector or ref');
}

export const clickHandler: ActionHandler<'click'> = {
  type: 'click',

  validate: (action) =>
    validateClickTarget(action.params.target as { ref?: string; candidates?: unknown[] }),

  describe: (action) => {
    const target = action.params.target;
    if (typeof (target as { ref?: string }).ref === 'string') {
      return `Click element ${(target as { ref: string }).ref}`;
    }
    return 'Click element';
  },

  run: async (ctx, action) => {
    return await executeClick(ctx, action);
  },
};

export const dblclickHandler: ActionHandler<'dblclick'> = {
  type: 'dblclick',

  validate: (action) =>
    validateClickTarget(action.params.target as { ref?: string; candidates?: unknown[] }),

  describe: (action) => {
    const target = action.params.target;
    if (typeof (target as { ref?: string }).ref === 'string') {
      return `Double-click element ${(target as { ref: string }).ref}`;
    }
    return 'Double-click element';
  },

  run: async (ctx, action) => {
    return await executeClick(ctx, action);
  },
};

```
Page 4/43FirstPrevNextLast