#
tokens: 49479/50000 58/574 files (page 2/43)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 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/popup/components/builder/components/properties/PropertyWhile.vue:
--------------------------------------------------------------------------------

```vue
<template>
  <div class="form-section">
    <div class="form-group">
      <label class="form-label">条件 (JSON)</label>
      <textarea
        class="form-textarea"
        v-model="whileJson"
        rows="3"
        placeholder='{"expression":"workflow.count < 3"}'
      ></textarea>
    </div>
    <div class="form-group">
      <label class="form-label">子流 ID</label>
      <input
        class="form-input"
        v-model="(node as any).config.subflowId"
        placeholder="选择或新建子流"
      />
      <button class="btn-sm" style="margin-top: 8px" @click="onCreateSubflow">新建子流</button>
    </div>
    <div class="form-group">
      <label class="form-label">最大迭代次数(可选)</label>
      <input
        class="form-input"
        type="number"
        min="0"
        v-model.number="(node as any).config.maxIterations"
      />
    </div>
  </div>
</template>

<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import { computed } from 'vue';
import type { NodeBase } from '@/entrypoints/background/record-replay/types';

const props = defineProps<{ node: NodeBase }>();
const emit = defineEmits<{ (e: 'create-subflow', id: string): void }>();

const whileJson = computed({
  get() {
    try {
      return JSON.stringify((props.node as any).config?.condition || {}, null, 2);
    } catch {
      return '';
    }
  },
  set(v: string) {
    try {
      (props.node as any).config = {
        ...((props.node as any).config || {}),
        condition: JSON.parse(v || '{}'),
      };
    } catch {}
  },
});

function onCreateSubflow() {
  const id = prompt('请输入新子流ID');
  if (!id) return;
  emit('create-subflow', id);
  const n = props.node as any;
  if (n && n.config) n.config.subflowId = id;
}
</script>

<style scoped></style>

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/utils/sidepanel.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Sidepanel Utilities
 *
 * Shared helpers for opening and managing the Chrome sidepanel from background modules.
 * Used by web-editor, quick-panel, and other modules that need to trigger sidepanel navigation.
 */

/**
 * Best-effort open the sidepanel with AgentChat tab selected.
 *
 * @param tabId - Tab ID to associate with sidepanel
 * @param windowId - Optional window ID for fallback when tab-level open fails
 * @param sessionId - Optional session ID to navigate directly to chat view (deep-link)
 *
 * @remarks
 * This function is intentionally resilient - it will not throw on failures.
 * Sidepanel availability varies across Chrome versions and contexts.
 */
export async function openAgentChatSidepanel(
  tabId: number,
  windowId?: number,
  sessionId?: string,
): Promise<void> {
  try {
    // Build deep-link path with optional session navigation
    let path = 'sidepanel.html?tab=agent-chat';
    if (sessionId) {
      path += `&view=chat&sessionId=${encodeURIComponent(sessionId)}`;
    }

    // Configure sidepanel options for this tab

    const sidePanel = chrome.sidePanel as any;

    if (sidePanel?.setOptions) {
      await sidePanel.setOptions({
        tabId,
        path,
        enabled: true,
      });
    }

    // Attempt to open the sidepanel
    if (sidePanel?.open) {
      try {
        await sidePanel.open({ tabId });
      } catch {
        // Fallback to window-level open if tab-level fails
        // This handles cases where the tab is in a special state
        if (typeof windowId === 'number') {
          await sidePanel.open({ windowId });
        }
      }
    }
  } catch {
    // Best-effort: side panel may be unavailable in some Chrome versions/environments
    // Intentionally suppress errors to avoid breaking calling code
  }
}

```

--------------------------------------------------------------------------------
/app/native-server/src/agent/storage.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Storage path helpers for agent-related state.
 *
 * Provides unified path resolution for:
 * - SQLite database file
 * - Data directory
 * - Default workspace directory
 *
 * All paths can be overridden via environment variables.
 */
import os from 'node:os';
import path from 'node:path';

const DEFAULT_DATA_DIR = path.join(os.homedir(), '.chrome-mcp-agent');

/**
 * Resolve base data directory for agent state.
 *
 * Environment:
 * - CHROME_MCP_AGENT_DATA_DIR: overrides the default base directory.
 */
export function getAgentDataDir(): string {
  const raw = process.env.CHROME_MCP_AGENT_DATA_DIR;
  if (raw && raw.trim()) {
    return path.resolve(raw.trim());
  }
  return DEFAULT_DATA_DIR;
}

/**
 * Resolve database file path.
 *
 * Environment:
 * - CHROME_MCP_AGENT_DB_FILE: overrides the default database path.
 */
export function getDatabasePath(): string {
  const raw = process.env.CHROME_MCP_AGENT_DB_FILE;
  if (raw && raw.trim()) {
    return path.resolve(raw.trim());
  }
  return path.join(getAgentDataDir(), 'agent.db');
}

/**
 * Get the default workspace directory for agent projects.
 * This is a subdirectory under the agent data directory.
 *
 * Cross-platform compatible:
 * - Mac/Linux: ~/.chrome-mcp-agent/workspaces
 * - Windows: %USERPROFILE%\.chrome-mcp-agent\workspaces
 */
export function getDefaultWorkspaceDir(): string {
  return path.join(getAgentDataDir(), 'workspaces');
}

/**
 * Generate a default project root path for a given project name.
 */
export function getDefaultProjectRoot(projectName: string): string {
  // Sanitize project name for use as directory name
  const safeName = projectName
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9_-]/g, '-')
    .replace(/-+/g, '-')
    .replace(/^-|-$/g, '');
  return path.join(getDefaultWorkspaceDir(), safeName || 'default-project');
}

```

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

```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { cdpSessionManager } from '@/utils/cdp-session-manager';

interface HandleDialogParams {
  action: 'accept' | 'dismiss';
  promptText?: string;
}

/**
 * Handle JavaScript dialogs (alert/confirm/prompt) via CDP Page.handleJavaScriptDialog
 */
class HandleDialogTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.HANDLE_DIALOG;

  async execute(args: HandleDialogParams): Promise<ToolResult> {
    const { action, promptText } = args || ({} as HandleDialogParams);
    if (!action || (action !== 'accept' && action !== 'dismiss')) {
      return createErrorResponse('action must be "accept" or "dismiss"');
    }

    try {
      const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
      if (!activeTab?.id) return createErrorResponse('No active tab found');
      const tabId = activeTab.id!;

      // Use shared CDP session manager for safe attach/detach with refcount
      await cdpSessionManager.withSession(tabId, 'dialog', async () => {
        await cdpSessionManager.sendCommand(tabId, 'Page.enable');
        await cdpSessionManager.sendCommand(tabId, 'Page.handleJavaScriptDialog', {
          accept: action === 'accept',
          promptText: action === 'accept' ? promptText : undefined,
        });
      });

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({ success: true, action, promptText: promptText || null }),
          },
        ],
        isError: false,
      };
    } catch (error) {
      return createErrorResponse(
        `Failed to handle dialog: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }
}

export const handleDialogTool = new HandleDialogTool();

```

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

```typescript
/**
 * Quick Panel UI Module Index
 *
 * Exports all UI components for the Quick Panel feature.
 */

// ============================================================
// Shell (unified container for search + chat views)
// ============================================================

export {
  mountQuickPanelShell,
  type QuickPanelShellElements,
  type QuickPanelShellManager,
  type QuickPanelShellOptions,
} from './panel-shell';

// ============================================================
// Shadow DOM host
// ============================================================

export {
  mountQuickPanelShadowHost,
  type QuickPanelShadowHostElements,
  type QuickPanelShadowHostManager,
  type QuickPanelShadowHostOptions,
} from './shadow-host';

// ============================================================
// Search UI Components
// ============================================================

export {
  createSearchInput,
  type SearchInputManager,
  type SearchInputOptions,
  type SearchInputState,
} from './search-input';

export {
  createQuickEntries,
  type QuickEntriesManager,
  type QuickEntriesOptions,
} from './quick-entries';

// ============================================================
// AI Chat Components
// ============================================================

export {
  createQuickPanelMessageRenderer,
  type QuickPanelMessageRenderer,
  type QuickPanelMessageRendererOptions,
} from './message-renderer';

export { createMarkdownRenderer, type MarkdownRendererInstance } from './markdown-renderer';

export {
  mountQuickPanelAiChatPanel,
  type QuickPanelAiChatPanelManager,
  type QuickPanelAiChatPanelOptions,
  type QuickPanelAiChatPanelState,
} from './ai-chat-panel';

// ============================================================
// Styles
// ============================================================

export { QUICK_PANEL_STYLES } from './styles';

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/engine/triggers/manual-trigger.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Manual Trigger Handler (P4-08)
 * @description
 * Manual triggers are the simplest trigger type - they don't auto-fire.
 * They're only triggered programmatically via RPC or UI.
 *
 * This handler just tracks installed triggers but doesn't set up any listeners.
 * Manual triggers are fired by calling TriggerManager's fire method directly.
 */

import type { TriggerId } from '../../domain/ids';
import type { TriggerSpecByKind } from '../../domain/triggers';
import type { TriggerFireCallback, TriggerHandler, TriggerHandlerFactory } from './trigger-handler';

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

export interface ManualTriggerHandlerDeps {
  logger?: Pick<Console, 'debug' | 'info' | 'warn' | 'error'>;
}

type ManualTriggerSpec = TriggerSpecByKind<'manual'>;

// ==================== Handler Implementation ====================

/**
 * Create manual trigger handler factory
 */
export function createManualTriggerHandlerFactory(
  deps?: ManualTriggerHandlerDeps,
): TriggerHandlerFactory<'manual'> {
  return (fireCallback) => createManualTriggerHandler(fireCallback, deps);
}

/**
 * Create manual trigger handler
 *
 * Manual triggers don't auto-fire - they're only triggered via RPC.
 * This handler just tracks which manual triggers are installed.
 */
export function createManualTriggerHandler(
  _fireCallback: TriggerFireCallback,
  _deps?: ManualTriggerHandlerDeps,
): TriggerHandler<'manual'> {
  const installed = new Map<TriggerId, ManualTriggerSpec>();

  return {
    kind: 'manual',

    async install(trigger: ManualTriggerSpec): Promise<void> {
      installed.set(trigger.id, trigger);
    },

    async uninstall(triggerId: string): Promise<void> {
      installed.delete(triggerId as TriggerId);
    },

    async uninstallAll(): Promise<void> {
      installed.clear();
    },

    getInstalledIds(): string[] {
      return Array.from(installed.keys());
    },
  };
}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "mcp-chrome-bridge-monorepo",
  "version": "1.0.0",
  "private": true,
  "author": "hangye",
  "type": "module",
  "scripts": {
    "build:shared": "pnpm --filter chrome-mcp-shared build",
    "build:native": "pnpm --filter mcp-chrome-bridge build",
    "build:extension": "pnpm --filter chrome-mcp-server build",
    "build:wasm": "pnpm --filter @chrome-mcp/wasm-simd build && pnpm run copy:wasm",
    "build": "pnpm -r --filter='!@chrome-mcp/wasm-simd' build",
    "copy:wasm": "cp ./packages/wasm-simd/pkg/simd_math.js ./packages/wasm-simd/pkg/simd_math_bg.wasm ./app/chrome-extension/workers/",
    "dev:shared": "pnpm --filter chrome-mcp-shared dev",
    "dev:native": "pnpm --filter mcp-chrome-bridge dev",
    "dev:extension": "pnpm --filter chrome-mcp-server dev",
    "dev": "pnpm --filter chrome-mcp-shared build && pnpm -r --parallel dev",
    "lint": "pnpm -r lint",
    "lint:fix": "pnpm -r lint:fix",
    "format": "pnpm -r format",
    "clean:dist": "pnpm -r exec rm -rf dist .turbo",
    "clean:modules": "pnpm -r exec rm -rf node_modules && rm -rf node_modules",
    "clean": "npm run clean:dist && npm run clean:modules",
    "typecheck": "pnpm -r exec tsc --noEmit",
    "prepare": "husky"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.8.1",
    "@commitlint/config-conventional": "^19.8.1",
    "@eslint/js": "^9.25.1",
    "@typescript-eslint/eslint-plugin": "^8.32.0",
    "@typescript-eslint/parser": "^8.32.0",
    "eslint": "^9.26.0",
    "eslint-config-prettier": "^10.1.5",
    "eslint-plugin-vue": "^10.0.0",
    "globals": "^16.1.0",
    "husky": "^9.1.7",
    "lint-staged": "^15.5.1",
    "prettier": "^3.5.3",
    "typescript": "^5.8.3",
    "typescript-eslint": "^8.32.0",
    "vue-eslint-parser": "^10.1.3"
  },
  "lint-staged": {
    "**/*.{js,jsx,ts,tsx,vue}": [
      "eslint --fix",
      "prettier --write"
    ],
    "**/*.{json,md,yaml,html,css}": [
      "prettier --write"
    ]
  }
}

```

--------------------------------------------------------------------------------
/packages/shared/src/node-spec.ts:
--------------------------------------------------------------------------------

```typescript
// node-spec.ts — shared NodeSpec types for UI-driven forms

export type FieldType = 'string' | 'number' | 'boolean' | 'select' | 'object' | 'array' | 'json';

export interface FieldSpecBase {
  key: string;
  label: string;
  type: FieldType;
  required?: boolean;
  placeholder?: string;
  help?: string;
  // widget name used by UI; runtime ignores it
  widget?: string;
  uiProps?: Record<string, any>;
}

export interface FieldString extends FieldSpecBase {
  type: 'string';
  default?: string;
}
export interface FieldNumber extends FieldSpecBase {
  type: 'number';
  min?: number;
  max?: number;
  step?: number;
  default?: number;
}
export interface FieldBoolean extends FieldSpecBase {
  type: 'boolean';
  default?: boolean;
}
export interface FieldSelect extends FieldSpecBase {
  type: 'select';
  options: Array<{ label: string; value: string | number | boolean }>;
  default?: string | number | boolean;
}
export interface FieldObject extends FieldSpecBase {
  type: 'object';
  fields: FieldSpec[];
  default?: Record<string, any>;
}
export interface FieldArray extends FieldSpecBase {
  type: 'array';
  item: FieldString | FieldNumber | FieldBoolean | FieldSelect | FieldObject | FieldJson;
  default?: any[];
}
export interface FieldJson extends FieldSpecBase {
  type: 'json';
  default?: any;
}

export type FieldSpec =
  | FieldString
  | FieldNumber
  | FieldBoolean
  | FieldSelect
  | FieldObject
  | FieldArray
  | FieldJson;

export type NodeCategory = 'Flow' | 'Actions' | 'Logic' | 'Tools' | 'Tabs' | 'Page';

export interface NodeSpecDisplay {
  label: string;
  iconClass: string;
  category: NodeCategory;
  docUrl?: string;
}

export interface NodeSpec {
  type: string; // Aligns with NodeType/StepType
  version: number;
  display: NodeSpecDisplay;
  ports: { inputs: number | 'any'; outputs: Array<{ label?: string }> | 'any' };
  schema: FieldSpec[];
  defaults: Record<string, any>;
  validate?: (config: any) => string[];
}

```

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

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

export const openTabNode: NodeRuntime<StepOpenTab> = {
  run: async (ctx, step) => {
    const s: any = expandTemplatesDeep(step as any, ctx.vars);
    if (s.newWindow) await chrome.windows.create({ url: s.url || undefined, focused: true });
    else await chrome.tabs.create({ url: s.url || undefined, active: true });
    return {} as ExecResult;
  },
};

export const switchTabNode: NodeRuntime<StepSwitchTab> = {
  run: async (ctx, step) => {
    const s: any = expandTemplatesDeep(step as any, ctx.vars);
    let targetTabId: number | undefined = s.tabId;
    if (!targetTabId) {
      const tabs = await chrome.tabs.query({});
      const hit = tabs.find(
        (t) =>
          (s.urlContains && (t.url || '').includes(String(s.urlContains))) ||
          (s.titleContains && (t.title || '').includes(String(s.titleContains))),
      );
      targetTabId = (hit && hit.id) as number | undefined;
    }
    if (!targetTabId) throw new Error('switchTab: no matching tab');
    const res = await handleCallTool({
      name: TOOL_NAMES.BROWSER.SWITCH_TAB,
      args: { tabId: targetTabId },
    });
    if ((res as any).isError) throw new Error('switchTab failed');
    return {} as ExecResult;
  },
};

export const closeTabNode: NodeRuntime<StepCloseTab> = {
  run: async (ctx, step) => {
    const s: any = expandTemplatesDeep(step as any, ctx.vars);
    const args: any = {};
    if (Array.isArray(s.tabIds) && s.tabIds.length) args.tabIds = s.tabIds;
    if (s.url) args.url = s.url;
    const res = await handleCallTool({ name: TOOL_NAMES.BROWSER.CLOSE_TABS, args });
    if ((res as any).isError) throw new Error('closeTab failed');
    return {} as ExecResult;
  },
};

```

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

```typescript
import type { Step } from '../types';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';

export const ifNode: NodeRuntime<any> = {
  validate: (step) => {
    const s = step as any;
    const hasBranches = Array.isArray(s.branches) && s.branches.length > 0;
    const ok = hasBranches || !!s.condition;
    return ok ? { ok } : { ok, errors: ['缺少条件或分支'] };
  },
  run: async (ctx: ExecCtx, step: Step) => {
    const s: any = step;
    if (Array.isArray(s.branches) && s.branches.length > 0) {
      const evalExpr = (expr: string): boolean => {
        const code = String(expr || '').trim();
        if (!code) return false;
        try {
          const fn = new Function(
            'vars',
            'workflow',
            `try { return !!(${code}); } catch (e) { return false; }`,
          );
          return !!fn(ctx.vars, ctx.vars);
        } catch {
          return false;
        }
      };
      for (const br of s.branches) {
        if (br?.expr && evalExpr(String(br.expr)))
          return { nextLabel: String(br.label || `case:${br.id || 'match'}`) } as ExecResult;
      }
      if ('else' in s) return { nextLabel: String(s.else || 'default') } as ExecResult;
      return { nextLabel: 'default' } as ExecResult;
    }
    // legacy condition: { var/equals | expression }
    try {
      let result = false;
      const cond = s.condition;
      if (cond && typeof cond.expression === 'string' && cond.expression.trim()) {
        const fn = new Function(
          'vars',
          `try { return !!(${cond.expression}); } catch (e) { return false; }`,
        );
        result = !!fn(ctx.vars);
      } else if (cond && typeof cond.var === 'string') {
        const v = ctx.vars[cond.var];
        if ('equals' in cond) result = String(v) === String(cond.equals);
        else result = !!v;
      }
      return { nextLabel: result ? 'true' : 'false' } as ExecResult;
    } catch {
      return { nextLabel: 'false' } as ExecResult;
    }
  },
};

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/logging/run-logger.ts:
--------------------------------------------------------------------------------

```typescript
// engine/logging/run-logger.ts — run logs, overlay and persistence
import type { RunLogEntry, RunRecord, Flow } from '../../types';
import { appendRun } from '../../flow-store';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { handleCallTool } from '@/entrypoints/background/tools';

export class RunLogger {
  private logs: RunLogEntry[] = [];
  constructor(private runId: string) {}

  push(e: RunLogEntry) {
    this.logs.push(e);
  }

  getLogs() {
    return this.logs;
  }

  async overlayInit() {
    try {
      const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
      if (tabs[0]?.id)
        await chrome.tabs.sendMessage(tabs[0].id, { action: 'rr_overlay', cmd: 'init' } as any);
    } catch {}
  }

  async overlayAppend(text: string) {
    try {
      const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
      if (tabs[0]?.id)
        await chrome.tabs.sendMessage(tabs[0].id, {
          action: 'rr_overlay',
          cmd: 'append',
          text,
        } as any);
    } catch {}
  }

  async overlayDone() {
    try {
      const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
      if (tabs[0]?.id)
        await chrome.tabs.sendMessage(tabs[0].id, { action: 'rr_overlay', cmd: 'done' } as any);
    } catch {}
  }

  async screenshotOnFailure() {
    try {
      const shot = await handleCallTool({
        name: TOOL_NAMES.BROWSER.COMPUTER,
        args: { action: 'screenshot' },
      });
      const img = (shot?.content?.find((c: any) => c.type === 'image') as any)?.data as string;
      if (img) this.logs[this.logs.length - 1].screenshotBase64 = img;
    } catch {}
  }

  async persist(flow: Flow, startedAt: number, success: boolean) {
    const record: RunRecord = {
      id: this.runId,
      flowId: flow.id,
      startedAt: new Date(startedAt).toISOString(),
      finishedAt: new Date().toISOString(),
      success,
      entries: this.logs,
    };
    await appendRun(record);
  }
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/tests/record-replay/_test-helpers.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Test helpers for record-replay contract tests.
 *
 * Provides minimal factories and mocks for testing the execution pipeline
 * without requiring real browser or tool dependencies.
 */

import { vi } from 'vitest';
import type { ExecCtx } from '@/entrypoints/background/record-replay/nodes/types';
import type { ActionExecutionContext } from '@/entrypoints/background/record-replay/actions/types';

/**
 * Create a minimal ExecCtx for testing
 */
export function createMockExecCtx(overrides: Partial<ExecCtx> = {}): ExecCtx {
  return {
    vars: {},
    logger: vi.fn(),
    ...overrides,
  };
}

/**
 * Create a minimal ActionExecutionContext for testing
 */
export function createMockActionCtx(
  overrides: Partial<ActionExecutionContext> = {},
): ActionExecutionContext {
  return {
    vars: {},
    tabId: 1,
    log: vi.fn(),
    ...overrides,
  };
}

/**
 * Create a minimal Step for testing
 */
export function createMockStep(type: string, overrides: Record<string, unknown> = {}): any {
  return {
    id: `step_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
    type,
    ...overrides,
  };
}

