This is page 1 of 43. Use http://codebase.md/hangwin/mcp-chrome?lines=false&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
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
*.onnx filter=lfs diff=lfs merge=lfs -text
```
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
```json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"printWidth": 100,
"endOfLine": "auto",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "strict"
}
```
--------------------------------------------------------------------------------
/packages/wasm-simd/.gitignore:
--------------------------------------------------------------------------------
```
# WASM build outputs
/pkg/
/target/
# Rust
Cargo.lock
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
```
--------------------------------------------------------------------------------
/app/chrome-extension/.env.example:
--------------------------------------------------------------------------------
```
# Chrome Extension Private Key
# Copy this file to .env and replace with your actual private key
# This key is used for Chrome extension packaging and should be kept secure
CHROME_EXTENSION_KEY=YOUR_PRIVATE_KEY_HERE
```
--------------------------------------------------------------------------------
/app/native-server/.npmignore:
--------------------------------------------------------------------------------
```
# Development-only files that should not be published to npm
# node_path.txt contains the absolute path to Node.js used during build.
# It's written by build.ts for development hot-reload, but is useless
# (and potentially confusing) in the published package since users will
# have their own Node.js path written by postinstall.
node_path.txt
**/node_path.txt
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.output
stats.html
stats-*.json
.wxt
web-ext.config.ts
dist
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.onnx
# Environment variables
.env
.env.local
.env.*.local
# Prevent npm metadata pollution
false/
metadata-v1.3/
registry.npmmirror.com/
registry.npmjs.com/
other/
tools_optimize.md
Agents.md
CLAUDE.md
**/*/coverage/*
.docs/
.claude/
```
--------------------------------------------------------------------------------
/app/chrome-extension/README.md:
--------------------------------------------------------------------------------
```markdown
# WXT + Vue 3
This template should help get you started developing with Vue 3 in WXT.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar).
```
--------------------------------------------------------------------------------
/packages/wasm-simd/README.md:
--------------------------------------------------------------------------------
```markdown
# @chrome-mcp/wasm-simd
SIMD-optimized WebAssembly math functions for high-performance vector operations.
## Features
- 🚀 **SIMD Acceleration**: Uses WebAssembly SIMD instructions for 4-8x performance boost
- 🧮 **Vector Operations**: Optimized cosine similarity, batch processing, and matrix operations
- 🔧 **Memory Efficient**: Smart memory pooling and aligned buffer management
- 🌐 **Browser Compatible**: Works in all modern browsers with WebAssembly SIMD support
## Performance
| Operation | JavaScript | SIMD WASM | Speedup |
| ------------------------------ | ---------- | --------- | ------- |
| Cosine Similarity (768d) | 100ms | 18ms | 5.6x |
| Batch Similarity (100x768d) | 850ms | 95ms | 8.9x |
| Similarity Matrix (50x50x384d) | 2.1s | 180ms | 11.7x |
## Usage
```rust
// The Rust implementation provides SIMD-optimized functions
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct SIMDMath;
#[wasm_bindgen]
impl SIMDMath {
#[wasm_bindgen(constructor)]
pub fn new() -> SIMDMath { SIMDMath }
#[wasm_bindgen]
pub fn cosine_similarity(&self, vec_a: &[f32], vec_b: &[f32]) -> f32 {
// SIMD-optimized implementation
}
}
```
## Building
```bash
# Install dependencies
cargo install wasm-pack
# Build for release
npm run build
# Build for development
npm run build:dev
```
## Browser Support
- Chrome 91+
- Firefox 89+
- Safari 16.4+
- Edge 91+
Older browsers automatically fall back to JavaScript implementations.
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Chrome MCP Server 🚀
[](https://img.shields.io/github/stars/hangwin/mcp-chrome)
[](https://opensource.org/licenses/MIT)
[](https://www.typescriptlang.org/)
[](https://developer.chrome.com/docs/extensions/)
[](https://img.shields.io/github/v/release/hangwin/mcp-chrome.svg)
> 🌟 **Turn your Chrome browser into your intelligent assistant** - Let AI take control of your browser, transforming it into a powerful AI-controlled automation tool.
**📖 Documentation**: [English](README.md) | [中文](README_zh.md)
> The project is still in its early stages and is under intensive development. More features, stability improvements, and other enhancements will follow.
---
## 🎯 What is Chrome MCP Server?
Chrome MCP Server is a Chrome extension-based **Model Context Protocol (MCP) server** that exposes your Chrome browser functionality to AI assistants like Claude, enabling complex browser automation, content analysis, and semantic search. Unlike traditional browser automation tools (like Playwright), **Chrome MCP Server** directly uses your daily Chrome browser, leveraging existing user habits, configurations, and login states, allowing various large models or chatbots to take control of your browser and truly become your everyday assistant.
## ✨ New Features(2025/12/30)
- **A New Visual Editor for Claude Code & Codex**, for more detail here: [VisualEditor](docs/VisualEditor.md)
## ✨ Core Features
- 😁 **Chatbot/Model Agnostic**: Let any LLM or chatbot client or agent you prefer automate your browser
- ⭐️ **Use Your Original Browser**: Seamlessly integrate with your existing browser environment (your configurations, login states, etc.)
- 💻 **Fully Local**: Pure local MCP server ensuring user privacy
- 🚄 **Streamable HTTP**: Streamable HTTP connection method
- 🏎 **Cross-Tab**: Cross-tab context
- 🧠 **Semantic Search**: Built-in vector database for intelligent browser tab content discovery
- 🔍 **Smart Content Analysis**: AI-powered text extraction and similarity matching
- 🌐 **20+ Tools**: Support for screenshots, network monitoring, interactive operations, bookmark management, browsing history, and 20+ other tools
- 🚀 **SIMD-Accelerated AI**: Custom WebAssembly SIMD optimization for 4-8x faster vector operations
## 🆚 Comparison with Similar Projects
| Comparison Dimension | Playwright-based MCP Server | Chrome Extension-based MCP Server |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Resource Usage** | ❌ Requires launching independent browser process, installing Playwright dependencies, downloading browser binaries, etc. | ✅ No need to launch independent browser process, directly utilizes user's already open Chrome browser |
| **User Session Reuse** | ❌ Requires re-login | ✅ Automatically uses existing login state |
| **Browser Environment** | ❌ Clean environment lacks user settings | ✅ Fully preserves user environment |
| **API Access** | ⚠️ Limited to Playwright API | ✅ Full access to Chrome native APIs |
| **Startup Speed** | ❌ Requires launching browser process | ✅ Only needs to activate extension |
| **Response Speed** | 50-200ms inter-process communication | ✅ Faster |
## 🚀 Quick Start
### Prerequisites
- Node.js >= 20.0.0 and pnpm/npm
- Chrome/Chromium browser
### Installation Steps
1. **Download the latest Chrome extension from GitHub**
Download link: https://github.com/hangwin/mcp-chrome/releases
2. **Install mcp-chrome-bridge globally**
npm
```bash
npm install -g mcp-chrome-bridge
```
pnpm
```bash
# Method 1: Enable scripts globally (recommended)
pnpm config set enable-pre-post-scripts true
pnpm install -g mcp-chrome-bridge
# Method 2: Manual registration (if postinstall doesn't run)
pnpm install -g mcp-chrome-bridge
mcp-chrome-bridge register
```
> Note: pnpm v7+ disables postinstall scripts by default for security. The `enable-pre-post-scripts` setting controls whether pre/post install scripts run. If automatic registration fails, use the manual registration command above.
3. **Load Chrome Extension**
- Open Chrome and go to `chrome://extensions/`
- Enable "Developer mode"
- Click "Load unpacked" and select `your/dowloaded/extension/folder`
- Click the extension icon to open the plugin, then click connect to see the MCP configuration
<img width="475" alt="Screenshot 2025-06-09 15 52 06" src="https://github.com/user-attachments/assets/241e57b8-c55f-41a4-9188-0367293dc5bc" />
### Usage with MCP Protocol Clients
#### Using Streamable HTTP Connection (👍🏻 Recommended)
Add the following configuration to your MCP client configuration (using CherryStudio as an example):
> Streamable HTTP connection method is recommended
```json
{
"mcpServers": {
"chrome-mcp-server": {
"type": "streamableHttp",
"url": "http://127.0.0.1:12306/mcp"
}
}
}
```
#### Using STDIO Connection (Alternative)
If your client only supports stdio connection method, please use the following approach:
1. First, check the installation location of the npm package you just installed
```sh
# npm check method
npm list -g mcp-chrome-bridge
# pnpm check method
pnpm list -g mcp-chrome-bridge
```
Assuming the command above outputs the path: /Users/xxx/Library/pnpm/global/5
Then your final path would be: /Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js
2. Replace the configuration below with the final path you just obtained
```json
{
"mcpServers": {
"chrome-mcp-stdio": {
"command": "npx",
"args": [
"node",
"/Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js"
]
}
}
}
```
eg:config in augment:
<img width="494" alt="截屏2025-06-22 22 11 25" src="https://github.com/user-attachments/assets/48eefc0c-a257-4d3b-8bbe-d7ff716de2bf" />
## 🛠️ Available Tools
Complete tool list: [Complete Tool List](docs/TOOLS.md)
<details>
<summary><strong>📊 Browser Management (6 tools)</strong></summary>
- `get_windows_and_tabs` - List all browser windows and tabs
- `chrome_navigate` - Navigate to URLs and control viewport
- `chrome_switch_tab` - Switch the current active tab
- `chrome_close_tabs` - Close specific tabs or windows
- `chrome_go_back_or_forward` - Browser navigation control
- `chrome_inject_script` - Inject content scripts into web pages
- `chrome_send_command_to_inject_script` - Send commands to injected content scripts
</details>
<details>
<summary><strong>📸 Screenshots & Visual (1 tool)</strong></summary>
- `chrome_screenshot` - Advanced screenshot capture with element targeting, full-page support, and custom dimensions
</details>
<details>
<summary><strong>🌐 Network Monitoring (4 tools)</strong></summary>
- `chrome_network_capture_start/stop` - webRequest API network capture
- `chrome_network_debugger_start/stop` - Debugger API with response bodies
- `chrome_network_request` - Send custom HTTP requests
</details>
<details>
<summary><strong>🔍 Content Analysis (4 tools)</strong></summary>
- `search_tabs_content` - AI-powered semantic search across browser tabs
- `chrome_get_web_content` - Extract HTML/text content from pages
- `chrome_get_interactive_elements` - Find clickable elements
- `chrome_console` - Capture and retrieve console output from browser tabs
</details>
<details>
<summary><strong>🎯 Interaction (3 tools)</strong></summary>
- `chrome_click_element` - Click elements using CSS selectors
- `chrome_fill_or_select` - Fill forms and select options
- `chrome_keyboard` - Simulate keyboard input and shortcuts
</details>
<details>
<summary><strong>📚 Data Management (5 tools)</strong></summary>
- `chrome_history` - Search browser history with time filters
- `chrome_bookmark_search` - Find bookmarks by keywords
- `chrome_bookmark_add` - Add new bookmarks with folder support
- `chrome_bookmark_delete` - Delete bookmarks
</details>
## 🧪 Usage Examples
### AI helps you summarize webpage content and automatically control Excalidraw for drawing
prompt: [excalidraw-prompt](prompt/excalidraw-prompt.md)
Instruction: Help me summarize the current page content, then draw a diagram to aid my understanding.
https://www.youtube.com/watch?v=3fBPdUBWVz0
https://github.com/user-attachments/assets/fd17209b-303d-48db-9e5e-3717141df183
### After analyzing the content of the image, the LLM automatically controls Excalidraw to replicate the image
prompt: [excalidraw-prompt](prompt/excalidraw-prompt.md)|[content-analize](prompt/content-analize.md)
Instruction: First, analyze the content of the image, and then replicate the image by combining the analysis with the content of the image.
https://www.youtube.com/watch?v=tEPdHZBzbZk
https://github.com/user-attachments/assets/60d12b1a-9b74-40f4-994c-95e8fa1fc8d3
### AI automatically injects scripts and modifies webpage styles
prompt: [modify-web-prompt](prompt/modify-web.md)
Instruction: Help me modify the current page's style and remove advertisements.
https://youtu.be/twI6apRKHsk
https://github.com/user-attachments/assets/69cb561c-2e1e-4665-9411-4a3185f9643e
### AI automatically captures network requests for you
query: I want to know what the search API for Xiaohongshu is and what the response structure looks like
https://youtu.be/1hHKr7XKqnQ
https://github.com/user-attachments/assets/dc7e5cab-b9af-4b9a-97ce-18e4837318d9
### AI helps analyze your browsing history
query: Analyze my browsing history from the past month
https://youtu.be/jf2UZfrR2Vk
https://github.com/user-attachments/assets/31b2e064-88c6-4adb-96d7-50748b826eae
### Web page conversation
query: Translate and summarize the current web page
https://youtu.be/FlJKS9UQyC8
https://github.com/user-attachments/assets/aa8ef2a1-2310-47e6-897a-769d85489396
### AI automatically takes screenshots for you (web page screenshots)
query: Take a screenshot of Hugging Face's homepage
https://youtu.be/7ycK6iksWi4
https://github.com/user-attachments/assets/65c6eee2-6366-493d-a3bd-2b27529ff5b3
### AI automatically takes screenshots for you (element screenshots)
query: Capture the icon from Hugging Face's homepage
https://youtu.be/ev8VivANIrk
https://github.com/user-attachments/assets/d0cf9785-c2fe-4729-a3c5-7f2b8b96fe0c
### AI helps manage bookmarks
query: Add the current page to bookmarks and put it in an appropriate folder
https://youtu.be/R_83arKmFTo
https://github.com/user-attachments/assets/15a7d04c-0196-4b40-84c2-bafb5c26dfe0
### Automatically close web pages
query: Close all shadcn-related web pages
https://youtu.be/2wzUT6eNVg4
https://github.com/user-attachments/assets/83de4008-bb7e-494d-9b0f-98325cfea592
## 🤝 Contributing
We welcome contributions! Please see [CONTRIBUTING.md](docs/CONTRIBUTING.md) for detailed guidelines.
## 🚧 Future Roadmap
We have exciting plans for the future development of Chrome MCP Server:
- [ ] Authentication
- [ ] Recording and Playback
- [ ] Workflow Automation
- [ ] Enhanced Browser Support (Firefox Extension)
---
**Want to contribute to any of these features?** Check out our [Contributing Guide](docs/CONTRIBUTING.md) and join our development community!
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 📚 More Documentation
- [Architecture Design](docs/ARCHITECTURE.md) - Detailed technical architecture documentation
- [TOOLS API](docs/TOOLS.md) - Complete tool API documentation
- [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issue solutions
```
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
# Contributing Guide 🤝
Thank you for your interest in contributing to Chrome MCP Server! This document provides guidelines and information for contributors.
## 🎯 How to Contribute
We welcome contributions in many forms:
- 🐛 Bug reports and fixes
- ✨ New features and tools
- 📚 Documentation improvements
- 🧪 Tests and performance optimizations
- 🌐 Translations and internationalization
- 💡 Ideas and suggestions
## 🚀 Getting Started
### Prerequisites
- **Node.js 20+** and **pnpm or npm** (latest version)
- **Chrome/Chromium** browser for testing
- **Git** for version control
- **Rust** (for WASM development, optional)
- **TypeScript** knowledge
### Development Setup
1. **Fork and clone the repository**
```bash
git clone https://github.com/YOUR_USERNAME/chrome-mcp-server.git
cd chrome-mcp-server
```
2. **Install dependencies**
```bash
pnpm install
```
3. **Start the project**
```bash
npm run dev
```
4. **Load the extension in Chrome**
- Open `chrome://extensions/`
- Enable "Developer mode"
- Click "Load unpacked" and select `your/extension/dist`
## 🏗️ Project Structure
```
chrome-mcp-server/
├── app/
│ ├── chrome-extension/ # Chrome extension (WXT + Vue 3)
│ │ ├── entrypoints/ # Background scripts, popup, content scripts
│ │ ├── utils/ # AI models, vector database, utilities
│ │ └── workers/ # Web Workers for AI processing
│ └── native-server/ # Native messaging server (Fastify + TypeScript)
│ ├── src/mcp/ # MCP protocol implementation
│ └── src/server/ # HTTP server and native messaging
├── packages/
│ ├── shared/ # Shared types and utilities
│ └── wasm-simd/ # SIMD-optimized WebAssembly math functions
└── docs/ # Documentation
```
## 🛠️ Development Workflow
### Adding New Tools
1. **Define the tool schema in `packages/shared/src/tools.ts`**:
```typescript
{
name: 'your_new_tool',
description: 'Description of what your tool does',
inputSchema: {
type: 'object',
properties: {
// Define parameters
},
required: ['param1']
}
}
```
2. **Implement the tool in `app/chrome-extension/entrypoints/background/tools/browser/`**:
```typescript
class YourNewTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.YOUR_NEW_TOOL;
async execute(args: YourToolParams): Promise<ToolResult> {
// Implementation
}
}
```
3. **Export the tool in `app/chrome-extension/entrypoints/background/tools/browser/index.ts`**
4. **Add tests in the appropriate test directory**
### Code Style Guidelines
- **TypeScript**: Use strict TypeScript with proper typing
- **ESLint**: Follow the configured ESLint rules (`pnpm lint`)
- **Prettier**: Format code with Prettier (`pnpm format`)
- **Naming**: Use descriptive names and follow existing patterns
- **Comments**: Add JSDoc comments for public APIs
- **Error Handling**: Always handle errors gracefully
## 📝 Pull Request Process
1. **Create a feature branch**
```bash
git checkout -b feature/your-feature-name
```
2. **Make your changes**
- Follow the code style guidelines
- Add tests for new functionality
- Update documentation if needed
3. **Test your changes**
- Ensure all existing tests pass
- Test the Chrome extension manually
- Verify MCP protocol compatibility
4. **Commit your changes**
```bash
git add .
git commit -m "feat: add your feature description"
```
We use [Conventional Commits](https://www.conventionalcommits.org/):
- `feat:` for new features
- `fix:` for bug fixes
- `docs:` for documentation changes
- `test:` for adding tests
- `refactor:` for code refactoring
5. **Push and create a Pull Request**
```bash
git push origin feature/your-feature-name
```
## 🐛 Bug Reports
When reporting bugs, please include:
- **Environment**: OS, Chrome version, Node.js version
- **Steps to reproduce**: Clear, step-by-step instructions
- **Expected behavior**: What should happen
- **Actual behavior**: What actually happens
- **Screenshots/logs**: If applicable
- **MCP client**: Which MCP client you're using (Claude Desktop, etc.)
## 💡 Feature Requests
For feature requests, please provide:
- **Use case**: Why is this feature needed?
- **Proposed solution**: How should it work?
- **Alternatives**: Any alternative solutions considered?
- **Additional context**: Screenshots, examples, etc.
## 🔧 Development Tips
### Using WASM SIMD
If you're contributing to the WASM SIMD package:
```bash
cd packages/wasm-simd
# Install Rust and wasm-pack if not already installed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install wasm-pack
# Build WASM package
pnpm build
# The built files will be copied to app/chrome-extension/workers/
```
### Debugging Chrome Extension
- Use Chrome DevTools for debugging extension popup and background scripts
- Check `chrome://extensions/` for extension errors
- Use `console.log` statements for debugging
- Monitor the native messaging connection in the background script
### Testing MCP Protocol
- Use MCP Inspector for protocol debugging
- Test with different MCP clients (Claude Desktop, custom clients)
- Verify tool schemas and responses match MCP specifications
## 📚 Resources
- [Model Context Protocol Specification](https://modelcontextprotocol.io/)
- [Chrome Extension Development](https://developer.chrome.com/docs/extensions/)
- [WXT Framework Documentation](https://wxt.dev/)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
## 🤝 Community
- **GitHub Issues**: For bug reports and feature requests
- **GitHub Discussions**: For questions and general discussion
- **Pull Requests**: For code contributions
## 📄 License
By contributing to Chrome MCP Server, you agree that your contributions will be licensed under the MIT License.
## 🎯 Contributor Guidelines
### New Contributors
If you're contributing to an open source project for the first time:
1. **Start small**: Look for issues labeled "good first issue"
2. **Read the code**: Familiarize yourself with the project structure and coding style
3. **Ask questions**: Ask questions in GitHub Discussions
4. **Learn the tools**: Get familiar with Git, GitHub, TypeScript, and other tools
### Experienced Contributors
- **Architecture improvements**: Propose system-level improvements
- **Performance optimization**: Identify and fix performance bottlenecks
- **New features**: Design and implement complex new features
- **Mentor newcomers**: Help new contributors get started
### Documentation Contributions
- **API documentation**: Improve tool documentation and examples
- **Tutorials**: Create usage guides and best practices
- **Translations**: Help translate documentation to other languages
- **Video content**: Create demo videos and tutorials
### Testing Contributions
- **Unit tests**: Write tests for new features
- **Integration tests**: Test interactions between components
- **Performance tests**: Benchmark testing and performance regression detection
- **User testing**: Functional testing in real-world scenarios
## 🏆 Contributor Recognition
We value every contribution, no matter how big or small. Contributors will be recognized in the following ways:
- **README acknowledgments**: Contributors listed in the project README
- **Release notes**: Contributors thanked in version release notes
- **Contributor badges**: Contributor badges on GitHub profiles
- **Community recognition**: Special thanks in community discussions
Thank you for considering contributing to Chrome MCP Server! Your participation makes this project better.
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/model/node-spec-registry.ts:
--------------------------------------------------------------------------------
```typescript
export * from 'chrome-mcp-shared';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/model/node-spec.ts:
--------------------------------------------------------------------------------
```typescript
export * from 'chrome-mcp-shared';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/model/node-specs-builtin.ts:
--------------------------------------------------------------------------------
```typescript
export * from 'chrome-mcp-shared';
```
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
```yaml
packages:
- 'app/*'
- 'packages/*'
```
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
```json
{
"recommendations": ["Vue.volar"]
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"extends": "./.wxt/tsconfig.json"
}
```
--------------------------------------------------------------------------------
/app/native-server/src/mcp/stdio-config.json:
--------------------------------------------------------------------------------
```json
{
"url": "http://127.0.0.1:12306/mcp"
}
```
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
```
module.exports = {
extends: ['@commitlint/config-conventional'],
};
```
--------------------------------------------------------------------------------
/app/native-server/src/agent/db/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Database module exports.
*/
export * from './schema';
export * from './client';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/content.ts:
--------------------------------------------------------------------------------
```typescript
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {},
});
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/options/main.ts:
--------------------------------------------------------------------------------
```typescript
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
```
--------------------------------------------------------------------------------
/packages/shared/src/constants.ts:
--------------------------------------------------------------------------------
```typescript
export const DEFAULT_SERVER_PORT = 12306;
export const HOST_NAME = 'com.chromemcp.nativehost';
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/register-dev.ts:
--------------------------------------------------------------------------------
```typescript
import { registerUserLevelHostWithNodePath } from './utils';
registerUserLevelHostWithNodePath();
```
--------------------------------------------------------------------------------
/app/native-server/src/server/routes/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Routes module exports.
*/
export { registerAgentRoutes, type AgentRoutesOptions } from './agent';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/workflows/index.ts:
--------------------------------------------------------------------------------
```typescript
export { default as WorkflowsView } from './WorkflowsView.vue';
export { default as WorkflowListItem } from './WorkflowListItem.vue';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/flow-runner.ts:
--------------------------------------------------------------------------------
```typescript
// thin re-export for backward compatibility
export { runFlow } from './engine/scheduler';
export type { RunOptions } from './engine/scheduler';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/offscreen/index.html:
--------------------------------------------------------------------------------
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/builder/main.ts:
--------------------------------------------------------------------------------
```typescript
import { createApp } from 'vue';
import App from './App.vue';
// Tailwind first, then custom tokens
import '../styles/tailwind.css';
createApp(App).mount('#app');
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/welcome/main.ts:
--------------------------------------------------------------------------------
```typescript
import { createApp } from 'vue';
import App from './App.vue';
// Tailwind first, then custom tokens
import '../styles/tailwind.css';
createApp(App).mount('#app');
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/quick-panel/providers/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Quick Panel Search Providers
*
* Exports all search providers for Quick Panel.
*/
export {
createTabsProvider,
type TabsProviderOptions,
type TabsSearchResultData,
} from './tabs-provider';
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/constant.ts:
--------------------------------------------------------------------------------
```typescript
export const COMMAND_NAME = 'mcp-chrome-bridge';
export const EXTENSION_ID = 'hbdgbgagpkpjffpklnamcljpakneikee';
export const HOST_NAME = 'com.chromemcp.nativehost';
export const DESCRIPTION = 'Node.js Host for Browser Bridge Extension';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2/ui/property-panel/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Property Panel Module
*
* Exports the property panel component and its types.
*/
export { createPropertyPanel } from './property-panel';
export type { PropertyPanel, PropertyPanelOptions, PropertyPanelTab, DesignControl } from './types';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/storage/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Storage 层导出入口
*/
export * from './db';
export * from './flows';
export * from './runs';
export * from './events';
export * from './queue';
export * from './persistent-vars';
export * from './triggers';
export * from './import';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/engine/kernel/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Kernel 模块导出入口
*/
export * from './kernel';
export * from './runner';
export * from './traversal';
export * from './breakpoints';
export * from './artifacts';
export * from './debug-controller';
export * from './recovery-kernel';
```
--------------------------------------------------------------------------------
/packages/shared/src/labels.ts:
--------------------------------------------------------------------------------
```typescript
// labels.ts — centralized labels for edges and other enums
export const EDGE_LABELS = {
DEFAULT: 'default',
TRUE: 'true',
FALSE: 'false',
ON_ERROR: 'onError',
} as const;
export type EdgeLabel = (typeof EDGE_LABELS)[keyof typeof EDGE_LABELS];
```
--------------------------------------------------------------------------------
/app/chrome-extension/common/step-types.ts:
--------------------------------------------------------------------------------
```typescript
// step-types.ts — re-export shared constants to keep single source of truth
export { STEP_TYPES } from 'chrome-mcp-shared';
export type StepTypeConst =
(typeof import('chrome-mcp-shared'))['STEP_TYPES'][keyof (typeof import('chrome-mcp-shared'))['STEP_TYPES']];
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/options/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Userscripts Manager</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/app/chrome-extension/env.d.ts:
--------------------------------------------------------------------------------
```typescript
/// <reference types="unplugin-icons/types/vue" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
type Props = Record<string, never>;
type RawBindings = Record<string, never>;
const component: DefineComponent<Props, RawBindings, any>;
export default component;
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/composables/useRRV3Rpc.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Re-export shared useRRV3Rpc composable
* @description This file re-exports the shared composable for backward compatibility
*/
export {
useRRV3Rpc,
type UseRRV3Rpc,
type UseRRV3RpcOptions,
type RpcRequestOptions,
} from '@/entrypoints/shared/composables/useRRV3Rpc';
```
--------------------------------------------------------------------------------
/packages/shared/src/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './constants';
export * from './types';
export * from './tools';
export * from './rr-graph';
export * from './step-types';
export * from './labels';
export * from './node-spec';
export * from './node-spec-registry';
export * from './node-specs-builtin';
export * from './agent-types';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/shared/utils/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Shared Utilities Index
* @description Utility functions shared between UI entrypoints
*/
// Flow conversion utilities
export {
flowV2ToV3ForRpc,
flowV3ToV2ForBuilder,
isFlowV3,
isFlowV2,
extractFlowCandidates,
type FlowConversionResult,
} from './rr-flow-convert';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/engine/triggers/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Triggers 模块导出入口
*/
export * from './trigger-handler';
export * from './trigger-manager';
export * from './url-trigger';
export * from './command-trigger';
export * from './context-menu-trigger';
export * from './dom-trigger';
export * from './cron-trigger';
export * from './manual-trigger';
```
--------------------------------------------------------------------------------
/packages/wasm-simd/Cargo.toml:
--------------------------------------------------------------------------------
```toml
[package]
name = "simd-math"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
wide = "0.7"
console_error_panic_hook = "0.1"
[dependencies.web-sys]
version = "0.3"
features = [
"console",
]
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
```
--------------------------------------------------------------------------------
/app/native-server/src/shims/devtools.d.ts:
--------------------------------------------------------------------------------
```typescript
// Single-file shim for all deep imports from chrome-devtools-frontend.
// This prevents TypeScript from type-checking the upstream DevTools source tree.
// Runtime still loads the real package; this file is types-only and local to TS.
declare module 'chrome-devtools-frontend/*' {
const anyExport: any;
export = anyExport;
}
```
--------------------------------------------------------------------------------
/packages/shared/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"declaration": true,
"outDir": "./dist",
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/StopIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" :class="className">
<rect x="6" y="6" width="12" height="12" rx="1" fill="currentColor" />
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/model/variables.ts:
--------------------------------------------------------------------------------
```typescript
// variables.ts — Shared variable suggestion types for builder UI
export type VariableOrigin = 'global' | 'node';
export interface VariableOption {
key: string;
origin: VariableOrigin;
nodeId?: string;
nodeName?: string;
}
export const VAR_TOKEN_OPEN = '{';
export const VAR_TOKEN_CLOSE = '}';
export const VAR_PLACEHOLDER = '{}';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>工作流管理</title>
<meta name="manifest.type" content="side_panel" />
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/builder/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>工作流编辑器</title>
<meta name="manifest.type" content="unlisted_page" />
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/element-picker/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Element Picker (UI)
*
* A Quick Panel-styled floating panel used by chrome_request_element_selection.
*/
export { createElementPickerController } from './controller';
export type {
ElementPickerController,
ElementPickerControllerOptions,
ElementPickerUiState,
ElementPickerUiRequest,
ElementPickerUiPatch,
} from './controller';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Default Popup Title</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/engine/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Engine 层导出入口
*/
// Kernel
export * from './kernel';
// Queue
export * from './queue';
// Plugins
export * from './plugins';
// Transport
export * from './transport';
// Keepalive
export * from './keepalive';
// Recovery
export * from './recovery';
// Triggers
export * from './triggers';
// Storage Port
export * from './storage';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyClick.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div>
<SelectorEditor :node="node" :allowPick="true" />
</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';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/welcome/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to Chrome MCP Server</title>
<meta name="manifest.type" content="unlisted_page" />
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/RecordIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" :class="className">
<circle cx="12" cy="12" r="8" :fill="recording ? '#ef4444' : 'currentColor'" />
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
recording?: boolean;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
recording: false,
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/types/icons.d.ts:
--------------------------------------------------------------------------------
```typescript
// Type shim for unplugin-icons virtual modules used as Vue components
// Keeps TS happy in IDE and during type-check without generating code.
declare module '~icons/*' {
import type { DefineComponent } from 'vue';
// Use explicit, non-empty object types to satisfy eslint rule
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, any>;
export default component;
}
```
--------------------------------------------------------------------------------
/app/native-server/jest.config.js:
--------------------------------------------------------------------------------
```javascript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/scripts/**/*'],
coverageDirectory: 'coverage',
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80,
},
},
};
```
--------------------------------------------------------------------------------
/packages/shared/src/node-spec-registry.ts:
--------------------------------------------------------------------------------
```typescript
// node-spec-registry.ts — runtime registry for NodeSpec (shared between UI/runtime)
import type { NodeSpec } from './node-spec';
const REG = new Map<string, NodeSpec>();
export function registerNodeSpec(spec: NodeSpec) {
REG.set(spec.type, spec);
}
export function getNodeSpec(type: string): NodeSpec | undefined {
return REG.get(type);
}
export function listNodeSpecs(): NodeSpec[] {
return Array.from(REG.values());
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/common/node-types.ts:
--------------------------------------------------------------------------------
```typescript
// node-types.ts — centralized node type constants for Builder/UI layer
// Combines all executable Step types with UI-only nodes (e.g., trigger, delay)
import { STEP_TYPES } from './step-types';
export const NODE_TYPES = {
// Executable step types (spread from STEP_TYPES)
...STEP_TYPES,
// UI-only nodes
TRIGGER: 'trigger',
DELAY: 'delay',
} as const;
export type NodeTypeConst = (typeof NODE_TYPES)[keyof typeof NODE_TYPES];
```
--------------------------------------------------------------------------------
/app/native-server/src/mcp/mcp-server.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { setupTools } from './register-tools';
export let mcpServer: Server | null = null;
export const getMcpServer = () => {
if (mcpServer) {
return mcpServer;
}
mcpServer = new Server(
{
name: 'ChromeMcpServer',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
},
);
setupTools(mcpServer);
return mcpServer;
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/tests/__mocks__/hnswlib-wasm-static.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Mock for hnswlib-wasm-static
* @description Provides a stub for vector database in test environment
*/
export const HierarchicalNSW = class MockHierarchicalNSW {
constructor() {}
initIndex() {}
addPoint() {}
searchKnn() {
return { neighbors: [], distances: [] };
}
getCurrentCount() {
return 0;
}
resizeIndex() {}
getPoint() {
return [];
}
markDelete() {}
};
export default { HierarchicalNSW };
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/shared/composables/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Shared UI Composables
* @description Composables shared between multiple UI entrypoints (Sidepanel, Builder, Popup, etc.)
*
* Note: These composables are for UI-only use. Do not import them in background scripts
* as they depend on Vue and will bloat the service worker bundle.
*/
// RR V3 RPC Client
export { useRRV3Rpc } from './useRRV3Rpc';
export type { UseRRV3Rpc, UseRRV3RpcOptions, RpcRequestOptions } from './useRRV3Rpc';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyDelay.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">延迟 (ms)</label>
<input class="form-input" type="number" v-model.number="(node as any).config.ms" min="0" />
</div>
</div>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/BoltIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/assets/vue.svg:
--------------------------------------------------------------------------------
```
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyKey.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">按键序列</label>
<input
class="form-input"
v-model="(node as any).config.keys"
placeholder="如 Backspace Enter 或 cmd+a"
/>
</div>
</div>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyNavigate.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group" data-field="navigate.url">
<label class="form-label">URL 地址</label>
<input
class="form-input"
v-model="(node as any).config.url"
placeholder="https://example.com"
/>
</div>
</div>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/model/toast.ts:
--------------------------------------------------------------------------------
```typescript
// toast.ts - lightweight toast event bus for builder UI
// Usage: import { toast } and call toast('message', 'warn'|'error'|'info')
export type ToastLevel = 'info' | 'warn' | 'error';
export function toast(message: string, level: ToastLevel = 'warn') {
try {
const ev = new CustomEvent('rr_toast', { detail: { message: String(message), level } });
window.dispatchEvent(ev);
} catch {
// as a last resort
console[level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log']('[toast]', message);
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/CheckIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
:class="className"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.052-.143Z"
clip-rule="evenodd"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-small',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/common/tool-handler.ts:
--------------------------------------------------------------------------------
```typescript
import type { CallToolResult, TextContent, ImageContent } from '@modelcontextprotocol/sdk/types.js';
export interface ToolResult extends CallToolResult {
content: (TextContent | ImageContent)[];
isError: boolean;
}
export interface ToolExecutor {
execute(args: any): Promise<ToolResult>;
}
export const createErrorResponse = (
message: string = 'Unknown error, please try again',
): ToolResult => {
return {
content: [
{
type: 'text',
text: message,
},
],
isError: true,
};
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/tailwind.config.ts:
--------------------------------------------------------------------------------
```typescript
import type { Config } from 'tailwindcss';
// Tailwind v4 config (TypeScript). The Vite plugin `@tailwindcss/vite`
// will auto-detect and load this file. No `content` field is required in v4.
export default {
theme: {
extend: {
colors: {
brand: {
DEFAULT: '#7C3AED',
dark: '#5B21B6',
light: '#A78BFA',
},
},
boxShadow: {
card: '0 6px 20px rgba(0,0,0,0.08)',
},
borderRadius: {
xl: '12px',
},
},
},
plugins: [],
} satisfies Config;
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/EditIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/AgentTimeline.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="pl-1 space-y-3">
<!-- Timeline container -->
<div class="relative pl-5 space-y-4 ml-1">
<AgentTimelineItem
v-for="(item, index) in items"
:key="item.id"
:item="item"
:is-last="index === items.length - 1"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import type { TimelineItem, AgentThreadState } from '../../composables/useAgentThreads';
import AgentTimelineItem from './AgentTimelineItem.vue';
defineProps<{
items: TimelineItem[];
state: AgentThreadState;
}>();
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/RefreshIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/native-server/src/agent/types.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Re-export agent types from shared package for backward compatibility.
* All types are now defined in packages/shared/src/agent-types.ts to ensure
* consistency between native-server and chrome-extension.
*/
export {
type AgentRole,
type AgentMessage,
type StreamTransport,
type AgentStatusEvent,
type AgentConnectedEvent,
type AgentHeartbeatEvent,
type RealtimeEvent,
type AgentAttachment,
type AgentCliPreference,
type AgentActRequest,
type AgentActResponse,
type AgentProject,
type AgentEngineInfo,
type AgentStoredMessage,
} from 'chrome-mcp-shared';
```
--------------------------------------------------------------------------------
/app/native-server/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2018",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2018", "DOM"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"chrome-devtools-frontend/*": ["src/shims/devtools.d.ts"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent/MessageList.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="flex-1 overflow-y-auto px-4 py-3 space-y-3 bg-slate-50">
<div v-if="messages.length === 0" class="text-xs text-slate-500 text-center mt-4">
Start a conversation with your local agent. Messages stream from the native server via SSE and
are persisted per project.
</div>
<MessageItem v-for="message in messages" :key="message.id" :message="message" />
</div>
</template>
<script lang="ts" setup>
import type { AgentMessage } from 'chrome-mcp-shared';
import MessageItem from './MessageItem.vue';
defineProps<{
messages: AgentMessage[];
}>();
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Agent Chat Components
* Export all sub-components for the agent chat feature.
*/
export { default as ConnectionStatus } from './ConnectionStatus.vue';
export { default as ProjectSelector } from './ProjectSelector.vue';
export { default as ProjectCreateForm } from './ProjectCreateForm.vue';
export { default as CliSettings } from './CliSettings.vue';
export { default as MessageList } from './MessageList.vue';
export { default as MessageItem } from './MessageItem.vue';
export { default as ChatInput } from './ChatInput.vue';
export { default as AttachmentPreview } from './AttachmentPreview.vue';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/DatabaseIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/plugins/breakpoint.ts:
--------------------------------------------------------------------------------
```typescript
import type { RunPlugin, StepContext } from './types';
import { runState } from '../state-manager';
export function breakpointPlugin(): RunPlugin {
return {
name: 'breakpoint',
async onBeforeStep(ctx: StepContext) {
try {
const step: any = ctx.step as any;
const hasBreakpoint = step?.$breakpoint === true || step?.breakpoint === true;
if (!hasBreakpoint) return;
// mark run paused for external UI to resume
await runState.update(ctx.runId, { status: 'stopped', updatedAt: Date.now() } as any);
return { pause: true };
} catch {}
return;
},
};
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/VectorIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 4.5a4.5 4.5 0 0 1 6 0M9 4.5V3a1.5 1.5 0 0 1 1.5-1.5h3A1.5 1.5 0 0 1 15 3v1.5M9 4.5a4.5 4.5 0 0 0-4.5 4.5v7.5A1.5 1.5 0 0 0 6 18h12a1.5 1.5 0 0 0 1.5-1.5V9a4.5 4.5 0 0 0-4.5-4.5M12 12l2.25 2.25M12 12l-2.25-2.25M12 12v6"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/navigate.ts:
--------------------------------------------------------------------------------
```typescript
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { handleCallTool } from '@/entrypoints/background/tools';
import type { Step } from '../types';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';
export const navigateNode: NodeRuntime<any> = {
validate: (step) => {
const ok = !!(step as any).url;
return ok ? { ok } : { ok, errors: ['缺少 URL'] };
},
run: async (_ctx: ExecCtx, step: Step) => {
const url = (step as any).url;
const res = await handleCallTool({ name: TOOL_NAMES.BROWSER.NAVIGATE, args: { url } });
if ((res as any).isError) throw new Error('navigate failed');
return {} as ExecResult;
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/TabIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-16.5 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 0 0 2.25-2.25V6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyOpenTab.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">URL 地址(可选)</label>
<input
class="form-input"
v-model="(node as any).config.url"
placeholder="https://example.com"
/>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label"
><input type="checkbox" v-model="(node as any).config.newWindow" /> 新窗口</label
>
</div>
</div>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent/AttachmentPreview.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="flex flex-wrap gap-2 mb-2">
<div
v-for="(att, idx) in attachments"
:key="idx"
class="flex items-center gap-1 bg-slate-100 px-2 py-1 rounded text-xs max-w-[150px]"
>
<span class="truncate" :title="att.name">{{ att.name }}</span>
<button
type="button"
class="text-red-500 hover:text-red-700 flex-shrink-0"
@click="$emit('remove', idx)"
>
×
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { AgentAttachment } from 'chrome-mcp-shared';
defineProps<{
attachments: AgentAttachment[];
}>();
defineEmits<{
remove: [index: number];
}>();
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/DocumentIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/MarkerIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/widgets/FieldKeySequence.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="keys">
<input class="form-input" :placeholder="placeholder" :value="text" @input="onInput" />
<div class="help">示例:Backspace Enter 或 cmd+a</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 || 'Backspace Enter 或 cmd+a';
function onInput(ev: any) {
const v = String(ev?.target?.value ?? '');
text.value = v;
emit('update:modelValue', v);
}
watchEffect(() => (text.value = props.modelValue ?? ''));
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/AgentConversation.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="px-5 py-6 space-y-8">
<!-- Empty State -->
<div v-if="threads.length === 0" class="py-10 text-center">
<p
class="text-2xl italic opacity-40"
:style="{
fontFamily: 'var(--ac-font-heading)',
color: 'var(--ac-text-subtle)',
}"
>
How can I help you code today?
</p>
</div>
<!-- Request Threads -->
<AgentRequestThread v-for="thread in threads" :key="thread.id" :thread="thread" />
</div>
</template>
<script lang="ts" setup>
import type { AgentThread } from '../../composables/useAgentThreads';
import AgentRequestThread from './AgentRequestThread.vue';
defineProps<{
threads: AgentThread[];
}>();
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/actions/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Action System - 导出模块
*/
// 类型导出
export * from './types';
// 注册表导出
export {
ActionRegistry,
createActionRegistry,
ok,
invalid,
failed,
tryResolveString,
tryResolveNumber,
tryResolveJson,
tryResolveValue,
type BeforeExecuteArgs,
type BeforeExecuteHook,
type AfterExecuteArgs,
type AfterExecuteHook,
type ActionRegistryHooks,
} from './registry';
// 适配器导出
export {
execCtxToActionCtx,
stepToAction,
actionResultToExecResult,
createStepExecutor,
isActionSupported,
getActionType,
type StepExecutionAttempt,
} from './adapter';
// Handler 工厂导出
export {
createReplayActionRegistry,
registerReplayHandlers,
getSupportedActionTypes,
isActionTypeSupported,
} from './handlers';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/index.ts:
--------------------------------------------------------------------------------
```typescript
export { default as DocumentIcon } from './DocumentIcon.vue';
export { default as DatabaseIcon } from './DatabaseIcon.vue';
export { default as BoltIcon } from './BoltIcon.vue';
export { default as TrashIcon } from './TrashIcon.vue';
export { default as CheckIcon } from './CheckIcon.vue';
export { default as TabIcon } from './TabIcon.vue';
export { default as VectorIcon } from './VectorIcon.vue';
export { default as RecordIcon } from './RecordIcon.vue';
export { default as StopIcon } from './StopIcon.vue';
export { default as WorkflowIcon } from './WorkflowIcon.vue';
export { default as RefreshIcon } from './RefreshIcon.vue';
export { default as EditIcon } from './EditIcon.vue';
export { default as MarkerIcon } from './MarkerIcon.vue';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/timeline/markstream-thinking.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Markstream-vue custom component registration for <thinking> tag.
*
* This module registers a custom renderer for <thinking> tags in markdown content.
* When markstream-vue encounters <thinking>...</thinking>, it will use ThinkingNode.vue
* to render a collapsible thinking section instead of raw HTML.
*
* Usage:
* 1. Import this module once (side-effect import) to register the component
* 2. Add `custom-id="agentchat"` and `:custom-html-tags="['thinking']"` to MarkdownRender
*/
import { setCustomComponents } from 'markstream-vue';
import ThinkingNode from './ThinkingNode.vue';
/** Scope ID for AgentChat markdown rendering */
export const AGENTCHAT_MD_SCOPE = 'agentchat';
// Register the thinking node component
setCustomComponents(AGENTCHAT_MD_SCOPE, {
thinking: ThinkingNode,
});
```
--------------------------------------------------------------------------------
/app/native-server/src/agent/project-types.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Re-export AgentProject from shared package and define local input types.
*/
import type { AgentCliPreference, AgentProject } from 'chrome-mcp-shared';
// Re-export for backward compatibility
export type { AgentProject };
export interface CreateOrUpdateProjectInput {
id?: string;
name: string;
description?: string;
rootPath: string;
preferredCli?: AgentCliPreference;
selectedModel?: string;
/**
* Whether to use Claude Code Router (CCR) for this project.
*/
useCcr?: boolean;
/**
* Whether to enable the local Chrome MCP server integration for this project.
* Defaults to true when omitted.
*/
enableChromeMcp?: boolean;
/**
* If true, create the directory if it doesn't exist.
* Should only be set after user confirmation.
*/
allowCreate?: boolean;
}
```
--------------------------------------------------------------------------------
/packages/shared/src/step-types.ts:
--------------------------------------------------------------------------------
```typescript
// step-types.ts — centralized step type constants for UI + runtime
export const STEP_TYPES = {
CLICK: 'click',
DBLCLICK: 'dblclick',
FILL: 'fill',
TRIGGER_EVENT: 'triggerEvent',
SET_ATTRIBUTE: 'setAttribute',
SCREENSHOT: 'screenshot',
SWITCH_FRAME: 'switchFrame',
LOOP_ELEMENTS: 'loopElements',
KEY: 'key',
SCROLL: 'scroll',
DRAG: 'drag',
WAIT: 'wait',
ASSERT: 'assert',
SCRIPT: 'script',
IF: 'if',
FOREACH: 'foreach',
WHILE: 'while',
NAVIGATE: 'navigate',
HTTP: 'http',
EXTRACT: 'extract',
OPEN_TAB: 'openTab',
SWITCH_TAB: 'switchTab',
CLOSE_TAB: 'closeTab',
HANDLE_DOWNLOAD: 'handleDownload',
EXECUTE_FLOW: 'executeFlow',
// UI-only helpers
TRIGGER: 'trigger',
DELAY: 'delay',
} as const;
export type StepTypeConst = (typeof STEP_TYPES)[keyof typeof STEP_TYPES];
```
--------------------------------------------------------------------------------
/packages/wasm-simd/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@chrome-mcp/wasm-simd",
"version": "0.1.0",
"description": "SIMD-optimized WebAssembly math functions for Chrome MCP",
"main": "pkg/simd_math.js",
"types": "pkg/simd_math.d.ts",
"files": [
"pkg/"
],
"scripts": {
"build": "wasm-pack build --target web --out-dir pkg --release",
"build:dev": "wasm-pack build --target web --out-dir pkg --dev",
"clean": "rimraf pkg/",
"test": "wasm-pack test --headless --firefox"
},
"keywords": [
"wasm",
"simd",
"webassembly",
"math",
"cosine-similarity",
"vector-operations"
],
"author": "hangye",
"license": "MIT",
"devDependencies": {
"rimraf": "^5.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/your-repo/chrome-mcp-server.git",
"directory": "packages/wasm-simd"
}
}
```
--------------------------------------------------------------------------------
/app/native-server/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import serverInstance from './server';
import nativeMessagingHostInstance from './native-messaging-host';
try {
serverInstance.setNativeHost(nativeMessagingHostInstance); // Server needs setNativeHost method
nativeMessagingHostInstance.setServer(serverInstance); // NativeHost needs setServer method
nativeMessagingHostInstance.start();
} catch (error) {
process.exit(1);
}
process.on('error', (error) => {
process.exit(1);
});
// Handle process signals and uncaught exceptions
process.on('SIGINT', () => {
process.exit(0);
});
process.on('SIGTERM', () => {
process.exit(0);
});
process.on('exit', (code) => {
});
process.on('uncaughtException', (error) => {
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
// Don't exit immediately, let the program continue running
});
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/TrashIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/widgets/FieldCode.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="code">
<textarea
class="form-input mono"
rows="6"
:placeholder="placeholder"
:value="text"
@input="onInput"
></textarea>
</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 || '/* code */';
function onInput(ev: any) {
const v = String(ev?.target?.value ?? '');
text.value = v;
emit('update:modelValue', v);
}
watchEffect(() => (text.value = props.modelValue ?? ''));
</script>
<style scoped>
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
}
</style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2/ui/property-panel/controls/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Design Controls Index
*
* Exports all design control factories.
*/
export { createSizeControl, type SizeControlOptions } from './size-control';
export { createSpacingControl, type SpacingControlOptions } from './spacing-control';
export { createPositionControl, type PositionControlOptions } from './position-control';
export { createLayoutControl, type LayoutControlOptions } from './layout-control';
export { createTypographyControl, type TypographyControlOptions } from './typography-control';
export { createAppearanceControl, type AppearanceControlOptions } from './appearance-control';
export { createEffectsControl, type EffectsControlOptions } from './effects-control';
export { createGradientControl, type GradientControlOptions } from './gradient-control';
export { createTokenPicker, type TokenPicker, type TokenPickerOptions } from './token-picker';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/main.ts:
--------------------------------------------------------------------------------
```typescript
import { createApp } from 'vue';
import { NativeMessageType } from 'chrome-mcp-shared';
import App from './App.vue';
// Tailwind first, then custom tokens
import '../styles/tailwind.css';
// AgentChat theme tokens
import './styles/agent-chat.css';
import { preloadAgentTheme } from './composables';
/**
* Initialize and mount the Vue app.
* Preloads theme before mounting to prevent flash.
*/
async function init(): Promise<void> {
// Preload theme from storage and apply to document
// This happens before Vue mounts, preventing theme flash
await preloadAgentTheme();
// Trigger ensure native connection (fire-and-forget, don't block UI mounting)
void chrome.runtime.sendMessage({ type: NativeMessageType.ENSURE_NATIVE }).catch(() => {
// Silent failure - background will handle reconnection
});
// Mount Vue app
createApp(App).mount('#app');
}
init();
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/WorkflowIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/policies/retry.ts:
--------------------------------------------------------------------------------
```typescript
// engine/policies/retry.ts — unified retry/backoff policy
export type BackoffKind = 'none' | 'exp';
export interface RetryOptions {
count?: number; // max attempts beyond the first run
intervalMs?: number;
backoff?: BackoffKind;
}
export async function withRetry<T>(
run: () => Promise<T>,
onRetry?: (attempt: number, err: any) => Promise<void> | void,
opts?: RetryOptions,
): Promise<T> {
const max = Math.max(0, Number(opts?.count ?? 0));
const base = Math.max(0, Number(opts?.intervalMs ?? 0));
const backoff = (opts?.backoff || 'none') as BackoffKind;
let attempt = 0;
while (true) {
try {
return await run();
} catch (e) {
if (attempt >= max) throw e;
if (onRetry) await onRetry(attempt, e);
const delay = base > 0 ? (backoff === 'exp' ? base * Math.pow(2, attempt) : base) : 0;
if (delay > 0) await new Promise((r) => setTimeout(r, delay));
attempt += 1;
}
}
}
```
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "chrome-mcp-shared",
"version": "1.0.1",
"author": "hangye",
"main": "dist/index.js",
"module": "./dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "npx eslint 'src/**/*.{js,ts}'",
"lint:fix": "npx eslint 'src/**/*.{js,ts}' --fix",
"format": "prettier --write 'src/**/*.{js,ts,json}'"
},
"files": [
"dist"
],
"devDependencies": {
"@types/node": "^18.0.0",
"@typescript-eslint/parser": "^8.32.0",
"tsup": "^8.4.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"zod": "^3.24.4"
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyTriggerEvent.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div>
<SelectorEditor :node="node" :allowPick="true" />
<div class="form-section">
<div class="form-group">
<label class="form-label">事件类型</label>
<input
class="form-input"
v-model="(node as any).config.event"
placeholder="如 input/change/mouseover"
/>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label"
><input type="checkbox" v-model="(node as any).config.bubbles" /> 冒泡</label
>
<label class="checkbox-label"
><input type="checkbox" v-model="(node as any).config.cancelable" /> 可取消</label
>
</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';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/constants.ts:
--------------------------------------------------------------------------------
```typescript
// constants.ts — centralized engine constants and labels
import { EDGE_LABELS } from 'chrome-mcp-shared';
export const ENGINE_CONSTANTS = {
DEFAULT_WAIT_MS: 5000,
MAX_WAIT_MS: 120000,
NETWORK_IDLE_SAMPLE_MS: 1200,
MAX_ITERATIONS: 1000,
MAX_FOREACH_CONCURRENCY: 16,
EDGE_LABELS: EDGE_LABELS,
} as const;
export type EdgeLabel =
(typeof ENGINE_CONSTANTS.EDGE_LABELS)[keyof typeof ENGINE_CONSTANTS.EDGE_LABELS];
// Centralized stepId values used in run logs for non-step events
export const LOG_STEP_IDS = {
GLOBAL_TIMEOUT: 'global-timeout',
PLUGIN_RUN_START: 'plugin-runStart',
VARIABLE_COLLECT: 'variable-collect',
BINDING_CHECK: 'binding-check',
NETWORK_CAPTURE: 'network-capture',
DAG_REQUIRED: 'dag-required',
DAG_CYCLE: 'dag-cycle',
LOOP_GUARD: 'loop-guard',
PLUGIN_RUN_END: 'plugin-runEnd',
RUNSTATE_UPDATE: 'runState-update',
RUNSTATE_DELETE: 'runState-delete',
} as const;
export type LogStepId = (typeof LOG_STEP_IDS)[keyof typeof LOG_STEP_IDS];
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse } from '@/common/tool-handler';
import { ERROR_MESSAGES } from '@/common/constants';
import * as browserTools from './browser';
import { flowRunTool, listPublishedFlowsTool } from './record-replay';
const tools = { ...browserTools, flowRunTool, listPublishedFlowsTool } as any;
const toolsMap = new Map(Object.values(tools).map((tool: any) => [tool.name, tool]));
/**
* Tool call parameter interface
*/
export interface ToolCallParam {
name: string;
args: any;
}
/**
* Handle tool execution
*/
export const handleCallTool = async (param: ToolCallParam) => {
const tool = toolsMap.get(param.name);
if (!tool) {
return createErrorResponse(`Tool ${param.name} not found`);
}
try {
return await tool.execute(param.args);
} catch (error) {
console.error(`Tool execution failed for ${param.name}:`, error);
return createErrorResponse(
error instanceof Error ? error.message : ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
);
}
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyWait.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">等待条件 (JSON)</label>
<textarea
class="form-textarea"
v-model="waitJson"
rows="4"
placeholder='{"text":"ok","appear":true}'
></textarea>
</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 waitJson = computed({
get() {
const n = props.node;
if (!n || n.type !== 'wait') return '';
try {
return JSON.stringify((n as any).config?.condition || {}, null, 2);
} catch {
return '';
}
},
set(v: string) {
const n = props.node;
if (!n || n.type !== 'wait') return;
try {
(n as any).config = { ...((n as any).config || {}), condition: JSON.parse(v || '{}') };
} catch {}
},
});
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/vitest.config.ts:
--------------------------------------------------------------------------------
```typescript
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
const rootDir = fileURLToPath(new URL('.', import.meta.url));
export default defineConfig({
resolve: {
alias: {
// Match WXT's path aliases from .wxt/tsconfig.json
'@': rootDir,
'~': rootDir,
// Mock hnswlib-wasm-static to avoid native module issues in tests
'hnswlib-wasm-static': `${rootDir}/tests/__mocks__/hnswlib-wasm-static.ts`,
},
},
test: {
environment: 'jsdom',
include: ['tests/**/*.test.ts'],
exclude: ['node_modules', '.output', 'dist', '.wxt'],
setupFiles: ['tests/vitest.setup.ts'],
environmentOptions: {
jsdom: {
// Provide a stable URL for anchor/href tests
url: 'https://example.com/',
},
},
// Auto-cleanup mocks between tests
clearMocks: true,
restoreMocks: true,
// TypeScript support via esbuild (faster than ts-jest)
typecheck: {
enabled: false, // Run separately with vue-tsc
},
},
});
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertySetAttribute.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div>
<SelectorEditor :node="node" :allowPick="true" />
<div class="form-section">
<div class="form-group">
<label class="form-label">属性名</label>
<input
class="form-input"
v-model="(node as any).config.name"
placeholder="如 value/src/disabled 等"
/>
</div>
<div class="form-group">
<label class="form-label">属性值(留空并勾选删除则移除)</label>
<input class="form-input" v-model="(node as any).config.value" placeholder="属性值" />
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label"
><input type="checkbox" v-model="(node as any).config.remove" /> 删除属性</label
>
</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';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyCloseTab.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">按 URL 关闭(可选)</label>
<input class="form-input" v-model="(node as any).config.url" placeholder="子串匹配 URL" />
</div>
<div class="form-group">
<label class="form-label">Tab IDs(JSON 数组,可选)</label>
<textarea class="form-textarea" v-model="tabIdsJson" rows="2" placeholder="[1,2]"></textarea>
</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 tabIdsJson = computed({
get() {
try {
const arr = (props.node as any).config?.tabIds;
return Array.isArray(arr) ? JSON.stringify(arr) : '';
} catch {
return '';
}
},
set(v: string) {
try {
(props.node as any).config.tabIds = v ? JSON.parse(v) : [];
} catch {}
},
});
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/key.ts:
--------------------------------------------------------------------------------
```typescript
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { handleCallTool } from '@/entrypoints/background/tools';
import type { StepKey } from '../types';
import { expandTemplatesDeep } from '../rr-utils';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';
export const keyNode: NodeRuntime<StepKey> = {
run: async (ctx, step: StepKey) => {
const s = expandTemplatesDeep(step as StepKey, ctx.vars) as StepKey;
const args: { keys: string; frameId?: number; selector?: string } = { keys: s.keys };
// Support target selector for focusing before key input
if (s.target && s.target.candidates?.length) {
const selector = s.target.candidates[0]?.value;
if (selector) {
args.selector = selector;
}
}
if (typeof ctx.frameId === 'number') {
args.frameId = ctx.frameId;
}
const res = await handleCallTool({
name: TOOL_NAMES.BROWSER.KEYBOARD,
args,
});
if ((res as any).isError) throw new Error('key failed');
return {} as ExecResult;
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/http.ts:
--------------------------------------------------------------------------------
```typescript
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { handleCallTool } from '@/entrypoints/background/tools';
import type { StepHttp } from '../types';
import { applyAssign, expandTemplatesDeep } from '../rr-utils';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';
export const httpNode: NodeRuntime<StepHttp> = {
run: async (ctx: ExecCtx, step: StepHttp) => {
const s: any = expandTemplatesDeep(step as any, ctx.vars);
const res = await handleCallTool({
name: TOOL_NAMES.BROWSER.NETWORK_REQUEST,
args: {
url: s.url,
method: s.method || 'GET',
headers: s.headers || {},
body: s.body,
formData: s.formData,
},
});
const text = (res as any)?.content?.find((c: any) => c.type === 'text')?.text;
try {
const payload = text ? JSON.parse(text) : null;
if (s.saveAs && payload !== undefined) ctx.vars[s.saveAs] = payload;
if (s.assign && payload !== undefined) applyAssign(ctx.vars, payload, s.assign);
} catch {}
return {} as ExecResult;
},
};
```
--------------------------------------------------------------------------------
/app/native-server/src/types/devtools-frontend.d.ts:
--------------------------------------------------------------------------------
```typescript
// Minimal ambient declarations to avoid compiling chrome-devtools-frontend sources.
// We intentionally treat these modules as `any` to keep our build lightweight and decoupled
// from DevTools' internal TypeScript and lib targets.
declare module 'chrome-devtools-frontend/front_end/models/trace/trace.js' {
// Shape used by our code: TraceModel + Types + Insights
export const TraceModel: any;
export const Types: any;
export const Insights: any;
}
declare module 'chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js' {
export class PerformanceTraceFormatter {
constructor(...args: any[]);
formatTraceSummary(): string;
}
}
declare module 'chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js' {
export class PerformanceInsightFormatter {
constructor(...args: any[]);
formatInsight(): string;
}
}
declare module 'chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js' {
export const AgentFocus: any;
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyHandleDownload.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">文件名包含(可选)</label>
<input
class="form-input"
v-model="(node as any).config.filenameContains"
placeholder="子串匹配文件名或URL"
/>
</div>
<div class="form-group">
<label class="form-label">超时(ms)</label>
<input class="form-input" v-model="(node as any).config.timeoutMs" placeholder="默认 60000" />
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label"
><input type="checkbox" v-model="(node as any).config.waitForComplete" />
等待下载完成</label
>
</div>
<div class="form-group">
<label class="form-label">保存到变量</label>
<input class="form-input" v-model="(node as any).config.saveAs" placeholder="默认 download" />
</div>
</div>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent/ConnectionStatus.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="px-4 py-2 border-b border-slate-200 flex items-center justify-between gap-2">
<div class="flex items-center gap-2 text-xs text-slate-600">
<span :class="['inline-flex h-2 w-2 rounded-full', statusColor]"></span>
<span>{{ statusText }}</span>
</div>
<button
class="btn-secondary !px-3 !py-1 text-xs"
:disabled="connecting"
@click="$emit('reconnect')"
>
{{ connecting ? 'Reconnecting...' : 'Reconnect' }}
</button>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps<{
isServerReady: boolean;
nativeConnected: boolean;
connecting: boolean;
}>();
defineEmits<{
reconnect: [];
}>();
const statusColor = computed(() => {
if (props.isServerReady) return 'bg-green-500';
if (props.nativeConnected) return 'bg-yellow-500';
return 'bg-slate-400';
});
const statusText = computed(() => {
if (props.isServerReady) return 'Agent server connected';
if (props.nativeConnected) return 'Connecting to agent server...';
return 'Native host not connected';
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/actions/handlers/delay.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Delay Action Handler
*
* Provides a simple pause in execution flow.
* Supports variable resolution for dynamic delay times.
*/
import { failed, invalid, ok, tryResolveNumber } from '../registry';
import type { ActionHandler } from '../types';
/** Maximum delay time to prevent integer overflow in setTimeout */
const MAX_DELAY_MS = 2_147_483_647;
export const delayHandler: ActionHandler<'delay'> = {
type: 'delay',
validate: (action) => {
if (action.params.sleep === undefined) {
return invalid('Missing sleep parameter');
}
return ok();
},
describe: (action) => {
const ms = typeof action.params.sleep === 'number' ? action.params.sleep : '(dynamic)';
return `Delay ${ms}ms`;
},
run: async (ctx, action) => {
const resolved = tryResolveNumber(action.params.sleep, ctx.vars);
if (!resolved.ok) {
return failed('VALIDATION_ERROR', resolved.error);
}
const ms = Math.max(0, Math.min(MAX_DELAY_MS, Math.floor(resolved.value)));
if (ms > 0) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
return { status: 'success' };
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/types.ts:
--------------------------------------------------------------------------------
```typescript
import type { RunLogEntry, Step, StepScript } from '../types';
/**
* Execution context for step execution.
* Contains runtime state that may change during flow execution.
*/
export interface ExecCtx {
/** Runtime variables accessible to steps */
vars: Record<string, any>;
/** Logger function for recording execution events */
logger: (e: RunLogEntry) => void;
/**
* Current tab ID for this execution context.
* Managed by Scheduler, may change after openTab/switchTab actions.
*/
tabId?: number;
/**
* Current frame ID within the tab.
* Used for iframe targeting, 0 for main frame.
*/
frameId?: number;
}
export interface ExecResult {
alreadyLogged?: boolean;
deferAfterScript?: StepScript | null;
nextLabel?: string;
control?:
| { kind: 'foreach'; listVar: string; itemVar: string; subflowId: string; concurrency?: number }
| { kind: 'while'; condition: any; subflowId: string; maxIterations: number };
}
export interface NodeRuntime<S extends Step = Step> {
validate?: (step: S) => { ok: boolean; errors?: string[] };
run: (ctx: ExecCtx, step: S) => Promise<ExecResult | void>;
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyExtract.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">元素选择器(可选)</label>
<input class="form-input" v-model="(node as any).config.selector" placeholder="CSS 选择器" />
</div>
<div class="form-group">
<label class="form-label">属性</label>
<input
class="form-input"
v-model="(node as any).config.attr"
placeholder="text/textContent 或属性名"
/>
</div>
<div class="form-group">
<label class="form-label">自定义 JS(返回值)</label>
<textarea
class="form-textarea"
v-model="(node as any).config.js"
rows="3"
placeholder="return document.title"
></textarea>
</div>
<div class="form-group" :class="{ invalid: !(node as any).config?.saveAs }">
<label class="form-label">保存为变量</label>
<input class="form-input" v-model="(node as any).config.saveAs" placeholder="变量名" />
</div>
</div>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent/MessageItem.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="flex flex-col gap-1 rounded-lg px-3 py-2 max-w-full" :class="messageClasses">
<div class="flex items-center justify-between gap-2 text-[11px] opacity-70">
<span>{{ senderName }}</span>
<span v-if="message.createdAt">
{{ formatTime(message.createdAt) }}
</span>
</div>
<div class="whitespace-pre-wrap break-words text-xs leading-relaxed">
{{ message.content }}
</div>
<div v-if="message.isStreaming && !message.isFinal" class="text-[10px] opacity-60 mt-0.5">
Streaming...
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { AgentMessage } from 'chrome-mcp-shared';
const props = defineProps<{
message: AgentMessage;
}>();
const messageClasses = computed(() => {
return props.message.role === 'user'
? 'bg-white border border-slate-200 self-end'
: 'bg-slate-900 text-slate-50 self-start';
});
const senderName = computed(() => {
return props.message.role === 'user' ? 'You' : props.message.cliSource || 'Agent';
});
function formatTime(dateStr: string): string {
return new Date(dateStr).toLocaleTimeString();
}
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/strategies/css-path.ts:
--------------------------------------------------------------------------------
```typescript
/**
* CSS Path Strategy - 基于 DOM 路径的选择器策略
* 使用 nth-of-type 生成完整的 CSS 路径
*/
import type { SelectorCandidate, SelectorStrategy } from '../types';
export const cssPathStrategy: SelectorStrategy = {
id: 'css-path',
generate(ctx) {
if (!ctx.options.includeCssPath) return [];
const { element } = ctx;
const segments: string[] = [];
let current: Element | null = element;
while (current) {
const tag = current.tagName?.toLowerCase?.() ?? '';
if (!tag) break;
let segment = tag;
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter((c) => c.tagName === current!.tagName);
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1;
if (index > 0) segment += `:nth-of-type(${index})`;
}
}
segments.unshift(segment);
if (tag === 'body') break;
current = parent;
}
const selector = segments.length ? segments.join(' > ') : 'body';
const out: SelectorCandidate[] = [
{ type: 'css', value: selector, source: 'generated', strategy: 'css-path' },
];
return out;
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/widgets/FieldExpression.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="expr">
<input class="form-input mono" :placeholder="placeholder" :value="text" @input="onInput" />
<div v-if="err" class="error-item">{{ err }}</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue';
import { evalExpression } from '@/entrypoints/background/record-replay/engine/utils/expression';
const props = defineProps<{ modelValue?: string; field?: any }>();
const emit = defineEmits<{ (e: 'update:modelValue', v?: string): void }>();
const text = ref<string>(props.modelValue ?? '');
const err = ref<string>('');
const placeholder = props.field?.placeholder || 'e.g. vars.a > 0 && vars.flag';
function onInput(ev: any) {
const v = String(ev?.target?.value ?? '');
text.value = v;
try {
// just validate; allow empty
if (v.trim()) {
evalExpression(v, { vars: {} as any });
}
err.value = '';
} catch (e: any) {
err.value = '表达式解析错误';
}
emit('update:modelValue', v);
}
watchEffect(() => {
text.value = props.modelValue ?? '';
});
</script>
<style scoped>
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
}
</style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyFill.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div>
<SelectorEditor :node="node" :allowPick="true" />
<div class="form-section">
<div class="form-group" data-field="fill.value">
<label class="form-label">输入值</label>
<VarInput v-model="value" :variables="variables" placeholder="支持 {变量名} 格式" />
</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';
import type { VariableOption } from '@/entrypoints/popup/components/builder/model/variables';
import SelectorEditor from './SelectorEditor.vue';
import VarInput from '@/entrypoints/popup/components/builder/widgets/VarInput.vue';
const props = defineProps<{ node: NodeBase; variables?: VariableOption[] }>();
const variables = computed<VariableOption[]>(() => (props.variables || []).slice());
const value = computed<string>({
get() {
return String((props.node as any)?.config?.value ?? '');
},
set(v: string) {
if (!props.node) return;
if (!(props.node as any).config) (props.node as any).config = {} as any;
(props.node as any).config.value = v;
},
});
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyForeach.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">列表变量</label>
<input
class="form-input"
v-model="(node as any).config.listVar"
placeholder="workflow.list"
/>
</div>
<div class="form-group">
<label class="form-label">循环项变量名</label>
<input class="form-input" v-model="(node as any).config.itemVar" placeholder="默认 item" />
</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>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
const props = defineProps<{ node: NodeBase }>();
const emit = defineEmits<{ (e: 'create-subflow', id: string): void }>();
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/record-replay-v3/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Record-Replay V3 公共 API 入口
* @description 导出所有公共类型和接口
*/
// ==================== Domain ====================
export * from './domain';
// ==================== Engine ====================
export * from './engine';
// ==================== Storage ====================
export * from './storage';
// ==================== Factory Functions ====================
import type { StoragePort } from './engine/storage/storage-port';
import { createFlowsStore } from './storage/flows';
import { createRunsStore } from './storage/runs';
import { createEventsStore } from './storage/events';
import { createQueueStore } from './storage/queue';
import { createPersistentVarsStore } from './storage/persistent-vars';
import { createTriggersStore } from './storage/triggers';
/**
* 创建完整的 StoragePort 实现
*/
export function createStoragePort(): StoragePort {
return {
flows: createFlowsStore(),
runs: createRunsStore(),
events: createEventsStore(),
queue: createQueueStore(),
persistentVars: createPersistentVarsStore(),
triggers: createTriggersStore(),
};
}
// ==================== Version ====================
/** V3 API 版本 */
export const RR_V3_VERSION = '3.0.0' as const;
/** 是否为 V3 API */
export const IS_RR_V3 = true as const;
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/widgets/FieldDuration.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="duration">
<div class="row">
<input class="form-input" type="number" :value="val" @input="onNum" min="0" />
<select class="form-input unit" :value="unit" @change="onUnit">
<option value="ms">ms</option>
<option value="s">s</option>
</select>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue';
const props = defineProps<{ modelValue?: number; field?: any }>();
const emit = defineEmits<{ (e: 'update:modelValue', v?: number): void }>();
const unit = ref<'ms' | 's'>('ms');
const val = ref<number>(Number(props.modelValue || 0));
watchEffect(() => {
const ms = Number(props.modelValue || 0);
if (ms % 1000 === 0 && ms >= 1000) {
unit.value = 's';
val.value = ms / 1000;
} else {
unit.value = 'ms';
val.value = ms;
}
});
function onNum(ev: any) {
const n = Number(ev?.target?.value || 0);
val.value = n;
emit('update:modelValue', unit.value === 's' ? n * 1000 : n);
}
function onUnit(ev: any) {
unit.value = ev?.target?.value === 's' ? 's' : 'ms';
emit('update:modelValue', unit.value === 's' ? val.value * 1000 : val.value);
}
</script>
<style scoped>
.row {
display: flex;
gap: 8px;
align-items: center;
}
.unit {
width: 84px;
}
</style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/script.ts:
--------------------------------------------------------------------------------
```typescript
import type { StepScript } from '../types';
import { expandTemplatesDeep, applyAssign } from '../rr-utils';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';
export const scriptNode: NodeRuntime<StepScript> = {
run: async (ctx: ExecCtx, step: StepScript) => {
const s: any = expandTemplatesDeep(step as any, ctx.vars);
if (s.when === 'after') return { deferAfterScript: s } as ExecResult;
const world = s.world || 'ISOLATED';
const code = String(s.code || '');
if (!code.trim()) return {} as ExecResult;
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;
const [{ result }] = await chrome.scripting.executeScript({
target: { tabId, frameIds } as any,
func: (userCode: string) => {
try {
return (0, eval)(userCode);
} catch {
return null;
}
},
args: [code],
world: world as any,
} as any);
if (s.saveAs) ctx.vars[s.saveAs] = result;
if (s.assign && typeof s.assign === 'object') applyAssign(ctx.vars, result, s.assign);
return {} as ExecResult;
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyAssert.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">断言条件 (JSON)</label>
<textarea
class="form-textarea"
v-model="assertJson"
rows="4"
placeholder='{"exists":"#id"}'
></textarea>
</div>
<div class="form-group">
<label class="form-label">失败策略</label>
<select class="form-select" v-model="(node as any).config.failStrategy">
<option value="stop">stop</option>
<option value="warn">warn</option>
<option value="retry">retry</option>
</select>
</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 assertJson = computed({
get() {
const n = props.node;
if (!n || n.type !== 'assert') return '';
try {
return JSON.stringify((n as any).config?.assert || {}, null, 2);
} catch {
return '';
}
},
set(v: string) {
const n = props.node;
if (!n || n.type !== 'assert') return;
try {
(n as any).config = { ...((n as any).config || {}), assert: JSON.parse(v || '{}') };
} catch {}
},
});
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/model/form-widget-registry.ts:
--------------------------------------------------------------------------------
```typescript
// form-widget-registry.ts — global widget registry for PropertyFormRenderer
import FieldExpression from '@/entrypoints/popup/components/builder/widgets/FieldExpression.vue';
import FieldSelector from '@/entrypoints/popup/components/builder/widgets/FieldSelector.vue';
import FieldDuration from '@/entrypoints/popup/components/builder/widgets/FieldDuration.vue';
import FieldCode from '@/entrypoints/popup/components/builder/widgets/FieldCode.vue';
import FieldKeySequence from '@/entrypoints/popup/components/builder/widgets/FieldKeySequence.vue';
import FieldTargetLocator from '@/entrypoints/popup/components/builder/widgets/FieldTargetLocator.vue';
import type { Component } from 'vue';
const REG = new Map<string, Component>();
export function registerDefaultWidgets() {
REG.set('expression', FieldExpression as unknown as Component);
REG.set('selector', FieldSelector as unknown as Component);
REG.set('duration', FieldDuration as unknown as Component);
REG.set('code', FieldCode as unknown as Component);
REG.set('keysequence', FieldKeySequence as unknown as Component);
// Structured TargetLocator based on a selector input
REG.set('targetlocator', FieldTargetLocator as unknown as Component);
}
export function getWidget(name?: string): Component | null {
if (!name) return null;
return REG.get(name) || null;
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/strategies/text.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Text Strategy - Text content based selector strategy
*
* This is the lowest priority fallback strategy. Text selectors are less
* stable than attribute-based or structural selectors because text content
* is more likely to change (i18n, dynamic content, etc.).
*/
import type { SelectorCandidate, SelectorStrategy } from '../types';
/**
* Weight penalty for text selectors.
* This ensures text selectors rank after all other strategies including anchor-relpath.
* anchor-relpath uses -10, so text uses -20 to rank lower.
*/
const TEXT_STRATEGY_WEIGHT = -20;
function normalizeText(value: string): string {
return String(value || '')
.replace(/\s+/g, ' ')
.trim();
}
export const textStrategy: SelectorStrategy = {
id: 'text',
generate(ctx) {
if (!ctx.options.includeText) return [];
const { element, options } = ctx;
const tag = element.tagName?.toLowerCase?.() ?? '';
if (!tag || !options.textTags.includes(tag)) return [];
const raw = element.textContent || '';
const text = normalizeText(raw).slice(0, options.textMaxLength);
if (!text) return [];
return [
{
type: 'text',
value: text,
match: 'contains',
tagNameHint: tag,
weight: TEXT_STRATEGY_WEIGHT,
source: 'generated',
strategy: 'text',
},
];
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import { defineConfig } from 'eslint/config';
import prettierConfig from 'eslint-config-prettier';
export default defineConfig([
// Global ignores - these apply to all configurations
{
ignores: [
'dist/**',
'.output/**',
'.wxt/**',
'node_modules/**',
'logs/**',
'*.log',
'.cache/**',
'.temp/**',
'.vscode/**',
'!.vscode/extensions.json',
'.idea/**',
'.DS_Store',
'Thumbs.db',
'*.zip',
'*.tar.gz',
'stats.html',
'stats-*.json',
'libs/**',
'workers/**',
'public/libs/**',
],
},
js.configs.recommended,
{
files: ['**/*.{js,mjs,cjs,ts,vue}'],
languageOptions: {
globals: {
...globals.browser,
chrome: 'readonly',
},
},
},
...tseslint.configs.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-empty': 'off',
},
},
pluginVue.configs['flat/essential'],
{ files: ['**/*.vue'], languageOptions: { parserOptions: { parser: tseslint.parser } } },
// Prettier configuration - must be placed last to override previous rules
prettierConfig,
]);
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertySwitchTab.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">Tab ID(可选)</label>
<input
class="form-input"
type="number"
v-model.number="(node as any).config.tabId"
placeholder="数字"
/>
</div>
<div class="form-group" :class="{ invalid: needOne && !hasAny }">
<label class="form-label">URL 包含(可选)</label>
<input class="form-input" v-model="(node as any).config.urlContains" placeholder="子串匹配" />
</div>
<div class="form-group" :class="{ invalid: needOne && !hasAny }">
<label class="form-label">标题包含(可选)</label>
<input
class="form-input"
v-model="(node as any).config.titleContains"
placeholder="子串匹配"
/>
</div>
<div
v-if="needOne && !hasAny"
class="text-xs text-slate-500"
style="padding: 0 20px; color: var(--rr-danger)"
>需提供 tabId 或 URL/标题包含</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 needOne = true;
const hasAny = computed(() => {
const c: any = (props.node as any).config || {};
return !!(c.tabId || c.urlContains || c.titleContains);
});
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyLoopElements.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">元素选择器</label>
<input class="form-input" v-model="(node as any).config.selector" placeholder="CSS 选择器" />
</div>
<div class="form-group">
<label class="form-label">列表变量名</label>
<input class="form-input" v-model="(node as any).config.saveAs" placeholder="默认 elements" />
</div>
<div class="form-group">
<label class="form-label">循环项变量名</label>
<input class="form-input" v-model="(node as any).config.itemVar" placeholder="默认 item" />
</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>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
const props = defineProps<{ node: NodeBase }>();
const emit = defineEmits<{ (e: 'create-subflow', id: string): void }>();
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/tools/browser/window.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
class WindowTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.GET_WINDOWS_AND_TABS;
async execute(): Promise<ToolResult> {
try {
const windows = await chrome.windows.getAll({ populate: true });
let tabCount = 0;
const structuredWindows = windows.map((window) => {
const tabs =
window.tabs?.map((tab) => {
tabCount++;
return {
tabId: tab.id || 0,
url: tab.url || '',
title: tab.title || '',
active: tab.active || false,
};
}) || [];
return {
windowId: window.id || 0,
tabs: tabs,
};
});
const result = {
windowCount: windows.length,
tabCount: tabCount,
windows: structuredWindows,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error) {
console.error('Error in WindowTool.execute:', error);
return createErrorResponse(
`Error getting windows and tabs information: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const windowTool = new WindowTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyScript.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="form-section">
<div class="form-group">
<label class="form-label">代码</label>
<textarea
class="form-textarea"
v-model="(node as any).config.code"
rows="6"
placeholder="// your JS code"
></textarea>
</div>
<div class="form-group">
<label class="form-label">执行环境</label>
<select class="form-select" v-model="(node as any).config.world">
<option value="ISOLATED">ISOLATED</option>
<option value="MAIN">MAIN</option>
</select>
</div>
<div class="form-group">
<label class="form-label">执行时机</label>
<select class="form-select" v-model="(node as any).config.when">
<option value="before">before</option>
<option value="after">after</option>
</select>
</div>
<div class="form-group">
<label class="form-label">保存为变量(可选)</label>
<input class="form-input" v-model="(node as any).config.saveAs" placeholder="变量名" />
</div>
<div class="form-group">
<label class="form-label">结果字段映射</label>
<KeyValueEditor v-model="(node as any).config.assign" />
</div>
</div>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
import KeyValueEditor from '@/entrypoints/popup/components/builder/components/KeyValueEditor.vue';
defineProps<{ node: NodeBase }>();
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/strategies/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Selector Strategies - Strategy exports and default configuration
*/
import type { SelectorStrategy } from '../types';
import { anchorRelpathStrategy } from './anchor-relpath';
import { ariaStrategy } from './aria';
import { cssPathStrategy } from './css-path';
import { cssUniqueStrategy } from './css-unique';
import { testIdStrategy } from './testid';
import { textStrategy } from './text';
/**
* Default selector strategy list (ordered by priority).
*
* Strategy order:
* 1. testid - Stable test attributes (data-testid, name, title, alt)
* 2. aria - Accessibility attributes (aria-label, role)
* 3. css-unique - Unique CSS selectors (id, class combinations)
* 4. css-path - Structural path selector (nth-of-type)
* 5. anchor-relpath - Anchor + relative path (fallback for elements without unique attrs)
* 6. text - Text content selector (lowest priority)
*
* Note: Final candidate order is determined by stability scoring,
* but strategy order affects which candidates are generated first.
*/
export const DEFAULT_SELECTOR_STRATEGIES: ReadonlyArray<SelectorStrategy> = [
testIdStrategy,
ariaStrategy,
cssUniqueStrategy,
cssPathStrategy,
anchorRelpathStrategy,
textStrategy,
];
export { anchorRelpathStrategy } from './anchor-relpath';
export { ariaStrategy } from './aria';
export { cssPathStrategy } from './css-path';
export { cssUniqueStrategy } from './css-unique';
export { testIdStrategy } from './testid';
export { textStrategy } from './text';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/loops.ts:
--------------------------------------------------------------------------------
```typescript
import type { ExecCtx, ExecResult, NodeRuntime } from './types';
import { ENGINE_CONSTANTS } from '../engine/constants';
export const foreachNode: NodeRuntime<any> = {
validate: (step) => {
const s = step as any;
const ok =
typeof s.listVar === 'string' && s.listVar && typeof s.subflowId === 'string' && s.subflowId;
return ok ? { ok } : { ok, errors: ['foreach: 需提供 listVar 与 subflowId'] };
},
run: async (_ctx: ExecCtx, step) => {
const s: any = step;
const itemVar = typeof s.itemVar === 'string' && s.itemVar ? s.itemVar : 'item';
return {
control: {
kind: 'foreach',
listVar: String(s.listVar),
itemVar,
subflowId: String(s.subflowId),
concurrency: Math.max(
1,
Math.min(ENGINE_CONSTANTS.MAX_FOREACH_CONCURRENCY, Number(s.concurrency ?? 1)),
),
},
} as ExecResult;
},
};
export const whileNode: NodeRuntime<any> = {
validate: (step) => {
const s = step as any;
const ok = !!s.condition && typeof s.subflowId === 'string' && s.subflowId;
return ok ? { ok } : { ok, errors: ['while: 需提供 condition 与 subflowId'] };
},
run: async (_ctx: ExecCtx, step) => {
const s: any = step;
const max = Math.max(1, Math.min(10000, Number(s.maxIterations ?? 100)));
return {
control: {
kind: 'while',
condition: s.condition,
subflowId: String(s.subflowId),
maxIterations: max,
},
} as ExecResult;
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/record-replay.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { listPublished } from '../record-replay/flow-store';
import { getFlow } from '../record-replay/flow-store';
import { runFlow } from '../record-replay/flow-runner';
class FlowRunTool {
name = TOOL_NAMES.RECORD_REPLAY.FLOW_RUN;
async execute(args: any): Promise<ToolResult> {
const {
flowId,
args: vars,
tabTarget,
refresh,
captureNetwork,
returnLogs,
timeoutMs,
startUrl,
} = args || {};
if (!flowId) return createErrorResponse('flowId is required');
const flow = await getFlow(flowId);
if (!flow) return createErrorResponse(`Flow not found: ${flowId}`);
const result = await runFlow(flow, {
tabTarget,
refresh,
captureNetwork,
returnLogs,
timeoutMs,
startUrl,
args: vars,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
}
}
class ListPublishedTool {
name = TOOL_NAMES.RECORD_REPLAY.LIST_PUBLISHED;
async execute(): Promise<ToolResult> {
const list = await listPublished();
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, published: list }),
},
],
isError: false,
};
}
}
export const flowRunTool = new FlowRunTool();
export const listPublishedFlowsTool = new ListPublishedTool();
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
import globals from 'globals';
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
// Global ignores first - these apply to all configurations
{
ignores: [
'node_modules/',
'dist/',
'.output/',
'.wxt/',
'logs/',
'*.log',
'.cache/',
'.temp/',
'.idea/',
'.DS_Store',
'Thumbs.db',
'*.zip',
'*.tar.gz',
'stats.html',
'stats-*.json',
'pnpm-lock.yaml',
'**/workers/**',
'app/**/workers/**',
'packages/**/workers/**',
'test-inject-script.js',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
// Global rule adjustments
{
// Allow intentionally empty catch blocks (common in extension code),
// while keeping other empty blocks reported.
rules: {
'no-empty': ['error', { allowEmptyCatch: true }],
},
},
{
files: ['app/**/*.{js,jsx,ts,tsx}', 'packages/**/*.{js,jsx,ts,tsx}'],
ignores: ['**/workers/**'], // Additional ignores for this specific config
languageOptions: {
ecmaVersion: 2021,
sourceType: 'module',
parser: tseslint.parser,
globals: {
...globals.node,
...globals.es2021,
},
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
eslintConfigPrettier,
);
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/trigger-store.ts:
--------------------------------------------------------------------------------
```typescript
import { IndexedDbStorage, ensureMigratedFromLocal } from './storage/indexeddb-manager';
export type TriggerType = 'url' | 'contextMenu' | 'command' | 'dom';
export interface BaseTrigger {
id: string;
type: TriggerType;
enabled: boolean;
flowId: string;
args?: Record<string, any>;
}
export interface UrlTrigger extends BaseTrigger {
type: 'url';
match: Array<{ kind: 'url' | 'domain' | 'path'; value: string }>;
}
export interface ContextMenuTrigger extends BaseTrigger {
type: 'contextMenu';
title: string;
contexts?: chrome.contextMenus.ContextType[];
}
export interface CommandTrigger extends BaseTrigger {
type: 'command';
commandKey: string; // e.g., run_quick_trigger_1
}
export interface DomTrigger extends BaseTrigger {
type: 'dom';
selector: string;
appear?: boolean; // default true
once?: boolean; // default true
debounceMs?: number; // default 800
}
export type FlowTrigger = UrlTrigger | ContextMenuTrigger | CommandTrigger | DomTrigger;
export async function listTriggers(): Promise<FlowTrigger[]> {
await ensureMigratedFromLocal();
return await IndexedDbStorage.triggers.list();
}
export async function saveTrigger(t: FlowTrigger): Promise<void> {
await ensureMigratedFromLocal();
await IndexedDbStorage.triggers.save(t);
}
export async function deleteTrigger(id: string): Promise<void> {
await ensureMigratedFromLocal();
await IndexedDbStorage.triggers.delete(id);
}
export function toId(prefix = 'trg') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/index.ts:
--------------------------------------------------------------------------------
```typescript
export { navigateTool, closeTabsTool, switchTabTool } from './common';
export { windowTool } from './window';
export { vectorSearchTabsContentTool as searchTabsContentTool } from './vector-search';
export { screenshotTool } from './screenshot';
export { webFetcherTool, getInteractiveElementsTool } from './web-fetcher';
export { clickTool, fillTool } from './interaction';
export { elementPickerTool } from './element-picker';
export { networkRequestTool } from './network-request';
export { networkCaptureTool } from './network-capture';
// Legacy exports (for internal use by networkCaptureTool)
export { networkDebuggerStartTool, networkDebuggerStopTool } from './network-capture-debugger';
export { networkCaptureStartTool, networkCaptureStopTool } from './network-capture-web-request';
export { keyboardTool } from './keyboard';
export { historyTool } from './history';
export { bookmarkSearchTool, bookmarkAddTool, bookmarkDeleteTool } from './bookmark';
export { injectScriptTool, sendCommandToInjectScriptTool } from './inject-script';
export { javascriptTool } from './javascript';
export { consoleTool } from './console';
export { fileUploadTool } from './file-upload';
export { readPageTool } from './read-page';
export { computerTool } from './computer';
export { handleDialogTool } from './dialog';
export { handleDownloadTool } from './download';
export { userscriptTool } from './userscript';
export {
performanceStartTraceTool,
performanceStopTraceTool,
performanceAnalyzeInsightTool,
} from './performance';
export { gifRecorderTool } from './gif-recorder';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2/core/design-tokens/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Design Tokens Module (Phase 5.4)
*
* Runtime CSS custom property detection, resolution, and application.
*
* Usage:
* ```typescript
* import { createDesignTokensService } from './core/design-tokens';
*
* const service = createDesignTokensService();
*
* // Get available tokens for an element
* const { tokens } = service.getContextTokens(element);
*
* // Apply a token to a style property
* service.applyTokenToStyle(transactionManager, element, 'color', '--color-primary');
*
* // Cleanup
* service.dispose();
* ```
*/
// Main service
export {
createDesignTokensService,
type DesignTokensService,
type DesignTokensServiceOptions,
type GetContextTokensOptions,
type GetRootTokensOptions,
} from './design-tokens-service';
// Detector
export {
createTokenDetector,
type TokenDetector,
type TokenDetectorOptions,
} from './token-detector';
// Resolver
export {
createTokenResolver,
type TokenResolver,
type TokenResolverOptions,
type ResolveForPropertyOptions,
} from './token-resolver';
// Types
export type {
// Core identifiers
CssVarName,
RootCacheKey,
RootType,
// Token classification
TokenKind,
// Declaration source
StyleSheetRef,
TokenDeclarationOrigin,
TokenDeclaration,
// Token model
DesignToken,
// Index and query
TokenIndexStats,
TokenIndex,
ContextToken,
TokenQueryResult,
// Resolution
CssVarReference,
TokenAvailability,
TokenResolutionMethod,
TokenResolution,
TokenResolvedForProperty,
// Cache invalidation
TokenInvalidationReason,
TokenInvalidationEvent,
Unsubscribe,
} from './types';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/ProgressIndicator.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div v-if="visible" class="progress-section">
<div class="progress-indicator">
<div class="spinner" v-if="showSpinner"></div>
<span class="progress-text">{{ text }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
interface Props {
visible?: boolean;
text: string;
showSpinner?: boolean;
}
withDefaults(defineProps<Props>(), {
visible: true,
showSpinner: true,
});
</script>
<style scoped>
.progress-section {
margin-top: 16px;
animation: slideIn 0.3s ease-out;
}
.progress-indicator {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border-radius: 8px;
border-left: 4px solid #667eea;
backdrop-filter: blur(10px);
border: 1px solid rgba(102, 126, 234, 0.2);
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(102, 126, 234, 0.2);
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.progress-text {
font-size: 14px;
color: #4a5568;
font-weight: 500;
line-height: 1.4;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 420px) {
.progress-indicator {
padding: 12px;
gap: 8px;
}
.spinner {
width: 16px;
height: 16px;
border-width: 2px;
}
.progress-text {
font-size: 13px;
}
}
</style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "chrome-mcp-server",
"description": "a chrome extension to use your own chrome as a mcp server",
"author": "hangye",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "vue-tsc --noEmit",
"postinstall": "wxt prepare",
"lint": "npx eslint .",
"lint:fix": "npx eslint . --fix",
"format": "npx prettier --write .",
"format:check": "npx prettier --check .",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.47.0",
"@vue-flow/minimap": "^1.5.4",
"@xenova/transformers": "^2.17.2",
"chrome-mcp-shared": "workspace:*",
"date-fns": "^4.1.0",
"elkjs": "^0.11.0",
"gifenc": "^1.0.3",
"hnswlib-wasm-static": "0.8.5",
"markstream-vue": "0.0.3-beta.5",
"vue": "^3.5.13",
"zod": "^3.24.4"
},
"devDependencies": {
"@iconify-json/lucide": "^1.1.0",
"@tailwindcss/vite": "^4.0.0",
"@types/chrome": "^0.0.318",
"@wxt-dev/module-vue": "^1.0.2",
"dotenv": "^16.5.0",
"fake-indexeddb": "^6.2.5",
"jsdom": "^26.0.0",
"tailwindcss": "^4.0.0",
"unplugin-icons": "^0.19.0",
"unplugin-vue-components": "^0.27.5",
"vite-plugin-static-copy": "^3.0.0",
"vitest": "^2.1.8",
"vue-tsc": "^2.2.8",
"wxt": "^0.20.0"
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/plugins/types.ts:
--------------------------------------------------------------------------------
```typescript
// Plugin system for record-replay engine
// Inspired by webpack-like lifecycle hooks, to avoid touching core for extensibility
import type { Flow, Step } from '../../types';
import type { ExecResult } from '../../nodes';
export interface RunContext {
runId: string;
flow: Flow;
vars: Record<string, any>;
}
export interface StepContext extends RunContext {
step: Step;
}
export interface StepErrorContext extends StepContext {
error: any;
}
export interface StepRetryContext extends StepErrorContext {
attempt: number;
}
export interface StepAfterContext extends StepContext {
result?: ExecResult;
}
export interface SubflowContext extends RunContext {
subflowId: string;
}
export interface RunEndContext extends RunContext {
success: boolean;
failed: number;
}
export interface HookControl {
pause?: boolean; // request scheduler to pause run (e.g., breakpoint)
nextLabel?: string; // override next edge label
}
export interface RunPlugin {
name: string;
onRunStart?(ctx: RunContext): Promise<void> | void;
onBeforeStep?(ctx: StepContext): Promise<HookControl | void> | HookControl | void;
onAfterStep?(ctx: StepAfterContext): Promise<void> | void;
onStepError?(ctx: StepErrorContext): Promise<HookControl | void> | HookControl | void;
onRetry?(ctx: StepRetryContext): Promise<void> | void;
onChooseNextLabel?(
ctx: StepContext & { suggested?: string },
): Promise<HookControl | void> | HookControl | void;
onSubflowStart?(ctx: SubflowContext): Promise<void> | void;
onSubflowEnd?(ctx: SubflowContext): Promise<void> | void;
onRunEnd?(ctx: RunEndContext): Promise<void> | void;
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Web Editor V2 - Inject Script Entry Point
*
* This is the main entry point for the visual editor, injected into web pages
* via chrome.scripting.executeScript from the background script.
*
* Architecture:
* - Uses WXT's defineUnlistedScript for TypeScript compilation
* - Exposes API on window.__MCP_WEB_EDITOR_V2__
* - Communicates with background via chrome.runtime.onMessage
*
* Module structure:
* - web-editor-v2/constants.ts - Configuration values
* - web-editor-v2/utils/disposables.ts - Resource cleanup
* - web-editor-v2/ui/shadow-host.ts - Shadow DOM isolation
* - web-editor-v2/core/editor.ts - Main orchestrator
* - web-editor-v2/core/message-listener.ts - Background communication
*
* Build output: .output/chrome-mv3/web-editor-v2.js
*/
import { WEB_EDITOR_V2_LOG_PREFIX } from './web-editor-v2/constants';
import { createWebEditorV2 } from './web-editor-v2/core/editor';
import { installMessageListener } from './web-editor-v2/core/message-listener';
export default defineUnlistedScript(() => {
// Phase 1: Only support top frame
// Phase 4 will add iframe support via content injection
if (window !== window.top) {
return;
}
// Singleton guard: prevent multiple instances
if (window.__MCP_WEB_EDITOR_V2__) {
console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Already installed, skipping initialization`);
return;
}
// Create and expose the API
const api = createWebEditorV2();
window.__MCP_WEB_EDITOR_V2__ = api;
// Install message listener for background communication
installMessageListener(api);
console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Installed successfully`);
});
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/extract.ts:
--------------------------------------------------------------------------------
```typescript
import type { StepExtract } from '../types';
import { expandTemplatesDeep } from '../rr-utils';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';
export const extractNode: NodeRuntime<StepExtract> = {
run: async (ctx: ExecCtx, step: StepExtract) => {
const s: any = expandTemplatesDeep(step as any, ctx.vars);
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');
let value: any = null;
if (s.js && String(s.js).trim()) {
const [{ result }] = await chrome.scripting.executeScript({
target: { tabId },
func: (code: string) => {
try {
return (0, eval)(code);
} catch (e) {
return null;
}
},
args: [String(s.js)],
} as any);
value = result;
} else if (s.selector) {
const attr = String(s.attr || 'text');
const sel = String(s.selector);
const [{ result }] = await chrome.scripting.executeScript({
target: { tabId },
func: (selector: string, attr: string) => {
try {
const el = document.querySelector(selector) as any;
if (!el) return null;
if (attr === 'text' || attr === 'textContent') return (el.textContent || '').trim();
return el.getAttribute ? el.getAttribute(attr) : null;
} catch {
return null;
}
},
args: [sel, attr],
} as any);
value = result;
}
if (s.saveAs) ctx.vars[s.saveAs] = value;
return {} as ExecResult;
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/screenshot-context.ts:
--------------------------------------------------------------------------------
```typescript
// Simple in-memory screenshot context manager per tab
// Used to scale coordinates from screenshot space to viewport space
export interface ScreenshotContext {
// Final screenshot dimensions (in CSS pixels after any scaling)
screenshotWidth: number;
screenshotHeight: number;
// Viewport dimensions (CSS pixels)
viewportWidth: number;
viewportHeight: number;
// Device pixel ratio at capture time (optional, for reference)
devicePixelRatio?: number;
// Hostname of the page when the screenshot was taken (used for domain safety checks)
hostname?: string;
// Timestamp
timestamp: number;
}
const TTL_MS = 5 * 60 * 1000; // 5 minutes
const contexts = new Map<number, ScreenshotContext>();
export const screenshotContextManager = {
setContext(tabId: number, ctx: Omit<ScreenshotContext, 'timestamp'>) {
contexts.set(tabId, { ...ctx, timestamp: Date.now() });
},
getContext(tabId: number): ScreenshotContext | undefined {
const ctx = contexts.get(tabId);
if (!ctx) return undefined;
if (Date.now() - ctx.timestamp > TTL_MS) {
contexts.delete(tabId);
return undefined;
}
return ctx;
},
clear(tabId: number) {
contexts.delete(tabId);
},
};
// Scale screenshot-space coordinates (x,y) to viewport CSS pixels
export function scaleCoordinates(
x: number,
y: number,
ctx: ScreenshotContext,
): { x: number; y: number } {
if (!ctx.screenshotWidth || !ctx.screenshotHeight || !ctx.viewportWidth || !ctx.viewportHeight) {
return { x, y };
}
const sx = (x / ctx.screenshotWidth) * ctx.viewportWidth;
const sy = (y / ctx.screenshotHeight) * ctx.viewportHeight;
return { x: Math.round(sx), y: Math.round(sy) };
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/types/gifenc.d.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Type declarations for gifenc library
* @see https://github.com/mattdesl/gifenc
*/
declare module 'gifenc' {
export interface GIFEncoderOptions {
auto?: boolean;
}
export interface WriteFrameOptions {
palette: number[];
delay?: number;
transparent?: boolean;
transparentIndex?: number;
dispose?: number;
}
export interface GIFEncoder {
writeFrame(
index: Uint8Array | Uint8ClampedArray,
width: number,
height: number,
options: WriteFrameOptions,
): void;
finish(): void;
bytes(): Uint8Array;
bytesView(): Uint8Array;
reset(): void;
}
export function GIFEncoder(options?: GIFEncoderOptions): GIFEncoder;
export interface QuantizeOptions {
format?: 'rgb565' | 'rgba4444' | 'rgb444';
oneBitAlpha?: boolean | number;
clearAlpha?: boolean;
clearAlphaColor?: number;
clearAlphaThreshold?: number;
}
export function quantize(
rgba: Uint8Array | Uint8ClampedArray,
maxColors: number,
options?: QuantizeOptions,
): number[];
export function applyPalette(
rgba: Uint8Array | Uint8ClampedArray,
palette: number[],
format?: 'rgb565' | 'rgba4444' | 'rgb444',
): Uint8Array;
export function nearestColorIndex(palette: number[], pixel: number[]): number;
export function nearestColorIndexWithDistance(
palette: number[],
pixel: number[],
): [number, number];
export function snapColorsToPalette(
palette: number[],
knownColors: number[][],
threshold?: number,
): void;
export function prequantize(
rgba: Uint8Array | Uint8ClampedArray,
options?: { roundRGB?: number; roundAlpha?: number; oneBitAlpha?: boolean | number },
): void;
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyExecuteFlow.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.flowId">
<option value="">请选择</option>
<option v-for="f in flows" :key="f.id" :value="f.id">{{ f.name || f.id }}</option>
</select>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label"
><input type="checkbox" v-model="(node as any).config.inline" />
内联执行(共享上下文变量)</label
>
</div>
<div class="form-group">
<label class="form-label">传参 (JSON)</label>
<textarea
class="form-textarea"
v-model="execArgsJson"
rows="3"
placeholder='{"k": "v"}'
></textarea>
</div>
</div>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import { computed, onMounted, ref } from 'vue';
import type { NodeBase } from '@/entrypoints/background/record-replay/types';
import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
const props = defineProps<{ node: NodeBase }>();
type FlowLite = { id: string; name?: string };
const flows = ref<FlowLite[]>([]);
onMounted(async () => {
try {
const res = await chrome.runtime.sendMessage({ type: BACKGROUND_MESSAGE_TYPES.RR_LIST_FLOWS });
if (res && res.success) flows.value = res.flows || [];
} catch {}
});
const execArgsJson = computed({
get() {
try {
return JSON.stringify((props.node as any).config?.args || {}, null, 2);
} catch {
return '';
}
},
set(v: string) {
try {
(props.node as any).config.args = v ? JSON.parse(v) : {};
} catch {}
},
});
</script>
<style scoped></style>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/drag.ts:
--------------------------------------------------------------------------------
```typescript
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { handleCallTool } from '@/entrypoints/background/tools';
import type { StepDrag } from '../types';
import { locateElement } from '../selector-engine';
import type { ExecCtx, ExecResult, NodeRuntime } from './types';
export const dragNode: NodeRuntime<StepDrag> = {
run: async (_ctx, step: StepDrag) => {
const s = step as StepDrag;
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tabId = tabs?.[0]?.id;
let startRef: string | undefined;
let endRef: string | undefined;
try {
if (typeof tabId === 'number') {
const locatedStart = await locateElement(tabId, (s as any).start);
const locatedEnd = await locateElement(tabId, (s as any).end);
startRef = (locatedStart as any)?.ref || (s as any).start.ref;
endRef = (locatedEnd as any)?.ref || (s as any).end.ref;
}
} catch {}
let startCoordinates: { x: number; y: number } | undefined;
let endCoordinates: { x: number; y: number } | undefined;
if ((!startRef || !endRef) && Array.isArray((s as any).path) && (s as any).path.length >= 2) {
startCoordinates = { x: Number((s as any).path[0].x), y: Number((s as any).path[0].y) };
const last = (s as any).path[(s as any).path.length - 1];
endCoordinates = { x: Number(last.x), y: Number(last.y) };
}
const res = await handleCallTool({
name: TOOL_NAMES.BROWSER.COMPUTER,
args: {
action: 'left_click_drag',
startRef,
ref: endRef,
startCoordinates,
coordinates: endCoordinates,
},
});
if ((res as any).isError) throw new Error('drag failed');
return {} as ExecResult;
},
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* AgentChat Components
* Export all components for the redesigned AgentChat UI.
*/
export { default as AgentChatShell } from './AgentChatShell.vue';
export { default as AgentTopBar } from './AgentTopBar.vue';
export { default as AgentComposer } from './AgentComposer.vue';
export { default as WebEditorChanges } from './WebEditorChanges.vue';
export { default as ElementChip } from './ElementChip.vue';
export { default as SelectionChip } from './SelectionChip.vue';
export { default as AgentConversation } from './AgentConversation.vue';
export { default as AgentRequestThread } from './AgentRequestThread.vue';
export { default as AgentTimeline } from './AgentTimeline.vue';
export { default as AgentTimelineItem } from './AgentTimelineItem.vue';
export { default as AgentSettingsMenu } from './AgentSettingsMenu.vue';
export { default as AgentProjectMenu } from './AgentProjectMenu.vue';
export { default as AgentSessionMenu } from './AgentSessionMenu.vue';
export { default as AgentSessionSettingsPanel } from './AgentSessionSettingsPanel.vue';
export { default as AgentSessionsView } from './AgentSessionsView.vue';
export { default as AgentSessionListItem } from './AgentSessionListItem.vue';
export { default as AgentOpenProjectMenu } from './AgentOpenProjectMenu.vue';
export { default as FakeCaretOverlay } from './FakeCaretOverlay.vue';
// Timeline step components
export { default as TimelineNarrativeStep } from './timeline/TimelineNarrativeStep.vue';
export { default as TimelineToolCallStep } from './timeline/TimelineToolCallStep.vue';
export { default as TimelineToolResultCardStep } from './timeline/TimelineToolResultCardStep.vue';
export { default as TimelineStatusStep } from './timeline/TimelineStatusStep.vue';
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/quick-panel/ui/markdown-renderer.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Quick Panel Markdown Renderer
*
* Simple markdown renderer for Quick Panel.
* Currently uses plain text rendering - markdown support to be added later
* when proper Vue/content-script integration is resolved.
*/
// ============================================================
// Types
// ============================================================
export interface MarkdownRendererInstance {
/** Update the markdown content */
setContent: (content: string, isStreaming?: boolean) => void;
/** Get current content */
getContent: () => string;
/** Dispose resources */
dispose: () => void;
}
// ============================================================
// Main Factory
// ============================================================
/**
* Create a markdown renderer instance that mounts to a container element.
* Currently renders as plain text - markdown support pending.
*
* @param container - The DOM element to render content into
* @returns Markdown renderer instance with setContent and dispose methods
*/
export function createMarkdownRenderer(container: HTMLElement): MarkdownRendererInstance {
let currentContent = '';
// Create a wrapper div for content
const contentEl = document.createElement('div');
contentEl.className = 'qp-markdown-content';
container.appendChild(contentEl);
return {
setContent(newContent: string, _streaming = false) {
currentContent = newContent;
// For now, render as plain text with basic whitespace preservation
contentEl.textContent = newContent;
},
getContent() {
return currentContent;
},
dispose() {
try {
contentEl.remove();
} catch {
// Best-effort cleanup
}
},
};
}
```