/**
 * Create a minimal Flow for testing (with nodes/edges for scheduler)
 */
export function createMockFlow(overrides: Record<string, unknown> = {}): any {
  const id = `flow_${Date.now()}`;
  return {
    id,
    name: 'Test Flow',
    version: 1,
    steps: [],
    nodes: [],
    edges: [],
    variables: [],
    meta: {
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    },
    ...overrides,
  };
}

/**
 * Create a mock ActionRegistry for testing
 */
export function createMockRegistry(handlers: Map<string, any> = new Map()) {
  const executeFn = vi.fn(async () => ({ status: 'success' as const }));

  return {
    get: vi.fn((type: string) => handlers.get(type) || { type }),
    execute: executeFn,
    register: vi.fn(),
    has: vi.fn((type: string) => handlers.has(type)),
    _executeFn: executeFn, // Expose for assertions
  };
}

```

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

```vue
<template>
  <div class="kve">
    <div v-for="(item, i) in rows" :key="i" class="kve-row">
      <input class="kve-key" v-model="item.k" placeholder="变量名" />
      <input class="kve-val" v-model="item.v" placeholder="结果路径(如 data.items[0].id)" />
      <button class="mini" @click="move(i, -1)" :disabled="i === 0">↑</button>
      <button class="mini" @click="move(i, 1)" :disabled="i === rows.length - 1">↓</button>
      <button class="mini danger" @click="remove(i)">删</button>
    </div>
    <button class="mini" @click="add">添加映射</button>
  </div>
</template>

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

const props = defineProps<{ modelValue: Record<string, string> | undefined }>();
const emit = defineEmits(['update:modelValue']);

const rows = reactive<Array<{ k: string; v: string }>>([]);

function syncFromModel() {
  rows.splice(0, rows.length);
  const obj = props.modelValue || {};
  for (const [k, v] of Object.entries(obj)) rows.push({ k, v: String(v) });
}
function syncToModel() {
  const out: Record<string, string> = {};
  for (const r of rows) if (r.k) out[r.k] = r.v || '';
  emit('update:modelValue', out);
}
watch(() => props.modelValue, syncFromModel, { immediate: true, deep: true });
watch(rows, syncToModel, { deep: true });

function add() {
  rows.push({ k: '', v: '' });
}
function remove(i: number) {
  rows.splice(i, 1);
}
function move(i: number, d: number) {
  const j = i + d;
  if (j < 0 || j >= rows.length) return;
  const t = rows[i];
  rows[i] = rows[j];
  rows[j] = t;
}
</script>

<style scoped>
.kve {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.kve-row {
  display: grid;
  grid-template-columns: 160px 1fr auto auto auto;
  gap: 6px;
  align-items: center;
}
.kve-key,
.kve-val {
  border: 1px solid #d1d5db;
  border-radius: 6px;
  padding: 6px;
}
.mini {
  font-size: 12px;
  padding: 4px 8px;
  border: 1px solid #d1d5db;
  background: #fff;
  border-radius: 6px;
  cursor: pointer;
}
.mini.danger {
  background: #fee2e2;
  border-color: #fecaca;
}
</style>

```

--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Selector Engine - Unified selector generation and element location
 *
 * Modules:
 * - types: Type definitions
 * - stability: Stability scoring
 * - strategies: Selector generation strategies
 * - generator: Selector target generation
 * - locator: Element location
 * - fingerprint: Element fingerprinting (Phase 1.2)
 * - dom-path: DOM path computation (Phase 1.2)
 * - shadow-dom: Shadow DOM utilities (Phase 1.2)
 */

// Type exports
export * from './types';

// Stability scoring
export { computeSelectorStability, withStability, compareSelectorCandidates } from './stability';

// Selector strategies
export { DEFAULT_SELECTOR_STRATEGIES } from './strategies';
export { anchorRelpathStrategy } from './strategies/anchor-relpath';
export { ariaStrategy } from './strategies/aria';
export { cssPathStrategy } from './strategies/css-path';
export { cssUniqueStrategy } from './strategies/css-unique';
export { testIdStrategy } from './strategies/testid';
export { textStrategy } from './strategies/text';

// Selector generation
export {
  generateSelectorTarget,
  generateExtendedSelectorTarget,
  normalizeSelectorGenerationOptions,
  cssEscape,
  type GenerateSelectorTargetOptions,
} from './generator';

// Element location
export {
  SelectorLocator,
  createChromeSelectorLocator,
  createChromeSelectorLocatorTransport,
  type SelectorLocatorTransport,
} from './locator';

// Fingerprint utilities (Phase 1.2)
export {
  computeFingerprint,
  parseFingerprint,
  verifyFingerprint,
  fingerprintSimilarity,
  fingerprintMatches,
  type ElementFingerprint,
  type FingerprintOptions,
} from './fingerprint';

// DOM path utilities (Phase 1.2)
export {
  computeDomPath,
  locateByDomPath,
  compareDomPaths,
  isAncestorPath,
  getRelativePath,
  type DomPath,
} from './dom-path';

// Shadow DOM utilities (Phase 1.2)
export {
  traverseShadowDom,
  traverseShadowDomWithDetails,
  queryInShadowDom,
  queryAllInShadowDom,
  isUniqueInShadowDom,
  type ShadowTraversalResult,
  type ShadowTraversalFailureReason,
} from './shadow-dom';

```

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

```typescript
// control-flow-runner.ts — foreach / while orchestration

import type { ExecCtx } from '../../nodes';
import { RunLogger } from '../logging/run-logger';

export interface ControlFlowEnv {
  vars: Record<string, any>;
  logger: RunLogger;
  evalCondition: (cond: any) => boolean;
  runSubflowById: (subflowId: string, ctx: ExecCtx) => Promise<void>;
  isPaused: () => boolean;
}

export class ControlFlowRunner {
  constructor(private env: ControlFlowEnv) {}

  async run(control: any, ctx: ExecCtx): Promise<'ok' | 'paused'> {
    if (control?.kind === 'foreach') {
      const list = Array.isArray(this.env.vars[control.listVar])
        ? (this.env.vars[control.listVar] as any[])
        : [];
      const concurrency = Math.max(1, Math.min(16, Number(control.concurrency ?? 1)));
      if (concurrency <= 1) {
        for (const it of list) {
          this.env.vars[control.itemVar] = it;
          await this.env.runSubflowById(control.subflowId, ctx);
          if (this.env.isPaused()) return 'paused';
        }
        return this.env.isPaused() ? 'paused' : 'ok';
      }
      // Parallel with shallow-cloned vars per task (no automatic merge)
      let idx = 0;
      const runOne = async () => {
        while (idx < list.length) {
          const cur = idx++;
          const it = list[cur];
          const childCtx: ExecCtx = { ...ctx, vars: { ...this.env.vars } };
          childCtx.vars[control.itemVar] = it;
          await this.env.runSubflowById(control.subflowId, childCtx);
          if (this.env.isPaused()) return;
        }
      };
      const workers = Array.from({ length: Math.min(concurrency, list.length) }, () => runOne());
      await Promise.all(workers);
      return this.env.isPaused() ? 'paused' : 'ok';
    }
    if (control?.kind === 'while') {
      let i = 0;
      while (i < control.maxIterations && this.env.evalCondition(control.condition)) {
        await this.env.runSubflowById(control.subflowId, ctx);
        if (this.env.isPaused()) return 'paused';
        i++;
      }
      return this.env.isPaused() ? 'paused' : 'ok';
    }
    // Unknown control type → no-op
    return 'ok';
  }
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/plugins/manager.ts:
--------------------------------------------------------------------------------

```typescript
import type {
  RunPlugin,
  HookControl,
  RunContext,
  StepContext,
  StepAfterContext,
  StepErrorContext,
  StepRetryContext,
  RunEndContext,
  SubflowContext,
} from './types';

export class PluginManager {
  constructor(private plugins: RunPlugin[]) {}

  async runStart(ctx: RunContext) {
    for (const p of this.plugins) await safeCall(p, 'onRunStart', ctx);
  }

  async beforeStep(ctx: StepContext): Promise<HookControl | undefined> {
    for (const p of this.plugins) {
      const out = await safeCall(p, 'onBeforeStep', ctx);
      if (out && (out.pause || out.nextLabel)) return out;
    }
    return undefined;
  }

  async afterStep(ctx: StepAfterContext) {
    for (const p of this.plugins) await safeCall(p, 'onAfterStep', ctx);
  }

  async onError(ctx: StepErrorContext): Promise<HookControl | undefined> {
    for (const p of this.plugins) {
      const out = await safeCall(p, 'onStepError', ctx);
      if (out && (out.pause || out.nextLabel)) return out;
    }
    return undefined;
  }

  async onRetry(ctx: StepRetryContext) {
    for (const p of this.plugins) await safeCall(p, 'onRetry', ctx);
  }

  async onChooseNextLabel(ctx: StepContext & { suggested?: string }): Promise<string | undefined> {
    for (const p of this.plugins) {
      const out = await safeCall(p, 'onChooseNextLabel', ctx);
      if (out && out.nextLabel) return String(out.nextLabel);
    }
    return undefined;
  }

  async subflowStart(ctx: SubflowContext) {
    for (const p of this.plugins) await safeCall(p, 'onSubflowStart', ctx);
  }

  async subflowEnd(ctx: SubflowContext) {
    for (const p of this.plugins) await safeCall(p, 'onSubflowEnd', ctx);
  }

  async runEnd(ctx: RunEndContext) {
    for (const p of this.plugins) await safeCall(p, 'onRunEnd', ctx);
  }
}

async function safeCall<T extends keyof RunPlugin>(plugin: RunPlugin, key: T, arg: any) {
  try {
    const fn = plugin[key] as any;
    if (typeof fn === 'function') return await fn.call(plugin, arg);
  } catch (e) {
    // swallow plugin errors to keep core stable
    // console.warn(`[plugin:${plugin.name}] ${String(key)} error:`, e);
  }
  return undefined;
}

```

--------------------------------------------------------------------------------
/.github/workflows/build-release.yml:
--------------------------------------------------------------------------------

```yaml
# name: Build and Release Chrome Extension

# on:
#   push:
#     branches: [ master, develop ]
#     paths:
#       - 'app/chrome-extension/**'
#   pull_request:
#     branches: [ master ]
#     paths:
#       - 'app/chrome-extension/**'
#   workflow_dispatch:

# jobs:
#   build-extension:
#     runs-on: ubuntu-latest
    
#     steps:
#     - name: Checkout code
#       uses: actions/checkout@v4
      
#     - name: Setup Node.js
#       uses: actions/setup-node@v4
#       with:
#         node-version: '18'
#         cache: 'npm'
#         cache-dependency-path: 'app/chrome-extension/package-lock.json'
        
#     - name: Install dependencies
#       run: |
#         cd app/chrome-extension
#         npm ci
        
#     - name: Build extension
#       run: |
#         cd app/chrome-extension
#         npm run build
        
#     - name: Create zip package
#       run: |
#         cd app/chrome-extension
#         npm run zip
        
#     - name: Prepare release directory
#       run: |
#         mkdir -p releases/chrome-extension/latest
#         mkdir -p releases/chrome-extension/$(date +%Y%m%d-%H%M%S)
        
#     - name: Copy release files
#       run: |
#         # Copy to latest
#         cp app/chrome-extension/.output/chrome-mv3-prod.zip releases/chrome-extension/latest/chrome-mcp-server-latest.zip
        
#         # Copy to timestamped version
#         TIMESTAMP=$(date +%Y%m%d-%H%M%S)
#         cp app/chrome-extension/.output/chrome-mv3-prod.zip releases/chrome-extension/$TIMESTAMP/chrome-mcp-server-$TIMESTAMP.zip
        
#     - name: Upload build artifacts
#       uses: actions/upload-artifact@v4
#       with:
#         name: chrome-extension-build
#         path: releases/chrome-extension/
#         retention-days: 30
        
#     - name: Commit and push releases (if on main branch)
#       if: github.ref == 'refs/heads/main'
#       run: |
#         git config --local user.email "[email protected]"
#         git config --local user.name "GitHub Action"
#         git add releases/
#         git diff --staged --quiet || git commit -m "Auto-build: Update Chrome extension release [skip ci]"
#         git push

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/storage/triggers.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview 触发器存储
 * @description 实现触发器的 CRUD 操作(Phase 4 完整实现)
 */

import type { TriggerId } from '../domain/ids';
import type { TriggerSpec } from '../domain/triggers';
import type { TriggersStore } from '../engine/storage/storage-port';
import { RR_V3_STORES, withTransaction } from './db';

/**
 * 创建 TriggersStore 实现
 */
export function createTriggersStore(): TriggersStore {
  return {
    async list(): Promise<TriggerSpec[]> {
      return withTransaction(RR_V3_STORES.TRIGGERS, 'readonly', async (stores) => {
        const store = stores[RR_V3_STORES.TRIGGERS];
        return new Promise<TriggerSpec[]>((resolve, reject) => {
          const request = store.getAll();
          request.onsuccess = () => resolve(request.result as TriggerSpec[]);
          request.onerror = () => reject(request.error);
        });
      });
    },

    async get(id: TriggerId): Promise<TriggerSpec | null> {
      return withTransaction(RR_V3_STORES.TRIGGERS, 'readonly', async (stores) => {
        const store = stores[RR_V3_STORES.TRIGGERS];
        return new Promise<TriggerSpec | null>((resolve, reject) => {
          const request = store.get(id);
          request.onsuccess = () => resolve((request.result as TriggerSpec) ?? null);
          request.onerror = () => reject(request.error);
        });
      });
    },

    async save(spec: TriggerSpec): Promise<void> {
      return withTransaction(RR_V3_STORES.TRIGGERS, 'readwrite', async (stores) => {
        const store = stores[RR_V3_STORES.TRIGGERS];
        return new Promise<void>((resolve, reject) => {
          const request = store.put(spec);
          request.onsuccess = () => resolve();
          request.onerror = () => reject(request.error);
        });
      });
    },

    async delete(id: TriggerId): Promise<void> {
      return withTransaction(RR_V3_STORES.TRIGGERS, 'readwrite', async (stores) => {
        const store = stores[RR_V3_STORES.TRIGGERS];
        return new Promise<void>((resolve, reject) => {
          const request = store.delete(id);
          request.onsuccess = () => resolve();
          request.onerror = () => reject(request.error);
        });
      });
    },
  };
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/strategies/aria.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * ARIA Strategy - 基于无障碍属性的选择器策略
 * 使用 aria-label, role 等属性生成选择器
 */

import type { SelectorCandidate, SelectorStrategy } from '../types';

function guessRoleByTag(tag: string): string | undefined {
  if (tag === 'input' || tag === 'textarea') return 'textbox';
  if (tag === 'button') return 'button';
  if (tag === 'a') return 'link';
  return undefined;
}

function uniqStrings(items: ReadonlyArray<string>): string[] {
  const seen = new Set<string>();
  const out: string[] = [];
  for (const s of items) {
    const v = s.trim();
    if (!v) continue;
    if (seen.has(v)) continue;
    seen.add(v);
    out.push(v);
  }
  return out;
}

export const ariaStrategy: SelectorStrategy = {
  id: 'aria',
  generate(ctx) {
    if (!ctx.options.includeAria) return [];

    const { element, helpers } = ctx;
    const out: SelectorCandidate[] = [];

    const name = element.getAttribute('aria-label')?.trim();
    if (!name) return out;

    const tag = element.tagName?.toLowerCase?.() ?? '';
    const role = element.getAttribute('role')?.trim() || guessRoleByTag(tag);

    const qName = JSON.stringify(name);
    const selectors: string[] = [];

    if (role) selectors.push(`[role=${JSON.stringify(role)}][aria-label=${qName}]`);
    selectors.push(`[aria-label=${qName}]`);

    if (role === 'textbox') {
      selectors.unshift(
        `input[aria-label=${qName}]`,
        `textarea[aria-label=${qName}]`,
        `[role="textbox"][aria-label=${qName}]`,
      );
    } else if (role === 'button') {
      selectors.unshift(`button[aria-label=${qName}]`, `[role="button"][aria-label=${qName}]`);
    } else if (role === 'link') {
      selectors.unshift(`a[aria-label=${qName}]`, `[role="link"][aria-label=${qName}]`);
    }

    for (const sel of uniqStrings(selectors)) {
      if (helpers.isUnique(sel)) {
        out.push({ type: 'attr', value: sel, source: 'generated', strategy: 'aria' });
      }
    }

    // Structured aria candidate for UI/debugging (locator can translate it too).
    out.push({
      type: 'aria',
      value: `${role ?? 'element'}[name=${JSON.stringify(name)}]`,
      role,
      name,
      source: 'generated',
      strategy: 'aria',
    });

    return out;
  },
};

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/state-manager.ts:
--------------------------------------------------------------------------------

```typescript
// engine/state-manager.ts — lightweight run state store with events and persistence

type Listener<T> = (payload: T) => void;

export interface RunState {
  id: string;
  flowId: string;
  name?: string;
  status: 'running' | 'completed' | 'failed' | 'stopped';
  startedAt: number;
  updatedAt: number;
}

export class StateManager<T extends { id: string }> {
  private key: string;
  private states = new Map<string, T>();
  private listeners: Record<string, Listener<any>[]> = Object.create(null);

  constructor(storageKey: string) {
    this.key = storageKey;
  }

  on<E = any>(name: string, listener: Listener<E>) {
    (this.listeners[name] = this.listeners[name] || []).push(listener);
  }

  off<E = any>(name: string, listener: Listener<E>) {
    const arr = this.listeners[name];
    if (!arr) return;
    const i = arr.indexOf(listener as any);
    if (i >= 0) arr.splice(i, 1);
  }

  private emit<E = any>(name: string, payload: E) {
    const arr = this.listeners[name] || [];
    for (const fn of arr)
      try {
        fn(payload);
      } catch {}
  }

  getAll(): Map<string, T> {
    return this.states;
  }

  get(id: string): T | undefined {
    return this.states.get(id);
  }

  async add(id: string, data: T): Promise<void> {
    this.states.set(id, data);
    this.emit('add', { id, data });
    await this.persist();
  }

  async update(id: string, patch: Partial<T>): Promise<void> {
    const cur = this.states.get(id);
    if (!cur) return;
    const next = Object.assign({}, cur, patch);
    this.states.set(id, next);
    this.emit('update', { id, data: next });
    await this.persist();
  }

  async delete(id: string): Promise<void> {
    this.states.delete(id);
    this.emit('delete', { id });
    await this.persist();
  }

  private async persist(): Promise<void> {
    try {
      const obj = Object.fromEntries(this.states.entries());
      await chrome.storage.local.set({ [this.key]: obj });
    } catch {}
  }

  async restore(): Promise<void> {
    try {
      const res = await chrome.storage.local.get(this.key);
      const obj = (res && res[this.key]) || {};
      this.states = new Map(Object.entries(obj) as any);
    } catch {}
  }
}

export const runState = new StateManager<RunState>('rr_run_states');

```

--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/dom-observer.js:
--------------------------------------------------------------------------------

```javascript
/* eslint-disable */
// dom-observer.js - observe DOM for triggers and notify background
(function () {
  if (window.__RR_DOM_OBSERVER__) return;
  window.__RR_DOM_OBSERVER__ = true;

  const active = { triggers: [], hits: new Map() };

  function now() {
    return Date.now();
  }

  function applyTriggers(list) {
    try {
      active.triggers = Array.isArray(list) ? list.slice() : [];
      active.hits.clear();
      checkAll();
    } catch (e) {}
  }

  function checkAll() {
    try {
      for (const t of active.triggers) {
        maybeFire(t);
      }
    } catch (e) {}
  }

  function maybeFire(t) {
    try {
      const appear = t.appear !== false; // default true
      const sel = String(t.selector || '').trim();
      if (!sel) return;
      const exists = !!document.querySelector(sel);
      const key = t.id;
      const last = active.hits.get(key) || 0;
      const debounce = Math.max(0, Number(t.debounceMs ?? 800));
      if (now() - last < debounce) return;
      const should = appear ? exists : !exists;
      if (should) {
        active.hits.set(key, now());
        chrome.runtime.sendMessage({
          action: 'dom_trigger_fired',
          triggerId: t.id,
          url: location.href,
        });
        if (t.once !== false) removeTrigger(t.id);
      }
    } catch (e) {}
  }

  function removeTrigger(id) {
    try {
      active.triggers = active.triggers.filter((x) => x.id !== id);
    } catch (e) {}
  }

  const mo = new MutationObserver(() => {
    checkAll();
  });
  try {
    mo.observe(document.documentElement || document, {
      childList: true,
      subtree: true,
      attributes: false,
      characterData: false,
    });
  } catch (e) {}

  chrome.runtime.onMessage.addListener((req, _sender, sendResponse) => {
    try {
      if (req && req.action === 'dom_observer_ping') {
        sendResponse({ status: 'pong' });
        return false;
      }
      if (req && req.action === 'set_dom_triggers') {
        applyTriggers(req.triggers || []);
        sendResponse({ success: true, count: active.triggers.length });
        return true;
      }
    } catch (e) {
      sendResponse({ success: false, error: String(e && e.message ? e.message : e) });
      return true;
    }
    return false;
  });
})();

```

--------------------------------------------------------------------------------
/app/native-server/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "mcp-chrome-bridge",
  "version": "1.0.29",
  "description": "Chrome Native-Messaging host (Node)",
  "main": "dist/index.js",
  "bin": {
    "mcp-chrome-bridge": "./dist/cli.js",
    "chrome-mcp-bridge": "./dist/cli.js",
    "mcp-chrome-stdio": "./dist/mcp/mcp-server-stdio.js"
  },
  "scripts": {
    "dev": "nodemon --watch src --ext ts,js,json --ignore dist/ --exec \"npm run build && npm run register:dev\"",
    "build": "ts-node src/scripts/build.ts",
    "test": "jest",
    "test:watch": "jest --watch",
    "lint": "eslint 'src/**/*.{js,ts}'",
    "lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
    "format": "prettier --write 'src/**/*.{js,ts,json}'",
    "register:dev": "node dist/scripts/register-dev.js",
    "postinstall": "node dist/scripts/postinstall.js"
  },
  "files": [
    "dist",
    "!dist/node_path.txt"
  ],
  "engines": {
    "node": ">=20.0.0"
  },
  "preferGlobal": true,
  "keywords": [
    "mcp",
    "chrome",
    "browser"
  ],
  "author": "hangye",
  "license": "MIT",
  "dependencies": {
    "@anthropic-ai/claude-agent-sdk": "^0.1.69",
    "@fastify/cors": "^11.0.1",
    "@modelcontextprotocol/sdk": "^1.11.0",
    "@types/node-fetch": "2",
    "better-sqlite3": "^11.6.0",
    "chalk": "^5.4.1",
    "chrome-devtools-frontend": "^1.0.1299282",
    "chrome-mcp-shared": "workspace:*",
    "commander": "^13.1.0",
    "drizzle-orm": "^0.38.2",
    "fastify": "^5.3.2",
    "is-admin": "^4.0.0",
    "node-fetch": "2",
    "pino": "^9.6.0",
    "uuid": "^11.1.0"
  },
  "devDependencies": {
    "@jest/globals": "^29.7.0",
    "@types/better-sqlite3": "^7.6.12",
    "@types/chrome": "^0.0.318",
    "@types/jest": "^29.5.14",
    "@types/node": "^22.15.3",
    "@types/supertest": "^6.0.3",
    "@typescript-eslint/parser": "^8.31.1",
    "cross-env": "^7.0.3",
    "husky": "^9.1.7",
    "jest": "^29.7.0",
    "lint-staged": "^15.5.1",
    "nodemon": "^3.1.10",
    "pino-pretty": "^13.0.0",
    "rimraf": "^6.0.1",
    "supertest": "^7.1.0",
    "ts-jest": "^29.3.2",
    "ts-node": "^10.9.2"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,ts}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

```

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

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

export const scrollNode: NodeRuntime<StepScroll> = {
  run: async (ctx, step: StepScroll) => {
    const s = expandTemplatesDeep(step as StepScroll, ctx.vars);
    const top = s.offset?.y ?? undefined;
    const left = s.offset?.x ?? undefined;
    const selectorFromTarget = (s as any).target?.candidates?.find(
      (c: any) => c.type === 'css' || c.type === 'attr',
    )?.value;
    let code = '';
    if (s.mode === 'offset' && !(s as any).target) {
      const t = top != null ? Number(top) : 'undefined';
      const l = left != null ? Number(left) : 'undefined';
      code = `try { window.scrollTo({ top: ${t}, left: ${l}, behavior: 'instant' }); } catch (e) {}`;
    } else if (s.mode === 'element' && selectorFromTarget) {
      code = `(() => { try { const el = document.querySelector(${JSON.stringify(selectorFromTarget)}); if (el) el.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest' }); } catch (e) {} })();`;
    } else if (s.mode === 'container' && selectorFromTarget) {
      const t = top != null ? Number(top) : 'undefined';
      const l = left != null ? Number(left) : 'undefined';
      code = `(() => { try { const el = document.querySelector(${JSON.stringify(selectorFromTarget)}); if (el && typeof el.scrollTo === 'function') el.scrollTo({ top: ${t}, left: ${l}, behavior: 'instant' }); } catch (e) {} })();`;
    } else {
      const direction = top != null && Number(top) < 0 ? 'up' : 'down';
      const amount = 3;
      const res = await handleCallTool({
        name: TOOL_NAMES.BROWSER.COMPUTER,
        args: { action: 'scroll', scrollDirection: direction, scrollAmount: amount },
      });
      if ((res as any).isError) throw new Error('scroll failed');
      return {} as ExecResult;
    }
    if (code) {
      const res = await handleCallTool({
        name: TOOL_NAMES.BROWSER.INJECT_SCRIPT,
        args: { type: 'MAIN', jsScript: code },
      });
      if ((res as any).isError) throw new Error('scroll failed');
    }
    return {} as ExecResult;
  },
};

```

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

```vue
<template>
  <div class="flex flex-col gap-2">
    <!-- Project name input -->
    <div class="flex items-center gap-2">
      <span class="whitespace-nowrap w-12">Name</span>
      <input
        :value="name"
        class="flex-1 border border-slate-200 rounded px-2 py-1 text-xs bg-white focus:outline-none focus:ring-1 focus:ring-slate-400"
        placeholder="Project name"
        @input="handleNameInput"
      />
    </div>

    <!-- Root path selection -->
    <div class="flex items-center gap-2">
      <span class="whitespace-nowrap w-12">Root</span>
      <input
        :value="rootPath"
        readonly
        class="flex-1 border border-slate-200 rounded px-2 py-1 text-xs bg-slate-50 text-slate-600 focus:outline-none cursor-default"
        :placeholder="isLoadingDefault ? 'Loading...' : 'Select a directory'"
      />
      <button
        class="btn-secondary !px-2 !py-1 text-[11px] whitespace-nowrap"
        type="button"
        :disabled="isPicking"
        title="Use default directory (~/.chrome-mcp-agent/workspaces/...)"
        @click="$emit('use-default')"
      >
        Default
      </button>
      <button
        class="btn-secondary !px-2 !py-1 text-[11px] whitespace-nowrap"
        type="button"
        :disabled="isPicking"
        title="Open system directory picker"
        @click="$emit('pick-directory')"
      >
        {{ isPicking ? '...' : 'Browse' }}
      </button>
      <button
        class="btn-primary !px-2 !py-1 text-[11px]"
        type="button"
        :disabled="isCreating || !canCreate"
        @click="$emit('create')"
      >
        {{ isCreating ? 'Creating...' : 'Create' }}
      </button>
    </div>

    <!-- Error message -->
    <div v-if="error" class="text-[11px] text-red-600">
      {{ error }}
    </div>
  </div>
</template>

<script lang="ts" setup>
defineProps<{
  name: string;
  rootPath: string;
  isCreating: boolean;
  isPicking: boolean;
  isLoadingDefault: boolean;
  canCreate: boolean;
  error: string | null;
}>();

const emit = defineEmits<{
  'update:name': [value: string];
  'update:root-path': [value: string];
  'use-default': [];
  'pick-directory': [];
  create: [];
}>();

function handleNameInput(event: Event): void {
  const value = (event.target as HTMLInputElement).value;
  emit('update:name', value);
}
</script>

```

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

```vue
<template>
  <div
    :class="['workflow-node', selected ? 'selected' : '', `type-${data.node.type}`]"
    @click="onSelect()"
  >
    <div v-if="hasErrors" class="node-error" :title="errorsTitle">
      <ILucideShieldX />
      <div class="tooltip">
        <div class="item" v-for="e in errList" :key="e">• {{ e }}</div>
      </div>
    </div>
    <div class="node-container">
      <div :class="['node-icon', `icon-${data.node.type}`]">
        <component :is="iconComp(data.node.type)" />
      </div>
      <div class="node-body">
        <div class="node-name">{{ data.node.name || getTypeLabel(data.node.type) }}</div>
        <div class="node-subtitle">{{ subtitle }}</div>
      </div>
    </div>

    <!-- Hide left target handle for trigger (no inputs allowed) -->
    <Handle
      v-if="data.node.type !== 'trigger'"
      type="target"
      :position="Position.Left"
      :class="['node-handle', hasIncoming ? 'connected' : 'unconnected']"
    />
    <Handle
      v-if="data.node.type !== 'if'"
      type="source"
      :position="Position.Right"
      :class="['node-handle', hasOutgoing ? 'connected' : 'unconnected']"
    />
  </div>
</template>

<script lang="ts" setup>
// Reusable card-like node for most operation nodes
import { computed } from 'vue';
import type { NodeBase, Edge as EdgeV2 } from '@/entrypoints/background/record-replay/types';
import { Handle, Position } from '@vue-flow/core';
import { iconComp, getTypeLabel, nodeSubtitle } from './node-util';
import ILucideShieldX from '~icons/lucide/shield-x';

const props = defineProps<{
  id: string;
  data: { node: NodeBase; edges: EdgeV2[]; onSelect: (id: string) => void; errors?: string[] };
  selected?: boolean;
}>();

const subtitle = computed(() => nodeSubtitle(props.data.node));
const hasIncoming = computed(
  () => props.data.edges?.some?.((e) => e && e.to === props.data.node.id) || false,
);
const hasOutgoing = computed(
  () => props.data.edges?.some?.((e) => e && e.from === props.data.node.id) || false,
);
const errList = computed(() => (props.data.errors || []) as string[]);
const hasErrors = computed(() => errList.value.length > 0);
const errorsTitle = computed(() => errList.value.join('\n'));

function onSelect() {
  // keep event as function to avoid emitting through VueFlow slots
  try {
    props.data.onSelect(props.id);
  } catch {}
}
</script>

```

--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/inject-bridge.js:
--------------------------------------------------------------------------------

```javascript
/* eslint-disable */

(() => {
  // Prevent duplicate injection of the bridge itself.
  if (window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__) return;
  window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__ = true;
  const EVENT_NAME = {
    RESPONSE: 'chrome-mcp:response',
    CLEANUP: 'chrome-mcp:cleanup',
    EXECUTE: 'chrome-mcp:execute',
  };
  const pendingRequests = new Map();

  const messageHandler = (request, _sender, sendResponse) => {
    // --- Lifecycle Command ---
    if (request.type === EVENT_NAME.CLEANUP) {
      window.dispatchEvent(new CustomEvent(EVENT_NAME.CLEANUP));
      // Acknowledge cleanup signal received, but don't hold the connection.
      sendResponse({ success: true });
      return true;
    }

    // --- Execution Command for MAIN world ---
    if (request.targetWorld === 'MAIN') {
      const requestId = `req-${Date.now()}-${Math.random()}`;
      pendingRequests.set(requestId, sendResponse);

      window.dispatchEvent(
        new CustomEvent(EVENT_NAME.EXECUTE, {
          detail: {
            action: request.action,
            payload: request.payload,
            requestId: requestId,
          },
        }),
      );
      return true; // Async response is expected.
    }
    // Note: Requests for ISOLATED world are handled by the user's isolatedWorldCode script directly.
    // This listener won't process them unless it's the only script in ISOLATED world.
  };

  chrome.runtime.onMessage.addListener(messageHandler);

  // Listen for responses coming back from the MAIN world.
  const responseHandler = (event) => {
    const { requestId, data, error } = event.detail;
    if (pendingRequests.has(requestId)) {
      const sendResponse = pendingRequests.get(requestId);
      sendResponse({ data, error });
      pendingRequests.delete(requestId);
    }
  };
  window.addEventListener(EVENT_NAME.RESPONSE, responseHandler);

  // --- Self Cleanup ---
  // When the cleanup signal arrives, this bridge must also clean itself up.
  const cleanupHandler = () => {
    chrome.runtime.onMessage.removeListener(messageHandler);
    window.removeEventListener(EVENT_NAME.RESPONSE, responseHandler);
    window.removeEventListener(EVENT_NAME.CLEANUP, cleanupHandler);
    delete window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__;
  };
  window.addEventListener(EVENT_NAME.CLEANUP, cleanupHandler);
})();

```

--------------------------------------------------------------------------------
/docs/VisualEditor.md:
--------------------------------------------------------------------------------

```markdown
# A Visual Editor for Claude Code & Codex

**How to enable:**
`Right Click > Chrome MCP Server > Toggle Web Editing Mode`
**Shortcut:** `Cmd/Ctrl` + `Shift` + `O`

### Interactive Sizing & Layout Adjustment

Directly drag element edges on the canvas to adjust width, height, and font sizes. All visual manipulations are automatically converted into code suggestions and applied to your source code by the Agent, bridging the gap between design and development in real-time.

<div align="center">
  <a href="https://youtu.be/76_DsUU7aHs">
    <img src="https://img.youtube.com/vi/76_DsUU7aHs/maxresdefault.jpg" alt="Interactive Sizing & Layout Adjustment" style="width:100%; max-width:600px;">
  </a>
</div>

### Visual Property Controls

Manage CSS properties directly through a visual inspector panel. Effortlessly tweak Flex/Grid layouts, margins, padding, and styling details with a single click. Perfect for rapid prototyping or UI fine-tuning, significantly reducing the time spent writing raw CSS.

<div align="center">
  <a href="https://youtu.be/ADOzT7El2mI">
    <img src="https://img.youtube.com/vi/76_DsUU7aHs/maxresdefault.jpg" alt="Interactive Sizing & Layout Adjustment" style="width:100%; max-width:600px;">
  </a>
</div>

### Live Component State Debugging (Vue/React)

Inspect and modify React and Vue component props in real-time. Test how your components render under different data states without ever leaving your current view or writing temporary console logs.

<div align="center">
  <a href="https://youtu.be/PaIxdpGcEEk">
    <img src="https://img.youtube.com/vi/76_DsUU7aHs/maxresdefault.jpg" alt="Interactive Sizing & Layout Adjustment" style="width:100%; max-width:600px;">
  </a>
</div>

### Point, Click & Prompt

Select any element on the page and send instructions directly to Claude Code or Codex. The tool automatically captures the component's structure and context, enabling the AI to provide modifications with far greater precision and lower latency than global chat contexts.

Simply click an element and say, _"Make this bigger"_ or _"Change the background to red"_, and watch Claude Code implement the exact changes in seconds.

<div align="center">
  <a href="https://youtu.be/dSkt5HaTU_s">
    <img src="https://img.youtube.com/vi/76_DsUU7aHs/maxresdefault.jpg" alt="Interactive Sizing & Layout Adjustment" style="width:100%; max-width:600px;">
  </a>
</div>

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/keepalive-manager.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Keepalive Manager
 * @description Global singleton service for managing Service Worker keepalive.
 *
 * This module provides a unified interface for acquiring and releasing keepalive
 * references. Multiple modules can acquire keepalive independently using tags,
 * and the underlying keepalive mechanism will remain active as long as at least
 * one reference is held.
 */

import {
  createOffscreenKeepaliveController,
  type KeepaliveController,
} from './record-replay-v3/engine/keepalive/offscreen-keepalive';

const LOG_PREFIX = '[KeepaliveManager]';

/**
 * Singleton keepalive controller instance.
 * Created lazily to avoid initialization issues during module loading.
 */
let controller: KeepaliveController | null = null;

/**
 * Get or create the singleton keepalive controller.
 */
function getController(): KeepaliveController {
  if (!controller) {
    controller = createOffscreenKeepaliveController({ logger: console });
    console.debug(`${LOG_PREFIX} Controller initialized`);
  }
  return controller;
}

/**
 * Acquire a keepalive reference with a tag.
 *
 * @param tag - Identifier for the reference (e.g., 'native-host', 'rr-engine')
 * @returns A release function to call when keepalive is no longer needed
 *
 * @example
 * ```typescript
 * const release = acquireKeepalive('native-host');
 * // ... do work that needs SW to stay alive ...
 * release(); // Release when done
 * ```
 */
export function acquireKeepalive(tag: string): () => void {
  try {
    const release = getController().acquire(tag);
    console.debug(`${LOG_PREFIX} Acquired keepalive for tag: ${tag}`);
    return () => {
      try {
        release();
        console.debug(`${LOG_PREFIX} Released keepalive for tag: ${tag}`);
      } catch (error) {
        console.warn(`${LOG_PREFIX} Failed to release keepalive for ${tag}:`, error);
      }
    };
  } catch (error) {
    console.warn(`${LOG_PREFIX} Failed to acquire keepalive for ${tag}:`, error);
    return () => {};
  }
}

/**
 * Check if keepalive is currently active (any references held).
 */
export function isKeepaliveActive(): boolean {
  try {
    return getController().isActive();
  } catch {
    return false;
  }
}

/**
 * Get the current keepalive reference count.
 * Useful for debugging.
 */
export function getKeepaliveRefCount(): number {
  try {
    return getController().getRefCount();
  } catch {
    return 0;
  }
}

```

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

```typescript
import type { Step } from '../types';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';
import { clickNode, dblclickNode } from './click';
import { fillNode } from './fill';
import { httpNode } from './http';
import { extractNode } from './extract';
import { scriptNode } from './script';
import { openTabNode, switchTabNode, closeTabNode } from './tabs';
import { scrollNode } from './scroll';
import { dragNode } from './drag';
import { keyNode } from './key';
import { waitNode } from './wait';
import { assertNode } from './assert';
import { navigateNode } from './navigate';
import { ifNode } from './conditional';
import { STEP_TYPES } from 'chrome-mcp-shared';
import { foreachNode, whileNode } from './loops';
import { executeFlowNode } from './execute-flow';
import {
  handleDownloadNode,
  screenshotNode,
  triggerEventNode,
  setAttributeNode,
  switchFrameNode,
  loopElementsNode,
} from './download-screenshot-attr-event-frame-loop';

const registry = new Map<string, NodeRuntime<any>>([
  [STEP_TYPES.CLICK, clickNode],
  [STEP_TYPES.DBLCLICK, dblclickNode],
  [STEP_TYPES.FILL, fillNode],
  [STEP_TYPES.HTTP, httpNode],
  [STEP_TYPES.EXTRACT, extractNode],
  [STEP_TYPES.SCRIPT, scriptNode],
  [STEP_TYPES.OPEN_TAB, openTabNode],
  [STEP_TYPES.SWITCH_TAB, switchTabNode],
  [STEP_TYPES.CLOSE_TAB, closeTabNode],
  [STEP_TYPES.SCROLL, scrollNode],
  [STEP_TYPES.DRAG, dragNode],
  [STEP_TYPES.KEY, keyNode],
  [STEP_TYPES.WAIT, waitNode],
  [STEP_TYPES.ASSERT, assertNode],
  [STEP_TYPES.NAVIGATE, navigateNode],
  [STEP_TYPES.IF, ifNode],
  [STEP_TYPES.FOREACH, foreachNode],
  [STEP_TYPES.WHILE, whileNode],
  [STEP_TYPES.EXECUTE_FLOW, executeFlowNode],
  [STEP_TYPES.HANDLE_DOWNLOAD, handleDownloadNode],
  [STEP_TYPES.SCREENSHOT, screenshotNode],
  [STEP_TYPES.TRIGGER_EVENT, triggerEventNode],
  [STEP_TYPES.SET_ATTRIBUTE, setAttributeNode],
  [STEP_TYPES.SWITCH_FRAME, switchFrameNode],
  [STEP_TYPES.LOOP_ELEMENTS, loopElementsNode],
]);

export async function executeStep(ctx: ExecCtx, step: Step): Promise<ExecResult> {
  const rt = registry.get(step.type);
  if (!rt) throw new Error(`unsupported step type: ${String(step.type)}`);
  const v = rt.validate ? rt.validate(step) : { ok: true };
  if (!v.ok) throw new Error((v.errors || []).join(', ') || 'validation failed');
  const out = await rt.run(ctx, step);
  return out || {};
}

export type { ExecCtx, ExecResult, NodeRuntime } from './types';

```

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

```vue
<template>
  <div>
    <div class="form-row">
      <label class="form-label">模式</label>
      <select v-model="cfg.mode" class="form-select-sm">
        <option value="element">滚动到元素</option>
        <option value="offset">窗口偏移</option>
        <option value="container">容器偏移</option>
      </select>
    </div>

    <div v-if="cfg.mode === 'element'" class="mt-2">
      <SelectorEditor :node="node" :allowPick="true" title="目标元素" targetKey="target" />
    </div>

    <div v-if="cfg.mode !== 'element'" class="mt-2">
      <div class="form-row">
        <label class="form-label">偏移 X</label>
        <input type="number" class="form-input-sm" v-model.number="cfg.offset.x" placeholder="0" />
      </div>
      <div class="form-row">
        <label class="form-label">偏移 Y</label>
        <input
          type="number"
          class="form-input-sm"
          v-model.number="cfg.offset.y"
          placeholder="300"
        />
      </div>
      <div v-if="cfg.mode === 'container'" class="mt-2">
        <SelectorEditor :node="node" :allowPick="true" title="容器选择器" targetKey="target" />
        <div class="hint"><small>容器需支持 scrollTo(top,left)</small></div>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
import SelectorEditor from './SelectorEditor.vue';

const props = defineProps<{ node: NodeBase }>();

function ensure() {
  const n: any = props.node;
  n.config = n.config || {};
  if (!n.config.mode) n.config.mode = 'offset';
  if (!n.config.offset) n.config.offset = { x: 0, y: 300 };
  if (!n.config.target) n.config.target = { candidates: [] };
}

const cfg = {
  get mode() {
    ensure();
    return (props.node as any).config.mode;
  },
  set mode(v: any) {
    ensure();
    (props.node as any).config.mode = v;
  },
  get offset() {
    ensure();
    return (props.node as any).config.offset;
  },
  set offset(v: any) {
    ensure();
    (props.node as any).config.offset = v;
  },
} as any;
</script>

<style scoped>
.hint {
  color: #64748b;
  margin-top: 8px;
}
.mt-2 {
  margin-top: 8px;
}
.form-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 6px 0;
}
.form-label {
  width: 80px;
  color: #334155;
  font-size: 12px;
}
.form-input-sm,
.form-select-sm {
  flex: 1;
  padding: 6px 8px;
  border: 1px solid var(--rr-border);
  border-radius: 6px;
}
</style>

```

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

```vue
<template>
  <div class="target-locator">
    <!-- Reuse FieldSelector UI for picking/typing a selector -->
    <FieldSelector v-model="text" :field="{ placeholder }" />
  </div>
</template>

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

type Candidate = { type: 'css' | 'attr' | 'aria' | 'text' | 'xpath'; value: string };
type TargetLocator = { ref?: string; candidates?: Candidate[] };

const props = defineProps<{ modelValue?: TargetLocator | string; field?: any }>();
const emit = defineEmits<{ (e: 'update:modelValue', v?: TargetLocator): void }>();

const placeholder = props.field?.placeholder || '.btn.primary';
const text = ref<string>('');
// guard to prevent emitting during initial/prop-driven sync
const updatingFromProps = ref<boolean>(false);

// derive text from incoming modelValue (supports string or structured object)
watch(
  () => props.modelValue,
  (mv: any) => {
    updatingFromProps.value = true;
    if (!mv) {
      text.value = '';
      nextTick(() => (updatingFromProps.value = false));
      return;
    }
    if (typeof mv === 'string') {
      text.value = mv;
      nextTick(() => (updatingFromProps.value = false));
      return;
    }
    try {
      const arr: Candidate[] = Array.isArray(mv.candidates) ? mv.candidates : [];
      const prefer = ['css', 'attr', 'aria', 'text', 'xpath'];
      let val = '';
      for (const t of prefer) {
        const c = arr.find((x) => x && x.type === t && x.value);
        if (c) {
          val = String(c.value || '');
          break;
        }
      }
      if (!val) val = arr[0]?.value ? String(arr[0].value) : '';
      text.value = val;
    } catch {
      text.value = '';
    }
    nextTick(() => (updatingFromProps.value = false));
  },
  { immediate: true, deep: true },
);

// whenever text changes, emit structured TargetLocator (skip when syncing from props)
watch(
  () => text.value,
  (v) => {
    if (updatingFromProps.value) return;
    const s = String(v || '').trim();
    if (!s) {
      emit('update:modelValue', { candidates: [] });
    } else {
      emit('update:modelValue', {
        ...(typeof props.modelValue === 'object' && props.modelValue
          ? (props.modelValue as any)
          : {}),
        candidates: [{ type: 'css', value: s }],
      });
    }
  },
);
</script>

<style scoped>
.target-locator {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
</style>

```

--------------------------------------------------------------------------------
/app/native-server/src/agent/tool-bridge.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { NATIVE_SERVER_PORT } from '../constant/index.js';

export interface CliToolInvocation {
  /**
   * The MCP server identifier (if provided by CLI).
   * When omitted, this bridge defaults to the local chrome MCP server.
   */
  server?: string;
  /**
   * The MCP tool name to invoke.
   */
  tool: string;
  /**
   * JSON-serializable arguments for the tool call.
   */
  args?: Record<string, unknown>;
}

export interface AgentToolBridgeOptions {
  /**
   * Base URL of the local MCP HTTP endpoint (e.g. http://127.0.0.1:12306/mcp).
   * If omitted, DEFAULT_SERVER_PORT from chrome-mcp-shared is used.
   */
  mcpUrl?: string;
}

/**
 * AgentToolBridge maps CLI tool events (Codex, etc.) to MCP tool calls
 * against the local chrome MCP server via the official MCP SDK client.
 *
 * 中文说明:该桥接层负责将 CLI 上报的工具调用统一转为标准 MCP CallTool 请求,
 * 复用现有 /mcp HTTP server,而不是在本项目内自研额外协议。
 */
export class AgentToolBridge {
  private readonly client: Client;
  private readonly transport: StreamableHTTPClientTransport;

  constructor(options: AgentToolBridgeOptions = {}) {
    const url =
      options.mcpUrl || `http://127.0.0.1:${process.env.MCP_HTTP_PORT || NATIVE_SERVER_PORT}/mcp`;

    this.transport = new StreamableHTTPClientTransport(new URL(url));
    this.client = new Client(
      {
        name: 'chrome-mcp-agent-bridge',
        version: '1.0.0',
      },
      {},
    );
  }

  /**
   * Connects the MCP client over Streamable HTTP if not already connected.
   */
  async ensureConnected(): Promise<void> {
    // Client.connect is idempotent; repeated calls reuse the same transport session.
    if ((this.transport as any)._sessionId) {
      return;
    }
    await this.client.connect(this.transport);
  }

  /**
   * Invoke an MCP tool based on a CLI tool event.
   * Returns the raw result from MCP client.callTool().
   */
  async callTool(invocation: CliToolInvocation): Promise<CallToolResult> {
    await this.ensureConnected();

    const args = invocation.args ?? {};
    const result = await this.client.callTool({
      name: invocation.tool,
      arguments: args,
    });

    // The SDK returns a compatible structure; cast to satisfy strict typing.
    return result as unknown as CallToolResult;
  }
}

```

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

```typescript
/**
 * Agent Chat Composables
 * Export all composables for agent chat functionality.
 */
export { useAgentServer } from './useAgentServer';
export { useAgentChat } from './useAgentChat';
export { useAgentProjects } from './useAgentProjects';
export { useAgentSessions } from './useAgentSessions';
export { useAttachments, type AttachmentWithPreview } from './useAttachments';
export { useAgentTheme, preloadAgentTheme, THEME_LABELS } from './useAgentTheme';
export { useAgentThreads, AGENT_SERVER_PORT_KEY } from './useAgentThreads';
export { useWebEditorTxState, WEB_EDITOR_TX_STATE_INJECTION_KEY } from './useWebEditorTxState';
export { useAgentChatViewRoute } from './useAgentChatViewRoute';

export type { UseAgentServerOptions } from './useAgentServer';
export type { UseAgentChatOptions } from './useAgentChat';
export type { UseAgentProjectsOptions } from './useAgentProjects';
export type { UseAgentSessionsOptions } from './useAgentSessions';
export type { AgentThemeId, UseAgentTheme } from './useAgentTheme';
export type {
  AgentThread,
  TimelineItem,
  ToolPresentation,
  ToolKind,
  ToolSeverity,
  AgentThreadState,
  UseAgentThreadsOptions,
  ThreadHeader,
  WebEditorApplyMeta,
} from './useAgentThreads';
export type { UseWebEditorTxStateOptions, WebEditorTxStateReturn } from './useWebEditorTxState';
export type {
  AgentChatView,
  AgentChatRouteState,
  UseAgentChatViewRouteOptions,
  UseAgentChatViewRoute,
} from './useAgentChatViewRoute';

// RR V3 Composables
export { useRRV3Rpc } from './useRRV3Rpc';
export { useRRV3Debugger } from './useRRV3Debugger';
export type { UseRRV3Rpc, UseRRV3RpcOptions, RpcRequestOptions } from './useRRV3Rpc';
export type { UseRRV3Debugger, UseRRV3DebuggerOptions } from './useRRV3Debugger';

// Textarea Auto-Resize
export { useTextareaAutoResize } from './useTextareaAutoResize';
export type {
  UseTextareaAutoResizeOptions,
  UseTextareaAutoResizeReturn,
} from './useTextareaAutoResize';

// Fake Caret (comet tail animation)
export { useFakeCaret } from './useFakeCaret';
export type { UseFakeCaretOptions, UseFakeCaretReturn, FakeCaretTrailPoint } from './useFakeCaret';

// Open Project Preference
export { useOpenProjectPreference } from './useOpenProjectPreference';
export type {
  UseOpenProjectPreferenceOptions,
  UseOpenProjectPreference,
} from './useOpenProjectPreference';

// Agent Input Preferences (fake caret, etc.)
export { useAgentInputPreferences } from './useAgentInputPreferences';
export type { UseAgentInputPreferences } from './useAgentInputPreferences';

```

--------------------------------------------------------------------------------
/app/chrome-extension/common/element-marker-types.ts:
--------------------------------------------------------------------------------

```typescript
// Element marker types shared across background, content scripts, and popup

export type UrlMatchType = 'exact' | 'prefix' | 'host';

export interface ElementMarker {
  id: string;
  // Original URL where the marker was created
  url: string;
  // Normalized pieces to support matching
  origin: string; // scheme + host + port
  host: string; // hostname
  path: string; // pathname part only
  matchType: UrlMatchType; // default: 'prefix'

  name: string; // Human-friendly name, e.g., "Login Button"
  selector: string; // Selector string
  selectorType?: 'css' | 'xpath'; // Default: css
  listMode?: boolean; // Whether this marker was created in list mode (allows multiple matches)
  action?: 'click' | 'fill' | 'custom'; // Intended action hint (optional)

  createdAt: number;
  updatedAt: number;
}

export interface UpsertMarkerRequest {
  id?: string;
  url: string;
  name: string;
  selector: string;
  selectorType?: 'css' | 'xpath';
  listMode?: boolean;
  matchType?: UrlMatchType;
  action?: 'click' | 'fill' | 'custom';
}

// Validation actions for MCP-integrated verification
export enum MarkerValidationAction {
  Hover = 'hover',
  LeftClick = 'left_click',
  RightClick = 'right_click',
  DoubleClick = 'double_click',
  TypeText = 'type_text',
  PressKeys = 'press_keys',
  Scroll = 'scroll',
}

export interface MarkerValidationRequest {
  selector: string;
  selectorType?: 'css' | 'xpath';
  action: MarkerValidationAction;
  // Optional payload for certain actions
  text?: string; // for type_text
  keys?: string; // for press_keys
  // Event options for click-like actions
  button?: 'left' | 'right' | 'middle';
  bubbles?: boolean;
  cancelable?: boolean;
  modifiers?: { altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean };
  // Targeting options
  coordinates?: { x: number; y: number }; // absolute viewport coords
  offsetX?: number; // relative to element center if relativeTo = 'element'
  offsetY?: number;
  relativeTo?: 'element' | 'viewport';
  // Navigation options for click-like actions
  waitForNavigation?: boolean;
  timeoutMs?: number;
  // Scroll options
  scrollDirection?: 'up' | 'down' | 'left' | 'right';
  scrollAmount?: number; // pixels per tick
}

export interface MarkerValidationResponse {
  success: boolean;
  resolved?: boolean;
  ref?: string;
  center?: { x: number; y: number };
  tool?: { name: string; ok: boolean; error?: string };
  error?: string;
}

export interface MarkerQuery {
  url?: string; // If present, query by URL match; otherwise list all
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/tests/vitest.setup.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Vitest Global Setup
 * @description Provides global configuration and polyfills for test environment
 */

import { vi } from 'vitest';

// Provide IndexedDB globals (jsdom doesn't include them)
import 'fake-indexeddb/auto';

// Mock chrome API (basic placeholder)
if (typeof globalThis.chrome === 'undefined') {
  (globalThis as unknown as { chrome: object }).chrome = {
    runtime: {
      id: 'test-extension-id',
      sendMessage: vi.fn().mockResolvedValue(undefined),
      onMessage: {
        addListener: vi.fn(),
        removeListener: vi.fn(),
      },
      connect: vi.fn().mockReturnValue({
        onMessage: { addListener: vi.fn(), removeListener: vi.fn() },
        onDisconnect: { addListener: vi.fn(), removeListener: vi.fn() },
        postMessage: vi.fn(),
        disconnect: vi.fn(),
      }),
    },
    storage: {
      local: {
        get: vi.fn().mockResolvedValue({}),
        set: vi.fn().mockResolvedValue(undefined),
        remove: vi.fn().mockResolvedValue(undefined),
      },
    },
    tabs: {
      query: vi.fn().mockResolvedValue([]),
      get: vi.fn().mockResolvedValue(null),
      create: vi.fn().mockResolvedValue({ id: 1 }),
      update: vi.fn().mockResolvedValue({}),
      remove: vi.fn().mockResolvedValue(undefined),
      captureVisibleTab: vi.fn().mockResolvedValue('data:image/png;base64,'),
      onRemoved: { addListener: vi.fn(), removeListener: vi.fn() },
      onCreated: { addListener: vi.fn(), removeListener: vi.fn() },
      onUpdated: { addListener: vi.fn(), removeListener: vi.fn() },
    },
    webRequest: {
      onBeforeRequest: { addListener: vi.fn(), removeListener: vi.fn() },
      onCompleted: { addListener: vi.fn(), removeListener: vi.fn() },
      onErrorOccurred: { addListener: vi.fn(), removeListener: vi.fn() },
    },
    webNavigation: {
      onCommitted: { addListener: vi.fn(), removeListener: vi.fn() },
      onDOMContentLoaded: { addListener: vi.fn(), removeListener: vi.fn() },
      onCompleted: { addListener: vi.fn(), removeListener: vi.fn() },
    },
    debugger: {
      onEvent: { addListener: vi.fn(), removeListener: vi.fn() },
      onDetach: { addListener: vi.fn(), removeListener: vi.fn() },
      attach: vi.fn().mockResolvedValue(undefined),
      detach: vi.fn().mockResolvedValue(undefined),
      sendCommand: vi.fn().mockResolvedValue({}),
    },
    commands: {
      onCommand: { addListener: vi.fn(), removeListener: vi.fn() },
    },
    contextMenus: {
      create: vi.fn(),
      remove: vi.fn(),
      onClicked: { addListener: vi.fn(), removeListener: vi.fn() },
    },
  };
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/recording/browser-event-listener.ts:
--------------------------------------------------------------------------------

```typescript
import { addNavigationStep } from './flow-builder';
import { STEP_TYPES } from '@/common/step-types';
import { ensureRecorderInjected, broadcastControlToTab, REC_CMD } from './content-injection';
import type { RecordingSessionManager } from './session-manager';
import type { Step } from '../types';

export function initBrowserEventListeners(session: RecordingSessionManager): void {
  chrome.tabs.onActivated.addListener(async (activeInfo) => {
    try {
      if (session.getStatus() !== 'recording') return;
      const tabId = activeInfo.tabId;
      await ensureRecorderInjected(tabId);
      await broadcastControlToTab(tabId, REC_CMD.START);
      // Track active tab for targeted STOP later
      session.addActiveTab(tabId);

      const flow = session.getFlow();
      if (!flow) return;
      const tab = await chrome.tabs.get(tabId);
      const url = tab.url;
      const step: Step = {
        id: '',
        type: STEP_TYPES.SWITCH_TAB,
        ...(url ? { urlContains: url } : {}),
      };
      session.appendSteps([step]);
    } catch (e) {
      console.warn('onActivated handler failed', e);
    }
  });

  chrome.webNavigation.onCommitted.addListener(async (details) => {
    try {
      if (session.getStatus() !== 'recording') return;
      if (details.frameId !== 0) return;
      const tabId = details.tabId;
      const t = details.transitionType;
      const link = t === 'link';
      if (!link) {
        const shouldRecord =
          t === 'reload' ||
          t === 'typed' ||
          t === 'generated' ||
          t === 'auto_bookmark' ||
          t === 'keyword' ||
          // include form_submit to better capture Enter-to-search navigations
          t === 'form_submit';
        if (shouldRecord) {
          const tab = await chrome.tabs.get(tabId);
          const url = tab.url || details.url;
          const flow = session.getFlow();
          if (flow && url) addNavigationStep(flow, url);
        }
      }
      await ensureRecorderInjected(tabId);
      await broadcastControlToTab(tabId, REC_CMD.START);
      // Track active tab for targeted STOP later
      session.addActiveTab(tabId);
      if (session.getFlow()) {
        session.broadcastTimelineUpdate();
      }
    } catch (e) {
      console.warn('onCommitted handler failed', e);
    }
  });

  // Remove closed tabs from the active set to avoid stale broadcasts
  chrome.tabs.onRemoved.addListener((tabId) => {
    try {
      // Even if not recording, removing is harmless; keep guard for clarity
      if (session.getStatus() !== 'recording') return;
      session.removeActiveTab(tabId);
    } catch (e) {
      console.warn('onRemoved handler failed', e);
    }
  });
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/recording/content-injection.ts:
--------------------------------------------------------------------------------

```typescript
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';

// Avoid magic strings for recorder control commands
export type RecorderCmd = 'start' | 'stop' | 'pause' | 'resume';
export const REC_CMD = {
  START: 'start',
  STOP: 'stop',
  PAUSE: 'pause',
  RESUME: 'resume',
} as const satisfies Record<string, RecorderCmd>;

const RECORDER_JS_SCRIPT = 'inject-scripts/recorder.js';

export async function ensureRecorderInjected(tabId: number): Promise<void> {
  // Discover frames (top + subframes)
  let frames: Array<{ frameId: number } & Record<string, any>> = [];
  try {
    const res = (await chrome.webNavigation.getAllFrames({ tabId })) as
      | Array<{ frameId: number } & Record<string, any>>
      | null
      | undefined;
    frames = Array.isArray(res) ? res : [];
  } catch {
    // ignore and fallback to top frame only
  }
  if (frames.length === 0) frames = [{ frameId: 0 }];

  const needRecorder: number[] = [];
  await Promise.all(
    frames.map(async (f) => {
      const frameId = f.frameId ?? 0;
      try {
        const res = await chrome.tabs.sendMessage(
          tabId,
          { action: 'rr_recorder_ping' },
          { frameId },
        );
        const pong = res?.status === 'pong';
        if (!pong) needRecorder.push(frameId);
      } catch {
        needRecorder.push(frameId);
      }
    }),
  );

  if (needRecorder.length > 0) {
    try {
      await chrome.scripting.executeScript({
        target: { tabId, frameIds: needRecorder },
        files: [RECORDER_JS_SCRIPT],
        world: 'ISOLATED',
      });
    } catch {
      // Fallback: try allFrames to cover dynamic/subframe changes; safe due to idempotent guard in recorder.js
      try {
        await chrome.scripting.executeScript({
          target: { tabId, allFrames: true },
          files: [RECORDER_JS_SCRIPT],
          world: 'ISOLATED',
        });
      } catch {
        // ignore injection failures per-tab
      }
    }
  }
}

export async function broadcastControlToTab(
  tabId: number,
  cmd: RecorderCmd,
  meta?: unknown,
): Promise<void> {
  try {
    const res = (await chrome.webNavigation.getAllFrames({ tabId })) as
      | Array<{ frameId: number } & Record<string, any>>
      | null
      | undefined;
    const targets = Array.isArray(res) && res.length ? res : [{ frameId: 0 }];
    await Promise.all(
      targets.map(async (f) => {
        try {
          await chrome.tabs.sendMessage(
            tabId,
            { action: TOOL_MESSAGE_TYPES.RR_RECORDER_CONTROL, cmd, meta },
            { frameId: f.frameId },
          );
        } catch {
          // ignore per-frame send failure
        }
      }),
    );
  } catch {
    // ignore
  }
}

```

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

```vue
<template>
  <div class="selector">
    <div class="row">
      <input class="form-input" :placeholder="placeholder" :value="text" @input="onInput" />
      <button class="btn-mini" type="button" title="从页面拾取" @click="onPick">拾取</button>
    </div>
    <div class="help">可输入 CSS 选择器,或点击“拾取”在页面中选择元素</div>
    <div v-if="err" class="error-item">{{ err }}</div>
  </div>
</template>

<script lang="ts" setup>
import { ref, watchEffect } from 'vue';
const props = defineProps<{ modelValue?: string; field?: any }>();
const emit = defineEmits<{ (e: 'update:modelValue', v?: string): void }>();
const text = ref<string>(props.modelValue ?? '');
const placeholder = props.field?.placeholder || '.btn.primary';
function onInput(ev: any) {
  const v = String(ev?.target?.value ?? '');
  text.value = v;
  emit('update:modelValue', v);
}
watchEffect(() => (text.value = props.modelValue ?? ''));

const err = ref<string>('');
async function ensurePickerInjected(tabId: number) {
  try {
    const pong = await chrome.tabs.sendMessage(tabId, { action: 'chrome_read_page_ping' } as any);
    if (pong && pong.status === 'pong') return;
  } catch {}
  try {
    await chrome.scripting.executeScript({
      target: { tabId },
      files: ['inject-scripts/accessibility-tree-helper.js'],
      world: 'ISOLATED',
    } as any);
  } catch (e) {
    console.warn('inject picker helper failed:', e);
  }
}

async function onPick() {
  try {
    err.value = '';
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const tabId = tabs?.[0]?.id;
    if (!tabId) throw new Error('未找到活动页签');
    await ensurePickerInjected(tabId);
    const res: any = await chrome.tabs.sendMessage(tabId, { action: 'rr_picker_start' } as any);
    if (!res || !res.success) {
      if (res?.cancelled) return;
      throw new Error(res?.error || '拾取失败');
    }
    const candidates = Array.isArray(res.candidates) ? res.candidates : [];
    const prefer = ['css', 'attr', 'aria', 'text'];
    let sel = '';
    for (const t of prefer) {
      const c = candidates.find((x: any) => x.type === t && x.value);
      if (c) {
        sel = String(c.value);
        break;
      }
    }
    if (!sel && candidates[0]?.value) sel = String(candidates[0].value);
    if (sel) {
      text.value = sel;
      emit('update:modelValue', sel);
    } else {
      err.value = '未生成有效选择器,请手动输入';
    }
  } catch (e: any) {
    err.value = e?.message || String(e);
  }
}
</script>

<style scoped>
.row {
  display: flex;
  gap: 8px;
  align-items: center;
}
.btn-mini {
  font-size: 12px;
  padding: 2px 6px;
  border: 1px solid var(--rr-border);
  border-radius: 6px;
}
.error-item {
  font-size: 12px;
  color: #ff6666;
  margin-top: 6px;
}
</style>

```

--------------------------------------------------------------------------------
/docs/mcp-cli-config.md:
--------------------------------------------------------------------------------

```markdown
# CLI MCP Configuration Guide

This guide explains how to configure Codex CLI and Claude Code to connect to the Chrome MCP Server.

## Overview

The Chrome MCP Server exposes its MCP interface at `http://127.0.0.1:12306/mcp` (default port).
Both Codex CLI and Claude Code can connect to this endpoint to use Chrome browser control tools.

## Codex CLI Configuration

### Option 1: HTTP MCP Server (Recommended)

Add the following to your `~/.codex/config.json`:

```json
{
  "mcpServers": {
    "chrome-mcp": {
      "url": "http://127.0.0.1:12306/mcp"
    }
  }
}
```

### Option 2: Via Environment Variable

Set the MCP URL via environment variable before running codex:

```bash
export MCP_HTTP_PORT=12306
```

## Claude Code Configuration

### Option 1: HTTP MCP Server

Add the following to your `~/.claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "chrome-mcp": {
      "url": "http://127.0.0.1:12306/mcp"
    }
  }
}
```

### Option 2: Stdio Server (Alternative)

If you prefer stdio-based MCP communication:

```json
{
  "mcpServers": {
    "chrome-mcp": {
      "command": "node",
      "args": ["/path/to/mcp-chrome/dist/mcp/mcp-server-stdio.js"]
    }
  }
}
```

## Verifying Connection

After configuration, the CLI tools should be able to see and use Chrome MCP tools such as:

- `chrome_get_windows_and_tabs` - Get browser window and tab information
- `chrome_navigate` - Navigate to a URL
- `chrome_click_element` - Click on page elements
- `chrome_get_page_content` - Get page content
- And more...

## Troubleshooting

### Connection Refused

If you get "connection refused" errors:

1. Ensure the Chrome extension is installed and the native server is running
2. Check that the port matches (default: 12306)
3. Verify no firewall is blocking localhost connections
4. Run `mcp-chrome-bridge doctor` to diagnose issues

### Tools Not Appearing

If MCP tools don't appear in the CLI:

1. Restart the CLI tool after configuration changes
2. Check the configuration file syntax (valid JSON)
3. Ensure the MCP server URL is accessible

### Port Conflicts

If port 12306 is already in use:

1. Set a custom port in the extension settings
2. Update the CLI configuration to match the new port
3. Run `mcp-chrome-bridge update-port <new-port>` to update the stdio config

## Environment Variables

| Variable                     | Description                            | Default |
| ---------------------------- | -------------------------------------- | ------- |
| `MCP_HTTP_PORT`              | HTTP port for MCP server               | 12306   |
| `MCP_ALLOWED_WORKSPACE_BASE` | Additional allowed workspace directory | (none)  |
| `CHROME_MCP_NODE_PATH`       | Override Node.js executable path       | (auto)  |

```

--------------------------------------------------------------------------------
/app/native-server/src/constant/index.ts:
--------------------------------------------------------------------------------

```typescript
export enum NATIVE_MESSAGE_TYPE {
  START = 'start',
  STARTED = 'started',
  STOP = 'stop',
  STOPPED = 'stopped',
  PING = 'ping',
  PONG = 'pong',
  ERROR = 'error',
}

export const NATIVE_SERVER_PORT = 12306;

// Timeout constants (in milliseconds)
export const TIMEOUTS = {
  DEFAULT_REQUEST_TIMEOUT: 15000,
  EXTENSION_REQUEST_TIMEOUT: 20000,
  PROCESS_DATA_TIMEOUT: 20000,
} as const;

// Server configuration
export const SERVER_CONFIG = {
  HOST: '127.0.0.1',
  /**
   * CORS origin whitelist - only allow Chrome/Firefox extensions and local debugging.
   * Use RegExp patterns for extension origins, string for exact match.
   */
  CORS_ORIGIN: [/^chrome-extension:\/\//, /^moz-extension:\/\//, 'http://127.0.0.1'] as const,
  LOGGER_ENABLED: false,
} as const;

// HTTP Status codes
export const HTTP_STATUS = {
  OK: 200,
  CREATED: 201,
  NO_CONTENT: 204,
  BAD_REQUEST: 400,
  NOT_FOUND: 404,
  INTERNAL_SERVER_ERROR: 500,
  GATEWAY_TIMEOUT: 504,
} as const;

// Error messages
export const ERROR_MESSAGES = {
  NATIVE_HOST_NOT_AVAILABLE: 'Native host connection not established.',
  SERVER_NOT_RUNNING: 'Server is not actively running.',
  REQUEST_TIMEOUT: 'Request to extension timed out.',
  INVALID_MCP_REQUEST: 'Invalid MCP request or session.',
  INVALID_SESSION_ID: 'Invalid or missing MCP session ID.',
  INTERNAL_SERVER_ERROR: 'Internal Server Error',
  MCP_SESSION_DELETION_ERROR: 'Internal server error during MCP session deletion.',
  MCP_REQUEST_PROCESSING_ERROR: 'Internal server error during MCP request processing.',
  INVALID_SSE_SESSION: 'Invalid or missing MCP session ID for SSE.',
} as const;

// ============================================================
// Chrome MCP Server Configuration
// ============================================================

/**
 * Environment variables for dynamically resolving the local MCP HTTP endpoint.
 * CHROME_MCP_PORT is the preferred source; MCP_HTTP_PORT is kept for backward compatibility.
 */
export const CHROME_MCP_PORT_ENV = 'CHROME_MCP_PORT';
export const MCP_HTTP_PORT_ENV = 'MCP_HTTP_PORT';

/**
 * Get the actual port the Chrome MCP server is listening on.
 * Priority: CHROME_MCP_PORT env > MCP_HTTP_PORT env > NATIVE_SERVER_PORT default
 */
export function getChromeMcpPort(): number {
  const raw = process.env[CHROME_MCP_PORT_ENV] || process.env[MCP_HTTP_PORT_ENV];
  const port = raw ? Number.parseInt(String(raw), 10) : NaN;
  return Number.isFinite(port) && port > 0 && port <= 65535 ? port : NATIVE_SERVER_PORT;
}

/**
 * Get the full URL to the local Chrome MCP HTTP endpoint.
 * This URL is used by Claude/Codex agents to connect to the MCP server.
 */
export function getChromeMcpUrl(): string {
  return `http://${SERVER_CONFIG.HOST}:${getChromeMcpPort()}/mcp`;
}

```

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

```typescript
/**
 * Composable for user-facing input preferences in AgentChat.
 * Preferences are persisted in chrome.storage.local.
 */
import { ref, type Ref } from 'vue';

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

const STORAGE_KEY_FAKE_CARET = 'agent-chat-fake-caret-enabled';

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

export interface UseAgentInputPreferences {
  /** Whether the fake caret + comet trail is enabled (opt-in). Default: false */
  fakeCaretEnabled: Ref<boolean>;
  /** Whether preferences have been loaded from storage */
  ready: Ref<boolean>;
  /** Load preferences from chrome.storage.local (call on mount) */
  init: () => Promise<void>;
  /** Persist and update fake caret preference */
  setFakeCaretEnabled: (enabled: boolean) => Promise<void>;
}

// =============================================================================
// Composable
// =============================================================================

/**
 * Composable for managing user input preferences.
 *
 * Features:
 * - Fake caret toggle (opt-in, default off)
 * - Persistence via chrome.storage.local
 * - Graceful fallback when storage is unavailable
 */
export function useAgentInputPreferences(): UseAgentInputPreferences {
  const fakeCaretEnabled = ref(false);
  const ready = ref(false);

  /**
   * Load preferences from chrome.storage.local.
   * Should be called during component mount.
   */
  async function init(): Promise<void> {
    try {
      if (typeof chrome === 'undefined' || !chrome.storage?.local) {
        ready.value = true;
        return;
      }

      const result = await chrome.storage.local.get(STORAGE_KEY_FAKE_CARET);
      const stored = result[STORAGE_KEY_FAKE_CARET];

      if (typeof stored === 'boolean') {
        fakeCaretEnabled.value = stored;
      }
    } catch (error) {
      console.error('[useAgentInputPreferences] Failed to load preferences:', error);
    } finally {
      ready.value = true;
    }
  }

  /**
   * Update and persist the fake caret preference.
   */
  async function setFakeCaretEnabled(enabled: boolean): Promise<void> {
    fakeCaretEnabled.value = enabled;

    try {
      if (typeof chrome === 'undefined' || !chrome.storage?.local) return;
      await chrome.storage.local.set({ [STORAGE_KEY_FAKE_CARET]: enabled });
    } catch (error) {
      console.error('[useAgentInputPreferences] Failed to save fake caret preference:', error);
    }
  }

  return {
    fakeCaretEnabled,
    ready,
    init,
    setFakeCaretEnabled,
  };
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/offscreen-manager.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Offscreen Document manager
 * Ensures only one offscreen document is created across the entire extension to avoid conflicts
 */

export class OffscreenManager {
  private static instance: OffscreenManager | null = null;
  private isCreated = false;
  private isCreating = false;
  private createPromise: Promise<void> | null = null;

  private constructor() {}

  /**
   * Get singleton instance
   */
  public static getInstance(): OffscreenManager {
    if (!OffscreenManager.instance) {
      OffscreenManager.instance = new OffscreenManager();
    }
    return OffscreenManager.instance;
  }

  /**
   * Ensure offscreen document exists
   */
  public async ensureOffscreenDocument(): Promise<void> {
    if (this.isCreated) {
      return;
    }

    if (this.isCreating && this.createPromise) {
      return this.createPromise;
    }

    this.isCreating = true;
    this.createPromise = this._doCreateOffscreenDocument().finally(() => {
      this.isCreating = false;
    });

    return this.createPromise;
  }

  private async _doCreateOffscreenDocument(): Promise<void> {
    try {
      if (!chrome.offscreen) {
        throw new Error('Offscreen API not available. Chrome 109+ required.');
      }

      const existingContexts = await (chrome.runtime as any).getContexts({
        contextTypes: ['OFFSCREEN_DOCUMENT'],
      });

      if (existingContexts && existingContexts.length > 0) {
        console.log('OffscreenManager: Offscreen document already exists');
        this.isCreated = true;
        return;
      }

      await chrome.offscreen.createDocument({
        url: 'offscreen.html',
        reasons: ['WORKERS'],
        justification: 'Need to run semantic similarity engine with workers',
      });

      this.isCreated = true;
      console.log('OffscreenManager: Offscreen document created successfully');
    } catch (error) {
      console.error('OffscreenManager: Failed to create offscreen document:', error);
      this.isCreated = false;
      throw error;
    }
  }

  /**
   * Check if offscreen document is created
   */
  public isOffscreenDocumentCreated(): boolean {
    return this.isCreated;
  }

  /**
   * Close offscreen document
   */
  public async closeOffscreenDocument(): Promise<void> {
    try {
      if (chrome.offscreen && this.isCreated) {
        await chrome.offscreen.closeDocument();
        this.isCreated = false;
        console.log('OffscreenManager: Offscreen document closed');
      }
    } catch (error) {
      console.error('OffscreenManager: Failed to close offscreen document:', error);
    }
  }

  /**
   * Reset state (for testing)
   */
  public reset(): void {
    this.isCreated = false;
    this.isCreating = false;
    this.createPromise = null;
  }
}


export const offscreenManager = OffscreenManager.getInstance();

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/recording/content-message-handler.ts:
--------------------------------------------------------------------------------

```typescript
import type { RecordingSessionManager } from './session-manager';
import type { Step, VariableDef } from '../types';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';

/**
 * Initialize the content message handler for receiving steps and variables from content scripts.
 *
 * Supports the following payload kinds:
 * - 'steps' | 'step': Append steps to the current flow
 * - 'variables': Append variables to the current flow (for sensitive input handling)
 * - 'finalize': Content script has finished flushing (used during stop barrier)
 */
export function initContentMessageHandler(session: RecordingSessionManager): void {
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
    try {
      if (!message || message.type !== TOOL_MESSAGE_TYPES.RR_RECORDER_EVENT) return false;

      // Accept messages during 'recording' or 'stopping' states
      // 'stopping' allows final steps to arrive during the drain phase
      if (!session.canAcceptSteps()) {
        sendResponse({ ok: true, ignored: true });
        return true;
      }

      const flow = session.getFlow();
      if (!flow) {
        sendResponse({ ok: true, ignored: true });
        return true;
      }

      const payload = message?.payload || {};

      // Handle steps
      if (payload.kind === 'steps' || payload.kind === 'step') {
        const steps: Step[] = Array.isArray(payload.steps)
          ? (payload.steps as Step[])
          : payload.step
            ? [payload.step as Step]
            : [];
        if (steps.length > 0) {
          session.appendSteps(steps);
        }
      }

      // Handle variables (for sensitive input handling)
      if (payload.kind === 'variables') {
        const variables: VariableDef[] = Array.isArray(payload.variables)
          ? (payload.variables as VariableDef[])
          : [];
        if (variables.length > 0) {
          session.appendVariables(variables);
        }
      }

      // Handle combined payload (steps + variables in one message)
      if (payload.kind === 'batch') {
        const steps: Step[] = Array.isArray(payload.steps) ? (payload.steps as Step[]) : [];
        const variables: VariableDef[] = Array.isArray(payload.variables)
          ? (payload.variables as VariableDef[])
          : [];
        if (steps.length > 0) {
          session.appendSteps(steps);
        }
        if (variables.length > 0) {
          session.appendVariables(variables);
        }
      }

      // payload.kind === 'start'|'stop'|'finalize' are no-ops here (lifecycle handled elsewhere)
      sendResponse({ ok: true });
      return true;
    } catch (e) {
      console.warn('ContentMessageHandler: processing message failed', e);
      sendResponse({ ok: false, error: String((e as Error)?.message || e) });
      return true;
    }
  });
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/engine/plugins/register-v2-replay-nodes.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Register RR-V2 replay action handlers as RR-V3 nodes
 * @description
 * Batch registration of V2 action handlers into the V3 PluginRegistry.
 * This enables V3 to execute flows that use V2 action types.
 */

import { createReplayActionRegistry } from '@/entrypoints/background/record-replay/actions/handlers';
import type {
  ActionHandler,
  ExecutableActionType,
} from '@/entrypoints/background/record-replay/actions/types';

import type { PluginRegistry } from './registry';
import {
  adaptV2ActionHandlerToV3NodeDefinition,
  type V2ActionNodeAdapterOptions,
} from './v2-action-adapter';

export interface RegisterV2ReplayNodesOptions extends V2ActionNodeAdapterOptions {
  /**
   * Only include these action types. If not specified, all V2 handlers are included.
   */
  include?: ReadonlyArray<string>;

  /**
   * Exclude these action types. Applied after include filter.
   */
  exclude?: ReadonlyArray<string>;
}

/**
 * Register V2 replay action handlers as V3 node definitions.
 *
 * @param registry The V3 PluginRegistry to register nodes into
 * @param options Configuration options
 * @returns Array of registered node kinds
 *
 * @example
 * ```ts
 * const plugins = new PluginRegistry();
 * const registered = registerV2ReplayNodesAsV3Nodes(plugins, {
 *   // Exclude control flow handlers that V3 runner doesn't support
 *   exclude: ['foreach', 'while'],
 * });
 * console.log('Registered:', registered);
 * ```
 */
export function registerV2ReplayNodesAsV3Nodes(
  registry: PluginRegistry,
  options: RegisterV2ReplayNodesOptions = {},
): string[] {
  const actionRegistry = createReplayActionRegistry();
  const handlers = actionRegistry.list();

  const include = options.include ? new Set(options.include) : null;
  const exclude = options.exclude ? new Set(options.exclude) : null;

  const registered: string[] = [];

  for (const handler of handlers) {
    if (include && !include.has(handler.type)) continue;
    if (exclude && exclude.has(handler.type)) continue;

    // Cast needed because V2 handler types don't perfectly align with V3 NodeKind
    const nodeDef = adaptV2ActionHandlerToV3NodeDefinition(
      handler as ActionHandler<ExecutableActionType>,
      options,
    );
    registry.registerNode(nodeDef as unknown as Parameters<typeof registry.registerNode>[0]);
    registered.push(handler.type);
  }

  return registered;
}

/**
 * Get list of V2 action types that can be registered.
 * Useful for debugging and documentation.
 */
export function listV2ActionTypes(): string[] {
  const actionRegistry = createReplayActionRegistry();
  return actionRegistry.list().map((h) => h.type);
}

/**
 * Default exclude list for V3 registration.
 * These handlers rely on V2 control directives that V3 runner doesn't support.
 */
export const DEFAULT_V2_EXCLUDE_LIST = ['foreach', 'while'] as const;

```

--------------------------------------------------------------------------------
/docs/TROUBLESHOOTING.md:
--------------------------------------------------------------------------------

```markdown
# 🚀 Installation and Connection Issues

## Quick Diagnosis

Run the diagnostic tool to identify common issues:

```bash
mcp-chrome-bridge doctor
```

To automatically fix common issues:

```bash
mcp-chrome-bridge doctor --fix
```

## Export Report for GitHub Issues

If you need to open an issue, export a diagnostic report:

```bash
# Print Markdown report to terminal (copy/paste into GitHub Issue)
mcp-chrome-bridge report

# Write to a file
mcp-chrome-bridge report --output mcp-report.md

# Copy directly to clipboard
mcp-chrome-bridge report --copy
```

By default, usernames, paths, and tokens are redacted. Use `--no-redact` if you're comfortable sharing full paths.

## If Connection Fails After Clicking the Connect Button on the Extension

1. **Run the diagnostic tool first**

```bash
mcp-chrome-bridge doctor
```

This will check installation, manifest, permissions, and Node.js path.

2. **Check if mcp-chrome-bridge is installed successfully**, ensure it's globally installed

```bash
mcp-chrome-bridge -V
```

<img width="612" alt="Screenshot 2025-06-11 15 09 57" src="https://github.com/user-attachments/assets/59458532-e6e1-457c-8c82-3756a5dbb28e" />

2. **Check if the manifest file is in the correct directory**

Windows path: C:\Users\xxx\AppData\Roaming\Google\Chrome\NativeMessagingHosts

Mac path: /Users/xxx/Library/Application\ Support/Google/Chrome/NativeMessagingHosts

If the npm package is installed correctly, a file named `com.chromemcp.nativehost.json` should be generated in this directory

3. **Check logs**
   Logs are now stored in user-writable directories:

- **macOS**: `~/Library/Logs/mcp-chrome-bridge/`
- **Windows**: `%LOCALAPPDATA%\mcp-chrome-bridge\logs\`
- **Linux**: `~/.local/state/mcp-chrome-bridge/logs/`

<img width="804" alt="Screenshot 2025-06-11 15 09 41" src="https://github.com/user-attachments/assets/ce7b7c94-7c84-409a-8210-c9317823aae1" />

4. **Check if you have execution permissions**
   You need to check your installation path (if unclear, open the manifest file in step 2, the path field shows the installation directory). For example, if the Mac installation path is as follows:

`xxx/node_modules/mcp-chrome-bridge/dist/run_host.sh`

Check if this script has execution permissions. Run to fix:

```bash
mcp-chrome-bridge fix-permissions
```

5. **Node.js not found**
   If you use a Node version manager (nvm, volta, asdf, fnm), the wrapper script may not find Node.js. Set the `CHROME_MCP_NODE_PATH` environment variable:

```bash
export CHROME_MCP_NODE_PATH=/path/to/your/node
```

Or run `mcp-chrome-bridge doctor --fix` to write the current Node path.

## Log Locations

Wrapper logs are now stored in user-writable locations:

- **macOS**: `~/Library/Logs/mcp-chrome-bridge/`
- **Windows**: `%LOCALAPPDATA%\mcp-chrome-bridge\logs\`
- **Linux**: `~/.local/state/mcp-chrome-bridge/logs/`

```

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

```vue
<template>
  <form class="p-3 border-t border-slate-200 bg-white space-y-2" @submit.prevent="handleSubmit">
    <!-- Attachments preview -->
    <AttachmentPreview
      v-if="attachments.length > 0"
      :attachments="attachments"
      @remove="$emit('remove-attachment', $event)"
    />

    <textarea
      v-model="inputValue"
      class="w-full border border-slate-200 rounded-md px-3 py-2 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-slate-400"
      rows="2"
      placeholder="Ask the agent to work with your browser via MCP..."
      @input="$emit('update:modelValue', inputValue)"
    ></textarea>

    <!-- Hidden file input -->
    <input
      ref="fileInputRef"
      type="file"
      class="hidden"
      accept="image/*"
      multiple
      @change="$emit('file-select', $event)"
    />

    <div class="flex items-center justify-between gap-2">
      <div class="flex items-center gap-2">
        <button
          type="button"
          class="text-slate-500 hover:text-slate-700 text-xs px-2 py-1 border border-slate-200 rounded hover:bg-slate-50"
          title="Attach images"
          @click="openFilePicker"
        >
          Attach
        </button>
        <div class="text-[11px] text-slate-500">
          {{ isStreaming ? 'Agent is thinking...' : 'Ready' }}
        </div>
      </div>
      <div class="flex gap-2">
        <button
          v-if="isStreaming && canCancel"
          type="button"
          class="btn-secondary !px-3 !py-2 text-xs"
          :disabled="cancelling"
          @click="$emit('cancel')"
        >
          {{ cancelling ? 'Cancelling...' : 'Stop' }}
        </button>
        <button
          type="submit"
          class="btn-primary !px-4 !py-2 text-xs"
          :disabled="!canSend || sending"
        >
          {{ sending ? 'Sending...' : 'Send' }}
        </button>
      </div>
    </div>
  </form>
</template>

<script lang="ts" setup>
import { ref, watch } from 'vue';
import type { AgentAttachment } from 'chrome-mcp-shared';
import AttachmentPreview from './AttachmentPreview.vue';

const props = defineProps<{
  modelValue: string;
  attachments: AgentAttachment[];
  isStreaming: boolean;
  sending: boolean;
  cancelling: boolean;
  canCancel: boolean;
  canSend: boolean;
}>();

const emit = defineEmits<{
  'update:modelValue': [value: string];
  submit: [];
  cancel: [];
  'file-select': [event: Event];
  'remove-attachment': [index: number];
}>();

const inputValue = ref(props.modelValue);
const fileInputRef = ref<HTMLInputElement | null>(null);

// Sync with parent
watch(
  () => props.modelValue,
  (newVal) => {
    inputValue.value = newVal;
  },
);

function openFilePicker(): void {
  fileInputRef.value?.click();
}

function handleSubmit(): void {
  emit('submit');
}

// Expose file input ref for parent
defineExpose({
  fileInputRef,
});
</script>

```

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

```vue
<template>
  <div class="form-section">
    <div class="form-group">
      <label class="form-label">请求方法</label>
      <select class="form-select" v-model="(node as any).config.method">
        <option>GET</option>
        <option>POST</option>
        <option>PUT</option>
        <option>PATCH</option>
        <option>DELETE</option>
      </select>
    </div>
    <div class="form-group" :class="{ invalid: !(node as any).config?.url }" data-field="http.url">
      <label class="form-label">URL 地址</label>
      <input
        class="form-input"
        v-model="(node as any).config.url"
        placeholder="https://api.example.com/data"
      />
    </div>
    <div class="form-group">
      <label class="form-label">Headers (JSON)</label>
      <textarea
        class="form-textarea"
        v-model="headersJson"
        rows="3"
        placeholder='{"Content-Type": "application/json"}'
      ></textarea>
    </div>
    <div class="form-group">
      <label class="form-label">Body (JSON)</label>
      <textarea
        class="form-textarea"
        v-model="bodyJson"
        rows="3"
        placeholder='{"key": "value"}'
      ></textarea>
    </div>
    <div class="form-group">
      <label class="form-label">FormData (JSON,可选,提供时覆盖 Body)</label>
      <textarea
        class="form-textarea"
        v-model="formDataJson"
        rows="3"
        placeholder='{"fields":{"k":"v"},"files":[{"name":"file","fileUrl":"https://...","filename":"a.png"}]}'
      ></textarea>
      <div class="text-xs text-slate-500" style="margin-top: 6px"
        >支持简洁数组形式:[["file","url:https://...","a.png"],["metadata","value"]]</div
      >
    </div>
  </div>
</template>

<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import { computed } from 'vue';
import type { NodeBase } from '@/entrypoints/background/record-replay/types';

const props = defineProps<{ node: NodeBase }>();

const headersJson = computed({
  get() {
    try {
      return JSON.stringify((props.node as any).config?.headers || {}, null, 2);
    } catch {
      return '';
    }
  },
  set(v: string) {
    try {
      (props.node as any).config.headers = JSON.parse(v || '{}');
    } catch {}
  },
});
const bodyJson = computed({
  get() {
    try {
      return JSON.stringify((props.node as any).config?.body ?? null, null, 2);
    } catch {
      return '';
    }
  },
  set(v: string) {
    try {
      (props.node as any).config.body = v ? JSON.parse(v) : null;
    } catch {}
  },
});
const formDataJson = computed({
  get() {
    try {
      return (props.node as any).config?.formData
        ? JSON.stringify((props.node as any).config.formData, null, 2)
        : '';
    } catch {
      return '';
    }
  },
  set(v: string) {
    try {
      (props.node as any).config.formData = v ? JSON.parse(v) : undefined;
    } catch {}
  },
});
</script>

<style scoped></style>

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/index.ts:
--------------------------------------------------------------------------------

```typescript
import { initNativeHostListener } from './native-host';
import {
  initSemanticSimilarityListener,
  initializeSemanticEngineIfCached,
} from './semantic-similarity';
import { initStorageManagerListener } from './storage-manager';
import { cleanupModelCache } from '@/utils/semantic-similarity-engine';
import { initRecordReplayListeners } from './record-replay';
import { initElementMarkerListeners } from './element-marker';
import { initWebEditorListeners } from './web-editor';
import { initQuickPanelAgentHandler } from './quick-panel/agent-handler';
import { initQuickPanelCommands } from './quick-panel/commands';
import { initQuickPanelTabsHandler } from './quick-panel/tabs-handler';

// Record-Replay V3 (feature flag)
import { bootstrapV3 } from './record-replay-v3/bootstrap';

/**
 * Feature flag for RR-V3
 * Set to true to enable the new Record-Replay V3 engine
 */
const ENABLE_RR_V3 = true;

/**
 * Background script entry point
 * Initializes all background services and listeners
 */
export default defineBackground(() => {
  // Open welcome page on first install
  chrome.runtime.onInstalled.addListener((details) => {
    if (details.reason === 'install') {
      // Open the welcome/onboarding page for new installations
      chrome.tabs.create({
        url: chrome.runtime.getURL('/welcome.html'),
      });
    }
  });

  // Initialize core services
  initNativeHostListener();
  initSemanticSimilarityListener();
  initStorageManagerListener();
  // Record & Replay V1/V2 listeners
  initRecordReplayListeners();

  // Record & Replay V3 (new engine)
  if (ENABLE_RR_V3) {
    bootstrapV3()
      .then((runtime) => {
        console.log(`[RR-V3] Bootstrap complete, ownerId: ${runtime.ownerId}`);
      })
      .catch((error) => {
        console.error('[RR-V3] Bootstrap failed:', error);
      });
  }

  // Element marker: context menu + CRUD listeners
  initElementMarkerListeners();
  // Web editor: toggle edit-mode overlay
  initWebEditorListeners();
  // Quick Panel: send messages to AgentChat via background-stream bridge
  initQuickPanelAgentHandler();
  // Quick Panel: tabs search bridge for content script UI
  initQuickPanelTabsHandler();
  // Quick Panel: keyboard shortcut handler
  initQuickPanelCommands();

  // Conditionally initialize semantic similarity engine if model cache exists
  initializeSemanticEngineIfCached()
    .then((initialized) => {
      if (initialized) {
        console.log('Background: Semantic similarity engine initialized from cache');
      } else {
        console.log(
          'Background: Semantic similarity engine initialization skipped (no cache found)',
        );
      }
    })
    .catch((error) => {
      console.warn('Background: Failed to conditionally initialize semantic engine:', error);
    });

  // Initial cleanup on startup
  cleanupModelCache().catch((error) => {
    console.warn('Background: Initial cache cleanup failed:', error);
  });
});

```

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

```vue
<template>
  <div
    v-if="open"
    class="fixed top-12 right-4 z-50 min-w-[160px] py-2"
    :style="{
      backgroundColor: 'var(--ac-surface, #ffffff)',
      border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
      borderRadius: 'var(--ac-radius-inner, 8px)',
      boxShadow: 'var(--ac-shadow-float, 0 4px 20px -2px rgba(0,0,0,0.1))',
    }"
  >
    <!-- Header -->
    <div
      class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider"
      :style="{ color: 'var(--ac-text-subtle, #a8a29e)' }"
    >
      Open In
    </div>

    <!-- VS Code Option -->
    <button
      class="w-full px-3 py-2 text-left text-sm flex items-center gap-2 ac-menu-item"
      :style="{
        color: defaultTarget === 'vscode' ? 'var(--ac-accent, #c87941)' : 'var(--ac-text, #1a1a1a)',
      }"
      @click="handleSelect('vscode')"
    >
      <!-- VS Code Icon -->
      <svg class="w-4 h-4 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor">
        <path
          d="M17.583 2L6.167 11.667 2 8.5v7l4.167-3.167L17.583 22 22 19.75V4.25L17.583 2zm0 3.5v13l-8-6.5 8-6.5z"
        />
      </svg>
      <span class="flex-1">VS Code</span>
      <!-- Default indicator -->
      <svg
        v-if="defaultTarget === 'vscode'"
        class="w-4 h-4 flex-shrink-0"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
      </svg>
    </button>

    <!-- Terminal Option -->
    <button
      class="w-full px-3 py-2 text-left text-sm flex items-center gap-2 ac-menu-item"
      :style="{
        color:
          defaultTarget === 'terminal' ? 'var(--ac-accent, #c87941)' : 'var(--ac-text, #1a1a1a)',
      }"
      @click="handleSelect('terminal')"
    >
      <!-- Terminal Icon -->
      <svg
        class="w-4 h-4 flex-shrink-0"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        stroke-width="2"
      >
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
        />
      </svg>
      <span class="flex-1">Terminal</span>
      <!-- Default indicator -->
      <svg
        v-if="defaultTarget === 'terminal'"
        class="w-4 h-4 flex-shrink-0"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
      </svg>
    </button>
  </div>
</template>

<script lang="ts" setup>
import type { OpenProjectTarget } from 'chrome-mcp-shared';

defineProps<{
  open: boolean;
  defaultTarget: OpenProjectTarget | null;
}>();

const emit = defineEmits<{
  select: [target: OpenProjectTarget];
  close: [];
}>();

function handleSelect(target: OpenProjectTarget): void {
  emit('select', target);
  emit('close');
}
</script>

```

--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/strategies/css-unique.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * CSS Unique Strategy - 基于唯一 ID 或 class 组合的选择器策略
 */

import type { SelectorCandidate, SelectorStrategy } from '../types';

const MAX_CLASS_COUNT = 24;
const MAX_COMBO_CLASSES = 8;
const MAX_CANDIDATES = 6;

function isValidClassToken(token: string): boolean {
  return /^[a-zA-Z0-9_-]+$/.test(token);
}

export const cssUniqueStrategy: SelectorStrategy = {
  id: 'css-unique',
  generate(ctx) {
    if (!ctx.options.includeCssUnique) return [];

    const { element, helpers } = ctx;
    const out: SelectorCandidate[] = [];

    const tag = element.tagName?.toLowerCase?.() ?? '';

    // 1) Unique ID selector
    const id = element.id?.trim();
    if (id) {
      const sel = `#${helpers.cssEscape(id)}`;
      if (helpers.isUnique(sel)) {
        out.push({ type: 'css', value: sel, source: 'generated', strategy: 'css-unique' });
      }
    }

    if (out.length >= MAX_CANDIDATES) return out;

    // 2) Unique class selectors
    const classList = Array.from(element.classList || [])
      .map((c) => String(c).trim())
      .filter((c) => c.length > 0 && isValidClassToken(c))
      .slice(0, MAX_CLASS_COUNT);

    for (const cls of classList) {
      if (out.length >= MAX_CANDIDATES) break;
      const sel = `.${helpers.cssEscape(cls)}`;
      if (helpers.isUnique(sel)) {
        out.push({ type: 'css', value: sel, source: 'generated', strategy: 'css-unique' });
      }
    }

    if (tag) {
      for (const cls of classList) {
        if (out.length >= MAX_CANDIDATES) break;
        const sel = `${tag}.${helpers.cssEscape(cls)}`;
        if (helpers.isUnique(sel)) {
          out.push({ type: 'css', value: sel, source: 'generated', strategy: 'css-unique' });
        }
      }
    }

    if (out.length >= MAX_CANDIDATES) return out;

    // 3) Class combinations (depth 2/3), limited to avoid heavy queries.
    const comboSource = classList.slice(0, MAX_COMBO_CLASSES).map((c) => helpers.cssEscape(c));

    const tryPush = (selector: string): void => {
      if (out.length >= MAX_CANDIDATES) return;
      if (!helpers.isUnique(selector)) return;
      out.push({ type: 'css', value: selector, source: 'generated', strategy: 'css-unique' });
    };

    const tryPushWithTag = (selector: string): void => {
      tryPush(selector);
      if (tag) tryPush(`${tag}${selector}`);
    };

    // Depth 2
    for (let i = 0; i < comboSource.length && out.length < MAX_CANDIDATES; i++) {
      for (let j = i + 1; j < comboSource.length && out.length < MAX_CANDIDATES; j++) {
        tryPushWithTag(`.${comboSource[i]}.${comboSource[j]}`);
      }
    }

    // Depth 3
    for (let i = 0; i < comboSource.length && out.length < MAX_CANDIDATES; i++) {
      for (let j = i + 1; j < comboSource.length && out.length < MAX_CANDIDATES; j++) {
        for (let k = j + 1; k < comboSource.length && out.length < MAX_CANDIDATES; k++) {
          tryPushWithTag(`.${comboSource[i]}.${comboSource[j]}.${comboSource[k]}`);
        }
      }
    }

    return out;
  },
};

```

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

```vue
<template>
  <div
    class="px-4 py-2 border-b border-slate-100 flex flex-col gap-2 text-xs text-slate-600 bg-slate-50"
  >
    <!-- Project selection & workspace -->
    <div class="flex items-center gap-2">
      <span class="whitespace-nowrap">Project</span>
      <select
        :value="selectedProjectId"
        class="flex-1 border border-slate-200 rounded px-2 py-1 text-xs bg-white focus:outline-none focus:ring-1 focus:ring-slate-400"
        @change="handleProjectChange"
      >
        <option v-for="p in projects" :key="p.id" :value="p.id">
          {{ p.name }}
        </option>
      </select>
      <button
        class="btn-secondary !px-2 !py-1 text-[11px]"
        type="button"
        :disabled="isPicking"
        title="Create new project from a directory"
        @click="$emit('new-project')"
      >
        {{ isPicking ? '...' : 'New' }}
      </button>
    </div>

    <!-- Current workspace path -->
    <div v-if="selectedProject" class="flex items-center gap-2 text-[11px] text-slate-500">
      <span class="whitespace-nowrap">Path</span>
      <span class="flex-1 font-mono truncate" :title="selectedProject.rootPath">
        {{ selectedProject.rootPath }}
      </span>
    </div>

    <!-- CLI & Model selection -->
    <CliSettings
      :project-root="projectRoot"
      :selected-cli="selectedCli"
      :model="model"
      :use-ccr="useCcr"
      :engines="engines"
      :selected-project="selectedProject"
      :is-saving-root="isSavingProjectRoot"
      :is-saving-preference="isSavingPreference"
      @update:project-root="$emit('update:projectRoot', $event)"
      @update:selected-cli="$emit('update:selectedCli', $event)"
      @update:model="$emit('update:model', $event)"
      @update:use-ccr="$emit('update:useCcr', $event)"
      @save-root="$emit('save-root')"
      @save-preference="$emit('save-preference')"
    />

    <!-- Error message -->
    <div v-if="error" class="text-[11px] text-red-600">
      {{ error }}
    </div>
  </div>
</template>

<script lang="ts" setup>
import type { AgentProject, AgentEngineInfo } from 'chrome-mcp-shared';
import CliSettings from './CliSettings.vue';

defineProps<{
  projects: AgentProject[];
  selectedProjectId: string;
  selectedProject: AgentProject | null;
  isPicking: boolean;
  error: string | null;
  projectRoot: string;
  selectedCli: string;
  model: string;
  useCcr: boolean;
  engines: AgentEngineInfo[];
  isSavingProjectRoot: boolean;
  isSavingPreference: boolean;
}>();

const emit = defineEmits<{
  'update:selectedProjectId': [value: string];
  'project-changed': [];
  'new-project': [];
  'update:projectRoot': [value: string];
  'update:selectedCli': [value: string];
  'update:model': [value: string];
  'update:useCcr': [value: boolean];
  'save-root': [];
  'save-preference': [];
}>();

function handleProjectChange(event: Event): void {
  const value = (event.target as HTMLSelectElement).value;
  emit('update:selectedProjectId', value);
  emit('project-changed');
}
</script>

```

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

```vue
<template>
  <div class="py-1">
    <div
      class="text-sm leading-relaxed markdown-content"
      :style="{
        color: 'var(--ac-text)',
        fontFamily: 'var(--ac-font-body)',
      }"
    >
      <MarkdownRender
        :content="item.text ?? ''"
        :custom-id="AGENTCHAT_MD_SCOPE"
        :custom-html-tags="CUSTOM_HTML_TAGS"
        :max-live-nodes="0"
        :render-batch-size="16"
        :render-batch-delay="8"
      />
    </div>
    <span
      v-if="item.isStreaming"
      class="inline-block w-1.5 h-4 ml-0.5 ac-pulse"
      :style="{ backgroundColor: 'var(--ac-accent)' }"
    />
  </div>
</template>

<script lang="ts" setup>
import type { TimelineItem } from '../../../composables/useAgentThreads';
import MarkdownRender from 'markstream-vue';
import 'markstream-vue/index.css';
// Import to register custom components (side-effect)
import { AGENTCHAT_MD_SCOPE } from './markstream-thinking';

/** Custom HTML tags to be rendered by registered custom components */
const CUSTOM_HTML_TAGS = ['thinking'] as const;

defineProps<{
  item: Extract<TimelineItem, { kind: 'assistant_text' }>;
}>();
</script>

<style scoped>
.markdown-content :deep(pre) {
  background-color: var(--ac-code-bg);
  border: var(--ac-border-width) solid var(--ac-code-border);
  border-radius: var(--ac-radius-inner);
  padding: 12px;
  overflow-x: auto;
}

.markdown-content :deep(code) {
  font-family: var(--ac-font-mono);
  font-size: 0.875em;
  color: var(--ac-code-text);
}

.markdown-content :deep(p) {
  margin: 0.5em 0;
}

.markdown-content :deep(p:first-child) {
  margin-top: 0;
}

.markdown-content :deep(p:last-child) {
  margin-bottom: 0;
}

.markdown-content :deep(ul),
.markdown-content :deep(ol) {
  margin: 0.5em 0;
  padding-left: 1.5em;
}

.markdown-content :deep(h1),
.markdown-content :deep(h2),
.markdown-content :deep(h3),
.markdown-content :deep(h4) {
  margin: 0.75em 0 0.5em;
  font-weight: 600;
}

.markdown-content :deep(h1:first-child),
.markdown-content :deep(h2:first-child),
.markdown-content :deep(h3:first-child),
.markdown-content :deep(h4:first-child) {
  margin-top: 0;
}

.markdown-content :deep(blockquote) {
  border-left: var(--ac-border-width-strong) solid var(--ac-border);
  padding-left: 1em;
  margin: 0.5em 0;
  color: var(--ac-text-muted);
}

.markdown-content :deep(a) {
  color: var(--ac-link);
  text-decoration: underline;
}

.markdown-content :deep(a:hover) {
  color: var(--ac-link-hover);
}

.markdown-content :deep(table) {
  border-collapse: collapse;
  margin: 0.5em 0;
  width: 100%;
}

.markdown-content :deep(th),
.markdown-content :deep(td) {
  border: var(--ac-border-width) solid var(--ac-border);
  padding: 0.5em;
  text-align: left;
}

.markdown-content :deep(th) {
  background-color: var(--ac-surface-muted);
}

.markdown-content :deep(hr) {
  border: none;
  border-top: var(--ac-border-width) solid var(--ac-border);
  margin: 1em 0;
}

.markdown-content :deep(img) {
  max-width: 100%;
  height: auto;
  border-radius: var(--ac-radius-inner);
}
</style>

```

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

```typescript
import type { StepWait } from '../types';
import { waitForNetworkIdle, waitForNavigation } from '../rr-utils';
import { expandTemplatesDeep } from '../rr-utils';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';

export const waitNode: NodeRuntime<StepWait> = {
  validate: (step) => {
    const ok = !!(step as any).condition;
    return ok ? { ok } : { ok, errors: ['缺少等待条件'] };
  },
  run: async (ctx: ExecCtx, step: StepWait) => {
    const s = expandTemplatesDeep(step as StepWait, ctx.vars);
    const cond = (s as StepWait).condition as
      | { selector: string; visible?: boolean }
      | { text: string; appear?: boolean }
      | { navigation: true }
      | { networkIdle: true }
      | { sleep: number };
    if ('text' in cond) {
      const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
      const tabId = tabs?.[0]?.id;
      if (typeof tabId !== 'number') throw new Error('Active tab not found');
      const frameIds = typeof ctx.frameId === 'number' ? [ctx.frameId] : undefined;
      await chrome.scripting.executeScript({
        target: { tabId, frameIds },
        files: ['inject-scripts/wait-helper.js'],
        world: 'ISOLATED',
      } as any);
      const resp: any = (await chrome.tabs.sendMessage(
        tabId,
        {
          action: 'waitForText',
          text: cond.text,
          appear: (cond as any).appear !== false,
          timeout: Math.max(0, Math.min((s as any).timeoutMs || 10000, 120000)),
        } as any,
        { frameId: ctx.frameId } as any,
      )) as any;
      if (!resp || resp.success !== true) throw new Error('wait text failed');
    } else if ('networkIdle' in cond) {
      const total = Math.min(Math.max(1000, (s as any).timeoutMs || 5000), 120000);
      const idle = Math.min(1500, Math.max(500, Math.floor(total / 3)));
      await waitForNetworkIdle(total, idle);
    } else if ('navigation' in cond) {
      await waitForNavigation((s as any).timeoutMs);
    } else if ('sleep' in cond) {
      const ms = Math.max(0, Number(cond.sleep ?? 0));
      await new Promise((r) => setTimeout(r, ms));
    } else if ('selector' in cond) {
      const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
      const tabId = tabs?.[0]?.id;
      if (typeof tabId !== 'number') throw new Error('Active tab not found');
      const frameIds = typeof ctx.frameId === 'number' ? [ctx.frameId] : undefined;
      await chrome.scripting.executeScript({
        target: { tabId, frameIds },
        files: ['inject-scripts/wait-helper.js'],
        world: 'ISOLATED',
      } as any);
      const resp: any = (await chrome.tabs.sendMessage(
        tabId,
        {
          action: 'waitForSelector',
          selector: (cond as any).selector,
          visible: (cond as any).visible !== false,
          timeout: Math.max(0, Math.min((s as any).timeoutMs || 10000, 120000)),
        } as any,
        { frameId: ctx.frameId } as any,
      )) as any;
      if (!resp || resp.success !== true) throw new Error('wait selector failed');
    }
    return {} as ExecResult;
  },
};

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/network-request.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';

const DEFAULT_NETWORK_REQUEST_TIMEOUT = 30000; // For sending a single request via content script

interface NetworkRequestToolParams {
  url: string; // URL is always required
  method?: string; // Defaults to GET
  headers?: Record<string, string>; // User-provided headers
  body?: any; // User-provided body
  timeout?: number; // Timeout for the network request itself
  // Optional multipart/form-data descriptor. When provided, overrides body and lets the helper build FormData.
  // Shape: { fields?: Record<string, string|number|boolean>, files?: Array<{ name: string, fileUrl?: string, filePath?: string, base64Data?: string, filename?: string, contentType?: string }> }
  // Or a compact array: [ [name, fileSpec, filename?], ... ] where fileSpec can be 'url:...', 'file:/abs/path', 'base64:...'
  formData?: any;
}

/**
 * NetworkRequestTool - Sends network requests based on provided parameters.
 */
class NetworkRequestTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.NETWORK_REQUEST;

  async execute(args: NetworkRequestToolParams): Promise<ToolResult> {
    const {
      url,
      method = 'GET',
      headers = {},
      body,
      timeout = DEFAULT_NETWORK_REQUEST_TIMEOUT,
    } = args;

    console.log(`NetworkRequestTool: Executing with options:`, args);

    if (!url) {
      return createErrorResponse('URL parameter is required.');
    }

    try {
      const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
      if (!tabs[0]?.id) {
        return createErrorResponse('No active tab found or tab has no ID.');
      }
      const activeTabId = tabs[0].id;

      // Ensure content script is available in the target tab
      await this.injectContentScript(activeTabId, ['inject-scripts/network-helper.js']);

      console.log(
        `NetworkRequestTool: Sending to content script: URL=${url}, Method=${method}, Headers=${Object.keys(headers).join(',')}, BodyType=${typeof body}`,
      );

      const resultFromContentScript = await this.sendMessageToTab(activeTabId, {
        action: TOOL_MESSAGE_TYPES.NETWORK_SEND_REQUEST,
        url: url,
        method: method,
        headers: headers,
        body: body,
        formData: args.formData || null,
        timeout: timeout,
      });

      console.log(`NetworkRequestTool: Response from content script:`, resultFromContentScript);

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(resultFromContentScript),
          },
        ],
        isError: !resultFromContentScript?.success,
      };
    } catch (error: any) {
      console.error('NetworkRequestTool: Error sending network request:', error);
      return createErrorResponse(
        `Error sending network request: ${error.message || String(error)}`,
      );
    }
  }
}

export const networkRequestTool = new NetworkRequestTool();

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/recording/flow-builder.ts:
--------------------------------------------------------------------------------

```typescript
import type { Edge, Flow, NodeBase, Step } from '../types';
import { STEP_TYPES } from '@/common/step-types';
import { recordingSession } from './session-manager';
import { mapStepToNodeConfig, EDGE_LABELS } from 'chrome-mcp-shared';

const WORKFLOW_VERSION = 1;

/**
 * Creates an initial flow structure for recording.
 * Initializes with nodes/edges (DAG) instead of steps.
 */
export function createInitialFlow(meta?: Partial<Flow>): Flow {
  const timeStamp = new Date().toISOString();
  const flow: Flow = {
    id: meta?.id || `flow_${Date.now()}`,
    name: meta?.name || 'new_workflow',
    version: WORKFLOW_VERSION,
    nodes: [],
    edges: [],
    variables: [],
    meta: {
      createdAt: timeStamp,
      updatedAt: timeStamp,
      ...meta?.meta,
    },
  };
  return flow;
}

export function generateStepId(): string {
  return `step_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
}

/**
 * Appends a navigation step to the flow.
 * Prefers centralized session append when recording is active.
 * Falls back to direct DAG mutation (does NOT write flow.steps).
 */
export function addNavigationStep(flow: Flow, url: string): void {
  const step: Step = { id: generateStepId(), type: STEP_TYPES.NAVIGATE, url } as Step;

  // Prefer centralized session append (single broadcast path) when active and matching flow
  const sessFlow = recordingSession.getFlow?.();
  if (recordingSession.getStatus?.() === 'recording' && sessFlow === flow) {
    recordingSession.appendSteps([step]);
    return;
  }

  // Fallback: mutate DAG directly (do not write flow.steps)
  appendNodeToFlow(flow, step);
}

/**
 * Appends a step as a node to the flow's DAG structure.
 * Creates node and edge from the previous node if exists.
 *
 * Internal helper - rarely invoked in practice. During active recording,
 * addNavigationStep() routes to session.appendSteps() which handles DAG
 * maintenance, caching, and timeline broadcast. This fallback only runs
 * when session is not active or flow reference doesn't match.
 */
function appendNodeToFlow(flow: Flow, step: Step): void {
  // Ensure DAG arrays exist
  if (!Array.isArray(flow.nodes)) flow.nodes = [];
  if (!Array.isArray(flow.edges)) flow.edges = [];

  const prevNodeId = flow.nodes.length > 0 ? flow.nodes[flow.nodes.length - 1]?.id : undefined;

  // Create new node
  const newNode: NodeBase = {
    id: step.id,
    type: step.type as NodeBase['type'],
    config: mapStepToNodeConfig(step),
  };
  flow.nodes.push(newNode);

  // Create edge from previous node if exists
  if (prevNodeId) {
    const edgeId = `e_${flow.edges.length}_${prevNodeId}_${step.id}`;
    const edge: Edge = {
      id: edgeId,
      from: prevNodeId,
      to: step.id,
      label: EDGE_LABELS.DEFAULT,
    };
    flow.edges.push(edge);
  }

  // Update meta timestamp (with error tolerance like session-manager)
  try {
    const timeStamp = new Date().toISOString();
    if (!flow.meta) {
      flow.meta = { createdAt: timeStamp, updatedAt: timeStamp };
    } else {
      flow.meta.updatedAt = timeStamp;
    }
  } catch {
    // ignore meta update errors to not block recording
  }
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/lru-cache.ts:
--------------------------------------------------------------------------------

```typescript
class LRUNode<K, V> {
  constructor(
    public key: K,
    public value: V,
    public prev: LRUNode<K, V> | null = null,
    public next: LRUNode<K, V> | null = null,
    public frequency: number = 1,
    public lastAccessed: number = Date.now(),
  ) {}
}

class LRUCache<K = string, V = any> {
  private capacity: number;
  private cache: Map<K, LRUNode<K, V>>;
  private head: LRUNode<K, V>;
  private tail: LRUNode<K, V>;

  constructor(capacity: number) {
    this.capacity = capacity > 0 ? capacity : 100;
    this.cache = new Map<K, LRUNode<K, V>>();

    this.head = new LRUNode<K, V>(null as any, null as any);
    this.tail = new LRUNode<K, V>(null as any, null as any);
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  private addToHead(node: LRUNode<K, V>): void {
    node.prev = this.head;
    node.next = this.head.next;
    this.head.next!.prev = node;
    this.head.next = node;
  }

  private removeNode(node: LRUNode<K, V>): void {
    node.prev!.next = node.next;
    node.next!.prev = node.prev;
  }

  private moveToHead(node: LRUNode<K, V>): void {
    this.removeNode(node);
    this.addToHead(node);
  }

  private findVictimNode(): LRUNode<K, V> {
    let victim = this.tail.prev!;
    let minScore = this.calculateEvictionScore(victim);

    let current = this.tail.prev;
    let count = 0;
    const maxCheck = Math.min(5, this.cache.size);

    while (current && current !== this.head && count < maxCheck) {
      const score = this.calculateEvictionScore(current);
      if (score < minScore) {
        minScore = score;
        victim = current;
      }
      current = current.prev;
      count++;
    }

    return victim;
  }

  private calculateEvictionScore(node: LRUNode<K, V>): number {
    const now = Date.now();
    const timeSinceAccess = now - node.lastAccessed;
    const timeWeight = 1 / (1 + timeSinceAccess / (1000 * 60));
    const frequencyWeight = Math.log(node.frequency + 1);

    return frequencyWeight * timeWeight;
  }

  get(key: K): V | null {
    const node = this.cache.get(key);
    if (node) {
      node.frequency++;
      node.lastAccessed = Date.now();
      this.moveToHead(node);
      return node.value;
    }
    return null;
  }

  set(key: K, value: V): void {
    const existingNode = this.cache.get(key);

    if (existingNode) {
      existingNode.value = value;
      this.moveToHead(existingNode);
    } else {
      const newNode = new LRUNode(key, value);

      if (this.cache.size >= this.capacity) {
        const victimNode = this.findVictimNode();
        this.removeNode(victimNode);
        this.cache.delete(victimNode.key);
      }

      this.cache.set(key, newNode);
      this.addToHead(newNode);
    }
  }

  has(key: K): boolean {
    return this.cache.has(key);
  }

  clear(): void {
    this.cache.clear();
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  get size(): number {
    return this.cache.size;
  }

  /**
   * Get cache statistics
   */
  getStats(): { size: number; capacity: number; usage: number } {
    return {
      size: this.cache.size,
      capacity: this.capacity,
      usage: this.cache.size / this.capacity,
    };
  }
}

export default LRUCache;

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/storage/persistent-vars.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview 持久化变量存储
 * @description 实现 $ 前缀变量的持久化,使用 LWW(Last-Write-Wins)策略
 */

import type { PersistentVarRecord, PersistentVariableName } from '../domain/variables';
import type { JsonValue } from '../domain/json';
import type { PersistentVarsStore } from '../engine/storage/storage-port';
import { RR_V3_STORES, withTransaction } from './db';

/**
 * 创建 PersistentVarsStore 实现
 */
export function createPersistentVarsStore(): PersistentVarsStore {
  return {
    async get(key: PersistentVariableName): Promise<PersistentVarRecord | undefined> {
      return withTransaction(RR_V3_STORES.PERSISTENT_VARS, 'readonly', async (stores) => {
        const store = stores[RR_V3_STORES.PERSISTENT_VARS];
        return new Promise<PersistentVarRecord | undefined>((resolve, reject) => {
          const request = store.get(key);
          request.onsuccess = () => resolve(request.result as PersistentVarRecord | undefined);
          request.onerror = () => reject(request.error);
        });
      });
    },

    async set(key: PersistentVariableName, value: JsonValue): Promise<PersistentVarRecord> {
      return withTransaction(RR_V3_STORES.PERSISTENT_VARS, 'readwrite', async (stores) => {
        const store = stores[RR_V3_STORES.PERSISTENT_VARS];

        // 先读取现有记录(用于 version 递增)
        const existing = await new Promise<PersistentVarRecord | undefined>((resolve, reject) => {
          const request = store.get(key);
          request.onsuccess = () => resolve(request.result as PersistentVarRecord | undefined);
          request.onerror = () => reject(request.error);
        });

        const now = Date.now();
        const record: PersistentVarRecord = {
          key,
          value,
          updatedAt: now,
          version: (existing?.version ?? 0) + 1,
        };

        await new Promise<void>((resolve, reject) => {
          const request = store.put(record);
          request.onsuccess = () => resolve();
          request.onerror = () => reject(request.error);
        });

        return record;
      });
    },

    async delete(key: PersistentVariableName): Promise<void> {
      return withTransaction(RR_V3_STORES.PERSISTENT_VARS, 'readwrite', async (stores) => {
        const store = stores[RR_V3_STORES.PERSISTENT_VARS];
        return new Promise<void>((resolve, reject) => {
          const request = store.delete(key);
          request.onsuccess = () => resolve();
          request.onerror = () => reject(request.error);
        });
      });
    },

    async list(prefix?: PersistentVariableName): Promise<PersistentVarRecord[]> {
      return withTransaction(RR_V3_STORES.PERSISTENT_VARS, 'readonly', async (stores) => {
        const store = stores[RR_V3_STORES.PERSISTENT_VARS];

        return new Promise<PersistentVarRecord[]>((resolve, reject) => {
          const request = store.getAll();
          request.onsuccess = () => {
            let results = request.result as PersistentVarRecord[];

            // 如果指定了前缀,过滤结果
            if (prefix) {
              results = results.filter((r) => r.key.startsWith(prefix));
            }

            resolve(results);
          };
          request.onerror = () => reject(request.error);
        });
      });
    },
  };
}

```

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

```vue
<template>
  <div
    :class="['workflow-node', selected ? 'selected' : '', `type-${data.node.type}`]"
    @click="onSelect()"
  >
    <div v-if="hasErrors" class="node-error" :title="errorsTitle">
      <ILucideShieldX />
      <div class="tooltip">
        <div class="item" v-for="e in errList" :key="e">• {{ e }}</div>
      </div>
    </div>
    <div class="node-container">
      <div :class="['node-icon', `icon-${data.node.type}`]">
        <component :is="iconComp(data.node.type)" />
      </div>
      <div class="node-body">
        <div class="node-name">{{ data.node.name || getTypeLabel(data.node.type) }}</div>
        <div class="node-subtitle">{{ subtitle }}</div>
      </div>
    </div>

    <div class="if-cases">
      <div v-for="(b, idx) in branches" :key="b.id" class="case-row">
        <div class="case-label">{{ b.name || `条件${idx + 1}` }}</div>
        <Handle
          type="source"
          :position="Position.Right"
          :id="`case:${b.id}`"
          :class="['node-handle', hasOutgoingLabel(`case:${b.id}`) ? 'connected' : 'unconnected']"
        />
      </div>
      <div v-if="hasElse" class="case-row else-row">
        <div class="case-label">Else</div>
        <Handle
          type="source"
          :position="Position.Right"
          id="case:else"
          :class="['node-handle', hasOutgoingLabel('case:else') ? 'connected' : 'unconnected']"
        />
      </div>
    </div>

    <Handle
      type="target"
      :position="Position.Left"
      :class="['node-handle', hasIncoming ? 'connected' : 'unconnected']"
    />
  </div>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import type { NodeBase, Edge as EdgeV2 } from '@/entrypoints/background/record-replay/types';
import { Handle, Position } from '@vue-flow/core';
import { iconComp, getTypeLabel, nodeSubtitle } from './node-util';
import ILucideShieldX from '~icons/lucide/shield-x';

const props = defineProps<{
  id: string;
  data: { node: NodeBase; edges: EdgeV2[]; onSelect: (id: string) => void; errors?: string[] };
  selected?: boolean;
}>();

const hasIncoming = computed(
  () => props.data.edges?.some?.((e) => e && e.to === props.data.node.id) || false,
);
const branches = computed(() => {
  try {
    return Array.isArray((props.data.node as any)?.config?.branches)
      ? ((props.data.node as any).config.branches as any[]).map((x) => ({
          id: String(x.id || ''),
          name: x.name,
          expr: x.expr,
        }))
      : [];
  } catch {
    return [];
  }
});
const hasElse = computed(() => {
  try {
    return (props.data.node as any)?.config?.else !== false;
  } catch {
    return true;
  }
});
const subtitle = computed(() => nodeSubtitle(props.data.node));
const errList = computed(() => (props.data.errors || []) as string[]);
const hasErrors = computed(() => errList.value.length > 0);
const errorsTitle = computed(() => errList.value.join('\n'));

function hasOutgoingLabel(label: string) {
  try {
    return (props.data.edges || []).some(
      (e: any) => e && e.from === props.data.node.id && String(e.label || '') === String(label),
    );
  } catch {
    return false;
  }
}

function onSelect() {
  try {
    props.data.onSelect(props.id);
  } catch {}
}
</script>

```
Page 2/43FirstPrevNextLast